{"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 .NET Development and Framework on Daily DevOps \u0026 .NET","favicon":"https://daily-devops.net/images/logo_hu_6465d873dfa490cf.png","feed_url":"https://daily-devops.net/tags/dotnet/feed.json","home_page_url":"https://daily-devops.net/tags/dotnet/","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\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\u003eEvery time you drop into a Claude Code session, it starts indexing your project. In a .NET solution, that means \u003ccode\u003ebin/\u003c/code\u003e and \u003ccode\u003eobj/\u003c/code\u003e directories first: tens of thousands of compiled IL files, PDB symbols, generated Razor views, and NuGet extraction caches that serve exactly zero purpose in an AI context window. Then come the test coverage reports, the generated code, the environment files. All of it lands in context unless you tell Claude otherwise.\u003c/p\u003e\n\u003cp\u003eSo you ask Claude how to control which files it can see.\u003c/p\u003e\n\u003cp\u003eAnd Claude tells you about \u003ccode\u003e.claudeignore\u003c/code\u003e.\u003c/p\u003e\n\u003cp\u003eOf course it does. The concept is obvious: \u003ccode\u003e.gitignore\u003c/code\u003e keeps files out of version control, \u003ccode\u003e.dockerignore\u003c/code\u003e keeps files out of Docker build context, so naturally \u003ccode\u003e.claudeignore\u003c/code\u003e keeps files out of Claude\u0026rsquo;s context. Clean, consistent, intuitive. You add it to the project root, list your secrets and production config files, commit it alongside your \u003ccode\u003e.gitignore\u003c/code\u003e, and get on with your day.\u003c/p\u003e\n\u003cp\u003eThe AI assistant is properly restricted. Your teammates are protected. The security audit will go smoothly.\u003c/p\u003e\n\u003cp\u003eOne small problem.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-problem-claude-hallucinated-its-own-feature\"\u003e\u003ca href=\"/posts/claudeignore-dotnet/#the-problem-claude-hallucinated-its-own-feature\" title=\"The Problem: Claude Hallucinated Its Own Feature\"\u003eThe Problem: Claude Hallucinated Its Own Feature\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eHere\u0026rsquo;s what actually happened. Someone asked Claude how to prevent it from touching third-party licensed code in their repository. Claude, being helpful, explained that Claude Code supports a \u003ccode\u003e.claudeignore\u003c/code\u003e file with the same syntax as \u003ccode\u003e.gitignore\u003c/code\u003e: just drop it in the project root, and Claude will leave those files alone.\u003c/p\u003e\n\u003cp\u003eExcept that feature doesn\u0026rsquo;t exist. Claude invented it.\u003c/p\u003e\n\u003cp\u003eThe hallucination spread remarkably well. It turned up in blog posts, Stack Overflow answers, Reddit threads, and engineering wikis. Claude then read those discussions, concluded that \u003ccode\u003e.claudeignore\u003c/code\u003e must be real because everyone was talking about it, and started recommending it again. Confidently. Every time. An AI coding assistant invented a capability it doesn\u0026rsquo;t have, the made-up documentation spread across the internet, and now the AI keeps training on that documentation to reinvent the same capability. You couldn\u0026rsquo;t design a better hallucination feedback loop if you tried.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"how-a-false-sense-of-security-spread\"\u003e\u003ca href=\"/posts/claudeignore-dotnet/#how-a-false-sense-of-security-spread\" title=\"How A False Sense Of Security Spread\"\u003eHow A False Sense Of Security Spread\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eMeanwhile, developers were sleeping soundly. They had a \u003ccode\u003e.claudeignore\u003c/code\u003e in their repository. They had listed \u003ccode\u003eappsettings.Production.json\u003c/code\u003e. They had excluded \u003ccode\u003e.env.*\u003c/code\u003e. They had blocked \u003ccode\u003esecrets/\u003c/code\u003e. They had done the responsible thing, ticked the box, and assured their security teams the AI assistant was properly restricted.\u003c/p\u003e\n\u003cp\u003eProduction database connection strings: protected. API keys: safe. OAuth client secrets: completely off-limits.\u003c/p\u003e\n\u003cp\u003eNone of that was true. The file did nothing. It still does nothing. It is a text file with glob patterns that no tool reads.\u003c/p\u003e\n\u003cp\u003eTo be precise about the scope of the problem: \u003ccode\u003e.claudeignore\u003c/code\u003e is fiction, and it isn\u0026rsquo;t unique to Claude. Neither Claude Code, nor GitHub Copilot, nor OpenAI Codex offers a simple \u003ccode\u003e.claudeignore\u003c/code\u003e-style file at the project root. The mechanisms that actually exist are more fragmented, more verbose, tool-specific, and in several important cases considerably less capable than a plain ignore file would be.\u003c/p\u003e\n\u003cp\u003eHere\u0026rsquo;s what you\u0026rsquo;re actually working with.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"claude-code-permissionsdeny-in-settings\"\u003e\u003ca href=\"/posts/claudeignore-dotnet/#claude-code-permissionsdeny-in-settings\" title=\"Claude Code: permissions.deny in Settings\"\u003eClaude Code: \u003ccode\u003epermissions.deny\u003c/code\u003e in Settings\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eClaude Code\u0026rsquo;s real mechanism for restricting file access is the \u003ccode\u003epermissions\u003c/code\u003e section in \u003ccode\u003e.claude/settings.json\u003c/code\u003e. Instead of a clean, readable standalone file, you get JSON with tool permission syntax:\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;permissions\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;deny\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=\"s2\"\u003e\u0026#34;Read(./.env)\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=\"s2\"\u003e\u0026#34;Read(./.env.*)\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=\"s2\"\u003e\u0026#34;Read(./secrets/**)\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=\"s2\"\u003e\u0026#34;Read(./appsettings.Production.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=\"s2\"\u003e\u0026#34;Read(./appsettings.Staging.json)\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\u003eEach entry in the \u003ccode\u003edeny\u003c/code\u003e array is a tool call pattern. \u003ccode\u003eRead(path)\u003c/code\u003e denies Claude Code\u0026rsquo;s Read tool access to the matched path. The path supports glob patterns, so \u003ccode\u003eRead(./secrets/**)\u003c/code\u003e blocks the entire secrets directory tree.\u003c/p\u003e\n\u003cp\u003eThe settings file comes in two variants. \u003ccode\u003e.claude/settings.json\u003c/code\u003e is committed to version control and applies to the whole team. \u003ccode\u003e.claude/settings.local.json\u003c/code\u003e is personal, automatically gitignored by Claude Code when created. For security-relevant exclusions, use \u003ccode\u003esettings.json\u003c/code\u003e so restrictions are consistent across the team.\u003c/p\u003e\n\u003cp\u003eA practical baseline for .NET projects:\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;permissions\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;deny\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=\"s2\"\u003e\u0026#34;Read(./.env)\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=\"s2\"\u003e\u0026#34;Read(./.env.*)\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=\"s2\"\u003e\u0026#34;Read(./secrets.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=\"s2\"\u003e\u0026#34;Read(./appsettings.Production.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=\"s2\"\u003e\u0026#34;Read(./appsettings.Staging.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=\"s2\"\u003e\u0026#34;Read(./**/bin/**)\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=\"s2\"\u003e\u0026#34;Read(./**/obj/**)\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\n\n\n\n\u003ch3 id=\"why-deny-rules-are-not-filesystem-blackouts\"\u003e\u003ca href=\"/posts/claudeignore-dotnet/#why-deny-rules-are-not-filesystem-blackouts\" title=\"Why Deny Rules Are Not Filesystem Blackouts\"\u003eWhy Deny Rules Are Not Filesystem Blackouts\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eNow here\u0026rsquo;s the part most writeups about this topic quietly skip: \u003cstrong\u003e\u003ccode\u003epermissions.deny\u003c/code\u003e rules are tool-level controls, not filesystem-level blackouts.\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003eRead(./bin/**)\u003c/code\u003e blocks Claude Code\u0026rsquo;s Read tool from opening files in \u003ccode\u003ebin/\u003c/code\u003e. But Glob and Grep are separate tools, and \u003ccode\u003eRead\u003c/code\u003e deny rules don\u0026rsquo;t cover them. Claude can still discover those paths and search their contents. It just can\u0026rsquo;t read a file directly.\u003c/p\u003e\n\u003cp\u003eI asked Claude Code directly whether it had access to \u003ccode\u003ebin/\u003c/code\u003e folders after a \u003ccode\u003eRead\u003c/code\u003e deny rule was active. The answer: yes, Glob found them just fine.\u003c/p\u003e\n\u003cp\u003eSo \u003ccode\u003epermissions.deny\u003c/code\u003e alone does not make a directory invisible. For build artifacts like \u003ccode\u003ebin/\u003c/code\u003e and \u003ccode\u003eobj/\u003c/code\u003e, the more reliable mechanism is the \u003ccode\u003e.gitignore\u003c/code\u003e file you already have. Claude Code respects \u003ccode\u003e.gitignore\u003c/code\u003e by default (\u003ccode\u003erespectGitignore: true\u003c/code\u003e), and that exclusion applies across all discovery mechanisms, not just the Read tool. In practice, directories already in \u003ccode\u003e.gitignore\u003c/code\u003e are not surfaced by Glob or Grep in normal operation. Adding \u003ccode\u003eRead\u003c/code\u003e deny rules on top gives you a second layer, but \u003ccode\u003e.gitignore\u003c/code\u003e is doing the heavier lifting.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"what-this-means-for-secrets-in-the-repo\"\u003e\u003ca href=\"/posts/claudeignore-dotnet/#what-this-means-for-secrets-in-the-repo\" title=\"What This Means For Secrets In The Repo\"\u003eWhat This Means For Secrets In The Repo\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eFor \u003cstrong\u003esecrets and credentials\u003c/strong\u003e specifically, this distinction matters in a way that should make you uncomfortable. A production config file sitting in the repository root is not protected by a \u003ccode\u003eRead\u003c/code\u003e deny rule the way you might assume. Claude can still see that the file exists, discover its path with Glob, and match content patterns against it with Grep. The deny rule prevents reading the full file contents, but the file remains visible. The real answer, as always, is not having that file in the repository at all.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"github-copilot-exists-but-read-the-fine-print\"\u003e\u003ca href=\"/posts/claudeignore-dotnet/#github-copilot-exists-but-read-the-fine-print\" title=\"GitHub Copilot: Exists, But Read the Fine Print\"\u003eGitHub Copilot: Exists, But Read the Fine Print\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eGitHub Copilot does have a content exclusion feature. Before you get optimistic: it requires \u003cstrong\u003eCopilot Business or Copilot Enterprise\u003c/strong\u003e. Individual, Free, and Pro tiers don\u0026rsquo;t get it.\u003c/p\u003e\n\u003cp\u003eIt also isn\u0026rsquo;t a file you commit to your repository. There\u0026rsquo;s no \u003ccode\u003e.copilotignore\u003c/code\u003e. Exclusions are configured through GitHub\u0026rsquo;s web UI at the repository, organization, or enterprise level, and the configuration lives on GitHub\u0026rsquo;s end, not yours. If you want to know what\u0026rsquo;s excluded in a given repository, you navigate through GitHub\u0026rsquo;s admin interface to find out.\u003c/p\u003e\n\u003cp\u003eThe configuration format at least uses familiar YAML path patterns:\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=\"s2\"\u003e\u0026#34;*\u0026#34;\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=\"s2\"\u003e\u0026#34;**/.env\u0026#34;\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=\"s2\"\u003e\u0026#34;**/appsettings.Production.json\u0026#34;\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=\"s2\"\u003e\u0026#34;**/secrets/**\u0026#34;\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\n\n\n\n\u003ch3 id=\"why-copilot-exclusions-skip-agent-mode\"\u003e\u003ca href=\"/posts/claudeignore-dotnet/#why-copilot-exclusions-skip-agent-mode\" title=\"Why Copilot Exclusions Skip Agent Mode\"\u003eWhy Copilot Exclusions Skip Agent Mode\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eHere\u0026rsquo;s the part that really matters: these restrictions apply when Copilot is accessed through the IDE extension for code completion and inline chat. \u003cstrong\u003eThey do not apply to Agent mode, Copilot CLI, or the cloud agent.\u003c/strong\u003e GitHub states this explicitly in their documentation.\u003c/p\u003e\n\u003cp\u003eThe agentic workflows are exactly the ones where unrestricted file access is most concerning. They\u0026rsquo;re also the ones the exclusion feature doesn\u0026rsquo;t cover.\u003c/p\u003e\n\u003cp\u003eGitHub\u0026rsquo;s content exclusion is a governance feature for enterprise administrators, not a developer-facing tool for managing what an agent can see. The use case it serves is \u0026ldquo;prevent this entire codebase from reaching the AI at the org level.\u0026rdquo; The use case it doesn\u0026rsquo;t serve is \u0026ldquo;control what the agent accesses when I\u0026rsquo;m actively using it.\u0026rdquo;\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"openai-codex-nothing\"\u003e\u003ca href=\"/posts/claudeignore-dotnet/#openai-codex-nothing\" title=\"OpenAI Codex: Nothing\"\u003eOpenAI Codex: Nothing\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eAs of May 2026, OpenAI Codex has no built-in mechanism for file exclusion. Codex is an API. What goes into the context window is entirely the responsibility of the client making the call. If you\u0026rsquo;re using Codex through a third-party tool or IDE integration, that tool may or may not have its own ignore mechanism. The model itself is indifferent to what you send it.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"what-this-means-for-net-projects\"\u003e\u003ca href=\"/posts/claudeignore-dotnet/#what-this-means-for-net-projects\" title=\"What This Means for .NET Projects\"\u003eWhat This Means for .NET Projects\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eGiven the actual state of tooling, here\u0026rsquo;s where that leaves you:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eOn Claude Code\u003c/strong\u003e: use \u003ccode\u003epermissions.deny\u003c/code\u003e in \u003ccode\u003e.claude/settings.json\u003c/code\u003e, but understand you\u0026rsquo;re adding a tool-level restriction, not a filesystem blackout. Prioritize secrets and environment-specific config files. Build artifacts are better handled by \u003ccode\u003e.gitignore\u003c/code\u003e, which you almost certainly already have and which provides broader coverage across all discovery mechanisms.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eOn GitHub Copilot Business or Enterprise\u003c/strong\u003e: configure content exclusions at the organization level for sensitive repositories if you need the governance coverage. Accept that none of this applies to agent-based workflows. For individual context management in the IDE, the exclusion feature is your only option. For agents, there is no equivalent.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eOn Codex or API-based tools\u003c/strong\u003e: the exclusion problem is entirely at the integration layer. Read your tool\u0026rsquo;s documentation, not Codex\u0026rsquo;s.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-feature-that-should-exist-but-doesnt\"\u003e\u003ca href=\"/posts/claudeignore-dotnet/#the-feature-that-should-exist-but-doesnt\" title=\"The Feature That Should Exist But Doesn\u0026rsquo;t\"\u003eThe Feature That Should Exist But Doesn\u0026rsquo;t\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe \u003ccode\u003e.claudeignore\u003c/code\u003e hallucination spread so effectively because it named something developers genuinely need: a simple, version-controlled file that tells an AI coding assistant which files to leave alone. Same syntax as \u003ccode\u003e.gitignore\u003c/code\u003e. Lives in the project root. Gets committed like any other configuration. Readable by anyone on the team.\u003c/p\u003e\n\u003cp\u003eThat doesn\u0026rsquo;t exist. What exists instead is a JSON config with Read tool denial patterns that doesn\u0026rsquo;t cover Glob and Grep, a web-based admin feature that costs extra and doesn\u0026rsquo;t work for agents, and an API that does whatever the client tells it to.\u003c/p\u003e\n\u003cp\u003eThe gap between what developers want and what the tooling provides is real. Claude filling that gap by inventing a feature called \u003ccode\u003e.claudeignore\u003c/code\u003e is understandable: the concept is obvious, the need is legitimate, the name is perfect. It\u0026rsquo;s also deeply unhelpful, because developers have implemented \u003ccode\u003e.claudeignore\u003c/code\u003e files in their repositories, committed them, documented them in their onboarding guides, and assumed they were doing something. They weren\u0026rsquo;t.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"keep-credentials-out-of-the-repo\"\u003e\u003ca href=\"/posts/claudeignore-dotnet/#keep-credentials-out-of-the-repo\" title=\"Keep Credentials Out Of The Repo\"\u003eKeep Credentials Out Of The Repo\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003eThe actual security takeaway\u003c/strong\u003e: don\u0026rsquo;t put credentials in your repository. Azure Key Vault, user secrets during development, environment variable injection in CI/CD: these are not just best practices, they\u0026rsquo;re the only mechanisms that actually guarantee your production credentials don\u0026rsquo;t end up in an AI context window. \u003ccode\u003epermissions.deny\u003c/code\u003e and GitHub\u0026rsquo;s content exclusion are useful layers on top. They are not a substitute for that foundation.\u003c/p\u003e\n\u003cp\u003eThe \u003ccode\u003e.claudeignore\u003c/code\u003e situation is a concrete illustration of a broader rule worth internalizing: AI tools recommend nonexistent features with the same confident tone they use for everything else. A hallucinated configuration file and a correct one read exactly the same way. Verify against official documentation before you trust it, especially for anything security-adjacent.\u003c/p\u003e\n\u003cp\u003eThe official documentation for Claude Code\u0026rsquo;s actual file exclusion mechanism is at \u003ca href=\"https://code.claude.com/docs/en/settings#excluding-sensitive-files\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003ecode.claude.com/docs/en/settings#excluding-sensitive-files\u003c/a\u003e. That\u0026rsquo;s where a search for \u003ccode\u003e.claudeignore\u003c/code\u003e should have taken you from the beginning.\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2026-05-05T17:00:00+02:00","id":"https://daily-devops.net/posts/claudeignore-dotnet/","language":"en","summary":".claudeignore is a hallucination. Claude invented it, the internet spread it, and now Claude keeps recommending it. Here is what actually works in .NET.\n","tags":["ai-code-assistant","dotnet","github-copilot","devops","security"],"title":".claudeignore Doesn't Exist. Here's What Does.","url":"https://daily-devops.net/posts/claudeignore-dotnet/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eEvery quarter, your compliance team gathers around conference tables reviewing spreadsheets that claim your organization processes personal data lawfully. Meanwhile, your production databases collect new PII fields nobody documented, consent records expire without notification, and deletion requests sit in ticketing systems with no automated verification they were honored. You\u0026rsquo;re not non-compliant because you don\u0026rsquo;t care. You\u0026rsquo;re non-compliant because manual privacy audits cannot keep pace with continuous deployment.\u003c/p\u003e\n\u003cp\u003eHere\u0026rsquo;s the uncomfortable truth: privacy compliance is a continuous state, not a quarterly checkpoint. Every code deployment potentially introduces new PII processing. Every schema migration might create compliance gaps. Every API endpoint modification could violate documented privacy purposes. Manual audits examine yesterday\u0026rsquo;s system while today\u0026rsquo;s changes accumulate unreviewed. By the time the next audit rolls around, you\u0026rsquo;ve already shipped three months of undocumented data collection.\u003c/p\u003e\n\u003cp\u003eThis article demonstrates building .NET CLI tools that automate privacy audit workflows. We\u0026rsquo;ll examine why manual approaches fail, then construct automated solutions using reflection for PII discovery, Entity Framework metadata for database schema analysis, and GitHub Actions for continuous compliance monitoring.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-fatal-manual-privacy-audit-pattern\"\u003e\u003ca href=\"/posts/privacy-audit-automation-dotnet-cli/#the-fatal-manual-privacy-audit-pattern\" title=\"The Fatal Manual Privacy Audit Pattern\"\u003eThe Fatal Manual Privacy Audit Pattern\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eLet\u0026rsquo;s examine what \u0026ldquo;compliance\u0026rdquo; looks like in organizations that rely on quarterly manual reviews:\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"schema-drift-the-silent-compliance-killer\"\u003e\u003ca href=\"/posts/privacy-audit-automation-dotnet-cli/#schema-drift-the-silent-compliance-killer\" title=\"Schema Drift: The Silent Compliance Killer\"\u003eSchema Drift: The Silent Compliance Killer\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\"\u003eUserProfile\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\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\"\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\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\"\u003eEmail\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=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eFullName\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// Added in sprint 47 - privacy docs? What privacy docs?\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\"\u003eSocialSecurityNumber\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=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eBiometricHash\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\u003eSensitive PII hits production while compliance reviews migration files from three months ago. By the next audit, you\u0026rsquo;ve collected biometric data for an entire quarter without documented legal basis. The spreadsheet says you\u0026rsquo;re compliant. Reality disagrees.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"the-consent-expiration-time-bomb\"\u003e\u003ca href=\"/posts/privacy-audit-automation-dotnet-cli/#the-consent-expiration-time-bomb\" title=\"The Consent Expiration Time Bomb\"\u003eThe Consent Expiration Time Bomb\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eYour consent records have expiration dates. Your tracking spreadsheet has a quarterly reminder to \u0026ldquo;check expired consents.\u0026rdquo; Meanwhile, 14,000 expired consent records remain marked active, and marketing emails continue flowing to users who withdrew consent months ago.\u003c/p\u003e\n\u003cp\u003eGDPR requires making consent withdrawal as easy as giving it. If your expiration tracking relies on someone remembering to run a database query every few months, you\u0026rsquo;re systematically violating this requirement. A daily automated check would catch this in hours, not quarters.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"deletion-requests-hope-is-not-a-strategy\"\u003e\u003ca href=\"/posts/privacy-audit-automation-dotnet-cli/#deletion-requests-hope-is-not-a-strategy\" title=\"Deletion Requests: Hope Is Not a Strategy\"\u003eDeletion Requests: Hope Is Not a Strategy\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=\"n\"\u003eDeleteUserAccount\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eGuid\u003c/span\u003e \u003cspan class=\"n\"\u003euserId\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Deletes from Users table\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_dbContext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUsers\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\"\u003eu\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eu\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eId\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"n\"\u003euserId\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\u003cspan class=\"n\"\u003eExecuteDeleteAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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// But PII remains in: OrderHistory, AuditLogs, EmailCampaigns, \u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// BackupSnapshots, and that analytics database nobody remembers exists\u003c/span\u003e\n\u003c/span\u003e\u003c/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\u003eUser requests deletion. Support marks ticket resolved. Compliance assumes it happened. Meanwhile, email addresses live on in five different tables across three systems nobody checked. Without automated scanning, deletion requests become best-effort guesses.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"documentation-drift-accurate-records-of-a-system-that-no-longer-exists\"\u003e\u003ca href=\"/posts/privacy-audit-automation-dotnet-cli/#documentation-drift-accurate-records-of-a-system-that-no-longer-exists\" title=\"Documentation Drift: Accurate Records of a System That No Longer Exists\"\u003eDocumentation Drift: Accurate Records of a System That No Longer Exists\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eYour processing records say you collect \u0026ldquo;email addresses, names, and purchase history\u0026rdquo; with 36-month retention. Your actual system now processes health information, geolocation data, and biometric logs (none documented) with 60-month retention. The documentation was accurate. Eighteen months ago. Now it\u0026rsquo;s compliant fiction, and auditors are reviewing a system that no longer exists.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"building-the-fix-net-cli-tools-for-privacy-automation\"\u003e\u003ca href=\"/posts/privacy-audit-automation-dotnet-cli/#building-the-fix-net-cli-tools-for-privacy-automation\" title=\"Building the Fix: .NET CLI Tools for Privacy Automation\"\u003eBuilding the Fix: .NET CLI Tools for Privacy Automation\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eEnough problems. Let\u0026rsquo;s build solutions. The goal: make privacy compliance a build-time fact rather than a quarterly hope. These CLI tools integrate into your CI/CD pipeline, failing builds when privacy violations exist.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"pii-discovery-find-everything-you-forgot-to-document\"\u003e\u003ca href=\"/posts/privacy-audit-automation-dotnet-cli/#pii-discovery-find-everything-you-forgot-to-document\" title=\"PII Discovery: Find Everything You Forgot to Document\"\u003ePII Discovery: Find Everything You Forgot to Document\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eFirst, a CLI tool that scans your codebase and database schema for PII attributes:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\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\"\u003eDiscoverPiiCommand\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\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\"\u003ePrivacyAuditReport\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eExecuteAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eassemblyPath\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003econnectionString\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \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\"\u003ereport\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003ePrivacyAuditReport\u003c/span\u003e\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\"\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=\"n\"\u003eassemblyPath\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// Find all properties marked with [PersonalData]\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\"\u003epiiProperties\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\"\u003eSelectMany\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003etype\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003etype\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetProperties\u003c/span\u003e\u003cspan class=\"p\"\u003e())\u003c/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\"\u003eprop\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eprop\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetCustomAttribute\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003ePersonalDataAttribute\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;()\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\u003cspan class=\"n\"\u003eSelect\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eprop\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003ePiiProperty\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"n\"\u003eTypeName\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eprop\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eDeclaringType\u003c/span\u003e\u003cspan class=\"p\"\u003e?.\u003c/span\u003e\u003cspan class=\"n\"\u003eFullName\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"n\"\u003ePropertyName\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eprop\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\"\u003eLegalBasis\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eprop\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetCustomAttribute\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eLegalBasisAttribute\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;()?.\u003c/span\u003e\u003cspan class=\"n\"\u003eBasis\u003c/span\u003e \u003cspan class=\"p\"\u003e??\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;NOT_DOCUMENTED\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        \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003ereport\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eDiscoveredPiiProperties\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003epiiProperties\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\u003cspan 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// Scan EF metadata for database columns\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\"\u003edbContext\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eApplicationDbContext\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003econnectionString\u003c/span\u003e\u003cspan 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=\"n\"\u003eentityType\u003c/span\u003e \u003cspan class=\"k\"\u003ein\u003c/span\u003e \u003cspan class=\"n\"\u003edbContext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eModel\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetEntityTypes\u003c/span\u003e\u003cspan class=\"p\"\u003e())\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \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\"\u003epiiColumns\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eentityType\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetProperties\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/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\"\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\"\u003eFindAnnotation\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Privacy:IsPII\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)?.\u003c/span\u003e\u003cspan class=\"n\"\u003eValue\u003c/span\u003e \u003cspan class=\"k\"\u003eas\u003c/span\u003e \u003cspan class=\"kt\"\u003ebool?\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=\"n\"\u003ereport\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eDatabasePiiColumns\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddRange\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003epiiColumns\u003c/span\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\"\u003ep\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eDatabasePiiColumn\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"n\"\u003eTableName\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eentityType\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetTableName\u003c/span\u003e\u003cspan class=\"p\"\u003e(),\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"n\"\u003eColumnName\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetColumnName\u003c/span\u003e\u003cspan class=\"p\"\u003e(),\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"n\"\u003eLegalBasis\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFindAnnotation\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Privacy:LegalBasis\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)?.\u003c/span\u003e\u003cspan class=\"n\"\u003eValue\u003c/span\u003e \u003cspan class=\"k\"\u003eas\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"p\"\u003e??\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;UNDOCUMENTED\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        \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// Cross-reference against processing records - anything missing is a compliance gap\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\"\u003edocumented\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eLoadProcessingRecordsAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003ereport\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUndocumentedPii\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ereport\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eDatabasePiiColumns\u003c/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\"\u003ecol\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"p\"\u003e!\u003c/span\u003e\u003cspan class=\"n\"\u003edocumented\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\"\u003erec\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003erec\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCoversColumn\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ecol\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTableName\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ecol\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eColumnName\u003c/span\u003e\u003cspan class=\"p\"\u003e)))\u003c/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        \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\"\u003ereport\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/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 tool scans code via reflection and database schema via EF metadata, cross-referencing against your processing records. Anything not documented shows up as a compliance gap. Run it in CI/CD and you\u0026rsquo;ll know within minutes when someone adds undocumented PII.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"consent-monitoring-catch-expirations-before-regulators-do\"\u003e\u003ca href=\"/posts/privacy-audit-automation-dotnet-cli/#consent-monitoring-catch-expirations-before-regulators-do\" title=\"Consent Monitoring: Catch Expirations Before Regulators Do\"\u003eConsent Monitoring: Catch Expirations Before Regulators Do\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eA CLI tool that runs daily to catch consent violations:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\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\"\u003eConsentAuditCommand\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\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\"\u003eConsentAuditReport\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eExecuteAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \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\"\u003etoday\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=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003ereport\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eConsentAuditReport\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"n\"\u003eAuditDate\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003etoday\u003c/span\u003e \u003cspan class=\"p\"\u003e};\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// Find expired consents still marked active\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003ereport\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eExpiredActiveConsents\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_dbContext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eMarketingConsents\u003c/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\"\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\"\u003eIsActive\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e \u003cspan class=\"n\"\u003ec\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eExpirationDate\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e \u003cspan class=\"n\"\u003etoday\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/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\"\u003ec\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eExpiredConsent\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"n\"\u003eUserId\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ec\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\"\u003ePurpose\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\"\u003ePurpose\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"n\"\u003eDaysOverdue\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e\u003cspan class=\"p\"\u003e)(\u003c/span\u003e\u003cspan class=\"n\"\u003etoday\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\"\u003eExpirationDate\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\u003cspan class=\"n\"\u003eTotalDays\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e})\u003c/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=\"c1\"\u003e// Verify \u0026#34;completed\u0026#34; deletion requests actually deleted everything\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\"\u003edeletionRequests\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_dbContext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGdprDeletionRequests\u003c/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\"\u003er\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003er\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\"\u003eDeletionStatus\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCompleted\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/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\"\u003er\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003er\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\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\"\u003eforeach\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003euserId\u003c/span\u003e \u003cspan class=\"k\"\u003ein\u003c/span\u003e \u003cspan class=\"n\"\u003edeletionRequests\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \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\"\u003eremainingData\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eScanAllTablesForUser\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\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eremainingData\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAny\u003c/span\u003e\u003cspan class=\"p\"\u003e())\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"n\"\u003ereport\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eIncompleteDeletions\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAdd\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eIncompleteDeletion\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\"\u003euserId\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eLocations\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eremainingData\u003c/span\u003e \u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"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\"\u003ereport\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/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\u003eRun this daily. When it finds 14,000 expired consents still active, you\u0026rsquo;ll know about it the same day, not three months later during the quarterly audit.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"change-detection-know-when-privacy-impact-assessments-need-updates\"\u003e\u003ca href=\"/posts/privacy-audit-automation-dotnet-cli/#change-detection-know-when-privacy-impact-assessments-need-updates\" title=\"Change Detection: Know When Privacy Impact Assessments Need Updates\"\u003eChange Detection: Know When Privacy Impact Assessments Need Updates\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eGit integration catches privacy-impacting changes automatically:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\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\"\u003eDpiaChangeDetectionCommand\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\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\"\u003eDpiaChangeReport\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eExecuteAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003ecurrentCommit\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003epreviousCommit\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \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\"\u003ereport\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eDpiaChangeReport\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"n\"\u003eCurrentCommit\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ecurrentCommit\u003c/span\u003e \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\"\u003egitDiff\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eGetGitDiffAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003epreviousCommit\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ecurrentCommit\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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\"\u003echangedFile\u003c/span\u003e \u003cspan class=\"k\"\u003ein\u003c/span\u003e \u003cspan class=\"n\"\u003egitDiff\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eChangedFiles\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\"\u003ef\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ef\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePath\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eEndsWith\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;.cs\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\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003ecurrentTree\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eCSharpSyntaxTree\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eParseText\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eFile\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eReadAllTextAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003echangedFile\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePath\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003epreviousTree\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eCSharpSyntaxTree\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eParseText\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eGetFileContentAtCommitAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003echangedFile\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePath\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003epreviousCommit\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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// Detect new [PersonalData] properties\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\"\u003enewPiiFields\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eGetPiiProperties\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ecurrentTree\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetRoot\u003c/span\u003e\u003cspan class=\"p\"\u003e())\u003c/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\"\u003eExcept\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eGetPiiProperties\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003epreviousTree\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetRoot\u003c/span\u003e\u003cspan class=\"p\"\u003e()))\u003c/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            \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\"\u003enewPiiFields\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAny\u003c/span\u003e\u003cspan class=\"p\"\u003e())\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"n\"\u003ereport\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFilesWithPrivacyChanges\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAdd\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003ePrivacyChange\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                    \u003cspan class=\"n\"\u003eFilePath\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003echangedFile\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePath\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                    \u003cspan class=\"n\"\u003eNewPiiFields\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003enewPiiFields\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                    \u003cspan class=\"n\"\u003eRequiresDpiaUpdate\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\u003cspan 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\"\u003ereport\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/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\u003eNew PII field added in a commit? The tool flags it. External data flow changed? Flagged. Now your Data Protection Impact Assessment updates happen as part of code review, not eighteen months later when an auditor notices the mismatch.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"deletion-verification-test-that-it-actually-works\"\u003e\u003ca href=\"/posts/privacy-audit-automation-dotnet-cli/#deletion-verification-test-that-it-actually-works\" title=\"Deletion Verification: Test That It Actually Works\"\u003eDeletion Verification: Test That It Actually Works\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eDon\u0026rsquo;t trust that deletion requests were honored. Verify:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\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\"\u003eDeletionVerificationCommand\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\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\"\u003eDeletionReport\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eExecuteAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \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 all tables containing user PII\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\"\u003epiiTables\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003e_dbContext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eModel\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetEntityTypes\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/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\"\u003ee\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ee\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetProperties\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\"\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\"\u003eFindAnnotation\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Privacy:IsPII\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)?.\u003c/span\u003e\u003cspan class=\"n\"\u003eValue\u003c/span\u003e \u003cspan class=\"k\"\u003eas\u003c/span\u003e \u003cspan class=\"kt\"\u003ebool?\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\"\u003eSelect\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ee\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ee\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetTableName\u003c/span\u003e\u003cspan class=\"p\"\u003e())\u003c/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        \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// Create synthetic test user with data in all PII tables\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\"\u003etestUserId\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eCreateSyntheticUserWithPii\u003c/span\u003e\u003cspan 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_deletionService\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eDeleteUserAccount\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003etestUserId\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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// Verify deletion actually worked\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\"\u003ereport\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eDeletionReport\u003c/span\u003e\u003cspan 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=\"n\"\u003etable\u003c/span\u003e \u003cspan class=\"k\"\u003ein\u003c/span\u003e \u003cspan class=\"n\"\u003epiiTables\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \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\"\u003eremainingRecords\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eCountRecordsForUser\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003etable\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003etestUserId\u003c/span\u003e\u003cspan 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\"\u003eremainingRecords\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=\"n\"\u003ereport\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eIssues\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAdd\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e$\u0026#34;Table {table} still contains {remainingRecords} records after deletion\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=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003ereport\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis creates a synthetic user, exercises deletion, then verifies every PII table is actually empty. No more assuming deletion worked. Prove it.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"processing-records-from-code-not-spreadsheets\"\u003e\u003ca href=\"/posts/privacy-audit-automation-dotnet-cli/#processing-records-from-code-not-spreadsheets\" title=\"Processing Records From Code, Not Spreadsheets\"\u003eProcessing Records From Code, Not Spreadsheets\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eGenerate compliance documentation from your actual system state:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\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\"\u003eGenerateProcessingRecordCommand\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\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\"\u003eExecuteAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \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=\"k\"\u003erecord\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eProcessingRecord\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"n\"\u003eGeneratedDate\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        \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\"\u003eentityType\u003c/span\u003e \u003cspan class=\"k\"\u003ein\u003c/span\u003e \u003cspan class=\"n\"\u003e_dbContext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eModel\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetEntityTypes\u003c/span\u003e\u003cspan class=\"p\"\u003e())\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \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\"\u003epiiProperties\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eentityType\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetProperties\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/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\"\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\"\u003eFindAnnotation\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Privacy:IsPII\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)?.\u003c/span\u003e\u003cspan class=\"n\"\u003eValue\u003c/span\u003e \u003cspan class=\"k\"\u003eas\u003c/span\u003e \u003cspan class=\"kt\"\u003ebool?\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\"\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            \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\"\u003epiiProperties\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=\"k\"\u003econtinue\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003erecord\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eActivities\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAdd\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eProcessingActivity\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan 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\"\u003eentityType\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eClrType\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\"\u003eDataCategories\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003epiiProperties\u003c/span\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\"\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\"\u003eFindAnnotation\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Privacy:Category\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)?.\u003c/span\u003e\u003cspan class=\"n\"\u003eValue\u003c/span\u003e \u003cspan class=\"k\"\u003eas\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\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\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"n\"\u003eLegalBasis\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eentityType\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFindAnnotation\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Privacy:LegalBasis\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)?.\u003c/span\u003e\u003cspan class=\"n\"\u003eValue\u003c/span\u003e \u003cspan class=\"k\"\u003eas\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"p\"\u003e??\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;NOT_DOCUMENTED\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\"\u003ePurpose\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eentityType\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFindAnnotation\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Privacy:Purpose\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)?.\u003c/span\u003e\u003cspan class=\"n\"\u003eValue\u003c/span\u003e \u003cspan class=\"k\"\u003eas\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"p\"\u003e??\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;NOT_DOCUMENTED\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        \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\"\u003eFile\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWriteAllTextAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;ProcessingRecord.json\u0026#34;\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\"\u003eSerialize\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003erecord\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eJsonSerializerOptions\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"n\"\u003eWriteIndented\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=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eDocumentation generated from code annotations is always current. When someone adds a new PII field and forgets to annotate it, the tool flags it as undocumented. The processing record reflects reality, not eighteen-month-old assumptions.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"github-actions-integration\"\u003e\u003ca href=\"/posts/privacy-audit-automation-dotnet-cli/#github-actions-integration\" title=\"GitHub Actions Integration\"\u003eGitHub Actions Integration\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eWire everything into CI/CD:\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\"\u003ePrivacy Compliance Audit\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\"\u003eon\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\"\u003epush\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003ebranches\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"l\"\u003emain, develop]\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\"\u003epull_request\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\"\u003eschedule\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\"\u003ecron\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;0 2 * * *\u0026#39;\u003c/span\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"c\"\u003e# Daily at 2 AM\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\"\u003ejobs\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\"\u003eprivacy-audit\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\"\u003eruns-on\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eubuntu-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    \u003c/span\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\"\u003euses\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eactions/checkout@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\"\u003efetch-depth\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"m\"\u003e2\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=\"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;9.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=\"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\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eDiscover Undocumented PII\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 run --project PrivacyAudit.Cli -- discover-pii --output pii-report.json\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=\"w\"\u003e      \u003c/span\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\"\u003eAudit Consent Records\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 run --project PrivacyAudit.Cli -- audit-consent --output consent-report.json\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=\"w\"\u003e      \u003c/span\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\"\u003eDetect Privacy-Impacting Changes\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 run --project PrivacyAudit.Cli -- detect-changes --output changes-report.json\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=\"w\"\u003e      \u003c/span\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\"\u003eVerify Deletion Completeness\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 run --project PrivacyAudit.Cli -- verify-deletion --output deletion-report.json\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=\"w\"\u003e      \u003c/span\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\"\u003eFail on Violations\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 run --project PrivacyAudit.Cli -- check-compliance --fail-on-violations true\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=\"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/upload-artifact@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\"\u003eif\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003ealways()\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\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eprivacy-audit-reports\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\"\u003epath\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;*-report.json\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\u003eUndocumented PII? Build fails. Expired consents? Build fails. Incomplete deletions? Build fails. Compliance becomes a technical gate, not a quarterly prayer meeting.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"what-these-tools-actually-prove\"\u003e\u003ca href=\"/posts/privacy-audit-automation-dotnet-cli/#what-these-tools-actually-prove\" title=\"What These Tools Actually Prove\"\u003eWhat These Tools Actually Prove\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eLet\u0026rsquo;s map this back to what regulators care about:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eLegal basis documentation\u003c/strong\u003e: The PII discovery tool scans everything and flags undocumented processing. No more hoping developers remembered to update the spreadsheet.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eConsent management\u003c/strong\u003e: Daily automated checks catch expired consents within 24 hours, not 90 days. When someone asks \u0026ldquo;how do you ensure consent is current?\u0026rdquo;, you have timestamped reports.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eDeletion verification\u003c/strong\u003e: Synthetic user tests prove deletion works across all systems. When someone asks \u0026ldquo;how do you verify erasure requests?\u0026rdquo;, you have test results, not assurances.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eChange detection\u003c/strong\u003e: Git-integrated analysis flags privacy-impacting code changes at PR time. Impact assessments update when code changes, not whenever someone remembers.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eProcessing records\u003c/strong\u003e: Documentation generated from code annotations reflects current reality. The record you show auditors matches the system they\u0026rsquo;re auditing.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"getting-started-without-boiling-the-ocean\"\u003e\u003ca href=\"/posts/privacy-audit-automation-dotnet-cli/#getting-started-without-boiling-the-ocean\" title=\"Getting Started Without Boiling the Ocean\"\u003eGetting Started Without Boiling the Ocean\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eDon\u0026rsquo;t try to implement everything at once:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eWeek 1-2\u003c/strong\u003e: Deploy PII discovery. Baseline your current state. You\u0026rsquo;ll find undocumented fields in 40-60% of your tables. That\u0026rsquo;s normal. Start documenting.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eWeek 3-4\u003c/strong\u003e: Add consent monitoring. Run it daily. When you discover 14,000 expired consents nobody knew about, you\u0026rsquo;ll understand why quarterly audits don\u0026rsquo;t work.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eWeek 5-6\u003c/strong\u003e: Integrate change detection into CI/CD. Make privacy compliance a build requirement. New undocumented PII stops reaching production.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eWeek 7-8\u003c/strong\u003e: Deploy deletion verification. Create synthetic test users. Prove erasure actually works.\u003c/p\u003e\n\u003cp\u003eStart with non-production environments to tune false positive rates. Privacy audit automation reveals uncomfortable truths. Expect resistance from teams who prefer checkbox compliance to technical accountability.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-uncomfortable-truth\"\u003e\u003ca href=\"/posts/privacy-audit-automation-dotnet-cli/#the-uncomfortable-truth\" title=\"The Uncomfortable Truth\"\u003eThe Uncomfortable Truth\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eManual privacy audits are security theater. Quarterly spreadsheet reviews cannot detect PII added last Tuesday, consent that expired this morning, or deletion requests honored in some tables but not others.\u003c/p\u003e\n\u003cp\u003ePrivacy compliance is a continuous technical state, not a point-in-time assessment. Your compliance documentation is either generated from your actual system state, or it\u0026rsquo;s fiction. There\u0026rsquo;s no middle ground.\u003c/p\u003e\n\u003cp\u003eBuild .NET CLI tools that make compliance a build-time fact. Use reflection to discover all PII. Use Entity Framework metadata to scan schemas. Use Git integration to catch privacy-impacting changes. Fail builds when violations exist.\u003c/p\u003e\n\u003cp\u003eThe tools in this article are a starting point. Real implementations need customization for your specific architecture and regulatory requirements. But the fundamental principle stands: automate privacy audits or accept that your compliance documentation is wishful thinking.\u003c/p\u003e\n\u003cp\u003eStop reviewing spreadsheets. Start building proof.\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2026-04-30T17:00:00+02:00","id":"https://daily-devops.net/posts/privacy-audit-automation-dotnet-cli/","language":"en","summary":"Quarterly audits can't catch PII added last Tuesday. Build .NET CLI tools that make compliance a build-time fact, not a spreadsheet fantasy.\n","tags":["iso-standards","privacy","cli","dotnet","automation","gdpr","compliance","security","codequality","devops"],"title":"Your Privacy Docs Are Fiction: Let's Fix That with .NET CLI Tools\n","url":"https://daily-devops.net/posts/privacy-audit-automation-dotnet-cli/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eYour security tests pass. Great. But when did they actually run? Against which code version? Can you prove it wasn\u0026rsquo;t last Tuesday\u0026rsquo;s build you\u0026rsquo;re showing?\u003c/p\u003e\n\u003cp\u003eMost security testing lives in Word documents, Postman exports, and screenshot folders on SharePoint. The tests themselves might be perfectly valid. The problem is traceability: there\u0026rsquo;s no systematic link between test execution and the code being validated.\u003c/p\u003e\n\u003cp\u003eCLI-based security testing changes this equation. Instead of tests that produce reports, you build tests that prove themselves. Every execution generates structured logs with timestamps, correlation IDs, and commit hashes. The evidence trail isn\u0026rsquo;t something you create after the fact. It\u0026rsquo;s a byproduct of running the tests.\u003c/p\u003e\n\u003cp\u003eThis approach works whether you\u0026rsquo;re preparing for compliance reviews or simply want confidence that your security controls actually function in the code you\u0026rsquo;re about to deploy.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-documentation-problem\"\u003e\u003ca href=\"/posts/cli-security-testing-audit/#the-documentation-problem\" title=\"The Documentation Problem\"\u003eThe Documentation Problem\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eRecognize this pattern?\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eSecurity_Test_Report_Q2_2024.docx\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e✓ Authentication bypass: Tried /admin without token, got 401\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e✓ SQL injection: Tried \u0026#39; OR \u0026#39;1\u0026#39;=\u0026#39;1, got error message  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e✓ Rate limiting: Sent 10 requests, got rate limited\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e✓ Authorization: User A couldn\u0026#39;t access User B\u0026#39;s data\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eEvidence: Screenshots in SharePoint\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eNext scheduled test: Q3 2024\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe tests are valid. The evidence isn\u0026rsquo;t.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eNo repeatability\u003c/strong\u003e: Manual tests run differently each time. Regression goes undetected.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eNo correlation\u003c/strong\u003e: Tests run quarterly. Code deploys daily. The gap between \u0026ldquo;tested\u0026rdquo; and \u0026ldquo;deployed\u0026rdquo; grows with every sprint.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eNo traceability\u003c/strong\u003e: Which deployment fixed which vulnerability? That question requires digging through months of documentation.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eNo automation\u003c/strong\u003e: Security validation waits for team availability instead of running with every build.\u003c/p\u003e\n\u003cp\u003eThe fix isn\u0026rsquo;t better documentation. It\u0026rsquo;s tests that document themselves.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"building-self-documenting-security-tests\"\u003e\u003ca href=\"/posts/cli-security-testing-audit/#building-self-documenting-security-tests\" title=\"Building Self-Documenting Security Tests\"\u003eBuilding Self-Documenting Security Tests\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe approach uses xUnit with ASP.NET Core\u0026rsquo;s \u003ccode\u003eWebApplicationFactory\u003c/code\u003e. This combination lets you test your application in-memory without deploying to actual infrastructure. More importantly, it integrates seamlessly with CI/CD pipelines that capture structured output.\u003c/p\u003e\n\u003cp\u003eThe key insight: every test should validate a specific security boundary and produce output that links execution to the code version being tested. You\u0026rsquo;re not writing tests that generate reports. You\u0026rsquo;re writing tests that generate evidence.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"the-core-pattern\"\u003e\u003ca href=\"/posts/cli-security-testing-audit/#the-core-pattern\" title=\"The Core Pattern\"\u003eThe Core Pattern\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eAuthentication boundaries are the natural starting point. They\u0026rsquo;re well-understood, frequently attacked, and straightforward to validate. A test for unauthenticated access checks three things: the response code, the presence of proper authentication headers, and the absence of sensitive information in error messages.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\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\"\u003eSecurityTests\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eIClassFixture\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eWebApplicationFactory\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=\"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\"\u003eHttpClient\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=\"n\"\u003eSecurityTests\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eWebApplicationFactory\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\"\u003efactory\u003c/span\u003e\u003cspan class=\"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_client\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003efactory\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCreateClient\u003c/span\u003e\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=\"na\"\u003e    [Trait(\u0026#34;Category\u0026#34;, \u0026#34;Security\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\"\u003eProtectedEndpoint_NoToken_Returns401\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \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\"\u003e_client\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;/api/users/profile\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\"\u003eAssert\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eEqual\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eHttpStatusCode\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUnauthorized\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eresponse\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eStatusCode\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// Error responses must not leak internal details\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\"\u003econtent\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eresponse\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\"\u003eReadAsStringAsync\u003c/span\u003e\u003cspan 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\"\u003eDoesNotContain\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;database\u0026#34;\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\"\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        \u003cspan class=\"n\"\u003eAssert\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eDoesNotContain\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;stack\u0026#34;\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\"\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    \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=\"na\"\u003e    [Trait(\u0026#34;Category\u0026#34;, \u0026#34;Security\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\"\u003eCrossUserAccess_Returns403\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan 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_client\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eDefaultRequestHeaders\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAuthorization\u003c/span\u003e \u003cspan 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\"\u003eAuthenticationHeaderValue\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Bearer\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;token-for-userA\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// User A attempts to access User B\u0026#39;s data\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\"\u003e_client\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;/api/users/userB-id/profile\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\"\u003eAssert\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eEqual\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eHttpStatusCode\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eForbidden\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eresponse\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eStatusCode\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"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    [Theory]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [InlineData(\u0026#34;\u0026#39; OR \u0026#39;1\u0026#39;=\u0026#39;1\u0026#34;)]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [InlineData(\u0026#34;\u0026lt;script\u0026gt;alert(\u0026#39;xss\u0026#39;)\u0026lt;/script\u0026gt;\u0026#34;)]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [Trait(\u0026#34;Category\u0026#34;, \u0026#34;Security\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\"\u003eSearchEndpoint_MaliciousInput_Sanitized\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\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=\"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\"\u003e_client\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetAsync\u003c/span\u003e\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;/api/search?q={Uri.EscapeDataString(payload)}\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\"\u003eresponse\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eIsSuccessStatusCode\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \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\"\u003econtent\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eresponse\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\"\u003eReadAsStringAsync\u003c/span\u003e\u003cspan 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\"\u003eDoesNotContain\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;\u0026lt;script\u0026gt;\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003econtent\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/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 \u003ccode\u003e[Trait(\u0026quot;Category\u0026quot;, \u0026quot;Security\u0026quot;)]\u003c/code\u003e attribute enables filtering. You can run \u003ccode\u003edotnet test --filter \u0026quot;Category=Security\u0026quot;\u003c/code\u003e to execute only security tests, which is useful for CI/CD pipelines where you want security validation as a separate gate.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"what-makes-this-self-documenting\"\u003e\u003ca href=\"/posts/cli-security-testing-audit/#what-makes-this-self-documenting\" title=\"What Makes This Self-Documenting\"\u003eWhat Makes This Self-Documenting\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThe test output itself becomes evidence. When tests run in CI/CD, the execution context (commit hash, build number, timestamp) gets captured automatically in the pipeline logs. You don\u0026rsquo;t need to generate separate reports. The test run \u003cem\u003eis\u003c/em\u003e the report.\u003c/p\u003e\n\u003cp\u003eFor explicit logging, add a helper that writes structured output:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\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\"\u003eSecurityTestLog\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\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\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eWrite\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003etestName\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003ebool\u003c/span\u003e \u003cspan class=\"n\"\u003epassed\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \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\"\u003eentry\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eTimestamp\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\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=\"n\"\u003eTestName\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003etestName\u003c/span\u003e\u003cspan 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\"\u003epassed\u003c/span\u003e \u003cspan class=\"p\"\u003e?\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;PASS\u0026#34;\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;FAIL\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\"\u003eCommitSha\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eEnvironment\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetEnvironmentVariable\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;GITHUB_SHA\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e??\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;local\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=\"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;SECURITY_TEST: {JsonSerializer.Serialize(entry)}\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// Save the entry to a file or database if needed\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\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 structured output gets captured in CI/CD logs, creating a searchable history of every security test execution across every deployment.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"running-tests-in-cicd\"\u003e\u003ca href=\"/posts/cli-security-testing-audit/#running-tests-in-cicd\" title=\"Running Tests in CI/CD\"\u003eRunning Tests in CI/CD\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe real value emerges when these tests run on every commit. In a CI/CD pipeline, each test execution automatically captures the commit hash, build number, and timestamp. This context transforms test results from \u0026ldquo;tests passed\u0026rdquo; into \u0026ldquo;tests passed for commit abc123 at 2024-06-15T14:32:00Z.\u0026rdquo;\u003c/p\u003e\n\u003cp\u003eA minimal GitHub Actions workflow runs security tests and preserves results:\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\"\u003eRun Security Tests\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 test --filter \u0026#34;Category=Security\u0026#34; --logger \u0026#34;trx\u0026#34;\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\"\u003eenv\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\"\u003eGITHUB_SHA\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003e${{ github.sha }}\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\"\u003eUpload Results\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/upload-artifact@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\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003esecurity-results-${{ github.run_number }}\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\"\u003epath\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003e./TestResults/**/*.trx\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\"\u003eretention-days\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"m\"\u003e90\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 90-day retention creates a historical record. When someone asks \u0026ldquo;was this tested before deployment?\u0026rdquo; you can point to specific artifacts linked to specific commits. The evidence exists independent of any documentation someone might have written.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"what-changes\"\u003e\u003ca href=\"/posts/cli-security-testing-audit/#what-changes\" title=\"What Changes\"\u003eWhat Changes\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eOnce security tests run in CI/CD with proper artifact retention, several things shift.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eRegression detection becomes automatic.\u003c/strong\u003e A vulnerability fixed in March stays fixed. If code changes reintroduce it in September, the test fails immediately rather than waiting for the next quarterly review.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eThe \u0026ldquo;tested vs. deployed\u0026rdquo; gap closes.\u003c/strong\u003e When tests run on every pull request, the code being deployed is the code that was tested. No more hoping that the security validation from three weeks ago still applies to today\u0026rsquo;s release.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eEvidence generation becomes passive.\u003c/strong\u003e You\u0026rsquo;re not creating compliance documentation. The documentation creates itself as a byproduct of running tests. Pipeline logs, test artifacts, and commit history combine into an evidence trail that\u0026rsquo;s harder to fabricate than a Word document.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eSecurity testing scales with development velocity.\u003c/strong\u003e The team deploys five times a day? Security tests run five times a day. No bottleneck waiting for security team availability.\u003c/p\u003e\n\u003cp\u003eThis matters for compliance reviews, certainly. But it also matters for the simpler question: \u0026ldquo;Do our security controls actually work in the code we\u0026rsquo;re shipping?\u0026rdquo; Automated tests answer that question continuously, not quarterly.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"where-to-start\"\u003e\u003ca href=\"/posts/cli-security-testing-audit/#where-to-start\" title=\"Where to Start\"\u003eWhere to Start\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eBegin with authentication tests. They validate the most commonly attacked boundary and demonstrate the pattern clearly. Use \u003ccode\u003eWebApplicationFactory\u0026lt;TProgram\u0026gt;\u003c/code\u003e to test your ASP.NET Core application in-memory. This requires no deployed infrastructure and runs fast enough for CI/CD feedback loops.\u003c/p\u003e\n\u003cp\u003eOrganize tests with \u003ccode\u003e[Trait(\u0026quot;Category\u0026quot;, \u0026quot;Security\u0026quot;)]\u003c/code\u003e from the start. This enables running security tests separately from unit tests, which is useful when you want security validation as a distinct pipeline gate. For teams seeking a cleaner approach, the open-source \u003ca href=\"https://www.nuget.org/packages/NetEvolve.Extensions.XUnit.V3\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eNetEvolve.Extensions.XUnit.V3\u003c/a\u003e package provides standardized attributes like \u003ccode\u003e[IntegrationTest]\u003c/code\u003e or \u003ccode\u003e[AcceptanceTest]\u003c/code\u003e that work consistently across xUnit, NUnit, MSTest, and TUnit with the same \u003ccode\u003edotnet test --filter TestCategory=...\u003c/code\u003e syntax.\u003c/p\u003e\n\u003ca href=\"https://github.com/dailydevops/extensions.test\" class=\"linked\" target=\"_blank\" rel=\"noopener external noreferrer\" title=\"Compatibility library for solutions using multiple .NET test frameworks.\"\u003e\n  \u003cimg src=\"/images/github-dailydevops-extensions.test.png\" class=\"repository\" width=\"1200\" height=\"630\" title=\"Compatibility library for solutions using multiple .NET test frameworks.\" alt=\"Compatibility library for solutions using multiple .NET test frameworks.\" /\u003e\n\u003c/a\u003e\n\u003cp\u003eConfigure artifact retention for at least 90 days. Shorter retention means you can\u0026rsquo;t demonstrate testing history during compliance reviews. Longer retention costs storage but provides deeper history.\u003c/p\u003e\n\u003cp\u003eStart small. Three or four authentication tests that run on every commit provide more value than 50 tests that run quarterly. The goal is continuous validation, not comprehensive coverage on day one.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-shift\"\u003e\u003ca href=\"/posts/cli-security-testing-audit/#the-shift\" title=\"The Shift\"\u003eThe Shift\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eManual testing produces documents. Automated testing produces evidence.\u003c/p\u003e\n\u003cp\u003eWhen someone asks \u0026ldquo;How do you verify security testing?\u0026rdquo; the answer changes. Instead of pointing to a quarterly report, you point to 847 test executions across 23 deployments, each linked to a specific commit and preserved in pipeline artifacts.\u003c/p\u003e\n\u003cp\u003eSecurity professionals still define what to test. The automation handles execution, logging, and retention. The result: security validation that runs continuously and proves itself without requiring anyone to write a report.\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2026-04-28T17:00:00+02:00","id":"https://daily-devops.net/posts/cli-security-testing-audit/","language":"en","summary":"Build xUnit and WebApplicationFactory security tests that emit timestamped evidence tied to commit hashes. Retire the SharePoint screenshot folder.","tags":["iso-standards","security","cli","dotnet","testing","compliance","devops"],"title":"Security Tests That Prove Themselves","url":"https://daily-devops.net/posts/cli-security-testing-audit/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eI\u0026rsquo;ve watched this trainwreck unfold a dozen times: Organization gets certified, consultants cash their checks, comprehensive documentation gets filed somewhere, and then\u0026hellip; compliance becomes a Word document ritual. \u0026ldquo;Just check the Azure portal\u0026rdquo; before each release. Screenshot some settings. Sign the checklist. Ship it.\u003c/p\u003e\n\u003cp\u003eThree months later, an audit exposes configuration drift, hardcoded secrets in production, and vulnerable dependencies nobody noticed. The compliance team swears everything was checked. The forensic evidence disagrees.\u003c/p\u003e\n\u003cp\u003eHere\u0026rsquo;s what nobody wants to admit: Manual compliance verification is fundamentally broken. Not \u0026ldquo;could be improved\u0026rdquo; broken. Broken in ways that would make your auditor weep if they understood software. Screenshots aren\u0026rsquo;t documentation. Different people finding different issues isn\u0026rsquo;t \u0026ldquo;thorough review,\u0026rdquo; it\u0026rsquo;s randomness. And \u0026ldquo;I checked it last Tuesday\u0026rdquo; isn\u0026rsquo;t an evidence trail.\u003c/p\u003e\n\u003cp\u003eThe fix isn\u0026rsquo;t more checklists or stricter sign-off procedures. It\u0026rsquo;s treating compliance as what it actually is: an engineering problem. One that demands automated tooling in your CI/CD pipeline, not a rotating sacrifice of whoever drew the short straw this sprint.\u003c/p\u003e\n\u003cp\u003e.NET gives you everything needed: \u003ccode\u003eSystem.CommandLine\u003c/code\u003e for CLI interfaces, Roslyn for code inspection, Azure SDK for infrastructure validation. Let me show you what real compliance automation looks like.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-checklist-ceremony-a-horror-story\"\u003e\u003ca href=\"/posts/compliance-verification-dotnet-cli/#the-checklist-ceremony-a-horror-story\" title=\"The Checklist Ceremony (A Horror Story)\"\u003eThe Checklist Ceremony (A Horror Story)\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eLet\u0026rsquo;s talk about what I see in organizations that claim to be \u0026ldquo;compliant.\u0026rdquo;\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"the-sacred-document\"\u003e\u003ca href=\"/posts/compliance-verification-dotnet-cli/#the-sacred-document\" title=\"The Sacred Document\"\u003eThe Sacred Document\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eA Word document titled \u003ccode\u003eSecurity_Compliance_Checklist_v3_FINAL_v2_REALLY_FINAL.docx\u003c/code\u003e gets circulated before each release. Someone (usually whoever lost the argument about who has to do it this time) manually verifies items:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e☐ Check code for hardcoded secrets (search GitHub for \u0026#34;password\u0026#34;, \u0026#34;apikey\u0026#34;)\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e☐ Verify all API endpoints require authentication (manually test 10 endpoints)\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e☐ Confirm Azure Key Vault configured correctly (log into portal, screenshot settings)\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e☐ Validate database encryption enabled (run SQL query, save results)\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e☐ Check dependency versions for known vulnerabilities (visit NuGet.org, manually search)\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e☐ Confirm HTTPS enforced on all services (curl a few endpoints)\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eEach check gets marked complete. Someone signs off. The deployment proceeds. Everyone feels very professional.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"what-actually-happens-spoiler-nothing-good\"\u003e\u003ca href=\"/posts/compliance-verification-dotnet-cli/#what-actually-happens-spoiler-nothing-good\" title=\"What Actually Happens (Spoiler: Nothing Good)\"\u003eWhat Actually Happens (Spoiler: Nothing Good)\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003eInconsistent execution.\u003c/strong\u003e Different team members interpret \u0026ldquo;check for secrets\u0026rdquo; differently. One searches for \u0026ldquo;password\u0026rdquo; in the codebase. Another skips config files because \u0026ldquo;those don\u0026rsquo;t count, right?\u0026rdquo; A third forgets entirely because they\u0026rsquo;re three days behind on a feature that should have shipped last week.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eNo evidence trail.\u003c/strong\u003e The checklist says \u0026ldquo;completed\u0026rdquo; but provides zero forensic evidence. An auditor asks, \u0026ldquo;Can you prove API endpoints required authentication on March 15th?\u0026rdquo; The answer is a signature and a shrug. Maybe a vague memory of clicking around in the portal.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eConfiguration drift goes unnoticed.\u003c/strong\u003e The Azure portal shows correct settings \u003cem\u003etoday\u003c/em\u003e. Last week when someone disabled encryption for \u0026ldquo;quick troubleshooting\u0026rdquo; and forgot to re-enable it? That shipped to production. Nobody knows.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eVulnerable dependencies everywhere.\u003c/strong\u003e Manually checking NuGet packages takes hours and misses transitive dependencies entirely. By the time someone googles the top-level packages, code with known CVEs is already running in prod.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eEnvironments? What environments?\u003c/strong\u003e The checklist gets run in production. Maybe. Does staging match? Dev? Nobody\u0026rsquo;s manually checking every environment every deployment. That would be insane. (It would also be the actual requirement.)\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"why-auditors-should-be-furious\"\u003e\u003ca href=\"/posts/compliance-verification-dotnet-cli/#why-auditors-should-be-furious\" title=\"Why Auditors Should Be Furious\"\u003eWhy Auditors Should Be Furious\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThe standard requires \u0026ldquo;regular\u0026rdquo; compliance reviews. Regular means every deployment, every config change, every commit. Not \u0026ldquo;quarterly when someone remembers.\u0026rdquo; Not \u0026ldquo;weekly if we\u0026rsquo;re feeling disciplined.\u0026rdquo;\u003c/p\u003e\n\u003cp\u003eIt also requires documented procedures. Screenshots in Word documents aren\u0026rsquo;t procedures. They\u0026rsquo;re artifacts from a specific moment that became lies the instant someone changed something.\u003c/p\u003e\n\u003cp\u003eManual compliance theater satisfies auditors who don\u0026rsquo;t understand software. It doesn\u0026rsquo;t satisfy the actual requirements. And increasingly, auditors \u003cem\u003edo\u003c/em\u003e understand software.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-fix-make-the-computer-do-it\"\u003e\u003ca href=\"/posts/compliance-verification-dotnet-cli/#the-fix-make-the-computer-do-it\" title=\"The Fix: Make the Computer Do It\"\u003eThe Fix: Make the Computer Do It\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eCompliance verification needs to be automated, repeatable, verifiable, fast, and comprehensive. Humans are bad at all of these things. Computers are great at them.\u003c/p\u003e\n\u003cp\u003e.NET CLI tools deliver exactly this. Here\u0026rsquo;s how to build them.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"building-your-compliance-scanner\"\u003e\u003ca href=\"/posts/compliance-verification-dotnet-cli/#building-your-compliance-scanner\" title=\"Building Your Compliance Scanner\"\u003eBuilding Your Compliance Scanner\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eWe\u0026rsquo;re creating a .NET Global Tool that scans code and infrastructure, then fails the pipeline when it finds problems. Simple concept, surprisingly effective.\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 new tool -n ComplianceScanner\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nb\"\u003ecd\u003c/span\u003e ComplianceScanner\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003edotnet add package System.CommandLine --version 2.0.2\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003edotnet add package Microsoft.CodeAnalysis.CSharp --version 5.0.0\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003edotnet add package Azure.Identity --version 1.17.1\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003edotnet add package Azure.ResourceManager --version 1.13.2\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe core CLI structure is straightforward:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\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.CommandLine\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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\"\u003erootCommand\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eRootCommand\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Compliance verification tool\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\"\u003escanCommand\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eCommand\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;scan\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Scan codebase for violations\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003epathOption\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eOption\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=\"s\"\u003e\u0026#34;--path\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eDirectory\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetCurrentDirectory\u003c/span\u003e\u003cspan class=\"p\"\u003e());\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003escanCommand\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddOption\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003epathOption\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003escanCommand\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSetHandler\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\"\u003epath\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\"\u003eviolations\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eList\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eComplianceViolation\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\"\u003eviolations\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddRange\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eSecretScanner\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eScanAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003epath\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eviolations\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddRange\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eAuthScanner\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eScanAsync\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=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eviolations\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAny\u003c/span\u003e\u003cspan class=\"p\"\u003e())\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eforeach\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003ev\u003c/span\u003e \u003cspan class=\"k\"\u003ein\u003c/span\u003e \u003cspan class=\"n\"\u003eviolations\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"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;[{v.Severity}] {v.Rule}: {v.Message}\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=\"m\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"c1\"\u003e// Fails the pipeline\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan 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\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/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\"\u003epathOption\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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\"\u003erootCommand\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddCommand\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003escanCommand\u003c/span\u003e\u003cspan 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\"\u003erootCommand\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eInvokeAsync\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\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe key insight: return non-zero exit codes. That\u0026rsquo;s what makes pipelines fail. Everything else is details.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"finding-secrets-the-ones-your-team-swears-arent-there\"\u003e\u003ca href=\"/posts/compliance-verification-dotnet-cli/#finding-secrets-the-ones-your-team-swears-arent-there\" title=\"Finding Secrets (The Ones Your Team Swears Aren\u0026rsquo;t There)\"\u003eFinding Secrets (The Ones Your Team Swears Aren\u0026rsquo;t There)\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eRoslyn makes this embarrassingly simple:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\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\"\u003eSecretScanner\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\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\"\u003estatic\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eRegex\u003c/span\u003e\u003cspan class=\"p\"\u003e[]\u003c/span\u003e \u003cspan class=\"n\"\u003ePatterns\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=\"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\"\u003eRegex\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e@\u0026#34;(?i)(password|pwd)\\s*=\\s*[\u0026#34;\u0026#34;\u0026#39;][^\u0026#34;\u0026#34;\u0026#39;]{8,}[\u0026#34;\u0026#34;\u0026#39;]\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\"\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)(api[_-]?key)\\s*=\\s*[\u0026#34;\u0026#34;\u0026#39;][^\u0026#34;\u0026#34;\u0026#39;]{20,}[\u0026#34;\u0026#34;\u0026#39;]\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\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eRegex\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e@\u0026#34;AKIA[0-9A-Z]{16}\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e),\u003c/span\u003e \u003cspan class=\"c1\"\u003e// AWS keys - always fun to find these\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"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\"\u003estatic\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\"\u003eList\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eViolation\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eScanAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003epath\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eviolations\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eList\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eViolation\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=\"k\"\u003eforeach\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003efile\u003c/span\u003e \u003cspan class=\"k\"\u003ein\u003c/span\u003e \u003cspan class=\"n\"\u003eDirectory\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetFiles\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003epath\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;*.cs\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eSearchOption\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAllDirectories\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \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\"\u003etree\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eCSharpSyntaxTree\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eParseText\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eFile\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eReadAllTextAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003efile\u003c/span\u003e\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\"\u003eliterals\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003etree\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetRootAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e()).\u003c/span\u003e\u003cspan class=\"n\"\u003eDescendantNodes\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/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\"\u003eOfType\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eLiteralExpressionSyntax\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\"\u003eWhere\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003el\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003el\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eIsKind\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eSyntaxKind\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eStringLiteralExpression\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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\"\u003eliteral\u003c/span\u003e \u003cspan class=\"k\"\u003ein\u003c/span\u003e \u003cspan class=\"n\"\u003eliterals\u003c/span\u003e\u003cspan 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\"\u003ePatterns\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\"\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\"\u003eIsMatch\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eliteral\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\"\u003eValueText\u003c/span\u003e\u003cspan class=\"p\"\u003e)))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                    \u003cspan class=\"n\"\u003eviolations\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAdd\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=\"s\"\u003e\u0026#34;SECRET\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\"\u003efile\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Hardcoded secret\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eviolations\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eYou\u0026rsquo;d be amazed how many \u0026ldquo;we definitely don\u0026rsquo;t have hardcoded secrets\u0026rdquo; codebases light up like Christmas trees when you run this.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"finding-naked-endpoints\"\u003e\u003ca href=\"/posts/compliance-verification-dotnet-cli/#finding-naked-endpoints\" title=\"Finding Naked Endpoints\"\u003eFinding Naked Endpoints\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eEvery ASP.NET Core controller endpoint should either have \u003ccode\u003e[Authorize]\u003c/code\u003e or an explicit \u003ccode\u003e[AllowAnonymous]\u003c/code\u003e with a documented reason. \u0026ldquo;We forgot\u0026rdquo; is not a documented 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=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eAuthScanner\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\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=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eList\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eViolation\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eScanAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003epath\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eviolations\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eList\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eViolation\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=\"k\"\u003eforeach\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003efile\u003c/span\u003e \u003cspan class=\"k\"\u003ein\u003c/span\u003e \u003cspan class=\"n\"\u003eDirectory\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetFiles\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003epath\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;*.cs\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eSearchOption\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAllDirectories\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \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\"\u003eroot\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eCSharpSyntaxTree\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eParseText\u003c/span\u003e\u003cspan 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\"\u003eFile\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eReadAllTextAsync\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\"\u003eGetRootAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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\"\u003econtroller\u003c/span\u003e \u003cspan class=\"k\"\u003ein\u003c/span\u003e \u003cspan class=\"n\"\u003eroot\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eDescendantNodes\u003c/span\u003e\u003cspan class=\"p\"\u003e().\u003c/span\u003e\u003cspan class=\"n\"\u003eOfType\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eClassDeclarationSyntax\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\"\u003eWhere\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\"\u003eIdentifier\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eText\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eEndsWith\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Controller\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\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003ehasClassAuth\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003econtroller\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAttributeLists\u003c/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\"\u003eSelectMany\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ea\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ea\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAttributes\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/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\"\u003ea\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ea\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\"\u003eToString\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=\"s\"\u003e\u0026#34;Authorize\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\"\u003emethod\u003c/span\u003e \u003cspan class=\"k\"\u003ein\u003c/span\u003e \u003cspan class=\"n\"\u003econtroller\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eDescendantNodes\u003c/span\u003e\u003cspan class=\"p\"\u003e().\u003c/span\u003e\u003cspan class=\"n\"\u003eOfType\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eMethodDeclarationSyntax\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\"\u003eattrs\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=\"n\"\u003eAttributeLists\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSelectMany\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ea\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ea\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAttributes\u003c/span\u003e\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\"\u003eisEndpoint\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eattrs\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\"\u003ea\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ea\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\"\u003eToString\u003c/span\u003e\u003cspan class=\"p\"\u003e().\u003c/span\u003e\u003cspan class=\"n\"\u003eStartsWith\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Http\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\"\u003ehasAuth\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eattrs\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\"\u003ea\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ea\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\"\u003eToString\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=\"s\"\u003e\u0026#34;Authorize\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\"\u003ea\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\"\u003eToString\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=\"s\"\u003e\u0026#34;AllowAnonymous\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\"\u003eisEndpoint\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e \u003cspan class=\"p\"\u003e!\u003c/span\u003e\u003cspan class=\"n\"\u003ehasClassAuth\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e \u003cspan class=\"p\"\u003e!\u003c/span\u003e\u003cspan class=\"n\"\u003ehasAuth\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                        \u003cspan class=\"n\"\u003eviolations\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAdd\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=\"s\"\u003e\u0026#34;AUTH\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;HIGH\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003efile\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                            \u003cspan class=\"s\"\u003e$\u0026#34;Endpoint \u0026#39;{method.Identifier}\u0026#39; has no auth attribute\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eviolations\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis catches the \u0026ldquo;I\u0026rsquo;ll add authentication later\u0026rdquo; endpoints that somehow made it to production three years ago.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"infrastructure-reality-checks\"\u003e\u003ca href=\"/posts/compliance-verification-dotnet-cli/#infrastructure-reality-checks\" title=\"Infrastructure Reality Checks\"\u003eInfrastructure Reality Checks\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eAzure SDK lets you verify that what\u0026rsquo;s \u003cem\u003eactually\u003c/em\u003e deployed matches what everyone \u003cem\u003ethinks\u003c/em\u003e is deployed. Spoiler: it often 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=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eInfraValidator\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\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\"\u003eArmClient\u003c/span\u003e \u003cspan class=\"n\"\u003e_client\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=\"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=\"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\"\u003eList\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eIssue\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eValidateAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \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\"\u003eissues\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eList\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eIssue\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eawait\u003c/span\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\"\u003esub\u003c/span\u003e \u003cspan class=\"k\"\u003ein\u003c/span\u003e \u003cspan class=\"n\"\u003e_client\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetSubscriptions\u003c/span\u003e\u003cspan class=\"p\"\u003e())\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \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// Storage: HTTPS only? Encryption on?\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\"\u003eforeach\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003estorage\u003c/span\u003e \u003cspan class=\"k\"\u003ein\u003c/span\u003e \u003cspan class=\"n\"\u003esub\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetStorageAccountsAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e())\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan 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\"\u003estorage\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\"\u003eEnableHttpsTrafficOnly\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetValueOrDefault\u003c/span\u003e\u003cspan class=\"p\"\u003e())\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                    \u003cspan class=\"n\"\u003eissues\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAdd\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\"\u003estorage\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\"\u003eName\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Storage allows HTTP\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003estorage\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\"\u003eEncryption\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\"\u003eBlob\u003c/span\u003e\u003cspan class=\"p\"\u003e?.\u003c/span\u003e\u003cspan class=\"n\"\u003eEnabled\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\"\u003eissues\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAdd\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\"\u003estorage\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\"\u003eName\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Blob encryption disabled\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// Key Vault: Soft delete? Purge protection?\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\"\u003eforeach\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003evault\u003c/span\u003e \u003cspan class=\"k\"\u003ein\u003c/span\u003e \u003cspan class=\"n\"\u003esub\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetKeyVaultsAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e())\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan 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\"\u003evault\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\"\u003eProperties\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eEnableSoftDelete\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetValueOrDefault\u003c/span\u003e\u003cspan class=\"p\"\u003e())\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                    \u003cspan class=\"n\"\u003eissues\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAdd\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\"\u003evault\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\"\u003eName\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;No soft delete - secrets can vanish\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eissues\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/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\u003eRun this the first time and prepare for uncomfortable conversations about \u0026ldquo;temporary\u0026rdquo; configuration changes from 2019.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"dependency-nightmares\"\u003e\u003ca href=\"/posts/compliance-verification-dotnet-cli/#dependency-nightmares\" title=\"Dependency Nightmares\"\u003eDependency Nightmares\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eGood news: .NET already has this built in. Bad news: nobody\u0026rsquo;s running 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=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003estatic\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\"\u003eList\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eViolation\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eScanVulnerabilitiesAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003epath\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eprocess\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eProcess\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eStart\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eProcessStartInfo\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eFileName\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;dotnet\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\"\u003eArguments\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;list package --vulnerable --include-transitive --format 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=\"n\"\u003eWorkingDirectory\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003epath\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eRedirectStandardOutput\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\"\u003eUseShellExecute\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"kc\"\u003efalse\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"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\"\u003eoutput\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eprocess\u003c/span\u003e\u003cspan class=\"p\"\u003e!.\u003c/span\u003e\u003cspan class=\"n\"\u003eStandardOutput\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eReadToEndAsync\u003c/span\u003e\u003cspan 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\"\u003eprocess\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWaitForExitAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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// Parse JSON, extract vulnerabilities, return violations\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// The JSON structure is well-documented and straightforward\u003c/span\u003e\n\u003c/span\u003e\u003c/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 \u003ccode\u003e--include-transitive\u003c/code\u003e flag is crucial. That\u0026rsquo;s where the real horrors live: three levels deep in dependencies nobody knew existed.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"wiring-it-into-your-pipeline\"\u003e\u003ca href=\"/posts/compliance-verification-dotnet-cli/#wiring-it-into-your-pipeline\" title=\"Wiring It Into Your Pipeline\"\u003eWiring It Into Your Pipeline\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eHere\u0026rsquo;s the GitHub Actions workflow that makes compliance a gate, not a suggestion:\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\"\u003eCompliance Gate\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\"\u003eon\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"l\"\u003epull_request, push]\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\"\u003ejobs\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\"\u003ecompliance\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\"\u003eruns-on\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eubuntu-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    \u003c/span\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\"\u003euses\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eactions/checkout@v6\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@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\"\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=\"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\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eInstall Scanner\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 tool install --global ComplianceScanner\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=\"w\"\u003e      \u003c/span\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\"\u003eScan Code\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\"\u003ecompliance-scanner scan --path .\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=\"w\"\u003e      \u003c/span\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\"\u003eCheck Vulnerabilities  \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 list package --vulnerable --include-transitive\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=\"w\"\u003e      \u003c/span\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\"\u003eValidate Infrastructure\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\"\u003ecompliance-scanner validate-infra\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\"\u003eenv\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\"\u003eAZURE_CLIENT_ID\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003e${{ secrets.AZURE_CLIENT_ID }}\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\"\u003eAZURE_TENANT_ID\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003e${{ secrets.AZURE_TENANT_ID }}\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\u003eMake this a required status check. No exceptions. \u0026ldquo;But we need to ship!\u0026rdquo; is exactly when you need it most.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"reports-for-the-auditors\"\u003e\u003ca href=\"/posts/compliance-verification-dotnet-cli/#reports-for-the-auditors\" title=\"Reports for the Auditors\"\u003eReports for the Auditors\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eGenerate JSON, HTML, or Markdown reports from your scan results. Auditors love artifacts they can file. More importantly, \u003cem\u003eyou\u003c/em\u003e love having evidence when someone asks \u0026ldquo;but did you actually check?\u0026rdquo;\u003c/p\u003e\n\u003cp\u003eThe report generator is straightforward: serialize your violations to the format of choice, timestamp it, and archive it. The structure matters less than the fact that it exists and is generated automatically.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"for-the-auditors-in-the-room\"\u003e\u003ca href=\"/posts/compliance-verification-dotnet-cli/#for-the-auditors-in-the-room\" title=\"For the Auditors in the Room\"\u003eFor the Auditors in the Room\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eYes, this satisfies the standard. Specifically:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eA.5.36 (Regular compliance review):\u003c/strong\u003e Running on every PR and deployment is \u0026ldquo;regular.\u0026rdquo; Quarterly manual checks are not.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eA.8.8 (Vulnerability management):\u003c/strong\u003e Automated scanning that blocks deployment beats \u0026ldquo;we\u0026rsquo;ll check NuGet.org when we remember.\u0026rdquo;\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eA.5.37 (Documented procedures):\u003c/strong\u003e The code \u003cem\u003eis\u003c/em\u003e the documentation. It runs the same way every time. Unlike Bob\u0026rsquo;s interpretation of the checklist.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eA.8.24 (Cryptography):\u003c/strong\u003e Automated validation that encryption is actually enabled, not just assumed to be.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"rolling-this-out-without-a-mutiny\"\u003e\u003ca href=\"/posts/compliance-verification-dotnet-cli/#rolling-this-out-without-a-mutiny\" title=\"Rolling This Out (Without a Mutiny)\"\u003eRolling This Out (Without a Mutiny)\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eDon\u0026rsquo;t try to implement everything at once. That\u0026rsquo;s how you get ignored.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eWeek 1: Secret scanning.\u003c/strong\u003e Start with warnings. Let the team see what\u0026rsquo;s been lurking. Move to failures once the initial panic subsides.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eWeek 2: Vulnerability checks.\u003c/strong\u003e Add \u003ccode\u003edotnet list package --vulnerable\u003c/code\u003e to the pipeline. Fail on CRITICAL. Watch the dependency update PRs roll in.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eWeek 3-4: Auth verification.\u003c/strong\u003e Audit existing endpoints first. You\u0026rsquo;ll find things. Fix them before enforcing.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eWeek 5-6: Infrastructure validation.\u003c/strong\u003e Run manually first. Discover the \u0026ldquo;temporary\u0026rdquo; configs. Remediate. Then automate.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eWeek 7: Reporting.\u003c/strong\u003e Generate artifacts. Distribute to stakeholders. Watch compliance become boring (which is exactly what you want).\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-bottom-line\"\u003e\u003ca href=\"/posts/compliance-verification-dotnet-cli/#the-bottom-line\" title=\"The Bottom Line\"\u003eThe Bottom Line\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eManual compliance is a lie everyone agrees to tell. \u0026ldquo;We checked\u0026rdquo; means \u0026ldquo;someone signed something.\u0026rdquo; It doesn\u0026rsquo;t mean the systems are actually secure.\u003c/p\u003e\n\u003cp\u003eAutomated CLI tools change the equation. They run every time. They check the same things. They generate evidence. They fail the build when something\u0026rsquo;s wrong.\u003c/p\u003e\n\u003cp\u003e.NET gives you \u003ccode\u003eSystem.CommandLine\u003c/code\u003e (finally at GA!), Roslyn, and the Azure SDK. The tooling exists. The patterns are straightforward. The only thing standing between your organization and actual compliance is the decision to stop pretending the Word doc is enough.\u003c/p\u003e\n\u003cp\u003eBuild the scanner. Wire it into the pipeline. Make it a required check.\u003c/p\u003e\n\u003cp\u003eOr keep signing the checklist. Your call.\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2026-04-23T17:00:00+02:00","id":"https://daily-devops.net/posts/compliance-verification-dotnet-cli/","language":"en","summary":"Consultants paid. Docs filed. Then compliance becomes a Word doc ritual until an audit exposes the drift. CLI tools fix what checklists never could.","tags":["iso-standards","cli","dotnet","automation","compliance","security","devops","cicd","azure","codequality"],"title":"Certified, Filed, Forgotten: The Compliance Trainwreck","url":"https://daily-devops.net/posts/compliance-verification-dotnet-cli/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eA developer runs \u003ccode\u003edotnet ef database update\u003c/code\u003e against production. Fifteen minutes later, the database behaves strangely. Three hours into the incident, someone asks the obvious question: \u0026ldquo;Who ran that migration?\u0026rdquo;\u003c/p\u003e\n\u003cp\u003eSilence.\u003c/p\u003e\n\u003cp\u003eThe terminal window closed. The build log expired last week. Nobody remembers. The migration tool printed \u0026ldquo;Success\u0026rdquo; to the console and promptly forgot everything.\u003c/p\u003e\n\u003cp\u003eThis scenario repeats across organizations constantly. Database migrations, deployment scripts, data cleanup tools, configuration utilities: they all share the same blind spot. They execute privileged operations that modify production systems, then vanish without a trace.\u003c/p\u003e\n\u003cp\u003eYour web applications log every user action. Your APIs track every request. But your CLI tools? They are operating in the shadows, modifying databases, deploying code, deleting records. All without leaving evidence of who did what, when they did it, or whether it even succeeded.\u003c/p\u003e\n\u003cp\u003eThat missing accountability will bite you. During incident investigations, when you need to understand what happened. During security reviews, when you need to prove who had access. During compliance assessments, when auditors ask uncomfortable questions about privileged operations.\u003c/p\u003e\n\u003cp\u003eThe fix is straightforward: treat CLI tools like the privileged applications they are. Structured logging. User identity tracking. Correlation IDs. Persistent storage. The same discipline you apply to production web apps.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-consolewriteline-problem\"\u003e\u003ca href=\"/posts/audit-trail-dotnet-cli-tools/#the-consolewriteline-problem\" title=\"The Console.WriteLine Problem\"\u003eThe Console.WriteLine Problem\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eEvery .NET developer has written code 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=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eProgram\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\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\"\u003estatic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e \u003cspan class=\"n\"\u003eMain\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\"\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=\"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;Starting database migration...\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=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eAppDbContext\u003c/span\u003e\u003cspan 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\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eDatabase\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eMigrateAsync\u003c/span\u003e\u003cspan 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;Migration completed 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\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\u003eFunctionally correct. Operationally blind.\u003c/p\u003e\n\u003cp\u003eThree months later, someone asks: \u0026ldquo;Who ran this migration? When exactly? Against which environment?\u0026rdquo; The answers do not exist. Console output died with the terminal session. Build logs expired. PowerShell transcripts only captured interactive sessions, and this ran from a scheduled task.\u003c/p\u003e\n\u003cp\u003eThe pattern repeats everywhere. Deployment tools that \u003ccode\u003eConsole.WriteLine\u003c/code\u003e their progress then forget everything. Admin scripts that run as service accounts, severing any link to the human who triggered them. Data cleanup utilities that delete records (sometimes including the logs that would track such deletions) without leaving a trace.\u003c/p\u003e\n\u003cp\u003eThis is not an edge case. This is the default behavior of virtually every custom CLI tool in enterprise .NET environments. And it creates real problems.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"why-missing-logs-hurt\"\u003e\u003ca href=\"/posts/audit-trail-dotnet-cli-tools/#why-missing-logs-hurt\" title=\"Why Missing Logs Hurt\"\u003eWhy Missing Logs Hurt\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThe problems surface in predictable scenarios.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eIncident investigations\u003c/strong\u003e stall when nobody can determine what changed. \u0026ldquo;Someone ran something last week\u0026rdquo; is not actionable intelligence. Without timestamps, user identities, and operation details, root cause analysis becomes archaeology instead of forensics.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eSecurity reviews\u003c/strong\u003e fail when you cannot demonstrate who has accessed what. Privileged operations require accountability. Service accounts that hide human identity defeat the purpose of access controls.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eCompliance assessments\u003c/strong\u003e get uncomfortable when auditors ask about privileged operation logging and you have to explain that your CLI tools print to console and hope someone is watching. Every security framework, from SOC 2 to ISO 27001 to GDPR, requires logging user actions, timestamps, successes, failures, and affected resources. Console output satisfies none of these requirements.\u003c/p\u003e\n\u003cp\u003eThe common thread: you need evidence. Evidence of who did what, when they did it, and what the outcome was. Evidence that persists beyond terminal sessions and build log retention periods. Evidence you can query, filter, and present.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-fix-structured-logging-with-identity\"\u003e\u003ca href=\"/posts/audit-trail-dotnet-cli-tools/#the-fix-structured-logging-with-identity\" title=\"The Fix: Structured Logging with Identity\"\u003eThe Fix: Structured Logging with Identity\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe solution is not complicated. It requires discipline, not genius. Every CLI tool needs four things: user identity, correlation IDs, structured events, and persistent storage.\u003c/p\u003e\n\u003cp\u003eHere is a practical implementation using \u003ccode\u003eMicrosoft.Extensions.Logging\u003c/code\u003e and Application Insights:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\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\"\u003eAuditedCliTool\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\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\"\u003eAuditedCliTool\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\"\u003eTelemetryClient\u003c/span\u003e \u003cspan class=\"n\"\u003e_telemetry\u003c/span\u003e\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\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003e_correlationId\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=\"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=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003e_userIdentity\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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\"\u003eAuditedCliTool\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\"\u003eAuditedCliTool\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=\"n\"\u003eTelemetryClient\u003c/span\u003e \u003cspan class=\"n\"\u003etelemetry\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan 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_telemetry\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003etelemetry\u003c/span\u003e\u003cspan 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_userIdentity\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eResolveUserIdentity\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eResolveUserIdentity\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \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// Try Windows identity first (corporate environments)\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\"\u003ewindowsIdentity\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eWindowsIdentity\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetCurrent\u003c/span\u003e\u003cspan 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\"\u003ewindowsIdentity\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=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003ewindowsIdentity\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\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// CI/CD pipelines set this via environment variable\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\"\u003eciUser\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eEnvironment\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetEnvironmentVariable\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;AUDIT_USER_PRINCIPAL\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(!\u003c/span\u003e\u003cspan class=\"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\"\u003eciUser\u003c/span\u003e\u003cspan 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\"\u003eciUser\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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// Fallback: machine + username (better than nothing)\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;{Environment.MachineName}\\\\{Environment.UserName}\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\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\"\u003eint\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eExecuteAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eoperation\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\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=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eDictionary\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\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        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e            [\u0026#34;CorrelationId\u0026#34;]\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003e_correlationId\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e            [\u0026#34;User\u0026#34;]\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003e_userIdentity\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e            [\u0026#34;Operation\u0026#34;]\u003c/span\u003e \u003cspan class=\"p\"\u003e=\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=\"na\"\u003e            [\u0026#34;Target\u0026#34;]\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=\"na\"\u003e            [\u0026#34;Machine\u0026#34;]\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eEnvironment\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eMachineName\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"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\"\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=\"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=\"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;OPERATION_START: {User} initiated {Operation} on {Target}\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_userIdentity\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eoperation\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=\"n\"\u003e_telemetry\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTrackEvent\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e$\u0026#34;{operation}Started\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\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003ePerformOperation\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eoperation\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\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=\"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=\"s\"\u003e\u0026#34;Success\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\"\u003eLogWarning\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;OPERATION_COMPLETE: {Operation} succeeded for {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=\"n\"\u003eoperation\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003e_userIdentity\u003c/span\u003e\u003cspan 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_telemetry\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTrackEvent\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e$\u0026#34;{operation}Completed\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\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=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan 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\"\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\"\u003econtext\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=\"s\"\u003e\u0026#34;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=\"n\"\u003econtext\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=\"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=\"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;OPERATION_FAILED: {Operation} failed for {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=\"n\"\u003eoperation\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003e_userIdentity\u003c/span\u003e\u003cspan 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_telemetry\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTrackEvent\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e$\u0026#34;{operation}Failed\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\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\"\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=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe key elements deserve explanation.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eUser identity resolution\u003c/strong\u003e tries multiple strategies because CLI tools run in diverse environments. Windows authentication works on corporate networks. Environment variables work in CI/CD pipelines where you control the execution context. The fallback ensures you always capture something, even if it is just the machine name.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eCorrelation IDs\u003c/strong\u003e tie everything together. When this CLI tool triggers downstream operations (API calls, database queries, message queue publishes), the correlation ID follows. During incident investigation, you can trace the entire operation flow across systems.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eStructured logging with scopes\u003c/strong\u003e attaches context to every log entry. The \u003ccode\u003eBeginScope\u003c/code\u003e call ensures that \u003ccode\u003eCorrelationId\u003c/code\u003e, \u003ccode\u003eUser\u003c/code\u003e, \u003ccode\u003eOperation\u003c/code\u003e, and \u003ccode\u003eTarget\u003c/code\u003e appear on every log line within that block. Log aggregation tools can filter and query these fields.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eDual-channel logging\u003c/strong\u003e sends events to both local logs and Application Insights. If one system fails, you still have records. Application Insights provides real-time querying; local logs provide backup and extended retention.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"cicd-pipelines-the-identity-problem\"\u003e\u003ca href=\"/posts/audit-trail-dotnet-cli-tools/#cicd-pipelines-the-identity-problem\" title=\"CI/CD Pipelines: The Identity Problem\"\u003eCI/CD Pipelines: The Identity Problem\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eCI/CD pipelines add a wrinkle. Your CLI tool runs, but it runs as \u003ccode\u003egithub-actions[bot]\u003c/code\u003e or whatever service account executes the pipeline. The human who triggered the deployment disappears from the record.\u003c/p\u003e\n\u003cp\u003eThe fix is propagating context. GitHub Actions provides \u003ccode\u003egithub.actor\u003c/code\u003e (the human who triggered the workflow), \u003ccode\u003egithub.run_id\u003c/code\u003e (unique identifier for correlation), and \u003ccode\u003egithub.sha\u003c/code\u003e (the exact code version). Pass these to your CLI tools:\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\"\u003eDeploy with audit context\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\"\u003eenv\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\"\u003eAUDIT_USER_PRINCIPAL\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003e${{ github.actor }}@github-actions\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\"\u003eAUDIT_CORRELATION_ID\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003e${{ github.run_id }}-${{ github.run_attempt }}\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=\"p\"\u003e|\u003c/span\u003e\u003cspan class=\"sd\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sd\"\u003e    dotnet run --project DeploymentTool -- \\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sd\"\u003e      --environment Production \\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sd\"\u003e      --correlation-id \u0026#34;${{ env.AUDIT_CORRELATION_ID }}\u0026#34;\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 your CLI tool receives the actual human identity through the environment variable. The correlation ID links Application Insights telemetry back to the specific GitHub Actions run. You can trace from a production incident to the exact workflow execution and the person who triggered it.\u003c/p\u003e\n\u003cp\u003eFor long-term retention, upload logs as artifacts with extended retention periods. GitHub Actions deletes run logs after 90 days by default. Artifacts can persist for years if you configure \u003ccode\u003eretention-days\u003c/code\u003e appropriately.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"querying-your-logs\"\u003e\u003ca href=\"/posts/audit-trail-dotnet-cli-tools/#querying-your-logs\" title=\"Querying Your Logs\"\u003eQuerying Your Logs\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eCollecting logs is pointless if you cannot query them. When someone asks \u0026ldquo;Who ran that migration last Tuesday?\u0026rdquo;, you need answers in minutes, not hours of log archaeology.\u003c/p\u003e\n\u003cp\u003eApplication Insights provides KQL (Kusto Query Language) for this:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003ecustomEvents\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e| where name == \u0026#34;MigrationCompleted\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e| where timestamp \u0026gt; ago(7d)\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e| project timestamp,\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e          user = customDimensions.User,\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e          target = customDimensions.Target,\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e          correlationId = customDimensions.CorrelationId\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e| order by timestamp desc\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis query finds all migrations in the last week, showing who ran them and what they targeted. The correlation ID lets you trace related operations across systems.\u003c/p\u003e\n\u003cp\u003eFor incident investigation, start with the correlation ID and expand outward:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003elet targetCorrelation = \u0026#34;abc-123-def\u0026#34;;\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eunion traces, customEvents, exceptions\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e| where customDimensions.CorrelationId == targetCorrelation\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e| order by timestamp asc\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis reconstructs the complete operation timeline: what started, what succeeded, what failed, and in what order. Forensics, not archaeology.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"protecting-your-logs\"\u003e\u003ca href=\"/posts/audit-trail-dotnet-cli-tools/#protecting-your-logs\" title=\"Protecting Your Logs\"\u003eProtecting Your Logs\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eLogs that can be modified or deleted are not evidence. They are suggestions. For logs to have value during investigations or compliance reviews, they need protection.\u003c/p\u003e\n\u003cp\u003eApplication Insights provides built-in immutability: you cannot modify or delete individual events. Only entire workspaces can be purged, and only after retention periods expire. For most scenarios, this is sufficient.\u003c/p\u003e\n\u003cp\u003eIf you need stronger guarantees, export to Azure Blob Storage with immutability policies enabled. Once uploaded, even administrators cannot modify or delete the files until the retention period expires. Seven years is typical for regulatory requirements.\u003c/p\u003e\n\u003cp\u003eFor the truly paranoid, cryptographic signatures provide tamper evidence. Hash the log export, sign the hash, store the signature separately. Any modification invalidates the signature, proving tampering occurred. This is overkill for most organizations, but some regulated industries require it.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-checklist\"\u003e\u003ca href=\"/posts/audit-trail-dotnet-cli-tools/#the-checklist\" title=\"The Checklist\"\u003eThe Checklist\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eImplementing this is straightforward once you commit to the discipline:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eEvery CLI tool needs:\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eStructured logging via \u003ccode\u003eMicrosoft.Extensions.Logging\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003eUser identity resolution (Windows, environment variable, fallback)\u003c/li\u003e\n\u003cli\u003eCorrelation IDs for cross-system tracing\u003c/li\u003e\n\u003cli\u003eStart, success, and failure events logged explicitly\u003c/li\u003e\n\u003cli\u003eSensitive data sanitized before logging\u003c/li\u003e\n\u003cli\u003eCentralized log storage (Application Insights, Elasticsearch, whatever you use)\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003eCI/CD pipelines need:\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eHuman identity propagated through environment variables\u003c/li\u003e\n\u003cli\u003eWorkflow run IDs used as correlation IDs\u003c/li\u003e\n\u003cli\u003eLog artifacts uploaded with appropriate retention periods\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003eLog storage needs:\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eRetention periods matching your compliance requirements\u003c/li\u003e\n\u003cli\u003eImmutability policies preventing modification\u003c/li\u003e\n\u003cli\u003eAccess controls limiting who can read sensitive logs\u003c/li\u003e\n\u003c/ul\u003e\n\n\n\n\n\u003ch2 id=\"the-real-payoff\"\u003e\u003ca href=\"/posts/audit-trail-dotnet-cli-tools/#the-real-payoff\" title=\"The Real Payoff\"\u003eThe Real Payoff\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eCompliance requirements drove this article, but compliance is not the real reason to implement proper CLI logging. The real reason is operational sanity.\u003c/p\u003e\n\u003cp\u003eI have investigated production incidents where the only evidence was \u0026ldquo;someone ran something last week.\u0026rdquo; Hours wasted on archaeology when forensics would have taken minutes. Proper logs with user identities, timestamps, correlation IDs, and outcomes transform incident investigation from guesswork into reconstruction.\u003c/p\u003e\n\u003cp\u003eSecurity incident response benefits identically. When your SIEM alerts on suspicious database activity, audit trails immediately answer whether this was a legitimate admin operation or an actual breach. Fast answers depend on complete records.\u003c/p\u003e\n\u003cp\u003eCompliance gives you the justification. Operational excellence is the payoff.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"conclusion\"\u003e\u003ca href=\"/posts/audit-trail-dotnet-cli-tools/#conclusion\" title=\"Conclusion\"\u003eConclusion\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eYour CLI tools are lying to you. They run, they print success, they forget everything.\u003c/p\u003e\n\u003cp\u003eThat amnesia creates real problems during incidents, security reviews, and compliance assessments. The fix is not complicated: structured logging, user identity tracking, correlation IDs, persistent storage. The same discipline you apply to production web applications.\u003c/p\u003e\n\u003cp\u003eThe .NET ecosystem provides everything you need. \u003ccode\u003eMicrosoft.Extensions.Logging\u003c/code\u003e handles structured logging. Application Insights provides centralized storage and querying. GitHub Actions context variables enable identity propagation in CI/CD pipelines.\u003c/p\u003e\n\u003cp\u003eBuild this into your CLI tools from day one. Not as an afterthought when someone asks uncomfortable questions. As a fundamental requirement for any tool that touches production systems.\u003c/p\u003e\n\u003cp\u003eFuture you, investigating a 2 AM incident, will appreciate the evidence.\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2026-04-21T17:00:00+02:00","id":"https://daily-devops.net/posts/audit-trail-dotnet-cli-tools/","language":"en","summary":"dotnet ef database update prints Success and forgets. Add structured logging, user identity, and correlation IDs so privileged CLI runs leave evidence.","tags":["iso-standards","security","cli","dotnet","logging"],"title":"Who Ran That Migration? Audit Trails for .NET CLI Tools","url":"https://daily-devops.net/posts/audit-trail-dotnet-cli-tools/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eYour password reset endpoint probably returns the user\u0026rsquo;s full profile, purchase history, and marketing preferences. The caller asked for an email address. You gave them everything.\u003c/p\u003e\n\u003cp\u003eThat\u0026rsquo;s purpose drift: exposing data beyond what the caller needs for its stated function. GDPR Article 5(1)(b) and ISO/IEC 27701 both require that personal data be processed only for the purpose it was collected. This isn\u0026rsquo;t a compliance checkbox. It\u0026rsquo;s an architectural constraint that determines how you structure API endpoints, implement authorization policies, and design response payloads.\u003c/p\u003e\n\u003cp\u003eMost .NET applications fail this test because they design APIs around database entities rather than caller purposes. The typical \u003ccode\u003eGetUser\u003c/code\u003e endpoint returns everything the database contains, regardless of why the caller requested it.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-fatal-pattern-entity-centric-api-design\"\u003e\u003ca href=\"/posts/purpose-limitation-api-design/#the-fatal-pattern-entity-centric-api-design\" title=\"The Fatal Pattern: Entity-Centric API Design\"\u003eThe Fatal Pattern: Entity-Centric API Design\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe typical ASP.NET Core API exposes personal data through broad entity endpoints:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e[HttpGet(\u0026#34;{id}\u0026#34;)]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e[Authorize]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eActionResult\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eUserDto\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eGetUser\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eid\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003euser\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_context\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUsers\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eInclude\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eu\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eu\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eProfile\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eInclude\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eu\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eu\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddresses\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eInclude\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eu\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eu\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePaymentMethods\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eInclude\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eu\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eu\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eOrderHistory\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFirstOrDefaultAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eu\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eu\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eId\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"n\"\u003eid\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003euser\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eNotFound\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Any authenticated user can retrieve full data for any user ID\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eOk\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eMapToDto\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003euser\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis pattern violates purpose limitation in multiple ways:\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"three-ways-entity-endpoints-leak-data\"\u003e\u003ca href=\"/posts/purpose-limitation-api-design/#three-ways-entity-endpoints-leak-data\" title=\"Three Ways Entity Endpoints Leak Data\"\u003eThree Ways Entity Endpoints Leak Data\u003c/a\u003e\u003c/h3\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eOver-exposure\u003c/strong\u003e: A dashboard widget showing \u0026ldquo;Welcome, John\u0026rdquo; receives the same payload as an order fulfillment system processing a shipment. The caller\u0026rsquo;s purpose doesn\u0026rsquo;t affect what data they receive.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eNo access controls\u003c/strong\u003e: Any authenticated user can retrieve full profile data for any user ID by incrementing the endpoint parameter. Authentication proves identity, but nothing validates purpose.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eInvisible data flows\u003c/strong\u003e: The API documentation describes endpoints but doesn\u0026rsquo;t specify why each field is returned. Developers consuming the API cannot determine whether their intended use complies with the original collection purpose.\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eFrom a .NET implementation perspective, this design treats \u003ccode\u003eUser\u003c/code\u003e as a data structure rather than a protected resource with context-dependent visibility.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"purpose-specific-endpoint-design\"\u003e\u003ca href=\"/posts/purpose-limitation-api-design/#purpose-specific-endpoint-design\" title=\"Purpose-Specific Endpoint Design\"\u003ePurpose-Specific Endpoint Design\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eEffective purpose limitation requires restructuring APIs around caller purposes rather than database entities:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e[ApiController]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e[Route(\u0026#34;api/users\u0026#34;)]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eUserProfileController\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eControllerBase\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eIAuthorizationService\u003c/span\u003e \u003cspan class=\"n\"\u003e_authorizationService\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eIUserService\u003c/span\u003e \u003cspan class=\"n\"\u003e_userService\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Purpose: Display user greeting in application header\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Data Minimization: First name only\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [HttpGet(\u0026#34;{id}/greeting\u0026#34;)]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [Authorize]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eActionResult\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eUserGreetingDto\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eGetUserGreeting\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eid\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003euser\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_userService\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetUserByIdAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eid\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003euser\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eNotFound\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eauthResult\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_authorizationService\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAuthorizeAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eUser\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003euser\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;ViewOwnProfile\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(!\u003c/span\u003e\u003cspan class=\"n\"\u003eauthResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSucceeded\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eForbid\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eOk\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eUserGreetingDto\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"n\"\u003eFirstName\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003euser\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFirstName\u003c/span\u003e \u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Purpose: Account settings management\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Data Minimization: Profile fields only, excludes order/payment history\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [HttpGet(\u0026#34;{id}/profile-settings\u0026#34;)]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [Authorize]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eActionResult\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eProfileSettingsDto\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eGetProfileSettings\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eid\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003euser\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_userService\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetUserByIdAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eid\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003euser\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eNotFound\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eauthResult\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_authorizationService\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAuthorizeAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eUser\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003euser\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;ManageOwnProfile\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(!\u003c/span\u003e\u003cspan class=\"n\"\u003eauthResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSucceeded\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eForbid\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eOk\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eProfileSettingsDto\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eEmail\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003euser\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eEmail\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eFirstName\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003euser\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFirstName\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eLastName\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003euser\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLastName\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003ePhoneNumber\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003euser\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePhoneNumber\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe key difference: each endpoint documents its purpose and returns only the fields needed for that purpose. The greeting endpoint returns a first name. The profile settings endpoint returns contact information. Neither returns payment methods or order history because those fields serve different purposes.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"resource-based-authorization-for-field-level-control\"\u003e\u003ca href=\"/posts/purpose-limitation-api-design/#resource-based-authorization-for-field-level-control\" title=\"Resource-Based Authorization for Field-Level Control\"\u003eResource-Based Authorization for Field-Level Control\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eASP.NET Core\u0026rsquo;s resource-based authorization enables purpose-aware access control by evaluating both the user\u0026rsquo;s identity and the specific resource being accessed:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Authorization requirement that validates purpose context\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003ePurposeLimitationRequirement\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eIAuthorizationRequirement\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eDataProcessingPurpose\u003c/span\u003e \u003cspan class=\"n\"\u003ePurpose\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"k\"\u003eget\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003ePurposeLimitationRequirement\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eDataProcessingPurpose\u003c/span\u003e \u003cspan class=\"n\"\u003epurpose\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003ePurpose\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003epurpose\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003eenum\u003c/span\u003e \u003cspan class=\"n\"\u003eDataProcessingPurpose\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eAccountManagement\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eOrderFulfillment\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eCustomerSupport\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eMarketingAnalytics\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eSecurityMonitoring\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Authorization handler that checks consent for purpose\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003ePurposeLimitationHandler\u003c/span\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eAuthorizationHandler\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003ePurposeLimitationRequirement\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eUser\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eIConsentService\u003c/span\u003e \u003cspan class=\"n\"\u003e_consentService\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003ePurposeLimitationHandler\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eIConsentService\u003c/span\u003e \u003cspan class=\"n\"\u003econsentService\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003e_consentService\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003econsentService\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprotected\u003c/span\u003e \u003cspan class=\"kd\"\u003eoverride\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e \u003cspan class=\"n\"\u003eHandleRequirementAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eAuthorizationHandlerContext\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003ePurposeLimitationRequirement\u003c/span\u003e \u003cspan class=\"n\"\u003erequirement\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eUser\u003c/span\u003e \u003cspan class=\"n\"\u003eresource\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003euserIdClaim\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUser\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFindFirst\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eClaimTypes\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eNameIdentifier\u003c/span\u003e\u003cspan class=\"p\"\u003e)?.\u003c/span\u003e\u003cspan class=\"n\"\u003eValue\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003euserIdClaim\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e \u003cspan class=\"p\"\u003e||\u003c/span\u003e \u003cspan class=\"p\"\u003e!\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTryParse\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003euserIdClaim\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"k\"\u003eout\u003c/span\u003e \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003euserId\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"c1\"\u003e// Invalid or missing user identifier - deny access\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// Users can always access their own data for account management\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003erequirement\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePurpose\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"n\"\u003eDataProcessingPurpose\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAccountManagement\u003c/span\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e \u003cspan class=\"n\"\u003euserId\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"n\"\u003eresource\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eId\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSucceed\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003erequirement\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// For other purposes, verify explicit consent\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003ehasConsent\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_consentService\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eHasConsentAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eresource\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eId\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eConsentPurposeFromDataProcessingPurpose\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003erequirement\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePurpose\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ehasConsent\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSucceed\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003erequirement\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"n\"\u003eConsentPurpose\u003c/span\u003e \u003cspan class=\"n\"\u003eConsentPurposeFromDataProcessingPurpose\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eDataProcessingPurpose\u003c/span\u003e \u003cspan class=\"n\"\u003epurpose\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003epurpose\u003c/span\u003e \u003cspan class=\"k\"\u003eswitch\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eDataProcessingPurpose\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eMarketingAnalytics\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eConsentPurpose\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eMarketingAnalytics\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eDataProcessingPurpose\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCustomerSupport\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eConsentPurpose\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCustomerSupport\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eDataProcessingPurpose\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSecurityMonitoring\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eConsentPurpose\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSecurityMonitoring\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003e_\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"k\"\u003ethrow\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eInvalidOperationException\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e$\u0026#34;No consent mapping for {purpose}\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e};\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Registration in Program.cs\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eServices\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddScoped\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eIAuthorizationHandler\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ePurposeLimitationHandler\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eServices\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddAuthorization\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eoptions\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eoptions\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddPolicy\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;AccountManagement\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003epolicy\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003epolicy\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRequirements\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAdd\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003ePurposeLimitationRequirement\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eDataProcessingPurpose\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAccountManagement\u003c/span\u003e\u003cspan class=\"p\"\u003e)));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eoptions\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddPolicy\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;MarketingAnalytics\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003epolicy\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003epolicy\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRequirements\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAdd\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003ePurposeLimitationRequirement\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eDataProcessingPurpose\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eMarketingAnalytics\u003c/span\u003e\u003cspan class=\"p\"\u003e)));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis authorization handler enforces purpose limitation at the framework level. An endpoint attempting to access user data for marketing analytics will be denied unless the user explicitly consented to that purpose—even if the same data would be accessible for account management purposes.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"field-level-authorization-with-graphql\"\u003e\u003ca href=\"/posts/purpose-limitation-api-design/#field-level-authorization-with-graphql\" title=\"Field-Level Authorization with GraphQL\"\u003eField-Level Authorization with GraphQL\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eGraphQL APIs pose unique purpose limitation challenges because clients construct queries dynamically, requesting arbitrary field combinations. A query for \u003ccode\u003e{ user { email } }\u003c/code\u003e serves a different purpose than \u003ccode\u003e{ user { email orderHistory { items total } } }\u003c/code\u003e, but traditional authorization sees both as \u0026ldquo;get user\u0026rdquo; requests.\u003c/p\u003e\n\u003cp\u003eHotChocolate, the most comprehensive .NET GraphQL server, supports field-level authorization that enforces purpose limitation for each requested field:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eUserType\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eObjectType\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eUser\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprotected\u003c/span\u003e \u003cspan class=\"kd\"\u003eoverride\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eConfigure\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eIObjectTypeDescriptor\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eUser\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003edescriptor\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003edescriptor\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eField\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eu\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eu\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eId\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAuthorize\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e \u003cspan class=\"c1\"\u003e// Basic authentication required\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003edescriptor\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eField\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eu\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eu\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eEmail\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAuthorize\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;AccountManagement\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e \u003cspan class=\"c1\"\u003e// Purpose: account functionality\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003edescriptor\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eField\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eu\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eu\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFirstName\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAuthorize\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;AccountManagement\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003edescriptor\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eField\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eu\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eu\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eBirthDate\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAuthorize\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;MarketingAnalytics\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e \u003cspan class=\"c1\"\u003e// Purpose: demographic analysis\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003edescriptor\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eField\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eu\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eu\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eOrderHistory\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAuthorize\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;OrderFulfillment\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e \u003cspan class=\"c1\"\u003e// Purpose: order processing\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003edescriptor\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eField\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eu\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eu\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePaymentMethods\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAuthorize\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;PaymentProcessing\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e \u003cspan class=\"c1\"\u003e// Purpose: payment handling\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// GraphQL query with field-level validation\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Query: { user(id: 123) { email birthDate orderHistory { total } } }\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Result: Returns email (account management), denies birthDate (no marketing consent),\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e//         denies orderHistory (no order fulfillment permission)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"s\"\u003e\u0026#34;data\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"s\"\u003e\u0026#34;user\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"s\"\u003e\u0026#34;email\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;john@example.com\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"s\"\u003e\u0026#34;birthDate\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e  \u003cspan class=\"c1\"\u003e// Field denied - insufficient consent\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"s\"\u003e\u0026#34;orderHistory\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e  \u003cspan class=\"c1\"\u003e// Field denied - insufficient permission\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"p\"\u003e},\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"s\"\u003e\u0026#34;errors\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"s\"\u003e\u0026#34;message\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;The current user is not authorized to access this resource.\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"s\"\u003e\u0026#34;path\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;user\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;birthDate\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e},\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"s\"\u003e\u0026#34;message\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;The current user is not authorized to access this resource.\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"s\"\u003e\u0026#34;path\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;user\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;orderHistory\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"p\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\n\n\n\u003ch3 id=\"how-field-denials-surface-to-clients\"\u003e\u003ca href=\"/posts/purpose-limitation-api-design/#how-field-denials-surface-to-clients\" title=\"How Field Denials Surface To Clients\"\u003eHow Field Denials Surface To Clients\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eEach field specifies the processing purpose required to access it. A user query for demographic analysis receives \u003ccode\u003ebirthDate\u003c/code\u003e only if marketing analytics consent exists. An order processing query receives \u003ccode\u003eorderHistory\u003c/code\u003e only if the caller has order fulfillment permissions. The same user resource returns different field sets depending on the caller\u0026rsquo;s stated purpose.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"consent-aware-middleware-for-audit-context\"\u003e\u003ca href=\"/posts/purpose-limitation-api-design/#consent-aware-middleware-for-audit-context\" title=\"Consent-Aware Middleware for Audit Context\"\u003eConsent-Aware Middleware for Audit Context\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003ePurpose limitation requires not just access control but audit trails documenting why data was accessed. Custom middleware captures the processing purpose for every request:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003ePurposeAuditMiddleware\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eRequestDelegate\u003c/span\u003e \u003cspan class=\"n\"\u003e_next\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eIAuditLogger\u003c/span\u003e \u003cspan class=\"n\"\u003e_auditLogger\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003ePurposeAuditMiddleware\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eRequestDelegate\u003c/span\u003e \u003cspan class=\"n\"\u003enext\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eIAuditLogger\u003c/span\u003e \u003cspan class=\"n\"\u003eauditLogger\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003e_next\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003enext\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003e_auditLogger\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eauditLogger\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e \u003cspan class=\"n\"\u003eInvokeAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eHttpContext\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// Extract purpose from route or header\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// Header approach allows clients to explicitly declare purpose\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// Route-based fallback provides default behavior\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003epurpose\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRequest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eHeaders\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;X-Processing-Purpose\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e].\u003c/span\u003e\u003cspan class=\"n\"\u003eFirstOrDefault\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e??\u003c/span\u003e \u003cspan class=\"n\"\u003eDeterminePurposeFromRoute\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRequest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePath\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// Store in HttpContext for downstream authorization\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eItems\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;ProcessingPurpose\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003epurpose\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// Capture audit data before request\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eauditEntry\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eDataAccessAuditEntry\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eTimestamp\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eDateTime\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUtcNow\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eUserId\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUser\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFindFirst\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eClaimTypes\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eNameIdentifier\u003c/span\u003e\u003cspan class=\"p\"\u003e)?.\u003c/span\u003e\u003cspan class=\"n\"\u003eValue\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eTargetUserId\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eExtractTargetUserIdFromPath\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRequest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePath\u003c/span\u003e\u003cspan class=\"p\"\u003e),\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eEndpoint\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRequest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePath\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003ePurpose\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003epurpose\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eIpAddress\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eConnection\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRemoteIpAddress\u003c/span\u003e\u003cspan class=\"p\"\u003e?.\u003c/span\u003e\u003cspan class=\"n\"\u003eToString\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e};\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_next\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// Log completed request with purpose context\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eauditEntry\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eStatusCode\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eResponse\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eStatusCode\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_auditLogger\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLogAccessAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eauditEntry\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eDeterminePurposeFromRoute\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ePathString\u003c/span\u003e \u003cspan class=\"n\"\u003epath\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003epath\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eStartsWithSegments\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;/api/users/greeting\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;AccountManagement\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003epath\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eStartsWithSegments\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;/api/users/shipping-addresses\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;OrderFulfillment\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003epath\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eStartsWithSegments\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;/api/analytics\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;MarketingAnalytics\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Unspecified\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eExtractTargetUserIdFromPath\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ePathString\u003c/span\u003e \u003cspan class=\"n\"\u003epath\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// Extract user ID from path like /api/users/123/greeting\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003esegments\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003epath\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eValue\u003c/span\u003e\u003cspan class=\"p\"\u003e?.\u003c/span\u003e\u003cspan class=\"n\"\u003eSplit\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"sc\"\u003e\u0026#39;/\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003esegments\u003c/span\u003e \u003cspan class=\"p\"\u003e!=\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e \u003cspan class=\"n\"\u003esegments\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLength\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"m\"\u003e3\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e \u003cspan class=\"n\"\u003esegments\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"m\"\u003e2\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;users\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003esegments\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"m\"\u003e3\u003c/span\u003e\u003cspan class=\"p\"\u003e];\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Registration in Program.cs\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eapp\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUseMiddleware\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003ePurposeAuditMiddleware\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\n\n\n\u003ch3 id=\"satisfying-article-15-access-requests\"\u003e\u003ca href=\"/posts/purpose-limitation-api-design/#satisfying-article-15-access-requests\" title=\"Satisfying Article 15 Access Requests\"\u003eSatisfying Article 15 Access Requests\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eEvery personal data access now includes the processing purpose in audit logs. When a GDPR Article 15 (Right of access) request arrives, you can generate a complete report showing every access to that user\u0026rsquo;s data, the stated purpose for each access, and whether those purposes align with original collection consent.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"monitoring-purpose-violations-with-health-checks\"\u003e\u003ca href=\"/posts/purpose-limitation-api-design/#monitoring-purpose-violations-with-health-checks\" title=\"Monitoring Purpose Violations with Health Checks\"\u003eMonitoring Purpose Violations with Health Checks\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eASP.NET Core health checks can detect purpose drift by monitoring unauthorized access patterns:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003ePurposeLimitationHealthCheck\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eIHealthCheck\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eIAuditLogRepository\u003c/span\u003e \u003cspan class=\"n\"\u003e_auditRepository\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eIConsentRepository\u003c/span\u003e \u003cspan class=\"n\"\u003e_consentRepository\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003ePurposeLimitationHealthCheck\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eIAuditLogRepository\u003c/span\u003e \u003cspan class=\"n\"\u003eauditRepository\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eIConsentRepository\u003c/span\u003e \u003cspan class=\"n\"\u003econsentRepository\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003e_auditRepository\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eauditRepository\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003e_consentRepository\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003econsentRepository\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eHealthCheckResult\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eCheckHealthAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eHealthCheckContext\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eCancellationToken\u003c/span\u003e \u003cspan class=\"n\"\u003ecancellationToken\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003edefault\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003elast24Hours\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eDateTime\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUtcNow\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddHours\u003c/span\u003e\u003cspan class=\"p\"\u003e(-\u003c/span\u003e\u003cspan class=\"m\"\u003e24\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// Find accesses where purpose doesn\u0026#39;t match consent\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eviolations\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_auditRepository\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetAccessesAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003esince\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003elast24Hours\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003efilter\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eentry\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eentry\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eStatusCode\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"m\"\u003e200\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e \u003cspan class=\"c1\"\u003e// Successful accesses\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003epurposeViolations\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eList\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eforeach\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eaccess\u003c/span\u003e \u003cspan class=\"k\"\u003ein\u003c/span\u003e \u003cspan class=\"n\"\u003eviolations\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eaccess\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePurpose\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Unspecified\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"n\"\u003epurposeViolations\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAdd\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                    \u003cspan class=\"s\"\u003e$\u0026#34;Unspecified purpose: {access.Endpoint} by {access.UserId}\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"k\"\u003econtinue\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003econsent\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_consentRepository\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetConsentAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"n\"\u003eaccess\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTargetUserId\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"n\"\u003eConsentPurposeFromString\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eaccess\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePurpose\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003econsent\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e \u003cspan class=\"p\"\u003e||\u003c/span\u003e \u003cspan class=\"p\"\u003e!\u003c/span\u003e\u003cspan class=\"n\"\u003econsent\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eIsActive\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"n\"\u003epurposeViolations\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAdd\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                    \u003cspan class=\"s\"\u003e$\u0026#34;No consent for {access.Purpose}: User {access.TargetUserId}, \u0026#34;\u003c/span\u003e \u003cspan class=\"p\"\u003e+\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                    \u003cspan class=\"s\"\u003e$\u0026#34;Endpoint {access.Endpoint}\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003epurposeViolations\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCount\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"m\"\u003e10\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eHealthCheckResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUnhealthy\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"s\"\u003e$\u0026#34;Detected {purposeViolations.Count} purpose limitation violations in last 24h\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eDictionary\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003eobject\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e                    [\u0026#34;ViolationCount\u0026#34;]\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003epurposeViolations\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCount\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e                    [\u0026#34;Violations\u0026#34;]\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003epurposeViolations\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003epurposeViolations\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAny\u003c/span\u003e\u003cspan class=\"p\"\u003e())\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eHealthCheckResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eDegraded\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"s\"\u003e$\u0026#34;Detected {purposeViolations.Count} potential violations\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eDictionary\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003eobject\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e                    [\u0026#34;Violations\u0026#34;]\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003epurposeViolations\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eHealthCheckResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eHealthy\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;No purpose limitation violations detected\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Registration in Program.cs\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eServices\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddHealthChecks\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddCheck\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003ePurposeLimitationHealthCheck\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"s\"\u003e\u0026#34;purpose-limitation\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003efailureStatus\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eHealthStatus\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eDegraded\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003etags\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e\u003cspan class=\"p\"\u003e[]\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;privacy\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;compliance\u0026#34;\u003c/span\u003e \u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\n\n\n\u003ch3 id=\"catching-drift-before-auditors-do\"\u003e\u003ca href=\"/posts/purpose-limitation-api-design/#catching-drift-before-auditors-do\" title=\"Catching Drift Before Auditors Do\"\u003eCatching Drift Before Auditors Do\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThis health check analyzes audit logs to identify data accesses without corresponding consent. If marketing analytics endpoints are being called for users who never consented to analytics, the health check fails, triggering alerts before a regulatory audit discovers the violation.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"practical-implementation-the-purpose-first-api-design-checklist\"\u003e\u003ca href=\"/posts/purpose-limitation-api-design/#practical-implementation-the-purpose-first-api-design-checklist\" title=\"Practical Implementation: The Purpose-First API Design Checklist\"\u003ePractical Implementation: The Purpose-First API Design Checklist\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003ePurpose limitation transforms from abstract compliance requirement to concrete engineering practice through systematic API design:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eBefore creating an endpoint:\u003c/strong\u003e\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eDocument the specific processing purpose this endpoint serves\u003c/li\u003e\n\u003cli\u003eIdentify the minimum data fields required for that purpose\u003c/li\u003e\n\u003cli\u003eDetermine the legal basis (contract, legitimate interest, consent)\u003c/li\u003e\n\u003cli\u003eIf consent-based, verify consent validation logic exists\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e\u003cstrong\u003eDuring implementation:\u003c/strong\u003e\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eCreate purpose-specific DTOs that expose only required fields\u003c/li\u003e\n\u003cli\u003eImplement resource-based authorization validating purpose permission\u003c/li\u003e\n\u003cli\u003eAdd audit logging capturing the processing purpose\u003c/li\u003e\n\u003cli\u003eDocument the purpose and legal basis in API specifications\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e\u003cstrong\u003eAfter deployment:\u003c/strong\u003e\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eMonitor access logs for purpose/consent mismatches\u003c/li\u003e\n\u003cli\u003eReview authorization failures for legitimate use cases requiring consent\u003c/li\u003e\n\u003cli\u003eAudit endpoint usage patterns against original purpose documentation\u003c/li\u003e\n\u003cli\u003eUpdate consent capture flows if new purposes are introduced\u003c/li\u003e\n\u003c/ol\u003e\n\n\n\n\n\u003ch2 id=\"beyond-compliance-purpose-as-architecture\"\u003e\u003ca href=\"/posts/purpose-limitation-api-design/#beyond-compliance-purpose-as-architecture\" title=\"Beyond Compliance: Purpose as Architecture\"\u003eBeyond Compliance: Purpose as Architecture\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eISO/IEC 27701 Control 7.2.6 (Limiting use, retention and disclosure) and GDPR Article 25 (Data protection by design) require purpose limitation not as a policy but as an architectural property. ASP.NET Core\u0026rsquo;s authorization framework, when applied to resources rather than just controllers, makes purpose-aware access control a natural extension of existing security patterns.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"identity-verification-is-not-purpose-validation\"\u003e\u003ca href=\"/posts/purpose-limitation-api-design/#identity-verification-is-not-purpose-validation\" title=\"Identity Verification Is Not Purpose Validation\"\u003eIdentity Verification Is Not Purpose Validation\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThe fatal mistake is treating all personal data as uniformly accessible once a user authenticates. The correct approach recognizes that data access requires both identity verification and purpose validation. A user proving who they are establishes the first authorization layer. The system determining why they need specific data establishes the second layer, the one that regulations actually mandate.\u003c/p\u003e\n\u003cp\u003ePurpose limitation is not an add-on feature. It\u0026rsquo;s the recognition that every API endpoint represents a contract: the caller states a purpose, and the API returns only data necessary to fulfill that purpose. When your endpoints leak more data than required, you\u0026rsquo;re not just violating regulatory standards. You\u0026rsquo;re violating the fundamental contract between data subjects and the systems that process their information.\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2026-04-16T17:00:00+02:00","id":"https://daily-devops.net/posts/purpose-limitation-api-design/","language":"en","summary":"Why your API returns too much personal data and how ASP.NET Core resource-based authorization enforces data minimization at the endpoint level.","tags":["iso-standards","privacy","gdpr","dotnet","softwareengineering"],"title":"Purpose Limitation in API Design: Leaking Data You Shouldn't\n","url":"https://daily-devops.net/posts/purpose-limitation-api-design/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\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\u003e\u0026ldquo;We just got a GDPR erasure request,\u0026rdquo; your product manager says casually on Monday morning. \u0026ldquo;Should be quick, right? Just delete the user?\u0026rdquo;\u003c/p\u003e\n\u003cp\u003eThree weeks later, you\u0026rsquo;ve discovered that user\u0026rsquo;s data lives in seventeen different places. Production database, analytics warehouse, blob storage, Redis cache, Elasticsearch indexes, backup tapes, third-party CRM, email provider archives, and that legacy system nobody wants to touch. Deleting the account breaks referential integrity in six tables, crashing the order pipeline.\u003c/p\u003e\n\u003cp\u003eWelcome to \u003cstrong\u003eISO/IEC 27701 Control 7.3.4\u003c/strong\u003e and \u003cstrong\u003eGDPR Article 17\u003c/strong\u003e. They don\u0026rsquo;t ask you to delete data. They require orchestrated erasure across distributed systems, audit trails, third-party notifications, preserved referential integrity, and proof it all worked. All while keeping your application running.\u003c/p\u003e\n\u003cp\u003eThis is where most privacy implementations die.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-fatal-pattern-database-scripts-and-hope\"\u003e\u003ca href=\"/posts/right-to-erasure-implementation-patterns/#the-fatal-pattern-database-scripts-and-hope\" title=\"The Fatal Pattern: Database Scripts and Hope\"\u003eThe Fatal Pattern: Database Scripts and Hope\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eHere\u0026rsquo;s what I\u0026rsquo;ve seen in production far too often:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\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: The \u0026#34;just delete it\u0026#34; 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=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e \u003cspan class=\"n\"\u003eDeleteUserAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eGuid\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\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003euser\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_context\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUsers\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFindAsync\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\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003euser\u003c/span\u003e \u003cspan class=\"p\"\u003e!=\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003e_context\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUsers\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRemove\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003euser\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_context\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSaveChangesAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \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 still in: Redis, blob storage, Elasticsearch, analytics DB,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Mailchimp, Salesforce, 7-year backups, JSON logs with PII...\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// No audit trail. No verification. Hope it worked.\u003c/span\u003e\n\u003c/span\u003e\u003c/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\u003eWhy this fails spectacularly:\u003c/strong\u003e\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003eReferential Integrity Violations\u003c/strong\u003e: Foreign key constraints crash your app\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eIncomplete Deletion\u003c/strong\u003e: Data persists in caches, logs, backups for years\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eNo Third-Party Notification\u003c/strong\u003e: GDPR Article 19 requires it. This doesn\u0026rsquo;t.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eNo Audit Trail\u003c/strong\u003e: Can\u0026rsquo;t prove to regulators what happened\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eBackup Problem\u003c/strong\u003e: Restoring backups resurrects \u0026ldquo;forgotten\u0026rdquo; users\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eOne company I consulted for discovered a 40% failure rate on erasure requests. Half the \u0026ldquo;deleted\u0026rdquo; users were still fully present in their analytics warehouse. The regulator audit was\u0026hellip; educational.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"understanding-the-requirements\"\u003e\u003ca href=\"/posts/right-to-erasure-implementation-patterns/#understanding-the-requirements\" title=\"Understanding the Requirements\"\u003eUnderstanding the Requirements\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eLet\u0026rsquo;s cut through the legal jargon:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eISO 27701 Control 7.3.4\u003c/strong\u003e requires mechanisms for data subjects to withdraw consent, request erasure, get it done \u0026ldquo;within reasonable timeframes,\u0026rdquo; and receive confirmation.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eGDPR Article 17\u003c/strong\u003e goes further: erasure \u0026ldquo;without undue delay,\u0026rdquo; mandatory processor notification (Article 19), documented exceptions for legal obligations. And here\u0026rsquo;s the kicker: you must demonstrate compliance to supervisory authorities.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eISO 27701 Control 7.4.8\u003c/strong\u003e (Disposal) adds secure disposal methods, verification that disposal is complete and irreversible, plus documentation.\u003c/p\u003e\n\u003cp\u003eTranslation: orchestrated, verifiable, audited deletion across every system that touches personal data. Not a database DELETE statement.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-correct-pattern-orchestrated-erasure\"\u003e\u003ca href=\"/posts/right-to-erasure-implementation-patterns/#the-correct-pattern-orchestrated-erasure\" title=\"The Correct Pattern: Orchestrated Erasure\"\u003eThe Correct Pattern: Orchestrated Erasure\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eHere\u0026rsquo;s what actually works. Using \u003cstrong\u003eAzure Durable Functions\u003c/strong\u003e for orchestration, soft-delete for referential integrity, and distributed coordination:\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"1-erasure-request-model\"\u003e\u003ca href=\"/posts/right-to-erasure-implementation-patterns/#1-erasure-request-model\" title=\"1. Erasure Request Model\"\u003e1. Erasure Request Model\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\"\u003eErasureRequest\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\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\"\u003eGuid\u003c/span\u003e \u003cspan class=\"n\"\u003eRequestId\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=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eGuid\u003c/span\u003e \u003cspan class=\"n\"\u003eUserId\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=\"kd\"\u003epublic\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=\"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=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eDateTime\u003c/span\u003e \u003cspan class=\"n\"\u003eRequestedAt\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=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eErasureStatus\u003c/span\u003e \u003cspan class=\"n\"\u003eStatus\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=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eList\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eErasureTask\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eTasks\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=\"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=\"p\"\u003e?\u003c/span\u003e \u003cspan class=\"n\"\u003eCompletedAt\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=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003estring?\u003c/span\u003e \u003cspan class=\"n\"\u003eFailureReason\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=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003eenum\u003c/span\u003e \u003cspan class=\"n\"\u003eErasureStatus\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\"\u003eInProgress\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eCompleted\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ePartiallyCompleted\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eFailed\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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\"\u003eErasureTask\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\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\"\u003eSystemName\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// \u0026#34;PrimaryDatabase\u0026#34;, \u0026#34;BlobStorage\u0026#34;, \u0026#34;Mailchimp\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=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eTaskType\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// \u0026#34;Delete\u0026#34;, \u0026#34;Anonymize\u0026#34;, \u0026#34;Notify\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\"\u003eTaskStatus\u003c/span\u003e \u003cspan class=\"n\"\u003eStatus\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=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eDateTime\u003c/span\u003e\u003cspan class=\"p\"\u003e?\u003c/span\u003e \u003cspan class=\"n\"\u003eCompletedAt\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=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003estring?\u003c/span\u003e \u003cspan class=\"n\"\u003eDetails\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=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003eenum\u003c/span\u003e \u003cspan class=\"n\"\u003eTaskStatus\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\"\u003eInProgress\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eCompleted\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eFailed\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eSkipped\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=\"2-durable-function-orchestration\"\u003e\u003ca href=\"/posts/right-to-erasure-implementation-patterns/#2-durable-function-orchestration\" title=\"2. Durable Function Orchestration\"\u003e2. Durable Function Orchestration\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThe orchestrator coordinates all erasure activities. It runs parallel where possible and sequential where rate limits demand:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\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[FunctionName(nameof(ErasureOrchestrator))]\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\"\u003eErasureResult\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eErasureOrchestrator\u003c/span\u003e\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    [OrchestrationTrigger]\u003c/span\u003e \u003cspan class=\"n\"\u003eIDurableOrchestrationContext\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\"\u003erequest\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\"\u003eGetInput\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eErasureRequest\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=\"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\"\u003eErasureResult\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"n\"\u003eRequestId\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\"\u003eRequestId\u003c/span\u003e \u003cspan class=\"p\"\u003e};\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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// Phase 1: Validate (legal holds, active contracts?)\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\"\u003evalidation\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCallActivityAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eValidationResult\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\"\u003enameof\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eValidateErasureRequest\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\"\u003evalidation\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eIsValid\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eresult\u003c/span\u003e \u003cspan class=\"n\"\u003ewith\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\"\u003eErasureStatus\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFailed\u003c/span\u003e \u003cspan class=\"p\"\u003e};\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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// Phase 2: Parallel erasure across systems\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\"\u003etasks\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\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\"\u003eWhenAll\u003c/span\u003e\u003cspan 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\"\u003eCallActivityAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eErasureTask\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;(\u003c/span\u003e\u003cspan class=\"n\"\u003enameof\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eAnonymizeUserInDatabase\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=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCallActivityAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eErasureTask\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;(\u003c/span\u003e\u003cspan class=\"n\"\u003enameof\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eDeleteBlobStorageData\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=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCallActivityAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eErasureTask\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;(\u003c/span\u003e\u003cspan class=\"n\"\u003enameof\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eInvalidateCaches\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=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCallActivityAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eErasureTask\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;(\u003c/span\u003e\u003cspan class=\"n\"\u003enameof\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eRemoveFromSearchIndexes\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=\"n\"\u003eresult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTasks\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddRange\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003etasks\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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// Phase 3: Sequential third-party notifications (rate limits)\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=\"n\"\u003eprocessor\u003c/span\u003e \u003cspan class=\"k\"\u003ein\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCallActivityAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eList\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u0026gt;(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003enameof\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eGetThirdPartyProcessors\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=\"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\"\u003eTasks\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAdd\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCallActivityAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eErasureTask\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\"\u003enameof\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eNotifyThirdPartyProcessor\u003c/span\u003e\u003cspan class=\"p\"\u003e),\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\"\u003eprocessor\u003c/span\u003e\u003cspan class=\"p\"\u003e)));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"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// Phase 4: Verify and audit\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\"\u003everification\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCallActivityAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eVerificationResult\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\"\u003enameof\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eVerifyErasureCompleteness\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=\"n\"\u003eresult\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\"\u003everification\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eIsComplete\u003c/span\u003e \u003cspan class=\"p\"\u003e?\u003c/span\u003e \u003cspan class=\"n\"\u003eErasureStatus\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCompleted\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eErasureStatus\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePartiallyCompleted\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCallActivityAsync\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\"\u003eCreateAuditRecord\u003c/span\u003e\u003cspan class=\"p\"\u003e),\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=\"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\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe key insight: parallel execution where systems are independent, sequential where they\u0026rsquo;re not. Third-party APIs have rate limits. Respect them or get blocked.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"3-database-anonymization-with-soft-delete\"\u003e\u003ca href=\"/posts/right-to-erasure-implementation-patterns/#3-database-anonymization-with-soft-delete\" title=\"3. Database Anonymization with Soft Delete\"\u003e3. Database Anonymization with Soft Delete\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eHard deletes break referential integrity. Soft delete with anonymization keeps your foreign keys happy while removing all personal 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=\"na\"\u003e[FunctionName(nameof(AnonymizeUserInDatabase))]\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\"\u003eErasureTask\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eAnonymizeUserInDatabase\u003c/span\u003e\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    [ActivityTrigger]\u003c/span\u003e \u003cspan class=\"n\"\u003eErasureRequest\u003c/span\u003e \u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eILogger\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=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eApplicationDbContext\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003euser\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUsers\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eInclude\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eu\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eu\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eOrders\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\u003cspan class=\"n\"\u003eInclude\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eu\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eu\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eComments\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFirstOrDefaultAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eu\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eu\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eId\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"n\"\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    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003euser\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eErasureTask\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\"\u003eTaskStatus\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSkipped\u003c/span\u003e \u003cspan class=\"p\"\u003e};\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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// Anonymize—keeps FK relationships intact\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003euser\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eEmail\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"s\"\u003e$\u0026#34;deleted-{request.UserId}@privacy.local\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\"\u003euser\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFirstName\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003euser\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLastName\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \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    \u003cspan class=\"n\"\u003euser\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePhoneNumber\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003euser\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddress\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\"\u003eDateOfBirth\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\"\u003eTaxId\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\"\u003euser\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eIsDeleted\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\"\u003euser\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eDeletedAt\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    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Related entities too\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=\"n\"\u003eorder\u003c/span\u003e \u003cspan class=\"k\"\u003ein\u003c/span\u003e \u003cspan class=\"n\"\u003euser\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eOrders\u003c/span\u003e\u003cspan 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\"\u003eShippingAddress\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\"\u003eBillingAddress\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    \u003cspan class=\"k\"\u003eforeach\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003ecomment\u003c/span\u003e \u003cspan class=\"k\"\u003ein\u003c/span\u003e \u003cspan class=\"n\"\u003euser\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eComments\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/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\"\u003ecomment\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAuthorName\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ecomment\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=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Anonymous\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;[Removed]\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\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSaveChangesAsync\u003c/span\u003e\u003cspan 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\"\u003eErasureTask\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"n\"\u003eSystemName\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Database\u0026#34;\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\"\u003eTaskStatus\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCompleted\u003c/span\u003e \u003cspan class=\"p\"\u003e};\u003c/span\u003e\n\u003c/span\u003e\u003c/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 email pattern (\u003ccode\u003edeleted-{userId}@privacy.local\u003c/code\u003e) is intentional. It\u0026rsquo;s unique, clearly anonymized, and lets you verify erasure later.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"4-third-party-notification\"\u003e\u003ca href=\"/posts/right-to-erasure-implementation-patterns/#4-third-party-notification\" title=\"4. Third-Party Notification\"\u003e4. Third-Party Notification\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eGDPR Article 19 is non-negotiable: if you shared data with processors, you must tell them to delete it too.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\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[FunctionName(nameof(NotifyThirdPartyProcessor))]\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\"\u003eErasureTask\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eNotifyThirdPartyProcessor\u003c/span\u003e\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    [ActivityTrigger]\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eErasureRequest\u003c/span\u003e \u003cspan class=\"n\"\u003eRequest\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eProcessor\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"n\"\u003einput\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\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\"\u003e_httpClient\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePostAsJsonAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eGetProcessorConfig\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=\"n\"\u003eProcessor\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\u003cspan class=\"n\"\u003eErasureEndpoint\u003c/span\u003e\u003cspan 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=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"n\"\u003einput\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 \u003cspan class=\"n\"\u003einput\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\"\u003eEmail\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eAction\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;ERASURE_REQUIRED\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=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eErasureTask\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eSystemName\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=\"n\"\u003eProcessor\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eStatus\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\"\u003eIsSuccessStatusCode\u003c/span\u003e \u003cspan class=\"p\"\u003e?\u003c/span\u003e \u003cspan class=\"n\"\u003eTaskStatus\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCompleted\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eTaskStatus\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFailed\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e};\u003c/span\u003e\n\u003c/span\u003e\u003c/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\u003eTrack acknowledgments. You\u0026rsquo;re liable even if your processor fails to delete. That\u0026rsquo;s the fun part of being a data controller.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"5-verification-and-audit\"\u003e\u003ca href=\"/posts/right-to-erasure-implementation-patterns/#5-verification-and-audit\" title=\"5. Verification and Audit\"\u003e5. Verification and Audit\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003e\u0026ldquo;Trust but verify\u0026rdquo; doesn\u0026rsquo;t cut it. Verify, then trust nothing:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\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[FunctionName(nameof(VerifyErasureCompleteness))]\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\"\u003eVerificationResult\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eVerifyErasureCompleteness\u003c/span\u003e\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    [ActivityTrigger]\u003c/span\u003e \u003cspan class=\"n\"\u003eErasureRequest\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\"\u003eissues\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eList\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003euser\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_context\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUsers\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFindAsync\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\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003euser\u003c/span\u003e \u003cspan class=\"k\"\u003eis\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"n\"\u003eIsDeleted\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\"\u003eissues\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAdd\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;User not marked deleted\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003euser\u003c/span\u003e \u003cspan class=\"p\"\u003e!=\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e \u003cspan class=\"p\"\u003e!\u003c/span\u003e\u003cspan class=\"n\"\u003euser\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eEmail\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eStartsWith\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;deleted-\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e \u003cspan class=\"n\"\u003eissues\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAdd\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Email not anonymized\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_blobContainer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetBlobsAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eprefix\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003e$\u0026#34;{request.UserId}/\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\u003cspan class=\"n\"\u003eAnyAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e())\u003c/span\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eissues\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAdd\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Blobs remain\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_cache\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e$\u0026#34;user:{request.UserId}\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\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\"\u003eissues\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAdd\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Still in cache\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=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eVerificationResult\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"n\"\u003eIsComplete\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eissues\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=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe audit log is your evidence when regulators ask \u0026ldquo;prove you deleted this person\u0026rsquo;s data.\u0026rdquo; Make it immutable. Cosmos DB with append-only access works well.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"testing-your-erasure-implementation\"\u003e\u003ca href=\"/posts/right-to-erasure-implementation-patterns/#testing-your-erasure-implementation\" title=\"Testing Your Erasure Implementation\"\u003eTesting Your Erasure Implementation\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eCompliance without tests is compliance on paper only:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\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[TestMethod]\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\"\u003eErasureOrchestrator_RemovesDataFromAllSystems\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\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// Arrange: seed user with data across all systems\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\"\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\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\"\u003eSeedTestUser\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\"\u003ewithOrders\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\"\u003ewithBlobs\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    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Act\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\"\u003eRunOrchestration\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eErasureRequest\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\"\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    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Assert: nothing remains\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eAssert\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAreEqual\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eErasureStatus\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCompleted\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\"\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=\"n\"\u003eAssert\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eIsTrue\u003c/span\u003e\u003cspan class=\"p\"\u003e((\u003c/span\u003e\u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eGetUser\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\"\u003eIsDeleted\u003c/span\u003e\u003cspan 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\"\u003eIsTrue\u003c/span\u003e\u003cspan class=\"p\"\u003e((\u003c/span\u003e\u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eGetUser\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\"\u003eEmail\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eStartsWith\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;deleted-\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\"\u003eAssert\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAreEqual\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eGetBlobCount\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\"\u003eAssert\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eIsNull\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eGetCachedUser\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\"\u003eAssert\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eIsNotNull\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eGetAuditRecord\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\"\u003eRequestId\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\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[TestMethod]\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\"\u003eErasureOrchestrator_PreservesReferentialIntegrity\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\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\"\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\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\"\u003eSeedTestUser\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\"\u003ewithOrders\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\"\u003ewithBlobs\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=\"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\"\u003eRunOrchestration\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eErasureRequest\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\"\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    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Orders exist but anonymized\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\"\u003eGetOrders\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\"\u003eAssert\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAreEqual\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e5\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\"\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\"\u003eAssert\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eIsTrue\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\"\u003eAll\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\"\u003eShippingAddress\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\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eTest the edge cases: partial failures, third-party timeouts, concurrent erasure requests. Your orchestrator will encounter all of them.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-backup-trap\"\u003e\u003ca href=\"/posts/right-to-erasure-implementation-patterns/#the-backup-trap\" title=\"The Backup Trap\"\u003eThe Backup Trap\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eGDPR doesn\u0026rsquo;t require deleting data from backups immediately, but restored data must be re-erased. Build a guard:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\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\"\u003eRestoreValidationResult\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eValidateRestoration\u003c/span\u003e\u003cspan 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\"\u003ebackupDate\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eIEnumerable\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eGuid\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003euserIdsInBackup\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\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\"\u003eerasedAfterBackup\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_auditLog\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetErasureRequestsAfter\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ebackupDate\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/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\"\u003er\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003er\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\"\u003eToHashSet\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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\"\u003ezombieUsers\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003euserIdsInBackup\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\"\u003eerasedAfterBackup\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\"\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    \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\"\u003eRestoreValidationResult\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eRequiresPostRestoreErasure\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ezombieUsers\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAny\u003c/span\u003e\u003cspan class=\"p\"\u003e(),\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eUsersToReErase\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ezombieUsers\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eMessage\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ezombieUsers\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAny\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e?\u003c/span\u003e \u003cspan class=\"s\"\u003e$\u0026#34;Restoration will resurrect {zombieUsers.Count} deleted users. Re-erasure required.\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=\"s\"\u003e\u0026#34;Clean restore\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\u003eEvery backup restoration must check the erasure audit log. Automate this or watch \u0026ldquo;forgotten\u0026rdquo; users come back to haunt you.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"when-deletion-is-illegal\"\u003e\u003ca href=\"/posts/right-to-erasure-implementation-patterns/#when-deletion-is-illegal\" title=\"When Deletion Is Illegal\"\u003eWhen Deletion Is Illegal\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eNot all data can be erased. Tax records, legal claims, active contracts: they override erasure requests:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\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\"\u003eValidationResult\u003c/span\u003e \u003cspan class=\"n\"\u003eValidateErasureRequest\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eGuid\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\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eblocks\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eList\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eHasActiveContract\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\"\u003eblocks\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAdd\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Active contract requires retention\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eHasTaxObligations\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\"\u003eblocks\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAdd\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Tax law: 7 years from last transaction\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eHasPendingLegalClaims\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\"\u003eblocks\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAdd\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Pending legal claims\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=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eValidationResult\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eIsValid\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eblocks\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=\"n\"\u003eReason\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\"\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\"\u003eblocks\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e};\u003c/span\u003e\n\u003c/span\u003e\u003c/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\u003eDocument your exceptions. Regulators accept legitimate retention reasons. They don\u0026rsquo;t accept \u0026ldquo;we forgot to check.\u0026rdquo;\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"what-actually-works\"\u003e\u003ca href=\"/posts/right-to-erasure-implementation-patterns/#what-actually-works\" title=\"What Actually Works\"\u003eWhat Actually Works\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eAfter implementing erasure workflows across multiple organizations:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eArchitecture\u003c/strong\u003e: Use orchestration (Durable Functions, Step Functions, Temporal). Soft-delete with anonymization. Design for eventual consistency because some systems lag.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eData\u003c/strong\u003e: Anonymize where you can\u0026rsquo;t delete. Immutable audit trails in separate datastores. Version your workflows because regulations evolve.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eThird Parties\u003c/strong\u003e: Document every processor\u0026rsquo;s erasure API. Test notifications regularly (APIs change). Track acknowledgments because you\u0026rsquo;re liable for their failures.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eVerification\u003c/strong\u003e: Automate checks in the orchestration. Run periodic sweeps for escaped data. Test backup restoration with erasure validation.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eOperations\u003c/strong\u003e: Monitor erasure SLAs (\u0026ldquo;without undue delay\u0026rdquo; is legally binding). Alert on failures. Practice disaster recovery with privacy in mind.\u003c/p\u003e\n\u003cp\u003eThe right to erasure isn\u0026rsquo;t optional. It\u0026rsquo;s a fundamental privacy right with substantial fines behind it. Organizations that build orchestration, verification, and audit trails from day one sleep well when regulators come knocking.\u003c/p\u003e\n\u003cp\u003eThe rest scramble to prove they deleted data they can\u0026rsquo;t actually prove they deleted.\u003c/p\u003e\n\u003cp\u003eDon\u0026rsquo;t be the rest.\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2026-04-07T17:00:00+02:00","id":"https://daily-devops.net/posts/right-to-erasure-implementation-patterns/","language":"en","summary":"That delete request touches 17 systems you'd forgotten existed. Here's how to erase data across distributed systems without nuking your database.\n","tags":["iso-standards","privacy","gdpr","dotnet","testing","azure","compliance"],"title":"\"Just Delete the User\": Famous Last Words Before the GDPR Audit\n","url":"https://daily-devops.net/posts/right-to-erasure-implementation-patterns/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003ePrivacy compliance isn\u0026rsquo;t something you achieve once and forget. It\u0026rsquo;s a continuous operational discipline—monitoring personal data lifecycle, detecting consent expiration, validating retention policy execution, and identifying anomalous access patterns before they become regulatory violations.\u003c/p\u003e\n\u003cp\u003eI\u0026rsquo;ve seen teams with meticulously documented privacy policies fail audits because nobody could demonstrate that those policies actually executed. The retention schedule said \u0026ldquo;delete after 24 months.\u0026rdquo; The database had records from 2019. The deletion job? It had been failing silently for eight months. The logs existed, buried somewhere in a storage account nobody monitored.\u003c/p\u003e\n\u003cp\u003eISO/IEC 27701 makes this explicit. The standard doesn\u0026rsquo;t just demand policies on paper—it requires operational verification that those policies actually execute as intended. Control 7.4.9 addresses temporary files, 8.4.3 covers data retention, 7.2.1 handles consent. All of them share one requirement: continuous monitoring.\u003c/p\u003e\n\u003cp\u003eFor .NET teams, this translates into a specific engineering challenge. You already have health checks for infrastructure—database connectivity, API responsiveness, cache status. The same pattern applies to privacy compliance. Same \u003ccode\u003eIHealthCheck\u003c/code\u003e interface, different questions.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-privacy-monitoring-gap\"\u003e\u003ca href=\"/posts/privacy-health-checks-data-access-patterns/#the-privacy-monitoring-gap\" title=\"The Privacy Monitoring Gap\"\u003eThe Privacy Monitoring Gap\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003ePrivacy health requires monitoring dimensions that infrastructure checks never touch:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eConsent lifecycle\u003c/strong\u003e: What percentage of active users have expired consents? When does the next bulk expiration hit?\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eData retention\u003c/strong\u003e: Did the nightly deletion job actually run? How much data exceeds retention periods right now?\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eAccess patterns\u003c/strong\u003e: Is someone bulk-exporting customer records? Are query volumes spiking unexpectedly?\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTemporary storage\u003c/strong\u003e: How many unprocessed export files are accumulating? For how long?\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eWithout active monitoring, retention policies remain aspirational documents. Consent expirations go unnoticed until user complaints escalate. Access anomalies blend into routine operational noise—until the audit.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-typical-monitoring-blind-spot\"\u003e\u003ca href=\"/posts/privacy-health-checks-data-access-patterns/#the-typical-monitoring-blind-spot\" title=\"The Typical Monitoring Blind Spot\"\u003eThe Typical Monitoring Blind Spot\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eMost .NET applications monitor infrastructure health religiously but ignore privacy compliance entirely:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\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 your health checks probably look like today\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eServices\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddHealthChecks\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddDbContextCheck\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eApplicationDbContext\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;()\u003c/span\u003e      \u003cspan class=\"c1\"\u003e// ✓ Database up?\u003c/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\"\u003eAddUrlGroup\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=\"s\"\u003e\u0026#34;https://api.stripe.com\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e \u003cspan class=\"c1\"\u003e// ✓ Payment API reachable?\u003c/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\"\u003eAddRedis\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eredisConnection\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e                     \u003cspan class=\"c1\"\u003e// ✓ Cache responding?\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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\u0026#39;s missing:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// - How many users have expired consents? (Nobody knows)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// - Did the retention policy actually run last night? (Hopefully)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// - Is someone bulk-exporting customer data right now? (Shrug)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eWhen a data protection authority audits this system, they find retention policies documented but no evidence of continuous monitoring. They see consent records but no expiration tracking. The gap isn\u0026rsquo;t missing privacy controls—it\u0026rsquo;s missing visibility into whether those controls actually function.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"privacy-health-checks-the-core-pattern\"\u003e\u003ca href=\"/posts/privacy-health-checks-data-access-patterns/#privacy-health-checks-the-core-pattern\" title=\"Privacy Health Checks: The Core Pattern\"\u003ePrivacy Health Checks: The Core Pattern\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eA privacy health check follows the same \u003ccode\u003eIHealthCheck\u003c/code\u003e interface you already know—but queries privacy-relevant data instead of infrastructure status:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\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\"\u003eConsentExpirationHealthCheck\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eIHealthCheck\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eApplicationDbContext\u003c/span\u003e \u003cspan class=\"n\"\u003e_context\u003c/span\u003e\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\"\u003edouble\u003c/span\u003e \u003cspan class=\"n\"\u003eWarningThreshold\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e0.15\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"c1\"\u003e// 15% expired = warning\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\"\u003edouble\u003c/span\u003e \u003cspan class=\"n\"\u003eCriticalThreshold\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e0.30\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"c1\"\u003e// 30% expired = critical\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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\"\u003eConsentExpirationHealthCheck\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eApplicationDbContext\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=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003e_context\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eHealthCheckResult\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eCheckHealthAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eHealthCheckContext\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eCancellationToken\u003c/span\u003e \u003cspan class=\"n\"\u003ecancellationToken\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003edefault\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003enow\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\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\"\u003etotalUsers\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_context\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUsers\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCountAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eu\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eu\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eIsActive\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=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eexpiredConsents\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\"\u003eUserConsents\u003c/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\"\u003eCountAsync\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\"\u003eExpirationDate\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e \u003cspan class=\"n\"\u003enow\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e \u003cspan class=\"n\"\u003ec\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\"\u003eIsActive\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=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eexpiredRatio\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003etotalUsers\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=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003edouble\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"n\"\u003eexpiredConsents\u003c/span\u003e \u003cspan class=\"p\"\u003e/\u003c/span\u003e \u003cspan class=\"n\"\u003etotalUsers\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=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eexpiredRatio\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\u0026gt;=\u003c/span\u003e \u003cspan class=\"n\"\u003eCriticalThreshold\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eHealthCheckResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUnhealthy\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"s\"\u003e$\u0026#34;{expiredRatio:P0} of users have expired consents\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\u0026gt;=\u003c/span\u003e \u003cspan class=\"n\"\u003eWarningThreshold\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eHealthCheckResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eDegraded\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"s\"\u003e$\u0026#34;{expiredRatio:P0} of users have expired consents\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_\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eHealthCheckResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eHealthy\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"s\"\u003e$\u0026#34;Consent status normal: {expiredConsents} expired of {totalUsers}\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e};\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThat\u0026rsquo;s it. Two queries, one ratio, three possible outcomes. The pattern applies to any privacy metric.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"choosing-your-thresholds\"\u003e\u003ca href=\"/posts/privacy-health-checks-data-access-patterns/#choosing-your-thresholds\" title=\"Choosing Your Thresholds\"\u003eChoosing Your Thresholds\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThe 15% and 30% thresholds in the example aren\u0026rsquo;t magic numbers—they\u0026rsquo;re starting points. Your thresholds depend on your business context and risk tolerance.\u003c/p\u003e\n\u003cp\u003eFor a B2B SaaS platform with annual contracts, 15% expired consents might indicate a renewal cycle issue rather than a compliance emergency. For a healthcare application processing patient data daily, even 5% could warrant immediate attention.\u003c/p\u003e\n\u003cp\u003eStart conservative. It\u0026rsquo;s easier to relax thresholds after you understand your baseline than to explain to an auditor why you ignored a 25% expiration rate for six months. The first week after deployment, you\u0026rsquo;ll learn what \u0026ldquo;normal\u0026rdquo; looks like for your system—then calibrate accordingly.\u003c/p\u003e\n\u003cp\u003eThe retention policy check follows the same logic:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\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\"\u003eRetentionPolicyHealthCheck\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eIHealthCheck\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eApplicationDbContext\u003c/span\u003e \u003cspan class=\"n\"\u003e_context\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eHealthCheckResult\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eCheckHealthAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eHealthCheckContext\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \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\"\u003elastRun\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\"\u003eRetentionPolicyExecutions\u003c/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\"\u003ee\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ee\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\"\u003eExecutionStatus\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\u003cspan class=\"n\"\u003eMaxAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ee\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\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\"\u003ee\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eExecutedAt\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=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003ehoursSinceLastRun\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003elastRun\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eHasValue\u003c/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(\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\"\u003elastRun\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\"\u003eTotalHours\u003c/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=\"kt\"\u003edouble\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eMaxValue\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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\"\u003ehoursSinceLastRun\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\u0026gt;\u003c/span\u003e \u003cspan class=\"m\"\u003e48\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eHealthCheckResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUnhealthy\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"s\"\u003e$\u0026#34;Retention policy hasn\u0026#39;t run in {hoursSinceLastRun:F0} hours\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\u0026gt;\u003c/span\u003e \u003cspan class=\"m\"\u003e24\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eHealthCheckResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eDegraded\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"s\"\u003e$\u0026#34;Retention policy last ran {hoursSinceLastRun:F0} hours ago\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_\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eHealthCheckResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eHealthy\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"s\"\u003e$\u0026#34;Retention policy ran {hoursSinceLastRun:F1} hours ago\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e};\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\n\n\n\u003ch2 id=\"registration-three-lines-of-code\"\u003e\u003ca href=\"/posts/privacy-health-checks-data-access-patterns/#registration-three-lines-of-code\" title=\"Registration: Three Lines of Code\"\u003eRegistration: Three Lines of Code\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=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eServices\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddHealthChecks\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddCheck\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eConsentExpirationHealthCheck\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;consent_expiration\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\"\u003etags\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;privacy\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e])\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddCheck\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eRetentionPolicyHealthCheck\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;retention_policy\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\"\u003etags\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;privacy\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e]);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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// Separate endpoint for privacy checks\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\"\u003eMapHealthChecks\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;/health/privacy\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eHealthCheckOptions\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003ePredicate\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003echeck\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003echeck\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTags\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=\"s\"\u003e\u0026#34;privacy\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\u003eNow \u003ccode\u003e/health/privacy\u003c/code\u003e tells you what \u003ccode\u003e/health\u003c/code\u003e never could: whether your privacy controls actually function.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"exporting-to-application-insights\"\u003e\u003ca href=\"/posts/privacy-health-checks-data-access-patterns/#exporting-to-application-insights\" title=\"Exporting to Application Insights\"\u003eExporting to Application Insights\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eHealth checks run on demand. For continuous monitoring, push metrics to Application Insights:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\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\"\u003eConsentExpirationHealthCheck\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eIHealthCheck\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eApplicationDbContext\u003c/span\u003e \u003cspan class=\"n\"\u003e_context\u003c/span\u003e\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\"\u003eTelemetryClient\u003c/span\u003e \u003cspan class=\"n\"\u003e_telemetry\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eHealthCheckResult\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eCheckHealthAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eHealthCheckContext\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \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=\"c1\"\u003e// ... same queries as before ...\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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// Push to Application Insights for trending\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003e_telemetry\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTrackMetric\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Privacy.Consent.ExpiredPercentage\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eexpiredRatio\u003c/span\u003e \u003cspan class=\"p\"\u003e*\u003c/span\u003e \u003cspan class=\"m\"\u003e100\u003c/span\u003e\u003cspan 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_telemetry\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTrackMetric\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Privacy.Consent.ExpiredCount\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eexpiredConsents\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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// ... return health status ...\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/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 enables dashboards and alerts that fire \u003cem\u003ebefore\u003c/em\u003e thresholds breach:\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\"\u003ecustomMetrics\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e| where name == \u0026#34;Privacy.Consent.ExpiredPercentage\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e| summarize AvgExpired = avg(value) by bin(timestamp, 1h)\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e| render timechart\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe beauty of this approach: you\u0026rsquo;re not learning anything new. Same \u003ccode\u003eIHealthCheck\u003c/code\u003e interface you\u0026rsquo;ve used for years, same threshold-based results, same registration pattern. The only difference is what you\u0026rsquo;re querying—privacy metrics instead of ping responses.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"what-to-monitor\"\u003e\u003ca href=\"/posts/privacy-health-checks-data-access-patterns/#what-to-monitor\" title=\"What to Monitor\"\u003eWhat to Monitor\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe pattern extends to any privacy-relevant metric. Here\u0026rsquo;s what matters for ISO/IEC 27701 compliance:\u003c/p\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003eHealth Check\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eWhat It Catches\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eISO 27701 Control\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\u003eConsent expiration\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eUsers processed without valid consent\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e7.2.1 (Consent)\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eRetention policy execution\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eSilent failures in data deletion jobs\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e8.4.3 (Retention)\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eTemporary file accumulation\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eUnprocessed exports, staging data\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e7.4.9 (Temporary files)\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eBulk export detection\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eUnusual data access patterns\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e8.2.2 (Purpose limitation)\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003ePick the metrics that matter for your regulatory context. A healthcare application needs different checks than an e-commerce platform.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"a-real-scenario-the-3-am-alert-that-wasnt\"\u003e\u003ca href=\"/posts/privacy-health-checks-data-access-patterns/#a-real-scenario-the-3-am-alert-that-wasnt\" title=\"A Real Scenario: The 3 AM Alert That Wasn\u0026rsquo;t\"\u003eA Real Scenario: The 3 AM Alert That Wasn\u0026rsquo;t\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eConsider what happens when your retention policy fails at 3 AM. Without privacy health checks, you discover the problem three weeks later during a routine database review—or worse, during an audit. The data that should have been deleted is still there, accumulating liability.\u003c/p\u003e\n\u003cp\u003eWith a \u003ccode\u003eRetentionPolicyHealthCheck\u003c/code\u003e running every five minutes, you know within the hour. The health endpoint returns \u003ccode\u003eUnhealthy\u003c/code\u003e, your monitoring system fires an alert, and someone investigates before breakfast. The fix might be trivial—a database connection timeout, a permission change that broke the service account. But you caught it in hours, not weeks.\u003c/p\u003e\n\u003cp\u003eThat\u0026rsquo;s the difference between \u0026ldquo;we have a retention policy\u0026rdquo; and \u0026ldquo;we can prove our retention policy works.\u0026rdquo;\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-audit-conversation-changes\"\u003e\u003ca href=\"/posts/privacy-health-checks-data-access-patterns/#the-audit-conversation-changes\" title=\"The Audit Conversation Changes\"\u003eThe Audit Conversation Changes\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eWithout privacy health checks:\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u0026ldquo;How do you verify your retention policies execute?\u0026rdquo;\u003cbr\u003e\n\u0026ldquo;We have logs\u0026hellip; somewhere. Let me check with the DBA.\u0026rdquo;\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eWith privacy health checks:\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u0026ldquo;How do you verify your retention policies execute?\u0026rdquo;\u003cbr\u003e\n\u0026ldquo;Here\u0026rsquo;s the Application Insights dashboard showing execution history, and here are the alerts that fire when execution fails. Last incident was three months ago—resolved in 47 minutes.\u0026rdquo;\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eThe implementation cost—two health check classes and a few lines of registration—delivers continuous compliance verification that turns audit questions into dashboard demos.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"start-here\"\u003e\u003ca href=\"/posts/privacy-health-checks-data-access-patterns/#start-here\" title=\"Start Here\"\u003eStart Here\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eYou don\u0026rsquo;t need to implement all of this at once. Add one privacy health check this week—pick whatever metric keeps you up at night:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eConsent expiration\u003c/strong\u003e if GDPR compliance is your concern\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eRetention policy execution\u003c/strong\u003e if your deletion jobs are a black box\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eData volume trends\u003c/strong\u003e if you\u0026rsquo;re not sure what\u0026rsquo;s accumulating where\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eOne health check. One metric. One new endpoint. That\u0026rsquo;s enough to answer questions your infrastructure health checks never could—and enough to change the tone of your next compliance conversation.\u003c/p\u003e\n\u003cp\u003ePrivacy compliance is an operational discipline. Your monitoring should reflect that.\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2026-03-24T17:00:00+01:00","id":"https://daily-devops.net/posts/privacy-health-checks-data-access-patterns/","language":"en","summary":"Database connectivity is green, yet 15% of users have expired consents. Add IHealthCheck probes for consent, retention, and access anomalies.","tags":["iso-standards","privacy","monitoring","dotnet","observability"],"title":"Privacy Health Checks: Beyond Database Connectivity","url":"https://daily-devops.net/posts/privacy-health-checks-data-access-patterns/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eYour application just crashed in production. Azure App Service kept routing traffic to the failing instance for ninety seconds. Users saw timeouts. Your monitoring dashboard stayed green because the web server responded with HTTP 200 while the database connection pool was exhausted.\u003c/p\u003e\n\u003cp\u003eI\u0026rsquo;ve watched this exact scenario play out at three different organizations in the past year. Each time, the post-mortem revealed the same root cause: health checks that verified the process was breathing without checking whether it could actually do its job. ISO/IEC 27001 Control A.17.2.1 exists precisely for this reason—availability is a security control, not an operational afterthought.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"why-availability-is-a-security-control\"\u003e\u003ca href=\"/posts/health-checks-operational-monitoring/#why-availability-is-a-security-control\" title=\"Why availability is a security control\"\u003eWhy availability is a security control\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eISO 27001 treats availability as a core pillar of information security alongside confidentiality and integrity. Control A.17.2.1 explicitly requires organizations to implement \u0026ldquo;information processing facilities\u0026rdquo; with sufficient redundancy to meet availability requirements. Redundancy without health awareness, though? That\u0026rsquo;s a dangerous illusion of resilience.\u003c/p\u003e\n\u003cp\u003eControl A.12.1.4 mandates environmental isolation to prevent development instability from affecting production. Health checks enforce this separation. An unhealthy instance—whether due to misconfiguration, dependency failure, or environmental contamination—should never receive production traffic. Period.\u003c/p\u003e\n\u003cp\u003eThen there\u0026rsquo;s A.12.6.1, which requires timely identification of technical vulnerabilities. A failed health check signals exactly that: a vulnerability in real-time. Unreachable Key Vault? Expired certificate? Overloaded message queue? These are security vulnerabilities that proper health checks expose before they cascade into complete system failure.\u003c/p\u003e\n\u003cp\u003eTeams treating health checks as operational monitoring miss the security implications entirely. Availability failures create security incidents. Degraded systems leak information through error messages, bypass authentication under load, or fail to log security events. Catching degradation early prevents these failures from becoming breaches.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-fatal-pattern-is-the-website-responding\"\u003e\u003ca href=\"/posts/health-checks-operational-monitoring/#the-fatal-pattern-is-the-website-responding\" title=\"The fatal pattern: \u0026ldquo;Is the website responding?\u0026rdquo;\"\u003eThe fatal pattern: \u0026ldquo;Is the website responding?\u0026rdquo;\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eMost applications I encounter implement health monitoring at the infrastructure layer only. Load balancers ping an endpoint, the endpoint returns HTTP 200 if the web server process is running, and everyone assumes the system works. This approach fails catastrophically because it conflates process health with application health.\u003c/p\u003e\n\u003cp\u003eHere\u0026rsquo;s the code I see everywhere:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\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// Program.cs - The \u0026#34;is it alive?\u0026#34; anti-pattern\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=\"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\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\"\u003eAddControllers\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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\"\u003eMapControllers\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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// \u0026#34;Health check\u0026#34; that checks nothing meaningful\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\"\u003eMapGet\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;/health\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\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 endpoint happily reports \u0026ldquo;healthy\u0026rdquo; when:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eThe database connection pool is exhausted\u003c/li\u003e\n\u003cli\u003eAzure Key Vault is unreachable (configuration secrets unavailable)\u003c/li\u003e\n\u003cli\u003eThe Redis cache is down (session state lost)\u003c/li\u003e\n\u003cli\u003eService Bus queue is full (messages dropped)\u003c/li\u003e\n\u003cli\u003eApplication Insights ingestion is failing (no telemetry)\u003c/li\u003e\n\u003cli\u003eCertificate validation is failing (external API calls rejected)\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eLoad balancers keep routing traffic to instances reporting \u0026ldquo;healthy\u0026rdquo; while the application cannot serve a single request. Users experience timeouts and errors. Your monitoring shows 100% uptime. Meanwhile, your organization violates ISO 27001 availability requirements while believing the system is compliant.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"the-information-disclosure-vulnerability\"\u003e\u003ca href=\"/posts/health-checks-operational-monitoring/#the-information-disclosure-vulnerability\" title=\"The information disclosure vulnerability\"\u003eThe information disclosure vulnerability\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThere\u0026rsquo;s something worse than inadequate health checks: health checks that leak configuration details.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\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// DO NOT DO THIS - Security vulnerability\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\"\u003eMapGet\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;/health\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\"\u003eApplicationDbContext\u003c/span\u003e \u003cspan class=\"n\"\u003edb\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 \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\"\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\"\u003edb\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eDatabase\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCanConnectAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eResults\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eOk\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eStatus\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\"\u003eDatabase\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=\"s\"\u003e\u0026#34;ConnectionStrings:Default\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e],\u003c/span\u003e  \u003cspan class=\"c1\"\u003e// Exposed!\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eKeyVault\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=\"s\"\u003e\u0026#34;Azure:KeyVault:Uri\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e],\u003c/span\u003e         \u003cspan class=\"c1\"\u003e// Exposed!\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eVersion\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\"\u003eGetExecutingAssembly\u003c/span\u003e\u003cspan class=\"p\"\u003e().\u003c/span\u003e\u003cspan class=\"n\"\u003eGetName\u003c/span\u003e\u003cspan class=\"p\"\u003e().\u003c/span\u003e\u003cspan class=\"n\"\u003eVersion\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eEnvironment\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\"\u003eEnvironment\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eEnvironmentName\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eMachineName\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eEnvironment\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eMachineName\u003c/span\u003e             \u003cspan class=\"c1\"\u003e// Internal infrastructure details\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan 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\"\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=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eResults\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eOk\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eStatus\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Unhealthy\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\"\u003eError\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\"\u003eToString\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e  \u003cspan class=\"c1\"\u003e// Stack trace exposure\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/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 violates Control A.9.4.5 by exposing internal configuration URIs and infrastructure topology. Unauthenticated health endpoints should return minimal information—detailed diagnostics belong behind authentication.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-correct-implementation-comprehensive-health-checks\"\u003e\u003ca href=\"/posts/health-checks-operational-monitoring/#the-correct-implementation-comprehensive-health-checks\" title=\"The correct implementation: Comprehensive health checks\"\u003eThe correct implementation: Comprehensive health checks\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eASP.NET Core\u0026rsquo;s health check middleware provides everything you need: dependency validation, startup verification, and runtime degradation detection. Done right, health monitoring transforms from a checkbox exercise into an actual security control.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"basic-health-check-registration\"\u003e\u003ca href=\"/posts/health-checks-operational-monitoring/#basic-health-check-registration\" title=\"Basic health check registration\"\u003eBasic health check registration\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eStart with the infrastructure:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\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// Program.cs\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=\"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\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eServices\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddHealthChecks\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddCheck\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;self\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\"\u003eHealthCheckResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eHealthy\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Application process is running\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\"\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=\"c1\"\u003e// Liveness endpoint - \u0026#34;Is the process alive?\u0026#34;\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\"\u003eMapHealthChecks\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;/health/live\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eHealthCheckOptions\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003ePredicate\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eregistration\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eregistration\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;self\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\"\u003eAllowCachingResponses\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"kc\"\u003efalse\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"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// Readiness endpoint - \u0026#34;Can the application serve requests?\u0026#34;\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\"\u003eMapHealthChecks\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;/health/ready\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eHealthCheckOptions\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003ePredicate\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003e_\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\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\"\u003eAllowCachingResponses\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"kc\"\u003efalse\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"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\u003eTwo endpoints, two different purposes:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003e/health/live\u003c/code\u003e answers \u0026ldquo;Is the process running?\u0026rdquo; Orchestrators use this to restart crashed instances.\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e/health/ready\u003c/code\u003e answers \u0026ldquo;Can the application serve requests?\u0026rdquo; Load balancers use this to route traffic.\u003c/li\u003e\n\u003c/ul\u003e\n\n\n\n\n\u003ch3 id=\"dependency-specific-health-checks\"\u003e\u003ca href=\"/posts/health-checks-operational-monitoring/#dependency-specific-health-checks\" title=\"Dependency-specific health checks\"\u003eDependency-specific health checks\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eNow add checks for actual dependencies:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\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.Diagnostics.HealthChecks\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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\"\u003eAddHealthChecks\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddCheck\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;self\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\"\u003eHealthCheckResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eHealthy\u003c/span\u003e\u003cspan class=\"p\"\u003e())\u003c/span\u003e\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\"\u003eAddDbContextCheck\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eApplicationDbContext\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;database\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003efailureStatus\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eHealthStatus\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUnhealthy\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003etags\u003c/span\u003e\u003cspan class=\"p\"\u003e:\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 \u003cspan class=\"s\"\u003e\u0026#34;ready\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\"\u003eAddSqlServer\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;sqlserver\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=\"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\u003cspan class=\"s\"\u003e\u0026#34;ConnectionStrings:SqlServer\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\"\u003e100\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e})\u003c/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\"\u003eAddRedis\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;redis\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=\"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\u003cspan class=\"s\"\u003e\u0026#34;ConnectionStrings:Redis\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\u003cspan class=\"n\"\u003eAddServiceBusQueue\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;servicebus-orders\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\"\u003eFullyQualifiedNamespace\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\u003cspan class=\"s\"\u003e\u0026#34;Azure:ServiceBus:Namespace\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\"\u003eQueueName\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;orders\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\u003eThese checks use health check packages from my open-source collection:\u003c/p\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\u003cp\u003eThe \u003ccode\u003eNetEvolve.HealthChecks.*\u003c/code\u003e packages provide a configuration-first approach. You can configure health checks via code as shown above, or through \u003ccode\u003eappsettings.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;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;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;ConnectionString\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;Server=tcp:localhost,1433;Database=master;...\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\"\u003e100\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \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;Redis\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;redis\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;localhost:6379\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\u003eThe \u003ccode\u003etags\u003c/code\u003e parameter on the DbContext check matters here. Tags control which checks run for which endpoint. The \u003ccode\u003eself\u003c/code\u003e check has no tags—it runs for liveness only. Dependency checks tagged \u003ccode\u003eready\u003c/code\u003e run for readiness.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"extended-azure-health-checks\"\u003e\u003ca href=\"/posts/health-checks-operational-monitoring/#extended-azure-health-checks\" title=\"Extended Azure health checks\"\u003eExtended Azure health checks\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eFor Azure-specific services, the \u003ccode\u003eNetEvolve.HealthChecks.Azure.*\u003c/code\u003e packages cover most scenarios out of the box:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\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\"\u003eAddHealthChecks\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddApplicationInsights\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;appinsights\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=\"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\u003cspan class=\"s\"\u003e\u0026#34;ApplicationInsights:ConnectionString\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\u003cspan class=\"n\"\u003eAddBlobServiceClient\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;blob-storage\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\"\u003eAccountName\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\u003cspan class=\"s\"\u003e\u0026#34;Azure:Storage:AccountName\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\u003cspan class=\"n\"\u003eAddServiceBusQueue\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;servicebus-orders\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\"\u003eFullyQualifiedNamespace\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\u003cspan class=\"s\"\u003e\u0026#34;Azure:ServiceBus:Namespace\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\"\u003eQueueName\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;orders\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\u003eApplication Insights failures degrade observability but shouldn\u0026rsquo;t stop the application from serving requests—the packages handle this distinction properly.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"startup-health-checks\"\u003e\u003ca href=\"/posts/health-checks-operational-monitoring/#startup-health-checks\" title=\"Startup health checks\"\u003eStartup health checks\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eISO 27001 Control A.12.1.4 requires environment separation. Startup health checks enforce this—they prevent misconfigured deployments from ever receiving traffic:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\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\"\u003eAddHealthChecks\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddCheck\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;startup-configuration\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\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \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\"\u003erequired\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=\"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;ConnectionStrings:Default\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;Azure:KeyVault:Uri\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;Azure:ServiceBus:ConnectionString\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\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\"\u003emissing\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003erequired\u003c/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\"\u003ekey\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\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\"\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\u003cspan class=\"n\"\u003ekey\u003c/span\u003e\u003cspan class=\"p\"\u003e]))\u003c/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\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\"\u003emissing\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAny\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e?\u003c/span\u003e \u003cspan class=\"n\"\u003eHealthCheckResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUnhealthy\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e$\u0026#34;Missing configuration: {string.Join(\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;, missing)}\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\"\u003eHealthCheckResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eHealthy\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;All required configuration 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 \u003cspan class=\"n\"\u003etags\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e\u003cspan class=\"p\"\u003e[]\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;startup\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// Run startup checks before accepting traffic\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\"\u003estartupHealthCheck\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\"\u003eGetRequiredService\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eHealthCheckService\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=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003estartupResult\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003estartupHealthCheck\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCheckHealthAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eregistration\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eregistration\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTags\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=\"s\"\u003e\u0026#34;startup\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\"\u003estartupResult\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\"\u003eHealthStatus\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eHealthy\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan 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=\"n\"\u003eentry\u003c/span\u003e \u003cspan class=\"k\"\u003ein\u003c/span\u003e \u003cspan class=\"n\"\u003estartupResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eEntries\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\"\u003ee\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ee\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\"\u003eStatus\u003c/span\u003e \u003cspan class=\"p\"\u003e!=\u003c/span\u003e \u003cspan class=\"n\"\u003eHealthStatus\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eHealthy\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan 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\"\u003eLogger\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLogCritical\u003c/span\u003e\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;Startup health check \u0026#39;{CheckName}\u0026#39; failed: {Description}\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\"\u003eentry\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\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\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\u003cspan class=\"n\"\u003eDescription\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"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\"\u003ethrow\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eInvalidOperationException\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"s\"\u003e\u0026#34;Application failed startup health checks. See logs for 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=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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\u003eMisconfigured instances never start. Deployment pipelines fail fast with clear error messages instead of deploying broken configurations to production.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"secure-health-check-ui\"\u003e\u003ca href=\"/posts/health-checks-operational-monitoring/#secure-health-check-ui\" title=\"Secure health check UI\"\u003eSecure health check UI\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003ePublic health endpoints should expose minimal information. Keep the detailed diagnostics behind authentication:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\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\"\u003eMapHealthChecks\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;/health/ready\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eHealthCheckOptions\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003ePredicate\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eregistration\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eregistration\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTags\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=\"s\"\u003e\u0026#34;ready\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\"\u003eAllowCachingResponses\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=\"n\"\u003eResultStatusCodes\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \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        [HealthStatus.Healthy]\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eStatusCodes\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eStatus200OK\u003c/span\u003e\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        [HealthStatus.Degraded]\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eStatusCodes\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eStatus200OK\u003c/span\u003e\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        [HealthStatus.Unhealthy]\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eStatusCodes\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eStatus503ServiceUnavailable\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e},\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eResponseWriter\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\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ereport\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\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eResponse\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eContentType\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;application/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\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eResponse\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWriteAsJsonAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003estatus\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ereport\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\"\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=\"c1\"\u003e// No detailed information in unauthenticated endpoint\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"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// Detailed diagnostics require authentication\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\"\u003eMapHealthChecks\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;/health/details\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eHealthCheckOptions\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003ePredicate\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003e_\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\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\"\u003eAllowCachingResponses\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=\"n\"\u003eResponseWriter\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\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ereport\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\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eResponse\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eContentType\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;application/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\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eResponse\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWriteAsJsonAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003estatus\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ereport\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\"\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=\"n\"\u003eduration\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ereport\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTotalDuration\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003echecks\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ereport\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eEntries\u003c/span\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\"\u003ee\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan 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\"\u003ee\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\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"n\"\u003estatus\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\"\u003eValue\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\"\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=\"n\"\u003edescription\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\"\u003eValue\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eDescription\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"n\"\u003eduration\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\"\u003eValue\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eDuration\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"n\"\u003etags\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ee\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\"\u003eTags\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e})\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/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\"\u003eRequireAuthorization\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;HealthCheckPolicy\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// Define authorization policy\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\"\u003eAddAuthorization\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eoptions\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eoptions\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddPolicy\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;HealthCheckPolicy\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003epolicy\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003epolicy\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRequireRole\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Administrator\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;HealthCheckReader\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 \u003ccode\u003e/health/ready\u003c/code\u003e endpoint returns minimal status for load balancers. \u003ccode\u003e/health/details\u003c/code\u003e requires authorization and returns the full picture for operations teams.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"integration-with-azure-monitor-and-alerting\"\u003e\u003ca href=\"/posts/health-checks-operational-monitoring/#integration-with-azure-monitor-and-alerting\" title=\"Integration with Azure Monitor and alerting\"\u003eIntegration with Azure Monitor and alerting\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eHealth checks only become a security control when you connect them to alerting. Azure Monitor provides the infrastructure:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\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.Monitor.OpenTelemetry.AspNetCore\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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\"\u003eAddOpenTelemetry\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/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\"\u003eUseAzureMonitor\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=\"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\u003cspan class=\"s\"\u003e\u0026#34;ApplicationInsights:ConnectionString\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\u003cspan class=\"n\"\u003eWithMetrics\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003emetrics\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003emetrics\u003c/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\"\u003eAddMeter\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Microsoft.AspNetCore.HealthChecks\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// Publish health check results as metrics\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eServices\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddHealthChecks\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddCheck\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;database\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"cm\"\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\u003cspan class=\"n\"\u003eAddCheck\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;keyvault\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"cm\"\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=\"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\"\u003eConfigure\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eHealthCheckPublisherOptions\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;(\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\"\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=\"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\"\u003eoptions\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePeriod\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\u003cspan 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\"\u003eAddSingleton\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eIHealthCheckPublisher\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eApplicationInsightsPublisher\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=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eApplicationInsightsPublisher\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eIHealthCheckPublisher\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\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\"\u003eTelemetryClient\u003c/span\u003e \u003cspan class=\"n\"\u003e_telemetryClient\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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\"\u003eApplicationInsightsPublisher\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eTelemetryClient\u003c/span\u003e \u003cspan class=\"n\"\u003etelemetryClient\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan 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_telemetryClient\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003etelemetryClient\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"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=\"n\"\u003eTask\u003c/span\u003e \u003cspan class=\"n\"\u003ePublishAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eHealthReport\u003c/span\u003e \u003cspan class=\"n\"\u003ereport\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=\"k\"\u003eforeach\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eentry\u003c/span\u003e \u003cspan class=\"k\"\u003ein\u003c/span\u003e \u003cspan class=\"n\"\u003ereport\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eEntries\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan 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_telemetryClient\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTrackMetric\u003c/span\u003e\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;HealthCheck.{entry.Key}\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\"\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\u003cspan class=\"n\"\u003eStatus\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"n\"\u003eHealthStatus\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eHealthy\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\"\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\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eDictionary\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\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                \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e                    [\u0026#34;Status\u0026#34;]\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\"\u003eValue\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\"\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=\"na\"\u003e                    [\u0026#34;Description\u0026#34;]\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\"\u003eValue\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eDescription\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\"\u003eEmpty\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"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\"\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\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 publishes health check results to Application Insights every thirty seconds. Create Azure Monitor alerts based on these metrics:\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# Azure CLI - Create alert rule for database health check failures\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eaz monitor metrics alert create \u003cspan class=\"se\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  --name \u003cspan class=\"s2\"\u003e\u0026#34;Database Health Check Failed\u0026#34;\u003c/span\u003e \u003cspan class=\"se\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  --resource-group \u003cspan class=\"s2\"\u003e\u0026#34;production-rg\u0026#34;\u003c/span\u003e \u003cspan class=\"se\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  --scopes \u003cspan class=\"s2\"\u003e\u0026#34;/subscriptions/{subscription-id}/resourceGroups/production-rg/providers/Microsoft.Insights/components/myapp-appinsights\u0026#34;\u003c/span\u003e \u003cspan class=\"se\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  --condition \u003cspan class=\"s2\"\u003e\u0026#34;max HealthCheck.database \u0026lt; 1\u0026#34;\u003c/span\u003e \u003cspan class=\"se\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  --window-size 5m \u003cspan class=\"se\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  --evaluation-frequency 1m \u003cspan class=\"se\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  --action \u003cspan class=\"s2\"\u003e\u0026#34;security-team-action-group\u0026#34;\u003c/span\u003e \u003cspan class=\"se\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  --description \u003cspan class=\"s2\"\u003e\u0026#34;Database health check has failed - potential availability impact\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eAlert failures notify security and operations teams before users notice. That\u0026rsquo;s Control A.17.2.1 in action.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"github-actions-deployment-gates\"\u003e\u003ca href=\"/posts/health-checks-operational-monitoring/#github-actions-deployment-gates\" title=\"GitHub Actions deployment gates\"\u003eGitHub Actions deployment gates\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eHealth checks should gate your deployments. Don\u0026rsquo;t let a deployment complete until the application proves it\u0026rsquo;s healthy:\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=\"c\"\u003e# .github/workflows/deploy.yml\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\"\u003eDeploy to Production\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\"\u003eon\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\"\u003epush\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003ebranches\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"l\"\u003emain]\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003ejobs\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\"\u003edeploy\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\"\u003eruns-on\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eubuntu-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    \u003c/span\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\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eDeploy to Azure App Service\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\"\u003eazure/webapps-deploy@v2\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\"\u003eapp-name\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003emyapp-production\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\"\u003epackage\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003e./publish\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=\"w\"\u003e      \u003c/span\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\"\u003eWait for deployment\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\"\u003esleep 30\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=\"w\"\u003e      \u003c/span\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\"\u003eVerify startup health\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=\"p\"\u003e|\u003c/span\u003e\u003cspan class=\"sd\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sd\"\u003e          for i in {1..10}; do\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sd\"\u003e            response=$(curl -s -o /dev/null -w \u0026#34;%{http_code}\u0026#34; https://myapp-production.azurewebsites.net/health/live)\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sd\"\u003e            if [ $response -eq 200 ]; then\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sd\"\u003e              echo \u0026#34;Liveness check passed\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sd\"\u003e              break\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sd\"\u003e            fi\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sd\"\u003e            if [ $i -eq 10 ]; then\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sd\"\u003e              echo \u0026#34;Liveness check failed after 10 attempts\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sd\"\u003e              exit 1\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sd\"\u003e            fi\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sd\"\u003e            sleep 10\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sd\"\u003e          done\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=\"w\"\u003e      \u003c/span\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\"\u003eVerify application readiness\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=\"p\"\u003e|\u003c/span\u003e\u003cspan class=\"sd\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sd\"\u003e          for i in {1..20}; do\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sd\"\u003e            response=$(curl -s -o /dev/null -w \u0026#34;%{http_code}\u0026#34; https://myapp-production.azurewebsites.net/health/ready)\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sd\"\u003e            if [ $response -eq 200 ]; then\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sd\"\u003e              echo \u0026#34;Readiness check passed - deployment successful\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sd\"\u003e              exit 0\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sd\"\u003e            fi\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sd\"\u003e            if [ $i -eq 20 ]; then\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sd\"\u003e              echo \u0026#34;Readiness check failed - rolling back deployment\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sd\"\u003e              exit 1\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sd\"\u003e            fi\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sd\"\u003e            sleep 15\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sd\"\u003e          done\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=\"w\"\u003e      \u003c/span\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\"\u003eRollback on failure\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\"\u003eif\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003efailure()\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=\"p\"\u003e|\u003c/span\u003e\u003cspan class=\"sd\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sd\"\u003e          # Trigger Azure App Service deployment slot swap back to previous version\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sd\"\u003e          az webapp deployment slot swap \\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sd\"\u003e            --resource-group production-rg \\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sd\"\u003e            --name myapp-production \\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sd\"\u003e            --slot staging \\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sd\"\u003e            --target-slot production\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\u003eThis workflow deploys, waits for startup, verifies liveness, then checks readiness. If readiness fails within five minutes, the deployment rolls back automatically. Unhealthy deployments never receive production traffic.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"what-ive-learned\"\u003e\u003ca href=\"/posts/health-checks-operational-monitoring/#what-ive-learned\" title=\"What I\u0026rsquo;ve learned\"\u003eWhat I\u0026rsquo;ve learned\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eAfter fifteen years implementing monitoring systems across enterprise environments, these patterns consistently separate teams that catch failures early from those that discover them via angry user reports:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e1. Separate liveness from readiness.\u003c/strong\u003e Orchestrators need to know if the process crashed. Load balancers need to know if the application can serve requests. These are different questions requiring different endpoints.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e2. Tag health checks by purpose.\u003c/strong\u003e Use tags to control which checks run for liveness, readiness, and startup verification. Not all checks apply to all scenarios.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e3. Use appropriate failure statuses.\u003c/strong\u003e Database failures are \u003ccode\u003eUnhealthy\u003c/code\u003e. Cache failures are \u003ccode\u003eDegraded\u003c/code\u003e. Telemetry failures are \u003ccode\u003eDegraded\u003c/code\u003e. Choose statuses that reflect actual impact on request handling.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e4. Authenticate detailed diagnostics.\u003c/strong\u003e Public endpoints return minimal status. Detailed information requires authorization. This prevents information disclosure while enabling troubleshooting.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e5. Implement startup health checks.\u003c/strong\u003e Fail deployments immediately when configuration is invalid. Don\u0026rsquo;t wait for runtime failures to discover environment separation violations.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e6. Publish health metrics to monitoring systems.\u003c/strong\u003e Health checks are worthless without alerting. Integrate with Azure Monitor, Application Insights, or your monitoring platform of choice.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e7. Automate deployment verification.\u003c/strong\u003e Health checks in CI/CD pipelines prevent broken deployments from reaching production. Automated rollback on health check failure implements Control A.12.1.4.\u003c/p\u003e\n\u003cp\u003eHealth checks are not optional observability features. They are security controls that implement ISO 27001 availability requirements. Every team I\u0026rsquo;ve seen treat them as afterthoughts eventually discovers this the hard way—during an incident when degraded instances serve errors to users while reporting \u0026ldquo;healthy\u0026rdquo; to monitoring systems.\u003c/p\u003e\n\u003cp\u003eYour availability posture depends on checking what actually matters, not just whether the process is breathing.\u003c/p\u003e","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2026-03-19T17:00:00+01:00","id":"https://daily-devops.net/posts/health-checks-operational-monitoring/","language":"en","summary":"HTTP 200 from /health while users see timeouts. The process runs, but the database pool is exhausted. Check what matters, not if it breathes.","tags":["iso-standards","dotnet","observability","monitoring","testing","azure","bestpractices","architecture"],"title":"Green Dashboard, Dead Application","url":"https://daily-devops.net/posts/health-checks-operational-monitoring/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eException stack traces in production API responses. Database connection strings leaked through error messages. Internal file paths revealed to unauthenticated clients. These aren\u0026rsquo;t hypothetical scenarios. They show up in nearly every security assessment I conduct on .NET applications, and they\u0026rsquo;re textbook ISO/IEC 27001 violations.\u003c/p\u003e\n\u003cp\u003eWhat makes this frustrating? Error handling sits in a blind spot for most teams. Developers invest real effort catching exceptions, returning \u0026ldquo;helpful\u0026rdquo; messages, configuring logging frameworks, building dashboards. All with good intentions. Yet that same exception handling strategy often violates security controls mandated by ISO 27001. The irony: being \u0026ldquo;helpful\u0026rdquo; to API consumers means being equally helpful to attackers.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-iso-27001-perspective-on-error-messages\"\u003e\u003ca href=\"/posts/error-handling-security-information-disclosure/#the-iso-27001-perspective-on-error-messages\" title=\"The ISO 27001 Perspective on Error Messages\"\u003eThe ISO 27001 Perspective on Error Messages\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eYou won\u0026rsquo;t find a control titled \u0026ldquo;Don\u0026rsquo;t leak stack traces\u0026rdquo; in ISO/IEC 27001. The requirements are subtler and more encompassing. Three controls directly govern how your .NET applications should handle errors:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eControl A.12.4.1 (Event Logging)\u003c/strong\u003e requires logging security events for detection and investigation. Exception details belong in logs accessible to your operations team, not in HTTP responses returned to potentially malicious clients. This distinction matters more than most teams realize: logs are protected internal records, while HTTP responses cross trust boundaries.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"environment-separation-is-non-negotiable\"\u003e\u003ca href=\"/posts/error-handling-security-information-disclosure/#environment-separation-is-non-negotiable\" title=\"Environment Separation Is Non-Negotiable\"\u003eEnvironment Separation Is Non-Negotiable\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003eControl A.14.2.6 (Secure Development Environment)\u003c/strong\u003e mandates separating development, testing, and production environments with different security configurations. That detailed stack trace in your development environment? Perfectly fine. The same behavior in production? Compliance violation. Environment-specific error handling isn\u0026rsquo;t a nice-to-have. It\u0026rsquo;s a requirement.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"stack-traces-as-intellectual-property-leaks\"\u003e\u003ca href=\"/posts/error-handling-security-information-disclosure/#stack-traces-as-intellectual-property-leaks\" title=\"Stack Traces As Intellectual Property Leaks\"\u003eStack Traces As Intellectual Property Leaks\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003eControl A.18.1.2 (Intellectual Property Rights)\u003c/strong\u003e addresses protection of organizational knowledge. Source code file paths, internal architecture details, and technology stack versions revealed through exception messages constitute intellectual property disclosure. An attacker learning that \u003ccode\u003eOrderProcessingService.cs\u003c/code\u003e line 347 threw a \u003ccode\u003eNullReferenceException\u003c/code\u003e now has architectural intelligence that aids further attacks.\u003c/p\u003e\n\u003cp\u003eThese controls don\u0026rsquo;t prohibit exceptions. They require deliberate separation between what gets logged internally and what gets returned externally. Your ASP.NET Core implementation determines compliance.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"fatal-pattern-the-helpful-exception\"\u003e\u003ca href=\"/posts/error-handling-security-information-disclosure/#fatal-pattern-the-helpful-exception\" title=\"Fatal Pattern: The Helpful Exception\"\u003eFatal Pattern: The Helpful Exception\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThis pattern appears in countless production APIs. It looks like proper error handling. It feels like you\u0026rsquo;re being helpful. It violates all three ISO 27001 controls:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\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: Detailed exception information returned directly to clients\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=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eActionResult\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eOrderResponse\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eCreateOrder\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eCreateOrderRequest\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=\"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\"\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_orderService\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\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eOk\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\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=\"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// Fatal: Returning full exception details to client\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\"\u003eBadRequest\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eerror\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=\"n\"\u003estackTrace\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\"\u003eStackTrace\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003einnerException\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\"\u003eInnerException\u003c/span\u003e\u003cspan class=\"p\"\u003e?.\u003c/span\u003e\u003cspan class=\"n\"\u003eMessage\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/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 this code encounters a database connectivity issue, the HTTP response includes:\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;error\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;A network-related error occurred establishing a connection to SQL Server...\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;stackTrace\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;at OrderService.CreateOrderAsync() in C:\\\\Projects\\\\ECommerce\\\\Services\\\\OrderService.cs:line 127\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\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis response violates all three ISO controls. No structured logging occurred (A.12.4.1). Production behaves identically to development (A.14.2.6). Internal file paths and technology stack details are fully exposed (A.18.1.2).\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"what-an-attacker-learns-from-this-response\"\u003e\u003ca href=\"/posts/error-handling-security-information-disclosure/#what-an-attacker-learns-from-this-response\" title=\"What An Attacker Learns From This Response\"\u003eWhat An Attacker Learns From This Response\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eAn attacker now knows the application uses SQL Server, the internal project structure, exact file paths, and line numbers. Equally problematic: no correlation ID was generated, no security event was logged, and no alert triggered when hundreds of these errors originate from the same source IP.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"fatal-pattern-the-model-validation-leak\"\u003e\u003ca href=\"/posts/error-handling-security-information-disclosure/#fatal-pattern-the-model-validation-leak\" title=\"Fatal Pattern: The Model Validation Leak\"\u003eFatal Pattern: The Model Validation Leak\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eModel validation errors fly under the radar more often than exception handling. They seem harmless, just input validation feedback. But they can expose your internal data model:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\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: Detailed validation errors exposing schema structure\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\"\u003eCreateOrderRequest\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\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    [Required]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [CreditCard]\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\"\u003ePaymentToken\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=\"na\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [Required]\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\"\u003eInternalProcessingCode\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// Internal 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\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\"\u003eActionResult\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eOrderResponse\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eCreateOrder\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eCreateOrderRequest\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=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(!\u003c/span\u003e\u003cspan class=\"n\"\u003eModelState\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=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eBadRequest\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eModelState\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e  \u003cspan class=\"c1\"\u003e// Fatal: Exposes all fields\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan 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\"\u003eOk\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eOrderResponse\u003c/span\u003e\u003cspan class=\"p\"\u003e());\u003c/span\u003e\n\u003c/span\u003e\u003c/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 a client submits an invalid request, ASP.NET Core\u0026rsquo;s default behavior returns:\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;errors\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026#34;PaymentToken\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;The PaymentToken field is not a valid credit card number.\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;InternalProcessingCode\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;The InternalProcessingCode field is required.\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 client now knows that an \u003ccode\u003eInternalProcessingCode\u003c/code\u003e field exists, even though it wasn\u0026rsquo;t documented in public API specs. This constitutes schema disclosure, revealing internal data model details beyond the public API contract. ISO 27001 Control A.18.1.2 applies here as much as to source code paths.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"correct-pattern-exception-middleware-with-environment-awareness\"\u003e\u003ca href=\"/posts/error-handling-security-information-disclosure/#correct-pattern-exception-middleware-with-environment-awareness\" title=\"Correct Pattern: Exception Middleware with Environment Awareness\"\u003eCorrect Pattern: Exception Middleware with Environment Awareness\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe fix centralizes exception management in middleware. Log everything internally, return nothing sensitive externally, and adapt behavior based on environment:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\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\"\u003eSecureExceptionMiddleware\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eRequestDelegate\u003c/span\u003e \u003cspan class=\"n\"\u003enext\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eILogger\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eSecureExceptionMiddleware\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=\"n\"\u003eIHostEnvironment\u003c/span\u003e \u003cspan class=\"n\"\u003eenvironment\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\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\"\u003eInvokeAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eHttpContext\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003etry\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003enext\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=\"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\"\u003eException\u003c/span\u003e \u003cspan class=\"n\"\u003eex\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eHandleExceptionAsync\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\"\u003eex\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\"\u003eprivate\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e \u003cspan class=\"n\"\u003eHandleExceptionAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eHttpContext\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eException\u003c/span\u003e \u003cspan class=\"n\"\u003eexception\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \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\"\u003ecorrelationId\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eActivity\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\"\u003eId\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\"\u003eTraceIdentifier\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// Log full details internally\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\"\u003eexception\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;CorrelationId: {CorrelationId}, Path: {Path}\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\"\u003ecorrelationId\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRequest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePath\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eResponse\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eContentType\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;application/problem+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=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eResponse\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eStatusCode\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \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\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\"\u003eproblemDetails\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eProblemDetails\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eType\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;https://httpstatuses.com/500\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\"\u003eTitle\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Internal Server Error\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\"\u003eStatus\u003c/span\u003e \u003cspan class=\"p\"\u003e=\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=\"n\"\u003eExtensions\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=\"s\"\u003e\u0026#34;correlationId\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ecorrelationId\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"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// Environment-aware detail level\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eproblemDetails\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eDetail\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eenvironment\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eIsDevelopment\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/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\"\u003eexception\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eMessage\u003c/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;Reference correlation ID {correlationId} when contacting support.\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\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eResponse\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWriteAsJsonAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eproblemDetails\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/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=\"wiring-the-middleware-into-the-pipeline\"\u003e\u003ca href=\"/posts/error-handling-security-information-disclosure/#wiring-the-middleware-into-the-pipeline\" title=\"Wiring The Middleware Into The Pipeline\"\u003eWiring The Middleware Into The Pipeline\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eRegister this middleware early 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\"\u003eapp\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUseMiddleware\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eSecureExceptionMiddleware\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\"\u003eapp\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUseAuthentication\u003c/span\u003e\u003cspan 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\"\u003eUseAuthorization\u003c/span\u003e\u003cspan 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\"\u003eMapControllers\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 the production response becomes generic but actionable:\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;title\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;Internal Server Error\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;status\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"mi\"\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=\"nt\"\u003e\u0026#34;detail\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;Reference correlation ID 00-4bf92f3577b34da6a3ce929d0e0e4736-00 when contacting support.\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;correlationId\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;00-4bf92f3577b34da6a3ce929d0e0e4736-00\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\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe full exception with stack trace goes to Application Insights, linked by that same correlation ID. Operations teams get everything they need for investigation. Clients get nothing useful for exploitation.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"correct-pattern-secure-model-validation\"\u003e\u003ca href=\"/posts/error-handling-security-information-disclosure/#correct-pattern-secure-model-validation\" title=\"Correct Pattern: Secure Model Validation\"\u003eCorrect Pattern: Secure Model Validation\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eModel validation needs the same discipline. Filter internal fields before they reach the response:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\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\"\u003eCreateOrderRequest\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\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    [Required(ErrorMessage = \u0026#34;Payment information is required.\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=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003ePaymentToken\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=\"na\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [JsonIgnore]\u003c/span\u003e  \u003cspan class=\"c1\"\u003e// Internal field: never exposed\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\"\u003eInternalProcessingCode\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\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\"\u003eActionResult\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eOrderResponse\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eCreateOrder\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eCreateOrderRequest\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=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(!\u003c/span\u003e\u003cspan class=\"n\"\u003eModelState\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// Filter out internal fields before returning\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\"\u003epublicErrors\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eModelState\u003c/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\"\u003ekvp\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"p\"\u003e!\u003c/span\u003e\u003cspan class=\"n\"\u003ekvp\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\"\u003eContains\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Internal\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\"\u003eToDictionary\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ekvp\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ekvp\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\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"n\"\u003ekvp\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ekvp\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\"\u003eErrors\u003c/span\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\"\u003ee\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ee\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eErrorMessage\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\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\"\u003eBadRequest\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eValidationProblemDetails\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003epublicErrors\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan 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\"\u003eOk\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eOrderResponse\u003c/span\u003e\u003cspan class=\"p\"\u003e());\u003c/span\u003e\n\u003c/span\u003e\u003c/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 client now receives only sanitized validation errors:\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;errors\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026#34;PaymentToken\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;Payment information is required.\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 \u003ccode\u003eInternalProcessingCode\u003c/code\u003e field never appears in the response. Full validation failure details exist only in logs.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"health-checks-error-rates-as-security-indicators\"\u003e\u003ca href=\"/posts/error-handling-security-information-disclosure/#health-checks-error-rates-as-security-indicators\" title=\"Health Checks: Error Rates as Security Indicators\"\u003eHealth Checks: Error Rates as Security Indicators\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eISO 27001 Control A.12.4.1 requires monitoring security events. Here\u0026rsquo;s what many teams miss: unusual error rates often signal active attacks. Enumeration attempts, credential stuffing, SQL injection probes, they all generate elevated error counts before any successful breach.\u003c/p\u003e\n\u003cp\u003eImplement an \u003ccode\u003eIHealthCheck\u003c/code\u003e that tracks errors per minute and returns \u003ccode\u003eHealthCheckResult.Degraded\u003c/code\u003e when rates exceed thresholds. Wire this into your exception middleware so every handled exception increments the counter. When error rates spike, the health check triggers alerts through Kubernetes readiness probes, Azure App Service monitoring, or your orchestration platform.\u003c/p\u003e\n\u003cp\u003eThe implementation is straightforward: record errors in middleware, expose metrics through health endpoints, let your infrastructure respond to anomalies. The value? You\u0026rsquo;re detecting potential attacks through a mechanism that already exists in most production deployments.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"correlation-ids-connecting-responses-to-logs\"\u003e\u003ca href=\"/posts/error-handling-security-information-disclosure/#correlation-ids-connecting-responses-to-logs\" title=\"Correlation IDs: Connecting Responses to Logs\"\u003eCorrelation IDs: Connecting Responses to Logs\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe correlation ID pattern appears throughout these examples for a reason. It solves the operational problem that secure error handling creates: how do support teams investigate a production error when the client received only a generic message?\u003c/p\u003e\n\u003cp\u003eASP.NET Core\u0026rsquo;s \u003ccode\u003eActivity.Current?.Id\u003c/code\u003e provides distributed tracing identifiers compatible with W3C Trace Context standards. When using Application Insights, these correlation IDs automatically link HTTP requests, exception logs, database queries, downstream service calls, and performance telemetry into a single trace.\u003c/p\u003e\n\u003cp\u003eA client reporting \u0026ldquo;Error with correlation ID 00-4bf92f3577b34da6a3ce929d0e0e4736-00\u0026rdquo; gives your operations team the complete picture in Application Insights. Full stack trace, request details, downstream failures, everything. No information disclosure required in the original error response.\u003c/p\u003e\n\u003cp\u003eThis satisfies ISO 27001\u0026rsquo;s dual requirement: comprehensive logging for incident investigation (A.12.4.1) without exposing internal details to potentially malicious clients (A.18.1.2).\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-compliance-reality\"\u003e\u003ca href=\"/posts/error-handling-security-information-disclosure/#the-compliance-reality\" title=\"The Compliance Reality\"\u003eThe Compliance Reality\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eISO 27001 certification audits examine error handling more closely than most teams expect. Auditors request sample API error responses and compare them against logged events. They verify environment-specific configurations exist and function correctly. They check that model validation errors don\u0026rsquo;t leak internal schema details.\u003c/p\u003e\n\u003cp\u003eThese aren\u0026rsquo;t theoretical controls. I\u0026rsquo;ve participated in audits where error handling implementations blocked certification until remediated. The fix required implementing exactly the patterns shown here: exception middleware with environment awareness, Problem Details responses, correlation IDs, and separation between logged events and client responses.\u003c/p\u003e\n\u003cp\u003eThe technical implementation is straightforward. The organizational challenge? Changing the mindset that \u0026ldquo;helpful error messages\u0026rdquo; improve developer experience. Detailed error messages in production don\u0026rsquo;t help legitimate users, they can\u0026rsquo;t act on \u0026ldquo;NullReferenceException at line 347\u0026rdquo; anyway. They assist attackers during reconnaissance and exploitation.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-path-forward\"\u003e\u003ca href=\"/posts/error-handling-security-information-disclosure/#the-path-forward\" title=\"The Path Forward\"\u003eThe Path Forward\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eSecure error handling in .NET applications comes down to six patterns:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003eException middleware\u003c/strong\u003e that intercepts all unhandled exceptions before they reach clients\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eEnvironment-aware responses\u003c/strong\u003e providing detail in Development, generic messages in Production\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eProblem Details (RFC 7807)\u003c/strong\u003e as the standard error response format\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCorrelation IDs\u003c/strong\u003e linking client responses to comprehensive internal logs\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSelective model validation\u003c/strong\u003e exposing only public API contract violations\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eHealth monitoring\u003c/strong\u003e detecting elevated error rates as potential security events\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eThese patterns satisfy ISO 27001 Controls A.12.4.1 (Event Logging), A.14.2.6 (Secure Development Environment), and A.18.1.2 (Intellectual Property Rights). More importantly, they implement defense in depth by ensuring that routine error handling doesn\u0026rsquo;t become an information disclosure vulnerability.\u003c/p\u003e\n\u003cp\u003eThe next time your API throws an exception, ask one question: who benefits from this error message? If the answer is \u0026ldquo;potential attackers,\u0026rdquo; your error handling violates ISO 27001 requirements and creates security risk. If the answer is \u0026ldquo;operations teams investigating via correlation IDs in logs,\u0026rdquo; you\u0026rsquo;ve implemented the control correctly.\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2026-03-12T17:00:00+01:00","id":"https://daily-devops.net/posts/error-handling-security-information-disclosure/","language":"en","summary":"That helpful stack trace in your API response is a roadmap for attackers. Learn secure error handling that logs everything but reveals nothing.","tags":["iso-standards","security","dotnet","aspnet-core"],"title":"Your Stack Traces Are Love Letters to Attackers","url":"https://daily-devops.net/posts/error-handling-security-information-disclosure/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eA developer receives a JIRA ticket: \u0026ldquo;Implement ISO 27001 Control A.9.4.3 for session management.\u0026rdquo; The ticket contains an 87-page PDF and a two-week deadline. The developer searches StackOverflow for \u0026ldquo;ASP.NET session timeout.\u0026rdquo; The first answer suggests \u003ccode\u003eSession.Timeout = Int32.MaxValue\u003c/code\u003e for \u0026ldquo;better user experience.\u0026rdquo;\u003c/p\u003e\n\u003cp\u003eThis is how security controls become checkbox theater. The code compiles, users can log in, and the feature gets marked complete. Six months later, a penetration tester discovers that sessions never expire, and the audit finding costs more to remediate than doing it right would have.\u003c/p\u003e\n\u003cp\u003eThe standard mandates outcomes: sessions must timeout after inactivity, absolute limits must exist regardless of activity, and authentication artifacts must be protected from interception. ASP.NET Core provides the primitives—cookie authentication with sliding and absolute expiration, JWT bearer tokens with refresh rotation, secure cookie flags. But these are configuration knobs, not security guarantees.\u003c/p\u003e\n\u003cp\u003eMisconfigured authentication satisfies functional requirements (users can log in) while violating every security principle (sessions never expire, tokens leak through insecure channels, logout doesn\u0026rsquo;t invalidate anything). The gap between \u0026ldquo;it works\u0026rdquo; and \u0026ldquo;it\u0026rsquo;s secure\u0026rdquo; is where breaches happen.\u003c/p\u003e\n\u003cp\u003eThis article contrasts fatal misconfigurations with implementations that actually pass security audits. The examples are production patterns—configurations running in certified systems today.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"what-auditors-actually-test\"\u003e\u003ca href=\"/posts/session-management-aspnet-authentication/#what-auditors-actually-test\" title=\"What Auditors Actually Test\"\u003eWhat Auditors Actually Test\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eSecurity auditors don\u0026rsquo;t read your documentation. They test:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eIdle timeout\u003c/strong\u003e: Does the session terminate after 15 minutes of inactivity?\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eAbsolute timeout\u003c/strong\u003e: Does an 8-hour session expire even with continuous activity?\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSecure transmission\u003c/strong\u003e: Are cookies \u003ccode\u003eHttpOnly\u003c/code\u003e, \u003ccode\u003eSecure\u003c/code\u003e, and \u003ccode\u003eSameSite=Strict\u003c/code\u003e?\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eLogout invalidation\u003c/strong\u003e: Does clicking logout actually destroy the token?\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eAudit trail\u003c/strong\u003e: Are session events logged for forensic analysis?\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eFailures delay certifications and trigger remediation projects. Worse, they indicate systemic gaps—if session management is wrong, what else got copy-pasted from StackOverflow without verification?\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"fatal-example-session-chaos\"\u003e\u003ca href=\"/posts/session-management-aspnet-authentication/#fatal-example-session-chaos\" title=\"Fatal Example: Session Chaos\"\u003eFatal Example: Session Chaos\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eI\u0026rsquo;ve seen this configuration in production codebases. It appears functional—users authenticate, sessions persist, the application works. But it fails every security audit:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\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// Program.cs - Fatal session management\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=\"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\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\"\u003eAddAuthentication\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eCookieAuthenticationDefaults\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAuthenticationScheme\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/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\"\u003eAddCookie\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=\"c1\"\u003e// Fatal: No expiration - sessions never timeout\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\"\u003eExpireTimeSpan\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\"\u003eMaxValue\u003c/span\u003e\u003cspan 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\"\u003eSlidingExpiration\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=\"c1\"\u003e// Fatal: Cookie accessible to JavaScript\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\"\u003eCookie\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eHttpOnly\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=\"c1\"\u003e// Fatal: Cookie works over HTTP\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\"\u003eCookie\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSecurePolicy\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eCookieSecurePolicy\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eNone\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// Fatal: No CSRF protection\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\"\u003eCookie\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSameSite\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eSameSiteMode\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eNone\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// Fatal: Events not hooked for logging\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\"\u003eEvents\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eCookieAuthenticationEvents\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"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\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\"\u003eUseAuthentication\u003c/span\u003e\u003cspan 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\"\u003eUseAuthorization\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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// Fatal: Logout doesn\u0026#39;t actually sign out\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eapp\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eMapPost\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;/logout\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\"\u003eHttpContext\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\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=\"c1\"\u003e// Just redirects - cookie remains valid!\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eResults\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRedirect\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\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\n\n\n\n\u003ch3 id=\"six-failures-in-twenty-lines\"\u003e\u003ca href=\"/posts/session-management-aspnet-authentication/#six-failures-in-twenty-lines\" title=\"Six Failures In Twenty Lines\"\u003eSix Failures In Twenty Lines\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003eWhy this fails:\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003eTimeSpan.MaxValue\u003c/code\u003e = sessions persist forever. An employee leaves, their session remains valid indefinitely.\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eHttpOnly = false\u003c/code\u003e = XSS attacks steal tokens via \u003ccode\u003edocument.cookie\u003c/code\u003e. One vulnerable dependency compromises every user.\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eSecurePolicy = None\u003c/code\u003e = cookies transmit over HTTP in plaintext. Coffee shop WiFi becomes a credential harvesting operation.\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eSameSite = None\u003c/code\u003e = CSRF attacks succeed. Malicious sites submit authenticated requests on behalf of victims.\u003c/li\u003e\n\u003cli\u003eNo logging = zero visibility into compromises. You won\u0026rsquo;t know you\u0026rsquo;ve been breached until someone else tells you.\u003c/li\u003e\n\u003cli\u003eLogout redirects without \u003ccode\u003eSignOutAsync()\u003c/code\u003e = cookie remains valid. Users think they\u0026rsquo;re logged out. They\u0026rsquo;re not.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eEvery one of these is a direct audit finding. Together, they\u0026rsquo;re a security incident waiting for a trigger.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"secure-cookie-authentication\"\u003e\u003ca href=\"/posts/session-management-aspnet-authentication/#secure-cookie-authentication\" title=\"Secure Cookie Authentication\"\u003eSecure Cookie Authentication\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe compliant configuration (see \u003ca href=\"https://learn.microsoft.com/en-us/aspnet/core/security/authentication/cookie\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eMicrosoft\u0026rsquo;s cookie authentication documentation\u003c/a\u003e for the full API reference):\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\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\"\u003eAddAuthentication\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eCookieAuthenticationDefaults\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAuthenticationScheme\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/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\"\u003eAddCookie\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=\"c1\"\u003e// 15 minutes idle timeout with sliding window\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\"\u003eExpireTimeSpan\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\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\"\u003eSlidingExpiration\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=\"c1\"\u003e// Secure cookie flags - all three are mandatory\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\"\u003eCookie\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eHttpOnly\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\"\u003eoptions\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCookie\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSecurePolicy\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eCookieSecurePolicy\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAlways\u003c/span\u003e\u003cspan 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\"\u003eCookie\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSameSite\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eSameSiteMode\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eStrict\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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\"\u003eEvents\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eCookieAuthenticationEvents\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eOnValidatePrincipal\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\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=\"c1\"\u003e// Enforce 8-hour absolute 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\"\u003eissuedUtc\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\"\u003eProperties\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eIssuedUtc\u003c/span\u003e\u003cspan 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\"\u003eissuedUtc\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eHasValue\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\"\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 \u003cspan class=\"n\"\u003eissuedUtc\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eValue\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eTimeSpan\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFromHours\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e8\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan 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\"\u003eRejectPrincipal\u003c/span\u003e\u003cspan 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\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eHttpContext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSignOutAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e};\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"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// Logout that actually works\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eapp\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eMapPost\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;/logout\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\"\u003eHttpContext\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\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\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSignOutAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eCookieAuthenticationDefaults\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAuthenticationScheme\u003c/span\u003e\u003cspan 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\"\u003eSession\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eClear\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eResults\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRedirect\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\u003cspan class=\"n\"\u003eRequireAuthorization\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\u003eWhat this achieves:\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eSliding expiration\u003c/strong\u003e: 15-minute inactivity timeout, extended on each request\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eAbsolute timeout\u003c/strong\u003e: 8-hour maximum regardless of activity (enforced in \u003ccode\u003eOnValidatePrincipal\u003c/code\u003e)\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eXSS protection\u003c/strong\u003e: \u003ccode\u003eHttpOnly\u003c/code\u003e prevents JavaScript access\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eHTTPS enforcement\u003c/strong\u003e: \u003ccode\u003eSecurePolicy.Always\u003c/code\u003e blocks HTTP transmission\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCSRF protection\u003c/strong\u003e: \u003ccode\u003eSameSite.Strict\u003c/code\u003e blocks cross-origin requests\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eReal logout\u003c/strong\u003e: \u003ccode\u003eSignOutAsync()\u003c/code\u003e destroys the cookie, \u003ccode\u003eSession.Clear()\u003c/code\u003e removes server state\u003c/li\u003e\n\u003c/ul\u003e\n\n\n\n\n\u003ch3 id=\"sharing-data-protection-keys-across-instances\"\u003e\u003ca href=\"/posts/session-management-aspnet-authentication/#sharing-data-protection-keys-across-instances\" title=\"Sharing Data Protection Keys Across Instances\"\u003eSharing Data Protection Keys Across Instances\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eFor multi-instance deployments, persist \u003ca href=\"https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/configuration/overview\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eData Protection keys\u003c/a\u003e to shared storage (Azure Blob, Redis, file share). Without this, application restarts invalidate all sessions—and in a Kubernetes environment with rolling deployments, that means random logouts during every release.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"jwt-tokens-refresh-rotation\"\u003e\u003ca href=\"/posts/session-management-aspnet-authentication/#jwt-tokens-refresh-rotation\" title=\"JWT Tokens: Refresh Rotation\"\u003eJWT Tokens: Refresh Rotation\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eAPI-first architectures can\u0026rsquo;t use cookie-based sessions—there\u0026rsquo;s no browser to manage cookies. JWTs require a different approach: short-lived access tokens paired with rotating refresh tokens. The access token is stateless and self-contained; the refresh token is server-validated and revocable. Microsoft\u0026rsquo;s \u003ca href=\"https://learn.microsoft.com/en-us/aspnet/core/security/authentication/configure-jwt-bearer-authentication\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eJWT bearer authentication documentation\u003c/a\u003e covers the foundational 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=\"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\"\u003eAddAuthentication\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eJwtBearerDefaults\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAuthenticationScheme\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/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\"\u003eAddJwtBearer\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\"\u003eTokenValidationParameters\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eTokenValidationParameters\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eValidateIssuer\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\"\u003eValidateAudience\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\"\u003eValidateLifetime\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\"\u003eValidateIssuerSigningKey\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\"\u003eClockSkew\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=\"c1\"\u003e// No tolerance for expired tokens\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e};\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"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// In production: use Redis or database, not in-memory\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\"\u003erefreshTokenStore\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eDictionary\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eRefreshTokenData\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\u003eThe token endpoint issues 15-minute access tokens and 7-day refresh tokens:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\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;/api/auth/token\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eLoginRequest\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\"\u003eaccessToken\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eGenerateAccessToken\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=\"c1\"\u003e// 15 min expiry\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\"\u003erefreshToken\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eGenerateRefreshToken\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e     \u003cspan class=\"c1\"\u003e// Cryptographically random\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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\"\u003erefreshTokenStore\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003erefreshToken\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\"\u003eRefreshTokenData\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eUserId\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\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\"\u003eExpiresAt\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\"\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=\"n\"\u003eCreatedAt\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=\"c1\"\u003e// Track for absolute timeout\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"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\"\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=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"n\"\u003eaccessToken\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003erefreshToken\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eexpiresIn\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e900\u003c/span\u003e \u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/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=\"enforcing-rotation-on-refresh\"\u003e\u003ca href=\"/posts/session-management-aspnet-authentication/#enforcing-rotation-on-refresh\" title=\"Enforcing Rotation On Refresh\"\u003eEnforcing Rotation On Refresh\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThe refresh endpoint enforces rotation and absolute timeout:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\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;/api/auth/refresh\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eRefreshRequest\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=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(!\u003c/span\u003e\u003cspan class=\"n\"\u003erefreshTokenStore\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\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRefreshToken\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\"\u003etokenData\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eResults\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\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=\"c1\"\u003e// Check expiration\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\"\u003etokenData\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eExpiresAt\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026lt;\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=\"n\"\u003erefreshTokenStore\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRemove\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\"\u003eRefreshToken\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eResults\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\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    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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// Absolute timeout: 8 hours from initial login\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\"\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\"\u003etokenData\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCreatedAt\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eTimeSpan\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFromHours\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e8\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003erefreshTokenStore\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRemove\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\"\u003eRefreshToken\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eResults\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\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    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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// Rotation: revoke old token, issue new one\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003erefreshTokenStore\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRemove\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\"\u003eRefreshToken\u003c/span\u003e\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\"\u003enewRefreshToken\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eGenerateRefreshToken\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003erefreshTokenStore\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003enewRefreshToken\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003etokenData\u003c/span\u003e \u003cspan class=\"n\"\u003ewith\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eExpiresAt\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\"\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=\"p\"\u003e};\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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\"\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=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"n\"\u003eaccessToken\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003erefreshToken\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003enewRefreshToken\u003c/span\u003e \u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/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 mechanisms:\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e15-minute access tokens\u003c/strong\u003e limit stolen token exposure\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eToken rotation\u003c/strong\u003e prevents reuse attacks—each refresh invalidates the previous token\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eAbsolute timeout\u003c/strong\u003e via \u003ccode\u003eCreatedAt\u003c/code\u003e forces re-authentication after 8 hours\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eLogout revocation\u003c/strong\u003e removes refresh token from storage immediately\u003c/li\u003e\n\u003c/ul\u003e\n\n\n\n\n\u003ch3 id=\"why-guids-make-bad-refresh-tokens\"\u003e\u003ca href=\"/posts/session-management-aspnet-authentication/#why-guids-make-bad-refresh-tokens\" title=\"Why GUIDs Make Bad Refresh Tokens\"\u003eWhy GUIDs Make Bad Refresh Tokens\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eUse \u003ccode\u003eRandomNumberGenerator\u003c/code\u003e for refresh tokens—64 bytes minimum. GUIDs aren\u0026rsquo;t cryptographically suitable for security tokens; they\u0026rsquo;re designed for uniqueness, not unpredictability. An attacker who can predict the next token value can forge valid refresh tokens.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"concurrent-session-limits\"\u003e\u003ca href=\"/posts/session-management-aspnet-authentication/#concurrent-session-limits\" title=\"Concurrent Session Limits\"\u003eConcurrent Session Limits\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eSome organizations require single-session enforcement—a new login invalidates existing sessions. Financial services, healthcare, and government systems often mandate this. It prevents credential sharing (one account, one user) and limits compromise impact (stealing credentials doesn\u0026rsquo;t grant parallel access). The implementation uses \u003ca href=\"https://learn.microsoft.com/en-us/aspnet/core/performance/caching/distributed\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003edistributed caching\u003c/a\u003e for cross-instance session tracking:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\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\"\u003eAddStackExchangeRedisCache\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\"\u003eConfiguration\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\u003cspan class=\"n\"\u003eGetConnectionString\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Redis\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=\"n\"\u003eoptions\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eEvents\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eCookieAuthenticationEvents\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eOnSigningIn\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\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\"\u003ecache\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\"\u003eHttpContext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRequestServices\u003c/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\"\u003eGetRequiredService\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eIDistributedCache\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=\"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\"\u003ePrincipal\u003c/span\u003e\u003cspan class=\"p\"\u003e?.\u003c/span\u003e\u003cspan class=\"n\"\u003eFindFirst\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;sub\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)?.\u003c/span\u003e\u003cspan class=\"n\"\u003eValue\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003euserId\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=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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\"\u003esessionId\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=\"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=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eProperties\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eItems\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;SessionId\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003esessionId\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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\"\u003ecache\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSetStringAsync\u003c/span\u003e\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;session:{userId}\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003esessionId\u003c/span\u003e\u003cspan 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\"\u003eDistributedCacheEntryOptions\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"n\"\u003eAbsoluteExpirationRelativeToNow\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\"\u003eFromHours\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e8\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"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\"\u003eOnValidatePrincipal\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\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\"\u003ecache\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\"\u003eHttpContext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRequestServices\u003c/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\"\u003eGetRequiredService\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eIDistributedCache\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=\"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\"\u003ePrincipal\u003c/span\u003e\u003cspan class=\"p\"\u003e?.\u003c/span\u003e\u003cspan class=\"n\"\u003eFindFirst\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;sub\u0026#34;\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=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003esessionId\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\"\u003eProperties\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\"\u003eGetValueOrDefault\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;SessionId\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\"\u003euserId\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=\"n\"\u003esessionId\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=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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\"\u003ecurrentSession\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003ecache\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=\"s\"\u003e$\u0026#34;session:{userId}\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ecurrentSession\u003c/span\u003e \u003cspan class=\"p\"\u003e!=\u003c/span\u003e \u003cspan class=\"n\"\u003esessionId\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan 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\"\u003eRejectPrincipal\u003c/span\u003e\u003cspan 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\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eHttpContext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSignOutAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/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 a user logs in from a new device, their previous session is invalidated on the next request. The Redis cache tracks which session ID is current—older sessions fail validation and force re-authentication.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"unexpected-logouts-as-breach-signals\"\u003e\u003ca href=\"/posts/session-management-aspnet-authentication/#unexpected-logouts-as-breach-signals\" title=\"Unexpected Logouts As Breach Signals\"\u003eUnexpected Logouts As Breach Signals\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThis pattern also provides breach detection. If a user reports they were logged out unexpectedly, someone else authenticated with their credentials. That\u0026rsquo;s an incident worth investigating.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"practical-takeaways\"\u003e\u003ca href=\"/posts/session-management-aspnet-authentication/#practical-takeaways\" title=\"Practical Takeaways\"\u003ePractical Takeaways\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eSession security reduces to a few non-negotiable configurations:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eBoth timeout types are mandatory\u003c/strong\u003e: 15-minute sliding expiration for idle sessions, 8-hour absolute maximum regardless of activity.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eCookie flags aren\u0026rsquo;t optional\u003c/strong\u003e: \u003ccode\u003eHttpOnly\u003c/code\u003e, \u003ccode\u003eSecure\u003c/code\u003e, \u003ccode\u003eSameSite=Strict\u003c/code\u003e. Each prevents a specific attack class.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eLogout must call \u003ccode\u003eSignOutAsync()\u003c/code\u003e\u003c/strong\u003e: Redirecting without destroying the token is cosmetic, not functional.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eJWT refresh tokens rotate\u003c/strong\u003e: Each refresh invalidates the previous token. Store the original creation time for absolute timeout enforcement.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eLog authentication events\u003c/strong\u003e: Session creation, renewal, timeout, and logout provide the audit trail auditors expect.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003ePersist Data Protection keys\u003c/strong\u003e: In multi-instance deployments, shared key storage prevents session invalidation on restarts.\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eThese patterns aren\u0026rsquo;t theoretical—they\u0026rsquo;re what certified systems actually run. I\u0026rsquo;ve implemented them across multiple ISO 27001 and SOC 2 audits. The configurations pass because they address the actual threat model, not because they check compliance boxes.\u003c/p\u003e\n\u003cp\u003eSecurity controls work when enforced in code, not documented in PDFs. The auditor doesn\u0026rsquo;t care what your security policy says; they care what your application does when they test it.\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2026-03-05T17:00:00+01:00","id":"https://daily-devops.net/posts/session-management-aspnet-authentication/","language":"en","summary":"Most ASP.NET session configs pass code review but fail security audits. Learn what actually matters for cookie authentication and JWT tokens.","tags":["iso-standards","security","authentication","dotnet","bestpractices"],"title":"Your Logout Button Is Lying: ASP.NET Session Security Done Right","url":"https://daily-devops.net/posts/session-management-aspnet-authentication/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003e\u003cstrong\u003eThat boolean column you call \u0026ldquo;consent\u0026rdquo;? It\u0026rsquo;s a liability bomb with a ticking clock.\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eI\u0026rsquo;ve lost count of how many production codebases I\u0026rsquo;ve audited where \u0026ldquo;consent management\u0026rdquo; meant a single \u003ccode\u003eAcceptedTerms\u003c/code\u003e property. Cookie banners everywhere. \u0026ldquo;Accept All\u0026rdquo; buttons doing the heavy lifting. Privacy policies buried in legal prose with one acknowledgment click covering everything from marketing emails to behavioral profiling.\u003c/p\u003e\n\u003cp\u003eThen the auditor asks: \u0026ldquo;Show me the consent record for user X, including when they agreed, to what version of your policy, and how they can withdraw.\u0026rdquo; And suddenly that boolean column looks very, very thin.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eThat\u0026rsquo;s not consent management. That\u0026rsquo;s compliance theater.\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://www.iso.org/standard/71670.html\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eISO/IEC 27701\u003c/a\u003e Control 7.3.2 (\u0026ldquo;Providing data subjects with choices\u0026rdquo;) and \u003ca href=\"https://gdpr-info.eu/art-7-gdpr/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eGDPR (General Data Protection Regulation) Article 7\u003c/a\u003e establish clear requirements: consent must be \u003cstrong\u003especific, informed, freely given, and provable\u003c/strong\u003e. Withdrawal must be as easy as granting. And the audit trail must survive regulatory scrutiny.\u003c/p\u003e\n\u003cp\u003eMost implementations fail all of these. Here\u0026rsquo;s how to build consent management in .NET that actually works.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-fatal-pattern-consent-as-an-afterthought\"\u003e\u003ca href=\"/posts/consent-management-aspnet-identity/#the-fatal-pattern-consent-as-an-afterthought\" title=\"The Fatal Pattern: Consent as an Afterthought\"\u003eThe Fatal Pattern: Consent as an Afterthought\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThis is the implementation I\u0026rsquo;ve seen deployed at companies processing millions of user records:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\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\"\u003eApplicationUser\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eIdentityUser\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\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\"\u003eAcceptedTerms\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// The entire \u0026#34;consent system\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\"\u003eDateTime\u003c/span\u003e\u003cspan class=\"p\"\u003e?\u003c/span\u003e \u003cspan class=\"n\"\u003eAcceptedTermsDate\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// Registration: one checkbox, forever consent\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003euser\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eApplicationUser\u003c/span\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eAcceptedTerms\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\"\u003eAcceptedTermsDate\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\n\u003c/span\u003e\u003c/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\u003eFive critical failures in six lines:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eNo granularity.\u003c/strong\u003e One checkbox covers terms, privacy policy, marketing emails, analytics, and third-party data sharing. ISO 27701 7.3.2 explicitly requires separate consent for each distinct processing purpose.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eNo audit trail.\u003c/strong\u003e When \u003ccode\u003eAcceptedTerms\u003c/code\u003e flips from \u003ccode\u003etrue\u003c/code\u003e to \u003ccode\u003efalse\u003c/code\u003e, you\u0026rsquo;ve lost the original consent record. No timestamp of the change. No reason captured. No policy version recorded.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eNo expiration.\u003c/strong\u003e Consent granted under your 2020 privacy policy remains \u0026ldquo;valid\u0026rdquo; under your 2026 policy, even though the processing activities may be completely different. GDPR Article 7(3) requires consent to remain valid only for the specific purpose and context in which it was given.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eNo withdrawal mechanism.\u003c/strong\u003e Users can\u0026rsquo;t selectively opt out of marketing while staying opted in to analytics. It\u0026rsquo;s nuclear: all or nothing. This violates ISO 27701 7.3.4\u0026rsquo;s requirement for granular control.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eNo enforcement.\u003c/strong\u003e Nothing stops your email service from blasting promotional content to users who never consented to marketing. The consent record is decorative, not functional.\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eThis pattern satisfies one requirement: showing auditors that a checkbox exists. It satisfies nothing else.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-correct-pattern-purpose-specific-consent-with-audit\"\u003e\u003ca href=\"/posts/consent-management-aspnet-identity/#the-correct-pattern-purpose-specific-consent-with-audit\" title=\"The Correct Pattern: Purpose-Specific Consent with Audit\"\u003eThe Correct Pattern: Purpose-Specific Consent with Audit\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eReal consent management requires four components working together: \u003cstrong\u003egranular consent records\u003c/strong\u003e, \u003cstrong\u003eimmutable audit logs\u003c/strong\u003e, \u003cstrong\u003evalidation middleware\u003c/strong\u003e, and \u003cstrong\u003eexpiration workflows\u003c/strong\u003e. Each piece addresses a specific regulatory requirement. Let\u0026rsquo;s build them.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"step-1-model-consent-granularly\"\u003e\u003ca href=\"/posts/consent-management-aspnet-identity/#step-1-model-consent-granularly\" title=\"Step 1: Model Consent Granularly\"\u003eStep 1: Model Consent Granularly\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eStart by defining consent purposes as distinct entities. Each represents a specific data processing activity requiring explicit user approval:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\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\"\u003eConsentPurpose\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eTermsOfService\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e              \u003cspan class=\"c1\"\u003e// Required: platform access\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003ePrivacyPolicy\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e               \u003cspan class=\"c1\"\u003e// Required: data processing basis\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eMarketingEmails\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e             \u003cspan class=\"c1\"\u003e// Optional: promotional content\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eAnalyticsTracking\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e           \u003cspan class=\"c1\"\u003e// Optional: usage telemetry\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eThirdPartyDataSharing\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e       \u003cspan class=\"c1\"\u003e// Optional: partner integrations\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eProfilingAndPersonalization\u003c/span\u003e  \u003cspan class=\"c1\"\u003e// Optional: behavioral targeting\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"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\"\u003eUserConsent\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\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\"\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\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\"\u003eUserId\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=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eConsentPurpose\u003c/span\u003e \u003cspan class=\"n\"\u003ePurpose\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"k\"\u003eget\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"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=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003ebool\u003c/span\u003e \u003cspan class=\"n\"\u003eIsGranted\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=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eDateTime\u003c/span\u003e \u003cspan class=\"n\"\u003eGrantedAt\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=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eDateTime\u003c/span\u003e\u003cspan class=\"p\"\u003e?\u003c/span\u003e \u003cspan class=\"n\"\u003eExpiresAt\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// Consent can expire\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\"\u003ePolicyVersion\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// Which policy version was accepted\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\"\u003eIpAddress\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// Audit context\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003estring?\u003c/span\u003e \u003cspan class=\"n\"\u003eUserAgent\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=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eApplicationUser\u003c/span\u003e \u003cspan class=\"n\"\u003eUser\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\u003eThis separates each consent purpose into its own record. Marketing consent lives independently from analytics consent. Withdrawing one leaves others intact. Exactly what ISO 27701 7.3.4 requires.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"step-2-create-an-immutable-audit-log\"\u003e\u003ca href=\"/posts/consent-management-aspnet-identity/#step-2-create-an-immutable-audit-log\" title=\"Step 2: Create an Immutable Audit Log\"\u003eStep 2: Create an Immutable Audit Log\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eConsent decisions change. ISO 27701 requires proving not just current state, but complete history. The key insight: \u003cstrong\u003enever update consent records, always insert new ones.\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\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eConsentAuditLog\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\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\"\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\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\"\u003eUserId\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=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eConsentPurpose\u003c/span\u003e \u003cspan class=\"n\"\u003ePurpose\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"k\"\u003eget\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"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=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003ebool\u003c/span\u003e \u003cspan class=\"n\"\u003eWasGranted\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// Previous state\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\"\u003eIsGranted\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// New state\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\"\u003eChangedAt\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=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eChangeReason\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// User action, policy update, or expiration\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\"\u003ePolicyVersion\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=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003estring?\u003c/span\u003e \u003cspan class=\"n\"\u003eIpAddress\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=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003estring?\u003c/span\u003e \u003cspan class=\"n\"\u003eUserAgent\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=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eApplicationUser\u003c/span\u003e \u003cspan class=\"n\"\u003eUser\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\u003eEvery consent change (granted, withdrawn, expired) creates an audit entry. This log is \u003cstrong\u003eappend-only\u003c/strong\u003e: no updates, no deletes. When an auditor asks \u0026ldquo;What did user X consent to on March 15, 2024, and under what policy version?\u0026rdquo; you have the answer.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"step-3-build-a-consent-service\"\u003e\u003ca href=\"/posts/consent-management-aspnet-identity/#step-3-build-a-consent-service\" title=\"Step 3: Build a Consent Service\"\u003eStep 3: Build a Consent Service\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eEncapsulate consent logic in a service that enforces business rules. The interface is straightforward:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\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\"\u003eIConsentService\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\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\"\u003eHasValidConsentAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003euserId\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eConsentPurpose\u003c/span\u003e \u003cspan class=\"n\"\u003epurpose\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eTask\u003c/span\u003e \u003cspan class=\"n\"\u003eGrantConsentAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003euserId\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eConsentPurpose\u003c/span\u003e \u003cspan class=\"n\"\u003epurpose\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003epolicyVersion\u003c/span\u003e\u003cspan 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=\"p\"\u003e?\u003c/span\u003e \u003cspan class=\"n\"\u003eexpiresAt\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=\"kt\"\u003estring?\u003c/span\u003e \u003cspan class=\"n\"\u003eipAddress\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=\"kt\"\u003estring?\u003c/span\u003e \u003cspan class=\"n\"\u003euserAgent\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\"\u003eTask\u003c/span\u003e \u003cspan class=\"n\"\u003eWithdrawConsentAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003euserId\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eConsentPurpose\u003c/span\u003e \u003cspan class=\"n\"\u003epurpose\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\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    \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\"\u003eUserConsent\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eGetUserConsentsAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\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\"\u003eTask\u003c/span\u003e \u003cspan class=\"n\"\u003eExpireStaleConsentsAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/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 implementation enforces critical invariants. Here\u0026rsquo;s the consent validation (note the expiration 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=\"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\"\u003eHasValidConsentAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003euserId\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eConsentPurpose\u003c/span\u003e \u003cspan class=\"n\"\u003epurpose\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003econsent\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_context\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUserConsents\u003c/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\"\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\"\u003eUserId\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"n\"\u003euserId\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e \u003cspan class=\"n\"\u003ec\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePurpose\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"n\"\u003epurpose\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eOrderByDescending\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\"\u003eGrantedAt\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFirstOrDefaultAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003econsent\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e \u003cspan class=\"p\"\u003e||\u003c/span\u003e \u003cspan class=\"p\"\u003e!\u003c/span\u003e\u003cspan class=\"n\"\u003econsent\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eIsGranted\u003c/span\u003e\u003cspan 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\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Expired consent is no consent\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\"\u003econsent\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eExpiresAt\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eHasValue\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e \u003cspan class=\"n\"\u003econsent\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eExpiresAt\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eValue\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026lt;\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=\"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;Consent {Purpose} for user {UserId} has expired\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003epurpose\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\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=\"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=\"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\u003eGranting consent creates both a record and an audit entry. Notice we capture the previous state for the audit trail:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\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\"\u003eGrantConsentAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003euserId\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eConsentPurpose\u003c/span\u003e \u003cspan class=\"n\"\u003epurpose\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003epolicyVersion\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\"\u003eexpiresAt\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=\"kt\"\u003estring?\u003c/span\u003e \u003cspan class=\"n\"\u003eipAddress\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=\"kt\"\u003estring?\u003c/span\u003e \u003cspan class=\"n\"\u003euserAgent\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=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003epreviousConsent\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\"\u003eUserConsents\u003c/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\"\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\"\u003eUserId\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"n\"\u003euserId\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e \u003cspan class=\"n\"\u003ec\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePurpose\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"n\"\u003epurpose\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eOrderByDescending\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\"\u003eGrantedAt\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFirstOrDefaultAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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_context\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUserConsents\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAdd\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eUserConsent\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eUserId\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003euserId\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ePurpose\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003epurpose\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eIsGranted\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\"\u003eGrantedAt\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\"\u003eExpiresAt\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eexpiresAt\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003ePolicyVersion\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003epolicyVersion\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eIpAddress\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eipAddress\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eUserAgent\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003euserAgent\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"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\"\u003e_context\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eConsentAuditLogs\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAdd\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eConsentAuditLog\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eUserId\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003euserId\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ePurpose\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003epurpose\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eWasGranted\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003epreviousConsent\u003c/span\u003e\u003cspan class=\"p\"\u003e?.\u003c/span\u003e\u003cspan class=\"n\"\u003eIsGranted\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\"\u003eIsGranted\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\"\u003eChangedAt\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\"\u003eChangeReason\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003epreviousConsent\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;Initial consent\u0026#34;\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Consent renewed\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\"\u003ePolicyVersion\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003epolicyVersion\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eIpAddress\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eipAddress\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eUserAgent\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003euserAgent\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_context\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSaveChangesAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/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\u003eWithdrawal follows the same pattern. It doesn\u0026rsquo;t delete records, it creates a new \u0026ldquo;not granted\u0026rdquo; record:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\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\"\u003eWithdrawConsentAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003euserId\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eConsentPurpose\u003c/span\u003e \u003cspan class=\"n\"\u003epurpose\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\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\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\"\u003ecurrentConsent\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\"\u003eUserConsents\u003c/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\"\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\"\u003eUserId\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"n\"\u003euserId\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e \u003cspan class=\"n\"\u003ec\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePurpose\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"n\"\u003epurpose\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eOrderByDescending\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\"\u003eGrantedAt\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFirstOrDefaultAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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\"\u003ecurrentConsent\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=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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_context\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUserConsents\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAdd\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eUserConsent\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eUserId\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003euserId\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ePurpose\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003epurpose\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eIsGranted\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=\"n\"\u003eGrantedAt\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\"\u003ePolicyVersion\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ecurrentConsent\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePolicyVersion\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"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\"\u003e_context\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eConsentAuditLogs\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAdd\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eConsentAuditLog\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eUserId\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003euserId\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ePurpose\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003epurpose\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eWasGranted\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ecurrentConsent\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eIsGranted\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eIsGranted\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=\"n\"\u003eChangedAt\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\"\u003eChangeReason\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        \u003cspan class=\"n\"\u003ePolicyVersion\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ecurrentConsent\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePolicyVersion\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_context\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSaveChangesAsync\u003c/span\u003e\u003cspan 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;Consent withdrawn: {Purpose} for user {UserId}\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003epurpose\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\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\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eEvery state change is logged. Nothing is deleted. The audit trail is complete.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"step-4-enforce-consent-with-middleware\"\u003e\u003ca href=\"/posts/consent-management-aspnet-identity/#step-4-enforce-consent-with-middleware\" title=\"Step 4: Enforce Consent with Middleware\"\u003eStep 4: Enforce Consent with Middleware\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eSprinkling consent checks across controller actions is error-prone and inconsistent. Use \u003ca href=\"https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003emiddleware\u003c/a\u003e to enforce consent requirements before requests reach your application logic:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\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\"\u003eConsentValidationMiddleware\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eRequestDelegate\u003c/span\u003e \u003cspan class=\"n\"\u003e_next\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eILogger\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eConsentValidationMiddleware\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003e_logger\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eConsentValidationMiddleware\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eRequestDelegate\u003c/span\u003e \u003cspan class=\"n\"\u003enext\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \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\"\u003eConsentValidationMiddleware\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=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003e_next\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003enext\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003e_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=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e \u003cspan class=\"n\"\u003eInvokeAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eHttpContext\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eIConsentService\u003c/span\u003e \u003cspan class=\"n\"\u003econsentService\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eUserManager\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eApplicationUser\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003euserManager\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan 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\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUser\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eIdentity\u003c/span\u003e\u003cspan class=\"p\"\u003e?.\u003c/span\u003e\u003cspan class=\"n\"\u003eIsAuthenticated\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=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_next\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \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=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003euser\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003euserManager\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetUserAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUser\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003euser\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_next\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e \u003cspan class=\"k\"\u003ereturn\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=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003ehasTerms\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003econsentService\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eHasValidConsentAsync\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\"\u003eId\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eConsentPurpose\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTermsOfService\u003c/span\u003e\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\"\u003ehasPrivacy\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003econsentService\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eHasValidConsentAsync\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\"\u003eId\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eConsentPurpose\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePrivacyPolicy\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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\"\u003ehasTerms\u003c/span\u003e \u003cspan class=\"p\"\u003e||\u003c/span\u003e \u003cspan class=\"p\"\u003e!\u003c/span\u003e\u003cspan class=\"n\"\u003ehasPrivacy\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan 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\"\u003eLogWarning\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;User {UserId} missing required consent\u0026#34;\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\"\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\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eResponse\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRedirect\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;/Account/ConsentRequired\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\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_next\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"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// Program.cs registration\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\"\u003eUseAuthentication\u003c/span\u003e\u003cspan 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\"\u003eUseAuthorization\u003c/span\u003e\u003cspan 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\"\u003eUseMiddleware\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eConsentValidationMiddleware\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\u003eUsers without valid required consents get redirected to update their preferences. No controller action ever executes without consent validation passing first.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"step-5-build-a-user-facing-consent-dashboard\"\u003e\u003ca href=\"/posts/consent-management-aspnet-identity/#step-5-build-a-user-facing-consent-dashboard\" title=\"Step 5: Build a User-Facing Consent Dashboard\"\u003eStep 5: Build a User-Facing Consent Dashboard\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eISO 27701 7.3.4 requires giving users a mechanism to modify or withdraw consent. Build a dashboard where users see exactly what they\u0026rsquo;ve agreed to and can change their minds:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\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\"\u003eConsentManagementModel\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003ePageModel\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eIConsentService\u003c/span\u003e \u003cspan class=\"n\"\u003e_consentService\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eUserManager\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eApplicationUser\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003e_userManager\u003c/span\u003e\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_configuration\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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\"\u003eIEnumerable\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eConsentViewModel\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eConsents\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\"\u003eOnGetAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003euser\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_userManager\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetUserAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eUser\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003econsents\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_consentService\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetUserConsentsAsync\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\"\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=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003epolicyVersion\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003e_configuration\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;ConsentManagement:PolicyVersion\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\"\u003eConsents\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=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eBuildConsent\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eConsentPurpose\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTermsOfService\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Terms of Service\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;Required to use the platform\u0026#34;\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\"\u003econsents\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003epolicyVersion\u003c/span\u003e\u003cspan class=\"p\"\u003e),\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eBuildConsent\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eConsentPurpose\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eMarketingEmails\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Marketing Communications\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;Receive promotional emails and product updates\u0026#34;\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\"\u003econsents\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003epolicyVersion\u003c/span\u003e\u003cspan class=\"p\"\u003e),\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eBuildConsent\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eConsentPurpose\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAnalyticsTracking\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Analytics \u0026amp; Performance\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;Help us improve with usage analytics\u0026#34;\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\"\u003econsents\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003epolicyVersion\u003c/span\u003e\u003cspan class=\"p\"\u003e),\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eBuildConsent\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eConsentPurpose\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eThirdPartyDataSharing\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Third-Party Data Sharing\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;Share anonymized data with trusted partners\u0026#34;\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\"\u003econsents\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003epolicyVersion\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e};\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"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\"\u003eIActionResult\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eOnPostAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eDictionary\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003ebool\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003econsentChoices\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003euser\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_userManager\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetUserAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eUser\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003epolicyVersion\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003e_configuration\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;ConsentManagement:PolicyVersion\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\"\u003eipAddress\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eHttpContext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eConnection\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRemoteIpAddress\u003c/span\u003e\u003cspan class=\"p\"\u003e?.\u003c/span\u003e\u003cspan class=\"n\"\u003eToString\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003euserAgent\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eHttpContext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRequest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eHeaders\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;User-Agent\u0026#34;\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\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=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ekey\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003egranted\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"k\"\u003ein\u003c/span\u003e \u003cspan class=\"n\"\u003econsentChoices\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan 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\"\u003eEnum\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTryParse\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eConsentPurpose\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;(\u003c/span\u003e\u003cspan class=\"n\"\u003ekey\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\"\u003epurpose\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e \u003cspan class=\"k\"\u003econtinue\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \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\"\u003egranted\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \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// Marketing consent expires after 2 years\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\"\u003eexpiresAt\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003epurpose\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"n\"\u003eConsentPurpose\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eMarketingEmails\u003c/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\"\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\"\u003eAddYears\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e2\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eDateTime\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\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_consentService\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGrantConsentAsync\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\"\u003eId\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003epurpose\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                    \u003cspan class=\"n\"\u003epolicyVersion\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eexpiresAt\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eipAddress\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003euserAgent\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan 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\"\u003e_consentService\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWithdrawConsentAsync\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\"\u003eId\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003epurpose\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                    \u003cspan class=\"s\"\u003e\u0026#34;User preference update\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eRedirectToPage\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"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=\"n\"\u003eConsentViewModel\u003c/span\u003e \u003cspan class=\"n\"\u003eBuildConsent\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eConsentPurpose\u003c/span\u003e \u003cspan class=\"n\"\u003epurpose\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=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003edesc\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003ebool\u003c/span\u003e \u003cspan class=\"n\"\u003erequired\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eIEnumerable\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eUserConsent\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003econsents\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eversion\u003c/span\u003e\u003cspan class=\"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\"\u003enew\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"n\"\u003ePurpose\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003epurpose\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eDisplayName\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\"\u003eDescription\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003edesc\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eIsRequired\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003erequired\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ePolicyVersion\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eversion\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eIsGranted\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003econsents\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFirstOrDefault\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\"\u003ePurpose\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"n\"\u003epurpose\u003c/span\u003e\u003cspan class=\"p\"\u003e)?.\u003c/span\u003e\u003cspan class=\"n\"\u003eIsGranted\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=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eUsers see exactly what they\u0026rsquo;ve consented to, when it expires, and can toggle optional consents freely. Required consents (Terms, Privacy) can only be withdrawn by closing the account.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"step-6-automate-consent-expiration-with-azure-functions\"\u003e\u003ca href=\"/posts/consent-management-aspnet-identity/#step-6-automate-consent-expiration-with-azure-functions\" title=\"Step 6: Automate Consent Expiration with Azure Functions\"\u003eStep 6: Automate Consent Expiration with Azure Functions\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eConsent shouldn\u0026rsquo;t live forever. Use \u003ca href=\"https://learn.microsoft.com/en-us/azure/azure-functions/functions-overview\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eAzure Functions\u003c/a\u003e to automatically expire stale consents:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\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\"\u003eConsentExpirationFunction\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eIConsentService\u003c/span\u003e \u003cspan class=\"n\"\u003e_consentService\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \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\"\u003eConsentExpirationFunction\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003e_logger\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eConsentExpirationFunction\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eIConsentService\u003c/span\u003e \u003cspan class=\"n\"\u003econsentService\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eILogger\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eConsentExpirationFunction\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=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003e_consentService\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003econsentService\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"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\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    [FunctionName(\u0026#34;ExpireStaleConsents\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\"\u003eRun\u003c/span\u003e\u003cspan class=\"p\"\u003e([\u003c/span\u003e\u003cspan class=\"n\"\u003eTimerTrigger\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\"\u003eTimerInfo\u003c/span\u003e \u003cspan class=\"n\"\u003etimer\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e  \u003cspan class=\"c1\"\u003e// Daily at 2 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        \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 consent expiration check\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\"\u003e_consentService\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eExpireStaleConsentsAsync\u003c/span\u003e\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// Optional: Send renewal reminders for consents expiring within 30 days\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/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 function runs daily, automatically expiring consents past their validity period and creating audit entries for each expiration. Users can be notified to renew consents before they lapse.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"step-7-respect-consent-in-application-insights\"\u003e\u003ca href=\"/posts/consent-management-aspnet-identity/#step-7-respect-consent-in-application-insights\" title=\"Step 7: Respect Consent in Application Insights\"\u003eStep 7: Respect Consent in Application Insights\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eWhen a user withdraws analytics consent, your telemetry system must honor that choice immediately. Configure \u003ca href=\"https://learn.microsoft.com/en-us/azure/azure-monitor/app/app-insights-overview\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eApplication Insights\u003c/a\u003e to check consent before logging.\u003c/p\u003e\n\u003cp\u003eThe challenge: \u003ccode\u003eITelemetryInitializer.Initialize()\u003c/code\u003e is synchronous, but our consent service is async. Using \u003ccode\u003e.Result\u003c/code\u003e or \u003ccode\u003e.GetAwaiter().GetResult()\u003c/code\u003e risks deadlocks. Instead, cache consent status in the \u003ccode\u003eHttpContext.Items\u003c/code\u003e during the request pipeline:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\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// Middleware to cache consent status early in the pipeline\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\"\u003eConsentCachingMiddleware\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eRequestDelegate\u003c/span\u003e \u003cspan class=\"n\"\u003e_next\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\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\"\u003eConsentCachingMiddleware\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eRequestDelegate\u003c/span\u003e \u003cspan class=\"n\"\u003enext\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003e_next\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003enext\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e \u003cspan class=\"n\"\u003eInvokeAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eHttpContext\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eIConsentService\u003c/span\u003e \u003cspan class=\"n\"\u003econsentService\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUser\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eIdentity\u003c/span\u003e\u003cspan class=\"p\"\u003e?.\u003c/span\u003e\u003cspan class=\"n\"\u003eIsAuthenticated\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\"\u003euserId\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUser\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFindFirstValue\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eClaimTypes\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eNameIdentifier\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\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\"\u003ehasAnalyticsConsent\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003econsentService\u003c/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\"\u003eHasValidConsentAsync\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\"\u003eConsentPurpose\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAnalyticsTracking\u003c/span\u003e\u003cspan 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\"\u003eItems\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;HasAnalyticsConsent\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ehasAnalyticsConsent\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan 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_next\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"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// Telemetry initializer reads from cache (no async needed)\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\"\u003eConsentAwareTelemetryInitializer\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eITelemetryInitializer\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\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\"\u003eIHttpContextAccessor\u003c/span\u003e \u003cspan class=\"n\"\u003e_httpContextAccessor\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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\"\u003eConsentAwareTelemetryInitializer\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eIHttpContextAccessor\u003c/span\u003e \u003cspan class=\"n\"\u003ehttpContextAccessor\u003c/span\u003e\u003cspan class=\"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_httpContextAccessor\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ehttpContextAccessor\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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\"\u003eInitialize\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eITelemetry\u003c/span\u003e \u003cspan class=\"n\"\u003etelemetry\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \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\"\u003econtext\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003e_httpContextAccessor\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eHttpContext\u003c/span\u003e\u003cspan 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\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e?.\u003c/span\u003e\u003cspan class=\"n\"\u003eUser\u003c/span\u003e\u003cspan class=\"p\"\u003e?.\u003c/span\u003e\u003cspan class=\"n\"\u003eIdentity\u003c/span\u003e\u003cspan class=\"p\"\u003e?.\u003c/span\u003e\u003cspan class=\"n\"\u003eIsAuthenticated\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=\"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\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003ehasConsent\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eItems\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;HasAnalyticsConsent\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"k\"\u003eas\u003c/span\u003e \u003cspan class=\"kt\"\u003ebool?\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\"\u003ehasConsent\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e \u003cspan class=\"n\"\u003etelemetry\u003c/span\u003e \u003cspan class=\"k\"\u003eis\u003c/span\u003e \u003cspan class=\"n\"\u003eISupportProperties\u003c/span\u003e \u003cspan class=\"n\"\u003epropTelemetry\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003epropTelemetry\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eProperties\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;UserId\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;anonymous\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Program.cs: Register middleware before telemetry initializer runs\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\"\u003eUseAuthentication\u003c/span\u003e\u003cspan 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\"\u003eUseMiddleware\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eConsentCachingMiddleware\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\"\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\"\u003eAddApplicationInsightsTelemetry\u003c/span\u003e\u003cspan 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\"\u003eAddSingleton\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eITelemetryInitializer\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eConsentAwareTelemetryInitializer\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\u003eYour analytics now respects user consent choices in real time. No consent, no tracking. As it should be.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"database-migration-setting-up-the-schema\"\u003e\u003ca href=\"/posts/consent-management-aspnet-identity/#database-migration-setting-up-the-schema\" title=\"Database Migration: Setting Up the Schema\"\u003eDatabase Migration: Setting Up the Schema\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eAdd the consent tables to your \u003ca href=\"https://learn.microsoft.com/en-us/ef/core/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eEntity Framework Core\u003c/a\u003e 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=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eApplicationDbContext\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eIdentityDbContext\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eApplicationUser\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\"\u003eDbSet\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eUserConsent\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eUserConsents\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=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eDbSet\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eConsentAuditLog\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eConsentAuditLogs\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\"\u003eprotected\u003c/span\u003e \u003cspan class=\"kd\"\u003eoverride\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eOnModelCreating\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eModelBuilder\u003c/span\u003e \u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ebase\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eOnModelCreating\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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\"\u003eEntity\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eUserConsent\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;(\u003c/span\u003e\u003cspan class=\"n\"\u003eentity\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\"\u003eentity\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eHasKey\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ee\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ee\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\"\u003eentity\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eHasIndex\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ee\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\"\u003ee\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\"\u003ee\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePurpose\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ee\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGrantedAt\u003c/span\u003e \u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eentity\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eProperty\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ee\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ee\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePolicyVersion\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\u003cspan class=\"n\"\u003eHasMaxLength\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e50\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\u003cspan class=\"n\"\u003eIsRequired\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eentity\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eProperty\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ee\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ee\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eIpAddress\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\u003cspan class=\"n\"\u003eHasMaxLength\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e45\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e  \u003cspan class=\"c1\"\u003e// IPv6 length\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eentity\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eHasOne\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ee\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ee\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\"\u003eWithMany\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/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\"\u003eHasForeignKey\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ee\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ee\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\"\u003eOnDelete\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eDeleteBehavior\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCascade\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"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\"\u003eEntity\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eConsentAuditLog\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;(\u003c/span\u003e\u003cspan class=\"n\"\u003eentity\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\"\u003eentity\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eHasKey\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ee\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ee\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\"\u003eentity\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eHasIndex\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ee\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\"\u003ee\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\"\u003ee\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eChangedAt\u003c/span\u003e \u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eentity\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eProperty\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ee\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ee\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eChangeReason\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\u003cspan class=\"n\"\u003eHasMaxLength\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e500\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\u003cspan class=\"n\"\u003eIsRequired\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eentity\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eProperty\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ee\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ee\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePolicyVersion\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\u003cspan class=\"n\"\u003eHasMaxLength\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e50\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\u003cspan class=\"n\"\u003eIsRequired\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eentity\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eHasOne\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ee\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ee\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\"\u003eWithMany\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/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\"\u003eHasForeignKey\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ee\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ee\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\"\u003eOnDelete\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eDeleteBehavior\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCascade\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/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\u003eThen create and apply the migration:\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 ef migrations add AddConsentManagement\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\n\n\n\n\u003ch2 id=\"what-this-actually-achieves\"\u003e\u003ca href=\"/posts/consent-management-aspnet-identity/#what-this-actually-achieves\" title=\"What This Actually Achieves\"\u003eWhat This Actually Achieves\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThis implementation satisfies \u003ca href=\"https://www.iso.org/standard/71670.html\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eISO 27701 Control 7.3.2\u003c/a\u003e and \u003ca href=\"https://gdpr-info.eu/art-7-gdpr/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eGDPR Article 7\u003c/a\u003e requirements:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eSpecific and granular.\u003c/strong\u003e Each consent purpose is tracked separately. Users can consent to marketing emails without consenting to third-party data sharing. Withdrawing one leaves others intact.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eInformed.\u003c/strong\u003e The dashboard clearly describes what each consent enables, with policy version references so users know exactly what they\u0026rsquo;re agreeing to.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eFreely given.\u003c/strong\u003e Optional consents can be granted or withdrawn independently. There\u0026rsquo;s no \u0026ldquo;consent to everything or leave\u0026rdquo; coercion.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eProvable.\u003c/strong\u003e The immutable audit log provides complete history of every consent decision: timestamps, policy versions, IP addresses, user agents. When an auditor asks what a user agreed to three years ago, you have the answer.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eRevocable.\u003c/strong\u003e Users withdraw consent through the dashboard, and the system enforces that withdrawal across all data processing. Analytics stops. Marketing emails stop. No manual intervention required.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eExpiring.\u003c/strong\u003e Consents expire automatically, requiring periodic re-confirmation. No more \u0026ldquo;perpetual consent\u0026rdquo; from a checkbox clicked in 2019.\u003c/p\u003e\n\u003cp\u003eThis isn\u0026rsquo;t compliance theater. It\u0026rsquo;s a defensible system that respects user autonomy while protecting your organization from regulatory liability.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-real-cost-of-fake-consent\"\u003e\u003ca href=\"/posts/consent-management-aspnet-identity/#the-real-cost-of-fake-consent\" title=\"The Real Cost of Fake Consent\"\u003eThe Real Cost of Fake Consent\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003eA cookie banner is not consent management.\u003c/strong\u003e It\u0026rsquo;s a legal fiction that experienced auditors see through in seconds.\u003c/p\u003e\n\u003cp\u003eWhen GDPR enforcement actions land, they don\u0026rsquo;t penalize missing cookie banners. They penalize missing \u003cstrong\u003eaudit trails\u003c/strong\u003e. They penalize organizations that can\u0026rsquo;t demonstrate what specific processing activities a user agreed to, when, under what policy version, and through what mechanism they can withdraw. They penalize dark patterns that make opting out harder than opting in.\u003c/p\u003e\n\u003cp\u003eI\u0026rsquo;ve watched teams scramble when auditors ask for consent records and discover their entire \u0026ldquo;consent management system\u0026rdquo; is a boolean column with no history. That discovery usually happens about 72 hours before the response deadline.\u003c/p\u003e\n\u003cp\u003eISO 27701 Control 7.3.2 exists because privacy isn\u0026rsquo;t about checkboxes. It\u0026rsquo;s about \u003cstrong\u003edata subject control\u003c/strong\u003e. If your users can\u0026rsquo;t meaningfully choose what you do with their data, you\u0026rsquo;re not managing consent. You\u0026rsquo;re accumulating liability.\u003c/p\u003e\n\u003cp\u003eThe implementation shown here isn\u0026rsquo;t perfect. Production systems need consent delegation for organizational accounts, consent transfer during mergers, consent handling for minors, and integration with data deletion workflows. But this foundation (purpose-specific records, immutable audit logs, validation middleware, expiration automation) gives you architecture that survives scrutiny.\u003c/p\u003e\n\u003cp\u003eBuild consent management that respects your users\u0026rsquo; agency. Build systems that produce defensible audit trails. And for the love of everything auditable, \u003cstrong\u003estop pretending a single checkbox satisfies ISO 27701\u003c/strong\u003e.\u003c/p\u003e\n\u003cp\u003eThat boolean column you\u0026rsquo;re calling \u0026ldquo;consent\u0026rdquo;? Replace it. Your users and your legal team will thank you.\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2026-02-24T17:00:00+01:00","id":"https://daily-devops.net/posts/consent-management-aspnet-identity/","language":"en","summary":"Your \"consent management\" is probably a boolean column with no audit trail. Here's what ISO 27701 and GDPR Article 7 actually require in .NET.\n","tags":["iso-standards","privacy","gdpr","dotnet","authentication"],"title":"Cookie Banners Won't Save You From ISO 27701\n","url":"https://daily-devops.net/posts/consent-management-aspnet-identity/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eYou wouldn\u0026rsquo;t let a stranger walk into your datacenter and install software on your production servers. Yet every time you execute \u003ccode\u003edotnet add package\u003c/code\u003e, you\u0026rsquo;re doing exactly that: inviting third-party code into your application without verification, without approval, and often without even knowing what transitive dependencies tagged along for the ride.\u003c/p\u003e\n\u003cp\u003eISO/IEC 27001 Control A.15.1 (Information security in supplier relationships) doesn\u0026rsquo;t care whether your \u0026ldquo;supplier\u0026rdquo; is a cloud vendor with million-dollar contracts or an open-source maintainer distributing NuGet packages from their basement. Both are suppliers. Both introduce supply chain risk. And both require the same systematic approach to security.\u003c/p\u003e\n\u003cp\u003eAfter seeing enterprise teams discover critical vulnerabilities in packages that had been sitting in production for years (packages they couldn\u0026rsquo;t even remember adding), I\u0026rsquo;ve learned that dependency management isn\u0026rsquo;t a developer convenience feature. It\u0026rsquo;s a fundamental security control that most organizations handle with alarming negligence.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-fatal-pattern-nuget-as-a-free-for-all\"\u003e\u003ca href=\"/posts/dependency-management-nuget-security/#the-fatal-pattern-nuget-as-a-free-for-all\" title=\"The Fatal Pattern: NuGet as a Free-for-All\"\u003eThe Fatal Pattern: NuGet as a Free-for-All\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eHere\u0026rsquo;s what I see in most .NET codebases when I perform security audits:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\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// ProjectA.csproj\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=\"n\"\u003eItemGroup\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=\"n\"\u003ePackageReference\u003c/span\u003e \u003cspan class=\"n\"\u003eInclude\u003c/span\u003e\u003cspan class=\"p\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Newtonsoft.Json\u0026#34;\u003c/span\u003e \u003cspan class=\"n\"\u003eVersion\u003c/span\u003e\u003cspan class=\"p\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;11.0.2\u0026#34;\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=\"n\"\u003ePackageReference\u003c/span\u003e \u003cspan class=\"n\"\u003eInclude\u003c/span\u003e\u003cspan class=\"p\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Serilog\u0026#34;\u003c/span\u003e \u003cspan class=\"n\"\u003eVersion\u003c/span\u003e\u003cspan class=\"p\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;2.8.0\u0026#34;\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=\"n\"\u003eItemGroup\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// ProjectB.csproj\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=\"n\"\u003eItemGroup\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=\"n\"\u003ePackageReference\u003c/span\u003e \u003cspan class=\"n\"\u003eInclude\u003c/span\u003e\u003cspan class=\"p\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Newtonsoft.Json\u0026#34;\u003c/span\u003e \u003cspan class=\"n\"\u003eVersion\u003c/span\u003e\u003cspan class=\"p\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;12.0.3\u0026#34;\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=\"n\"\u003ePackageReference\u003c/span\u003e \u003cspan class=\"n\"\u003eInclude\u003c/span\u003e\u003cspan class=\"p\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;AutoMapper\u0026#34;\u003c/span\u003e \u003cspan class=\"n\"\u003eVersion\u003c/span\u003e\u003cspan class=\"p\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;9.0.0\u0026#34;\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=\"n\"\u003eItemGroup\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// ProjectC.csproj\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=\"n\"\u003eItemGroup\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=\"n\"\u003ePackageReference\u003c/span\u003e \u003cspan class=\"n\"\u003eInclude\u003c/span\u003e\u003cspan class=\"p\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Newtonsoft.Json\u0026#34;\u003c/span\u003e \u003cspan class=\"n\"\u003eVersion\u003c/span\u003e\u003cspan class=\"p\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;10.0.3\u0026#34;\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=\"n\"\u003ePackageReference\u003c/span\u003e \u003cspan class=\"n\"\u003eInclude\u003c/span\u003e\u003cspan class=\"p\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Serilog\u0026#34;\u003c/span\u003e \u003cspan class=\"n\"\u003eVersion\u003c/span\u003e\u003cspan class=\"p\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;2.10.0\u0026#34;\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=\"n\"\u003eItemGroup\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\u003eThree projects, three different versions of the same package, zero visibility into transitive dependencies, and absolutely no process for:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eVulnerability scanning\u003c/strong\u003e: Nobody knows which CVEs affect any of these versions\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eApproval workflow\u003c/strong\u003e: Developers add packages with \u003ccode\u003eInstall-Package\u003c/code\u003e during development\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eVersion consistency\u003c/strong\u003e: Each project picks its own version creating assembly binding nightmares\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eDependency tracking\u003c/strong\u003e: What pulled in that obscure \u003ccode\u003eSystem.Text.Encodings.Web\u003c/code\u003e version?\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSecurity patches\u003c/strong\u003e: Versions pinned years ago, never updated, accumulating vulnerabilities\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eAnd the internal package repository? Configured like this:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-xml\" data-lang=\"xml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c\"\u003e\u0026lt;!-- nuget.config --\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cp\"\u003e\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;utf-8\u0026#34;?\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003e\u0026lt;configuration\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026lt;packageSources\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026lt;add\u003c/span\u003e \u003cspan class=\"na\"\u003ekey=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;nuget.org\u0026#34;\u003c/span\u003e \u003cspan class=\"na\"\u003evalue=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;https://api.nuget.org/v3/index.json\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;add\u003c/span\u003e \u003cspan class=\"na\"\u003ekey=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;internal\u0026#34;\u003c/span\u003e \u003cspan class=\"na\"\u003evalue=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;http://internal-nuget.company.local/nuget\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;/packageSources\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"c\"\u003e\u0026lt;!-- No authentication configured --\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"c\"\u003e\u0026lt;!-- No signature verification --\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"c\"\u003e\u0026lt;!-- No HTTPS enforcement --\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003e\u0026lt;/configuration\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis violates \u003cstrong\u003eISO/IEC 27001 A.15.1.1\u003c/strong\u003e (Information security policy for supplier relationships) by failing to establish security requirements for third-party code. It violates \u003cstrong\u003eA.15.1.3\u003c/strong\u003e (Supply chain security) by not monitoring the supply chain for security events. And it completely ignores \u003cstrong\u003eA.18.2.3\u003c/strong\u003e (Technical compliance review) by skipping any technical verification of dependencies.\u003c/p\u003e\n\u003cp\u003eIn practical terms: any developer can add any package from any source, those packages can pull in dozens of transitive dependencies, and nobody discovers the vulnerable packages until a security incident forces an emergency audit.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-systematic-approach-dependency-management-as-security-control\"\u003e\u003ca href=\"/posts/dependency-management-nuget-security/#the-systematic-approach-dependency-management-as-security-control\" title=\"The Systematic Approach: Dependency Management as Security Control\"\u003eThe Systematic Approach: Dependency Management as Security Control\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eISO 27001 requires treating suppliers as part of your security perimeter. For NuGet packages, this means implementing controls at multiple levels:\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"1-central-package-management-directorypackagesprops\"\u003e\u003ca href=\"/posts/dependency-management-nuget-security/#1-central-package-management-directorypackagesprops\" title=\"1. Central Package Management (Directory.Packages.props)\"\u003e1. Central Package Management (Directory.Packages.props)\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eFirst, enforce version consistency across your entire codebase:\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;!-- Directory.Packages.props (at solution root) --\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\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;ManagePackageVersionsCentrally\u0026gt;\u003c/span\u003etrue\u003cspan class=\"nt\"\u003e\u0026lt;/ManagePackageVersionsCentrally\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=\"nt\"\u003e\u0026lt;PackageVersion\u003c/span\u003e \u003cspan class=\"na\"\u003eInclude=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Newtonsoft.Json\u0026#34;\u003c/span\u003e \u003cspan class=\"na\"\u003eVersion=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;13.0.3\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;PackageVersion\u003c/span\u003e \u003cspan class=\"na\"\u003eInclude=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Serilog\u0026#34;\u003c/span\u003e \u003cspan class=\"na\"\u003eVersion=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;3.1.1\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;!-- Version range: auto-update patches, block major versions --\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026lt;PackageVersion\u003c/span\u003e \u003cspan class=\"na\"\u003eInclude=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Azure.Identity\u0026#34;\u003c/span\u003e \u003cspan class=\"na\"\u003eVersion=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;[1.10.4,2.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\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\u003eProject files now reference packages without versions:\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;!-- ProjectA.csproj --\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  \u003cspan class=\"nt\"\u003e\u0026lt;PackageReference\u003c/span\u003e \u003cspan class=\"na\"\u003eInclude=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Newtonsoft.Json\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;Serilog\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\u003cstrong\u003eWhy this matters for ISO 27001\u003c/strong\u003e: Control A.15.1.1 requires establishing security requirements. Central Package Management enforces a single source of truth for dependency versions, making vulnerability tracking and patching possible across your entire codebase.\u003c/p\u003e\n\u003cp\u003eNote the version range syntax \u003ccode\u003e[8.0.1,9.0)\u003c/code\u003e for security-critical packages. This allows NuGet to automatically pull patch versions (8.0.2, 8.0.3, etc.) that fix security vulnerabilities while preventing breaking changes from major version updates.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"2-automated-vulnerability-scanning-in-cicd\"\u003e\u003ca href=\"/posts/dependency-management-nuget-security/#2-automated-vulnerability-scanning-in-cicd\" title=\"2. Automated Vulnerability Scanning in CI/CD\"\u003e2. Automated Vulnerability Scanning in CI/CD\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eEvery build must check for known vulnerabilities:\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=\"c\"\u003e# .github/workflows/build.yml\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\"\u003eBuild with Security Checks\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\"\u003eon\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"l\"\u003epush, pull_request]\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\"\u003ejobs\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\"\u003esecurity-scan\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\"\u003eruns-on\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eubuntu-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    \u003c/span\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\"\u003euses\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eactions/checkout@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\"\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;8.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=\"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\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eCheck for vulnerable packages\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=\"p\"\u003e|\u003c/span\u003e\u003cspan class=\"sd\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sd\"\u003e        dotnet restore\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sd\"\u003e        dotnet list package --vulnerable --include-transitive\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sd\"\u003e        # Fails if vulnerabilities found\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sd\"\u003e        dotnet build /p:TreatWarningsAsErrors=true\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\u003eThis implements \u003cstrong\u003eA.18.2.3\u003c/strong\u003e (Technical compliance review) by automatically verifying that dependencies meet security requirements before code reaches production.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"3-github-dependabot-configuration\"\u003e\u003ca href=\"/posts/dependency-management-nuget-security/#3-github-dependabot-configuration\" title=\"3. GitHub Dependabot Configuration\"\u003e3. GitHub Dependabot Configuration\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eEnable automated vulnerability alerts and patching:\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=\"c\"\u003e# .github/dependabot.yml\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\"\u003eversion\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"m\"\u003e2\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\"\u003eupdates\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\"\u003epackage-ecosystem\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;nuget\u0026#34;\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\"\u003edirectory\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;/\u0026#34;\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\"\u003eschedule\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\"\u003einterval\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;weekly\u0026#34;\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\"\u003egroups\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\"\u003esecurity-updates\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\"\u003eupdate-types\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=\"s2\"\u003e\u0026#34;security\u0026#34;\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\"\u003ereviewers\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=\"s2\"\u003e\u0026#34;security-team\u0026#34;\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\"\u003elabels\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=\"s2\"\u003e\u0026#34;dependencies\u0026#34;\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\u003eThis addresses \u003cstrong\u003eA.15.1.3\u003c/strong\u003e (Supply chain security) by continuously monitoring your supply chain for security events and automatically proposing remediation.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"4-package-source-authentication-and-verification\"\u003e\u003ca href=\"/posts/dependency-management-nuget-security/#4-package-source-authentication-and-verification\" title=\"4. Package Source Authentication and Verification\"\u003e4. Package Source Authentication and Verification\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eSecure your package sources with authentication and signature verification:\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;!-- nuget.config --\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003e\u0026lt;configuration\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026lt;packageSources\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026lt;clear\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;add\u003c/span\u003e \u003cspan class=\"na\"\u003ekey=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;nuget.org\u0026#34;\u003c/span\u003e \u003cspan class=\"na\"\u003evalue=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;https://api.nuget.org/v3/index.json\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;/packageSources\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;packageSourceMapping\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026lt;packageSource\u003c/span\u003e \u003cspan class=\"na\"\u003ekey=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;nuget.org\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;package\u003c/span\u003e \u003cspan class=\"na\"\u003epattern=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Microsoft.*\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;package\u003c/span\u003e \u003cspan class=\"na\"\u003epattern=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;System.*\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;/packageSource\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026lt;/packageSourceMapping\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;config\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026lt;add\u003c/span\u003e \u003cspan class=\"na\"\u003ekey=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;signatureValidationMode\u0026#34;\u003c/span\u003e \u003cspan class=\"na\"\u003evalue=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;require\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;/config\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003e\u0026lt;/configuration\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis configuration clears default sources and whitelists approved feeds, maps package patterns to specific sources (preventing substitution attacks), and enables signature verification.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"5-health-checks-for-package-repository-availability\"\u003e\u003ca href=\"/posts/dependency-management-nuget-security/#5-health-checks-for-package-repository-availability\" title=\"5. Health Checks for Package Repository Availability\"\u003e5. Health Checks for Package Repository Availability\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eYour CI/CD pipeline depends on NuGet feeds being available. If \u003ccode\u003enuget.org\u003c/code\u003e goes down during a critical deployment, you need to know. Add a simple health check to your monitoring:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\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// Check NuGet feed availability\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=\"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\"\u003eresponse\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\"\u003eGetAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;https://api.nuget.org/v3/index.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\"\u003eif\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\"\u003eIsSuccessStatusCode\u003c/span\u003e\u003cspan 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\"\u003eLogWarning\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;NuGet feed unavailable\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 is critical for \u003cstrong\u003eA.15.1.3\u003c/strong\u003e (Supply chain security): you need to know when your dependency supply chain is disrupted.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"regular-dependency-audits\"\u003e\u003ca href=\"/posts/dependency-management-nuget-security/#regular-dependency-audits\" title=\"Regular Dependency Audits\"\u003eRegular Dependency Audits\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eFinally, schedule regular dependency reviews as part of your security process:\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# Monthly security audit\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003edotnet list package --outdated\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003edotnet list package --vulnerable --include-transitive\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003edotnet list package --deprecated\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eRun this monthly and review results with your security team. This provides evidence for \u003cstrong\u003eA.18.2.3\u003c/strong\u003e (Technical compliance review).\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-pragmatic-reality\"\u003e\u003ca href=\"/posts/dependency-management-nuget-security/#the-pragmatic-reality\" title=\"The Pragmatic Reality\"\u003eThe Pragmatic Reality\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eI\u0026rsquo;ve seen teams spend months achieving ISO 27001 certification only to completely ignore Control A.15.1 when it comes to NuGet packages. The assumption seems to be that because packages are \u0026ldquo;just open source\u0026rdquo; or \u0026ldquo;from Microsoft,\u0026rdquo; they don\u0026rsquo;t count as suppliers.\u003c/p\u003e\n\u003cp\u003eThey absolutely count. Every package is a supplier relationship. Every transitive dependency is a third-party component in your production system. And every vulnerability in those dependencies is your responsibility under ISO 27001.\u003c/p\u003e\n\u003cp\u003eThe good news: modern tooling makes this manageable. Central Package Management eliminates version chaos. Dependabot provides automated monitoring. GitHub Actions enables vulnerability scanning without infrastructure investment. Package signing prevents tampering.\u003c/p\u003e\n\u003cp\u003eThe bad news: none of this happens automatically. You must implement these controls deliberately, enforce them consistently, and audit them regularly.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"key-takeaways\"\u003e\u003ca href=\"/posts/dependency-management-nuget-security/#key-takeaways\" title=\"Key Takeaways\"\u003eKey Takeaways\u003c/a\u003e\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eEvery NuGet package is a supplier relationship\u003c/strong\u003e under ISO/IEC 27001 Control A.15.1, requiring security evaluation and monitoring.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eCentral Package Management is mandatory\u003c/strong\u003e for any codebase with multiple projects. It\u0026rsquo;s the foundation for vulnerability tracking and consistent patching.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eFail builds on known vulnerabilities\u003c/strong\u003e using \u003ccode\u003edotnet list package --vulnerable\u003c/code\u003e in CI/CD pipelines. Security issues should block deployment, not generate ignored warnings.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eVersion ranges enable automatic security patches\u003c/strong\u003e: use \u003ccode\u003e[8.0.1,9.0)\u003c/code\u003e syntax for dependencies where security matters more than version stability.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003ePackage signature verification prevents supply chain attacks\u003c/strong\u003e: configure trusted signers and require signature validation in nuget.config.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eAutomated scanning finds problems; human review approves solutions\u003c/strong\u003e: let Dependabot detect issues, but require security team approval for dependency changes.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eMonitor your supply chain availability\u003c/strong\u003e: health checks for package repositories should be part of your operational monitoring.\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eSupply chain security isn\u0026rsquo;t a checkbox on a compliance form. It\u0026rsquo;s a systematic approach to managing the third-party code that forms 70-90% of modern applications. ISO 27001 provides the framework; NuGet tooling provides the implementation. Your job is to connect them before the next CVE announcement forces you to audit thousands of dependencies under emergency conditions.\u003c/p\u003e\n\u003cp\u003eAfter 15 years of seeing teams scramble to patch vulnerabilities in packages they didn\u0026rsquo;t know they had, I can promise you this: the time you invest in dependency management today will save you countless emergency responses tomorrow. And unlike most security theater, these controls actually work.\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2026-02-19T17:00:00+01:00","id":"https://daily-devops.net/posts/dependency-management-nuget-security/","language":"en","summary":"dotnet add package invites unvetted suppliers into production. Enforce Central Package Management, signature checks, and vulnerability scans.","tags":["iso-standards","security","nuget","dotnet","dependency-management"],"title":"NuGet Packages: The Suppliers You Forgot to Audit","url":"https://daily-devops.net/posts/dependency-management-nuget-security/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003e\u0026ldquo;We might need it someday.\u0026rdquo;\u003c/p\u003e\n\u003cp\u003eThat sentence has cost teams more compliance headaches than any technical decision I\u0026rsquo;ve encountered. It\u0026rsquo;s the battle cry of lazy schema design, the excuse that turns every user table into a dumping ground for speculative data collection. Development teams hoard every conceivable piece of personal information during initial implementation—birth dates, phone numbers, employment history, marital status—creating sprawling user tables that seem prudent at the time but are actually architectural time bombs.\u003c/p\u003e\n\u003cp\u003eThree years later, when GDPR deletion requests arrive or ISO 27701 audits roll around, these same teams discover they\u0026rsquo;re storing data that serves no business purpose whatsoever. And by then, the cost isn\u0026rsquo;t just regulatory fines. It\u0026rsquo;s the architectural debt that makes compliant deletion technically impossible.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-regulatory-reality\"\u003e\u003ca href=\"/posts/data-minimization-entity-framework/#the-regulatory-reality\" title=\"The Regulatory Reality\"\u003eThe Regulatory Reality\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eISO 27701 and GDPR both demand something straightforward: collect only what you need, document why you need it, and delete it when asked. None of this is revolutionary. It\u0026rsquo;s basic data hygiene that somehow became optional in the \u0026ldquo;move fast and break things\u0026rdquo; era.\u003c/p\u003e\n\u003cp\u003eThe problem is that \u0026ldquo;straightforward\u0026rdquo; becomes \u0026ldquo;impossible\u0026rdquo; when your schema was designed by someone who confused data collection with data strategy. Control 7.2.2 requires identifying the specific purpose \u003cem\u003ebefore\u003c/em\u003e collection—not retroactively inventing justifications when auditors show up. Control 7.2.8 limits collection to what\u0026rsquo;s adequate and necessary for that documented purpose. And Control 7.3.1 requires the ability to actually fulfill deletion requests, which is awkward when your schema makes deletion a referential integrity nightmare.\u003c/p\u003e\n\u003cp\u003eWhen auditors examine your database schemas, they ask uncomfortable questions: Why do you store this field? What business process requires it? Can you delete it when requested?\u003c/p\u003e\n\u003cp\u003eIf your answer involves the phrase \u0026ldquo;the original developer thought,\u0026rdquo; you\u0026rsquo;ve already failed. If your answer is \u0026ldquo;we\u0026rsquo;ve always collected that,\u0026rdquo; you\u0026rsquo;ve failed harder.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-monolithic-user-entity-problem\"\u003e\u003ca href=\"/posts/data-minimization-entity-framework/#the-monolithic-user-entity-problem\" title=\"The Monolithic User Entity Problem\"\u003eThe Monolithic User Entity Problem\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eHere\u0026rsquo;s the pattern I see 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=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eUser\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\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\"\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\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\"\u003eEmail\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=\"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=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003ePasswordHash\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=\"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    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// \u0026#34;Comprehensive\u0026#34; personal information\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\"\u003eFirstName\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=\"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=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eLastName\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=\"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=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eDateTime\u003c/span\u003e \u003cspan class=\"n\"\u003eDateOfBirth\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=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003ePhoneNumber\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=\"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=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eStreetAddress\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=\"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=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eCity\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=\"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    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Employment data \u0026#34;just in case\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=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eEmployerName\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=\"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=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eJobTitle\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=\"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=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003edecimal\u003c/span\u003e \u003cspan class=\"n\"\u003eAnnualIncome\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// Demographics \u0026#34;for future analytics\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=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eMaritalStatus\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=\"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=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eNumberOfChildren\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=\"n\"\u003eDateTime\u003c/span\u003e \u003cspan class=\"n\"\u003eCreatedAt\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=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eICollection\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\"\u003eOrders\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=\"p\"\u003e[];\u003c/span\u003e\n\u003c/span\u003e\u003c/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=\"when-speculative-fields-become-compliance-deadlocks\"\u003e\u003ca href=\"/posts/data-minimization-entity-framework/#when-speculative-fields-become-compliance-deadlocks\" title=\"When Speculative Fields Become Compliance Deadlocks\"\u003eWhen Speculative Fields Become Compliance Deadlocks\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eWhy does an e-commerce system need marital status? Nobody knows anymore. The field exists because someone thought demographic analysis \u003cem\u003emight\u003c/em\u003e be valuable someday. That person left the company two years ago. The analytics feature was never built. But the data collection persists, a monument to speculative thinking that nobody had the courage to question.\u003c/p\u003e\n\u003cp\u003eThe real problem emerges when a customer requests deletion. The \u003ccode\u003eUser\u003c/code\u003e entity has foreign key relationships with \u003ccode\u003eOrders\u003c/code\u003e. Delete the user? Breaks referential integrity. Keep it? Violates the deletion request. Anonymize it? You\u0026rsquo;re still retaining fields like \u003ccode\u003eAnnualIncome\u003c/code\u003e and \u003ccode\u003eNumberOfChildren\u003c/code\u003e in backups.\u003c/p\u003e\n\u003cp\u003eYou\u0026rsquo;ve created an architectural deadlock before writing a single DELETE statement. Congratulations—your database schema is now a compliance liability that will cost more to fix than it cost to build.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"purpose-driven-data-separation\"\u003e\u003ca href=\"/posts/data-minimization-entity-framework/#purpose-driven-data-separation\" title=\"Purpose-Driven Data Separation\"\u003ePurpose-Driven Data Separation\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe fix isn\u0026rsquo;t complex, which makes it all the more frustrating that teams don\u0026rsquo;t implement it from the start. Separate operational data from personal 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=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eUserAccount\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\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\"\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\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\"\u003eEmail\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=\"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=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003ePasswordHash\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=\"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=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eDateTime\u003c/span\u003e \u003cspan class=\"n\"\u003eCreatedAt\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=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eDateTime\u003c/span\u003e\u003cspan class=\"p\"\u003e?\u003c/span\u003e \u003cspan class=\"n\"\u003eDeletedAt\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=\"n\"\u003eUserProfile\u003c/span\u003e\u003cspan class=\"p\"\u003e?\u003c/span\u003e \u003cspan class=\"n\"\u003eProfile\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=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eICollection\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\"\u003eOrders\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=\"p\"\u003e[];\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"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\"\u003eUserProfile\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\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\"\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\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\"\u003eUserAccountId\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=\"kt\"\u003estring?\u003c/span\u003e \u003cspan class=\"n\"\u003eFirstName\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=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003estring?\u003c/span\u003e \u003cspan class=\"n\"\u003eLastName\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=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eDateTime\u003c/span\u003e\u003cspan class=\"p\"\u003e?\u003c/span\u003e \u003cspan class=\"n\"\u003eDateOfBirth\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=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003estring?\u003c/span\u003e \u003cspan class=\"n\"\u003eShippingAddress\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=\"n\"\u003eDateTime\u003c/span\u003e \u003cspan class=\"n\"\u003eConsentGrantedAt\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=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eConsentPurpose\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=\"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    \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\"\u003eUserAccount\u003c/span\u003e \u003cspan class=\"n\"\u003eUserAccount\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=\"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\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eNotice what changed: \u003ccode\u003eUserAccount\u003c/code\u003e contains only what\u0026rsquo;s necessary for system operation. \u003ccode\u003eUserProfile\u003c/code\u003e contains optional personal data—every field nullable, every field requiring explicit consent. No employment history. No marital status. No speculative demographics.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"wiring-the-split-into-ef-core\"\u003e\u003ca href=\"/posts/data-minimization-entity-framework/#wiring-the-split-into-ef-core\" title=\"Wiring The Split Into EF Core\"\u003eWiring The Split Into EF Core\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThe Entity Framework configuration enforces this separation:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\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\"\u003eConfigure\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eEntityTypeBuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eUserAccount\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan 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\"\u003eHasKey\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eu\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eu\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eId\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eProperty\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eu\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eu\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eEmail\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\u003cspan class=\"n\"\u003eIsRequired\u003c/span\u003e\u003cspan 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\"\u003eProperty\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eu\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eu\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePasswordHash\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\u003cspan class=\"n\"\u003eIsRequired\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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\"\u003eHasQueryFilter\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eu\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eu\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eDeletedAt\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    \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\"\u003eHasOne\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eu\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eu\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eProfile\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWithOne\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\"\u003eUserAccount\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/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\"\u003eHasForeignKey\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eUserProfile\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;(\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\"\u003eUserAccountId\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/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\"\u003eOnDelete\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eDeleteBehavior\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCascade\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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\"\u003eHasMany\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eu\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eu\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eOrders\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/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\"\u003eWithOne\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\"\u003eUser\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eHasForeignKey\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\"\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\u003cspan class=\"n\"\u003eOnDelete\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eDeleteBehavior\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRestrict\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/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 \u003ccode\u003e.HasQueryFilter(u =\u0026gt; u.DeletedAt == null)\u003c/code\u003e is critical. It automatically excludes soft-deleted accounts from queries, preventing accidental exposure while preserving referential integrity for order history.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"compliant-deletion-that-actually-works\"\u003e\u003ca href=\"/posts/data-minimization-entity-framework/#compliant-deletion-that-actually-works\" title=\"Compliant Deletion That Actually Works\"\u003eCompliant Deletion That Actually Works\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eWith proper separation, deletion requests become tractable:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\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\"\u003eProcessDeletionRequest\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003euserAccountId\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\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\"\u003eaccount\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\"\u003eUserAccounts\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eInclude\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eu\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eu\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eProfile\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eIgnoreQueryFilters\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFirstOrDefaultAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eu\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eu\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eId\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"n\"\u003euserAccountId\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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\"\u003eaccount\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=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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\"\u003eaccount\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eProfile\u003c/span\u003e \u003cspan class=\"k\"\u003eis\u003c/span\u003e \u003cspan class=\"n\"\u003enot\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_context\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUserProfiles\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRemove\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\"\u003eProfile\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \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\"\u003eDeletedAt\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\"\u003eaccount\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=\"s\"\u003e$\u0026#34;deleted-{account.Id}@example.invalid\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\"\u003eaccount\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePasswordHash\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\"\u003eEmpty\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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_context\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSaveChangesAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/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 \u003ccode\u003eUserProfile\u003c/code\u003e gets hard-deleted with all personal information. The \u003ccode\u003eUserAccount\u003c/code\u003e gets soft-deleted, maintaining foreign key relationships with orders. Authentication credentials get cleared. Subsequent queries automatically exclude the account due to the query filter.\u003c/p\u003e\n\u003cp\u003eThe customer effectively ceases to exist from an operational perspective while your database maintains consistency for historical records. No architectural gymnastics. No complex anonymization logic. No explaining to auditors why you still have someone\u0026rsquo;s annual income stored.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"validation-through-integration-tests\"\u003e\u003ca href=\"/posts/data-minimization-entity-framework/#validation-through-integration-tests\" title=\"Validation Through Integration Tests\"\u003eValidation Through Integration Tests\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eCompliance isn\u0026rsquo;t a one-time configuration. It requires continuous validation. Write integration tests that verify your API endpoints don\u0026rsquo;t leak unnecessary 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=\"na\"\u003e[Fact]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e \u003cspan class=\"n\"\u003eGetUser_ReturnsOnlyOperationalFields\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\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\"\u003e_factory\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCreateClient\u003c/span\u003e\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\"\u003eclient\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;/api/users/me\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\"\u003ejson\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eresponse\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\"\u003eReadAsStringAsync\u003c/span\u003e\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\"\u003euserData\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eJsonDocument\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=\"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    \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\"\u003euserData\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRootElement\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTryGetProperty\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;id\u0026#34;\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\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\"\u003euserData\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRootElement\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTryGetProperty\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;email\u0026#34;\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\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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\"\u003eFalse\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003euserData\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRootElement\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTryGetProperty\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;dateOfBirth\u0026#34;\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\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\"\u003eFalse\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003euserData\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRootElement\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTryGetProperty\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;passwordHash\u0026#34;\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\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\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=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e \u003cspan class=\"n\"\u003eDeletedUser_IsExcludedFromQueries\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\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\"\u003e_factory\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCreateClient\u003c/span\u003e\u003cspan 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\"\u003eclient\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eDeleteAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;/api/users/me\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\"\u003eresponse\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\"\u003eGetAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;/api/users/me\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\"\u003eAssert\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eEqual\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eHttpStatusCode\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eNotFound\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eresponse\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eStatusCode\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eRun these in CI. When someone inadvertently exposes additional personal data through a new endpoint, the tests fail before the code reaches production. Compliance violations don\u0026rsquo;t ship.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"failing-the-build-on-leaked-fields\"\u003e\u003ca href=\"/posts/data-minimization-entity-framework/#failing-the-build-on-leaked-fields\" title=\"Failing The Build On Leaked Fields\"\u003eFailing The Build On Leaked Fields\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eYou can take this further with a GitHub Actions workflow that runs these tests on every pull request affecting your data layer:\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\"\u003eData Minimization Compliance\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\"\u003eon\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\"\u003epull_request\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\"\u003epaths\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e      \u003c/span\u003e- \u003cspan class=\"s1\"\u003e\u0026#39;src/**/*.cs\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\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003ejobs\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\"\u003ecompliance\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\"\u003eruns-on\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eubuntu-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    \u003c/span\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\"\u003euses\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eactions/checkout@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\"\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;9.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=\"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 test --filter \u0026#34;Category=DataMinimization\u0026#34;\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 tests become guardrails. Someone adds a new property to the API response? The test fails. Someone forgets to exclude a sensitive field from serialization? The test fails. The compliance requirement becomes an engineering constraint that CI enforces automatically.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"document-your-purposes\"\u003e\u003ca href=\"/posts/data-minimization-entity-framework/#document-your-purposes\" title=\"Document Your Purposes\"\u003eDocument Your Purposes\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eCode alone doesn\u0026rsquo;t satisfy audit requirements. You need documented business purposes for each field:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eUserProfile\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\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/// Purpose: Personalized communication in order confirmations.\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"cs\"\u003e/// Legal basis: Consent granted during registration.\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"cs\"\u003e/// Retention: Until account deletion or consent withdrawal.\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"cs\"\u003e/// \u0026lt;/summary\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003estring?\u003c/span\u003e \u003cspan class=\"n\"\u003eFirstName\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=\"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/// Purpose: Order fulfillment and delivery.\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"cs\"\u003e/// Legal basis: Contract performance (GDPR Art. 6(1)(b)).\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"cs\"\u003e/// Retention: 90 days after order completion.\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"cs\"\u003e/// \u0026lt;/summary\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003estring?\u003c/span\u003e \u003cspan class=\"n\"\u003eShippingAddress\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\u003eWhen auditors review your code, they can trace each field to its documented purpose. If you can\u0026rsquo;t articulate why a field exists, remove it. That\u0026rsquo;s the entire point.\u003c/p\u003e\n\u003cp\u003eThis documentation serves a dual purpose. First, it satisfies the audit requirement for documented purposes. Second, it forces developers to think before adding fields. When you have to write a justification in the XML docs, you\u0026rsquo;re far less likely to add \u003ccode\u003eMaritalStatus\u003c/code\u003e just because someone mentioned demographics in a planning meeting.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"collect-data-when-it-becomes-necessary\"\u003e\u003ca href=\"/posts/data-minimization-entity-framework/#collect-data-when-it-becomes-necessary\" title=\"Collect Data When It Becomes Necessary\"\u003eCollect Data When It Becomes Necessary\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eI\u0026rsquo;ve reviewed applications where shipping addresses are required during registration—before a customer has placed any orders. This is compliance theater: collecting data you can\u0026rsquo;t justify because the registration form had empty fields that felt incomplete. It violates the principle that data collection must be necessary at the time of collection, and it tells me the team never actually thought about \u003cem\u003ewhy\u003c/em\u003e they were collecting what they were collecting.\u003c/p\u003e\n\u003cp\u003eThe timing matters. Collecting a shipping address during registration for a user who might never place an order means you\u0026rsquo;re storing personal data without a current legitimate purpose. When that user requests deletion three months later without having purchased anything, you\u0026rsquo;ve been storing their address for no reason.\u003c/p\u003e\n\u003cp\u003eCollect data in context. If the shipping feature doesn\u0026rsquo;t exist yet, don\u0026rsquo;t collect addresses speculatively. When checkout happens, prompt for the address. When a feature launches, update the flow to collect newly necessary information with proper consent. Progressive data collection aligns with how users actually interact with your application—they provide information when it becomes relevant, not upfront in a registration form that asks for everything.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-backup-problem-nobody-talks-about\"\u003e\u003ca href=\"/posts/data-minimization-entity-framework/#the-backup-problem-nobody-talks-about\" title=\"The Backup Problem Nobody Talks About\"\u003eThe Backup Problem Nobody Talks About\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eEven with proper separation and deletion logic, there\u0026rsquo;s a compliance trap that catches most teams: database backups.\u003c/p\u003e\n\u003cp\u003eWhen you soft-delete a user account and hard-delete their profile, the data is gone from your live database. But what about last night\u0026rsquo;s backup? Last week\u0026rsquo;s? The monthly snapshot from six months ago? That profile data still exists, sitting in backup storage, violating the deletion request.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"three-ways-to-reconcile-backups-with-deletion\"\u003e\u003ca href=\"/posts/data-minimization-entity-framework/#three-ways-to-reconcile-backups-with-deletion\" title=\"Three Ways To Reconcile Backups With Deletion\"\u003eThree Ways To Reconcile Backups With Deletion\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eYour retention policy for backups needs to align with your deletion obligations. Some options:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eEncryption with user-specific keys\u003c/strong\u003e: If you encrypt personal data with a key derived from the user\u0026rsquo;s account, deleting that key makes the backup data unreadable.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eBackup rotation aligned with retention\u003c/strong\u003e: If your stated retention period is 90 days, your backup rotation should match.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSelective restore procedures\u003c/strong\u003e: Document that restored backups will have deletion requests re-applied before going live.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eNone of these are simple. But ignoring the backup problem doesn\u0026rsquo;t make it go away—it just means you\u0026rsquo;re lying to customers when you confirm their data has been deleted. And that\u0026rsquo;s exactly what auditors will call it: a lie backed by technical negligence.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-real-cost-of-data-hoarding\"\u003e\u003ca href=\"/posts/data-minimization-entity-framework/#the-real-cost-of-data-hoarding\" title=\"The Real Cost of Data Hoarding\"\u003eThe Real Cost of Data Hoarding\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eEvery unnecessary field in your database represents breach exposure, technical debt, regulatory risk, and development friction. It\u0026rsquo;s technical debt that accrues interest in the form of compliance emergencies. Teams waste hours crafting complex anonymization queries for data that shouldn\u0026rsquo;t exist. During audits, they scramble to justify fields they\u0026rsquo;ve forgotten the purpose of—inventing post-hoc rationalizations for decisions made years ago by people who didn\u0026rsquo;t consider the consequences.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"data-you-did-not-collect-cannot-leak\"\u003e\u003ca href=\"/posts/data-minimization-entity-framework/#data-you-did-not-collect-cannot-leak\" title=\"Data You Did Not Collect Cannot Leak\"\u003eData You Did Not Collect Cannot Leak\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThe breach exposure angle deserves emphasis. When your database contains only operational essentials and purpose-justified personal data, a breach is bad but bounded. When your database contains speculative demographics, employment history, and family information, a breach becomes catastrophic. The attacker gets everything. The notification requirements expand. The regulatory scrutiny intensifies. The headlines get worse.\u003c/p\u003e\n\u003cp\u003eData you don\u0026rsquo;t collect can\u0026rsquo;t be breached. That\u0026rsquo;s the simplest security control in existence, and it\u0026rsquo;s also a compliance requirement.\u003c/p\u003e\n\u003cp\u003eThe separated architecture I\u0026rsquo;ve shown costs nothing additional to implement initially. It saves thousands in compliance remediation later. More importantly, it makes the honest answer to audit questions actually honest: \u0026ldquo;We collect what we need, we documented why, and we can delete it when asked.\u0026rdquo;\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"making-this-part-of-your-process\"\u003e\u003ca href=\"/posts/data-minimization-entity-framework/#making-this-part-of-your-process\" title=\"Making This Part of Your Process\"\u003eMaking This Part of Your Process\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eData minimization works when it\u0026rsquo;s embedded in how you build software, not bolted on during audit preparation. A few practices that help:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eSchema reviews\u003c/strong\u003e: Treat entity model changes like code reviews. When someone adds a property, the reviewer asks: What\u0026rsquo;s the documented purpose? Is it nullable? When is it collected? How is it deleted?\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eArchitecture decision records\u003c/strong\u003e: Document why you chose to collect specific data. When someone asks in two years why \u003ccode\u003eDateOfBirth\u003c/code\u003e exists, the ADR explains it\u0026rsquo;s for age verification on restricted products—not because someone thought demographics might be interesting.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eDeletion dry runs\u003c/strong\u003e: Periodically test your deletion logic against production-like data. Does it complete without errors? Does the query filter exclude deleted accounts? Can you still query order history for a deleted user\u0026rsquo;s past purchases?\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003ePeriodic field audits\u003c/strong\u003e: Once a quarter, export your entity models and review each property. Is it still used? Does the original purpose still apply? Has the feature it supported been deprecated? Fields that no longer serve a purpose should be removed, not retained indefinitely.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-bottom-line\"\u003e\u003ca href=\"/posts/data-minimization-entity-framework/#the-bottom-line\" title=\"The Bottom Line\"\u003eThe Bottom Line\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eStop hoarding personal data \u0026ldquo;just in case.\u0026rdquo; Define purposes. Collect minimally. Delete ruthlessly. Your deletion logic will be straightforward, your audit responses will be honest, and your customers\u0026rsquo; privacy will be respected.\u003c/p\u003e\n\u003cp\u003eThe monolithic \u003ccode\u003eUser\u003c/code\u003e entity pattern isn\u0026rsquo;t just non-compliant—it\u0026rsquo;s a symptom of teams that never asked \u0026ldquo;should we?\u0026rdquo; before asking \u0026ldquo;can we?\u0026rdquo; It\u0026rsquo;s expensive, risky, and harder to maintain than the separated alternative. Purpose-driven data architecture with \u003ccode\u003eUserAccount\u003c/code\u003e and \u003ccode\u003eUserProfile\u003c/code\u003e entities, nullable personal data fields, query filters for soft deletes, and integration tests for API boundaries isn\u0026rsquo;t regulatory overhead. It\u0026rsquo;s how data management should have worked all along.\u003c/p\u003e\n\u003cp\u003eThat\u0026rsquo;s not a constraint that makes development harder. It\u0026rsquo;s the bare minimum of responsible engineering that somehow became optional. Fix your schemas before the auditors do it for you.\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2026-02-10T17:00:00+01:00","id":"https://daily-devops.net/posts/data-minimization-entity-framework/","language":"en","summary":"Monolithic user entities make GDPR deletion impossible. Separate operational from personal data in EF Core with nullable, purpose-documented fields.\n","tags":["privacy","dotnet","testing","architecture","bestpractices","codequality"],"title":"Stop Hoarding Personal Data in Entity Framework\n","url":"https://daily-devops.net/posts/data-minimization-entity-framework/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eEvery developer who has attempted \u003cem\u003esimple encryption\u003c/em\u003e with \u003ccode\u003eEncoding.UTF8.GetBytes()\u003c/code\u003e and XOR operations eventually learns why \u003cstrong\u003ecryptography is a specialization, not a feature sprint\u003c/strong\u003e. Decades of spectacular failures proved that security through obscurity fails, hardcoded keys leak, and MD5 hashing passwords invites credential stuffing attacks. ISO/IEC 27001 exists because of these lessons—the standard demands policy-driven cryptographic controls and proper key management. Requirements that .NET\u0026rsquo;s Data Protection API satisfies, if developers abandon the illusion that they can implement encryption better than Microsoft\u0026rsquo;s cryptography team.\u003c/p\u003e\n\u003cp\u003eThis isn\u0026rsquo;t theoretical compliance. The cryptographic controls apply directly to .NET applications storing personally identifiable information (PII), authentication tokens, or any data requiring confidentiality. \u003ca href=\"https://www.iso.org/standard/27001\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eISO 27001:2022\u003c/a\u003e Annex A consolidates cryptographic requirements under \u003cstrong\u003eA.8.24\u003c/strong\u003e (Use of cryptography) with supporting controls for key management. The engineering decisions remain identical: encryption libraries, key storage infrastructure, and rotation schedules. Cloud-native .NET applications deployed to Azure must integrate these controls into their architecture, not bolt them on during audit preparation.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-fatal-pattern-homegrown-cryptography\"\u003e\u003ca href=\"/posts/cryptography-dotnet-data-protection/#the-fatal-pattern-homegrown-cryptography\" title=\"The Fatal Pattern: Homegrown Cryptography\"\u003eThe Fatal Pattern: Homegrown Cryptography\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eBefore examining the correct implementation, let\u0026rsquo;s dissect the recurring anti-pattern that violates ISO 27001 and creates exploitable vulnerabilities. This example synthesizes patterns observed in production codebases during security audits—systems that passed initial deployment reviews but failed compliance assessments.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"the-secure-encryption-class\"\u003e\u003ca href=\"/posts/cryptography-dotnet-data-protection/#the-secure-encryption-class\" title=\"The \u0026ldquo;Secure\u0026rdquo; Encryption Class\"\u003eThe \u0026ldquo;Secure\u0026rdquo; Encryption Class\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\"\u003eDataEncryption\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\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#34;Nobody will find the key in the compiled assembly\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\"\u003econst\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eEncryptionKey\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;MyS3cr3tK3y!2024\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\"\u003eEncrypt\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eplainText\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \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\"\u003eplainBytes\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eEncoding\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUTF8\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetBytes\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eplainText\u003c/span\u003e\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\"\u003ekeyBytes\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eEncoding\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUTF8\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetBytes\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eEncryptionKey\u003c/span\u003e\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\"\u003eencryptedBytes\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"kt\"\u003ebyte\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003eplainBytes\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=\"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\"\u003eplainBytes\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=\"n\"\u003eencryptedBytes\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\"\u003eplainBytes\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\"\u003ekeyBytes\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\"\u003ekeyBytes\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=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eConvert\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eToBase64String\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eencryptedBytes\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"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// Password \u0026#34;hashing\u0026#34; with MD5\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\"\u003eUserAuthentication\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\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=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eHashPassword\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003epassword\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan 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\"\u003emd5\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eMD5\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=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003ehashBytes\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003emd5\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eComputeHash\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eEncoding\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUTF8\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetBytes\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003epassword\u003c/span\u003e\u003cspan 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\"\u003eBitConverter\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=\"n\"\u003ehashBytes\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=\"s\"\u003e\u0026#34;-\u0026#34;\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\"\u003eToLower\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/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=\"why-this-fails-iso-270012022-control-a824\"\u003e\u003ca href=\"/posts/cryptography-dotnet-data-protection/#why-this-fails-iso-270012022-control-a824\" title=\"Why This Fails ISO 27001:2022 Control A.8.24\"\u003eWhy This Fails ISO 27001:2022 Control A.8.24\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThis implementation violates multiple cryptographic principles and ISO requirements:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eXOR is not encryption\u003c/strong\u003e — Simple XOR operations provide zero cryptographic strength. The pattern is reversible through frequency analysis, and identical plaintext produces identical ciphertext, exposing data patterns.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eHardcoded keys violate A.8.24\u003c/strong\u003e — The key is embedded in source code, compiled into assemblies, and trivially extractable using tools like \u003ca href=\"https://github.com/icsharpcode/ILSpy\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eILSpy\u003c/a\u003e or dnSpy. ISO 27001:2022 requires secure key generation, storage, and lifecycle management.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eNo initialization vector (IV)\u003c/strong\u003e — The same plaintext always encrypts to the same ciphertext, enabling pattern recognition attacks. Professional encryption algorithms use random IVs to ensure semantic security.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eMD5 is cryptographically broken\u003c/strong\u003e — The MD5 hashing algorithm has been deprecated since 2004 due to collision vulnerabilities. Password hashes require computationally expensive algorithms like bcrypt or Argon2 to resist brute-force attacks.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eNo key rotation mechanism\u003c/strong\u003e — The system has no provision for updating encryption keys without re-encrypting the entire database. ISO 27001 mandates key lifecycle management, including rotation schedules.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003ePlain text storage assumption\u003c/strong\u003e — The underlying belief—\u003cem\u003ethe database is secure, so encryption is optional\u003c/em\u003e—contradicts defense-in-depth principles. ISO 27001:2022 Control A.8.24 requires cryptographic controls specifically to protect data at rest.\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eAudit failures from this pattern include \u003cstrong\u003eGDPR violations\u003c/strong\u003e (personal data inadequately protected), \u003cstrong\u003ePCI-DSS non-compliance\u003c/strong\u003e (cardholder data exposure), and \u003cstrong\u003eISO 27001 control deficiencies\u003c/strong\u003e. The financial consequences range from regulatory fines to breach notification costs when the inevitable compromise occurs.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-correct-implementation-net-data-protection-api\"\u003e\u003ca href=\"/posts/cryptography-dotnet-data-protection/#the-correct-implementation-net-data-protection-api\" title=\"The Correct Implementation: .NET Data Protection API\"\u003eThe Correct Implementation: .NET Data Protection API\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe \u003ca href=\"https://learn.microsoft.com/aspnet/core/security/data-protection/introduction\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e.NET Data Protection API\u003c/a\u003e provides a purpose-built framework for protecting data at rest with proper key management, algorithm selection, and integration with Azure Key Vault. Unlike the homegrown approach, it abstracts cryptographic complexity while enforcing security best practices.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"configuration-with-azure-key-vault\"\u003e\u003ca href=\"/posts/cryptography-dotnet-data-protection/#configuration-with-azure-key-vault\" title=\"Configuration with Azure Key Vault\"\u003eConfiguration with Azure Key Vault\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eFirst, configure the Data Protection stack with \u003ca href=\"https://learn.microsoft.com/azure/key-vault/general/overview\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eAzure Key Vault\u003c/a\u003e for key storage, satisfying external key management requirements:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\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\"\u003eProgram\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\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\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eMain\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\"\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=\"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=\"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        \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// Configure Data Protection with Azure Key Vault\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\"\u003eAddDataProtection\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/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\"\u003eSetApplicationName\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;customer-management-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\"\u003ePersistKeysToAzureBlobStorage\u003c/span\u003e\u003cspan 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://yourstorageaccount.blob.core.windows.net/dataprotection-keys/keys.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=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eProtectKeysWithAzureKeyVault\u003c/span\u003e\u003cspan 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://yourkeyvault.vault.azure.net/keys/dataprotection\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\"\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            \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSetDefaultKeyLifetime\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\"\u003eFromDays\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e90\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e \u003cspan class=\"c1\"\u003e// Key rotation every 90 days\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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\"\u003eRun\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/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 establishes several critical controls. \u003cstrong\u003eKey Storage\u003c/strong\u003e ensures cryptographic keys persist in \u003ca href=\"https://learn.microsoft.com/azure/storage/blobs/storage-blobs-introduction\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eAzure Blob Storage\u003c/a\u003e, not in configuration files or environment variables. \u003cstrong\u003eKey Encryption\u003c/strong\u003e means the master key in Azure Key Vault encrypts data protection keys—a proper key hierarchy. \u003cstrong\u003eKey Rotation\u003c/strong\u003e through the 90-day lifetime triggers automatic key generation while maintaining backward compatibility. \u003cstrong\u003eApplication Isolation\u003c/strong\u003e via the application name scopes keys to the specific application, preventing cross-application key reuse.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"encrypting-database-columns-with-entity-framework\"\u003e\u003ca href=\"/posts/cryptography-dotnet-data-protection/#encrypting-database-columns-with-entity-framework\" title=\"Encrypting Database Columns with Entity Framework\"\u003eEncrypting Database Columns with Entity Framework\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eTo protect sensitive data at the persistence layer, integrate Data Protection with Entity Framework using purpose-limited encryption contexts:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\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\"\u003eCustomerDataProtector\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\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\"\u003eIDataProtector\u003c/span\u003e \u003cspan class=\"n\"\u003e_protector\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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\"\u003eCustomerDataProtector\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eIDataProtectionProvider\u003c/span\u003e \u003cspan class=\"n\"\u003eprovider\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \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// Purpose string limits scope—credit cards can\u0026#39;t be decrypted with user profile protector\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003e_protector\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eprovider\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCreateProtector\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;CustomerModule.CreditCards\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eProtect\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eplainText\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan 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\"\u003eplainText\u003c/span\u003e\u003cspan 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\"\u003eplainText\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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\"\u003e_protector\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eProtect\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eplainText\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"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\"\u003eUnprotect\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003ecipherText\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan 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\"\u003ecipherText\u003c/span\u003e\u003cspan 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\"\u003ecipherText\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003e_protector\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUnprotect\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ecipherText\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan 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\"\u003eCryptographicException\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \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// Key rotation or corruption—handle gracefully\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;[Decryption 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=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"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// Entity Framework model with encrypted properties\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\"\u003eint\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\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    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Encrypted storage\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\"\u003eEncryptedCreditCardNumber\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=\"kd\"\u003eprivate\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=\"na\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [NotMapped]\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\"\u003eCreditCardNumber\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=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eProtectSensitiveData\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eCustomerDataProtector\u003c/span\u003e \u003cspan class=\"n\"\u003eprotector\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan 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\"\u003eCreditCardNumber\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eEncryptedCreditCardNumber\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eprotector\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eProtect\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eCreditCardNumber\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eCreditCardNumber\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// Clear from memory\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"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\"\u003eUnprotectSensitiveData\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eCustomerDataProtector\u003c/span\u003e \u003cspan class=\"n\"\u003eprotector\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan 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\"\u003eEncryptedCreditCardNumber\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eCreditCardNumber\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eprotector\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUnprotect\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eEncryptedCreditCardNumber\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"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// Repository implementation\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\"\u003eCustomerRepository\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\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\"\u003eApplicationDbContext\u003c/span\u003e \u003cspan class=\"n\"\u003e_context\u003c/span\u003e\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\"\u003eCustomerDataProtector\u003c/span\u003e \u003cspan class=\"n\"\u003e_protector\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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\"\u003eCustomerRepository\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eApplicationDbContext\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eCustomerDataProtector\u003c/span\u003e \u003cspan class=\"n\"\u003eprotector\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan 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_context\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003e_protector\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eprotector\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"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\"\u003eCustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eGetByIdAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eid\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003ecustomer\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\"\u003eCustomers\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\"\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\"\u003ecustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e?.\u003c/span\u003e\u003cspan class=\"n\"\u003eUnprotectSensitiveData\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003e_protector\u003c/span\u003e\u003cspan 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    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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\"\u003eSaveAsync\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\"\u003ecustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eProtectSensitiveData\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003e_protector\u003c/span\u003e\u003cspan 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_context\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCustomers\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUpdate\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=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_context\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSaveChangesAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/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 \u003cstrong\u003epurpose string\u003c/strong\u003e (\u003ccode\u003e\u0026quot;CustomerModule.CreditCards\u0026quot;\u003c/code\u003e) creates a cryptographic boundary. Data encrypted with this protector cannot be decrypted by a protector created with a different purpose, even within the same application. This implements the \u003cstrong\u003eprinciple of least privilege\u003c/strong\u003e—code handling user authentication tokens cannot access credit card encryption keys.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"cookie-encryption-for-authentication-tokens\"\u003e\u003ca href=\"/posts/cryptography-dotnet-data-protection/#cookie-encryption-for-authentication-tokens\" title=\"Cookie Encryption for Authentication Tokens\"\u003eCookie Encryption for Authentication Tokens\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eASP.NET Core\u0026rsquo;s authentication middleware integrates Data Protection automatically for cookie-based authentication, but explicit configuration ensures compliance:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\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\"\u003eAddAuthentication\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eCookieAuthenticationDefaults\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAuthenticationScheme\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/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\"\u003eAddCookie\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\"\u003eCookie\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;CustomerPortal.Auth\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\"\u003eCookie\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eHttpOnly\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\"\u003eoptions\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCookie\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSecurePolicy\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eCookieSecurePolicy\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAlways\u003c/span\u003e\u003cspan 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\"\u003eCookie\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSameSite\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eSameSiteMode\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eStrict\u003c/span\u003e\u003cspan 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\"\u003eExpireTimeSpan\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\"\u003eFromHours\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e8\u003c/span\u003e\u003cspan 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\"\u003eSlidingExpiration\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=\"c1\"\u003e// Data Protection handles encryption transparently\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\"\u003eDataProtectionProvider\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\"\u003eServices\u003c/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\"\u003eBuildServiceProvider\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/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\"\u003eGetRequiredService\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eIDataProtectionProvider\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\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eAuthentication cookies are automatically encrypted using the configured Data Protection system. This prevents \u003cstrong\u003esession hijacking\u003c/strong\u003e, \u003cstrong\u003ereplay attacks\u003c/strong\u003e, and \u003cstrong\u003etampering\u003c/strong\u003e—security controls directly mapped to ISO 27001:2022 requirements for access control (A.5.15) and cryptographic protection (A.8.24).\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"password-hashing-with-argon2\"\u003e\u003ca href=\"/posts/cryptography-dotnet-data-protection/#password-hashing-with-argon2\" title=\"Password Hashing with Argon2\"\u003ePassword Hashing with Argon2\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003ePassword storage requires computationally expensive hashing algorithms resistant to brute-force attacks. While Data Protection handles data encryption, password verification needs specialized algorithms like \u003ca href=\"https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#argon2id\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eArgon2id\u003c/a\u003e—the current \u003ca href=\"https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eOWASP recommendation\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=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003eKonscious.Security.Cryptography\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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\"\u003ePasswordHasher\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\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\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eSaltSize\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    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eHashSize\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e32\u003c/span\u003e\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\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eMemorySize\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e1024\u003c/span\u003e \u003cspan class=\"p\"\u003e*\u003c/span\u003e \u003cspan class=\"m\"\u003e128\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"c1\"\u003e// 128 MB\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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\"\u003eHashPassword\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003epassword\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \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\"\u003esalt\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eRandomNumberGenerator\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetBytes\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eSaltSize\u003c/span\u003e\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\"\u003ehash\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eComputeHash\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003epassword\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003esalt\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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\"\u003enew\u003c/span\u003e \u003cspan class=\"kt\"\u003ebyte\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003eSaltSize\u003c/span\u003e \u003cspan class=\"p\"\u003e+\u003c/span\u003e \u003cspan class=\"n\"\u003eHashSize\u003c/span\u003e\u003cspan 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\"\u003eBlockCopy\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003esalt\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\"\u003eresult\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\"\u003eSaltSize\u003c/span\u003e\u003cspan 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\"\u003eBlockCopy\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ehash\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\"\u003eresult\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eSaltSize\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eHashSize\u003c/span\u003e\u003cspan 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\"\u003eConvert\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eToBase64String\u003c/span\u003e\u003cspan class=\"p\"\u003e(\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    \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\"\u003ebool\u003c/span\u003e \u003cspan class=\"n\"\u003eVerifyPassword\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003epassword\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003ehashedPassword\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \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\"\u003ehashBytes\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eConvert\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFromBase64String\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ehashedPassword\u003c/span\u003e\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\"\u003esalt\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ehashBytes\u003c/span\u003e\u003cspan class=\"p\"\u003e[..\u003c/span\u003e\u003cspan class=\"n\"\u003eSaltSize\u003c/span\u003e\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\"\u003estoredHash\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ehashBytes\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003eSaltSize\u003c/span\u003e\u003cspan class=\"p\"\u003e..];\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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\"\u003ecomputedHash\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eComputeHash\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003epassword\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003esalt\u003c/span\u003e\u003cspan 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\"\u003eCryptographicOperations\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFixedTimeEquals\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003estoredHash\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ecomputedHash\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"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\"\u003ebyte\u003c/span\u003e\u003cspan class=\"p\"\u003e[]\u003c/span\u003e \u003cspan class=\"n\"\u003eComputeHash\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003epassword\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\"\u003esalt\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan 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\"\u003eargon2\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eArgon2id\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eEncoding\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUTF8\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetBytes\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003epassword\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eSalt\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003esalt\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eDegreeOfParallelism\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\"\u003eMemorySize\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eMemorySize\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eIterations\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e4\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan 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\"\u003eargon2\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetBytes\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eHashSize\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/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\u003eArgon2id combines the memory-hardness of Argon2i with the GPU-resistance of Argon2d. The algorithm\u0026rsquo;s computational cost (128 MB memory, 4 iterations) makes brute-force attacks economically infeasible while remaining acceptable for single authentication attempts. The \u003ca href=\"https://www.nuget.org/packages/Konscious.Security.Cryptography.Argon2\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eKonscious.Security.Cryptography\u003c/a\u003e NuGet package provides a solid implementation.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"key-lifecycle-management\"\u003e\u003ca href=\"/posts/cryptography-dotnet-data-protection/#key-lifecycle-management\" title=\"Key Lifecycle Management\"\u003eKey Lifecycle Management\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eProper key management requires documented procedures for generation, storage, distribution, rotation, and destruction. The \u003ca href=\"https://learn.microsoft.com/aspnet/core/security/data-protection/implementation/key-management\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eData Protection API handles rotation automatically\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=\"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\"\u003eAddDataProtection\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/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\"\u003eSetDefaultKeyLifetime\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\"\u003eFromDays\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e90\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/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\"\u003eAddKeyManagementOptions\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\"\u003eAutoGenerateKeys\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\"\u003eoptions\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eNewKeyLifetime\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\"\u003eFromDays\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e90\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/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 a key expires, the system generates a new key for all new encryption operations. Existing data encrypted with old keys remains decryptable because the key ring maintains historical keys. For manual rotation—say, after a security incident—use \u003ccode\u003eIKeyManager.CreateNewKey()\u003c/code\u003e with immediate activation.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"validating-encryption-with-github-actions\"\u003e\u003ca href=\"/posts/cryptography-dotnet-data-protection/#validating-encryption-with-github-actions\" title=\"Validating Encryption with GitHub Actions\"\u003eValidating Encryption with GitHub Actions\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eAutomated scanning ensures sensitive data never reaches the repository unencrypted. Use \u003ca href=\"https://github.com/trufflesecurity/trufflehog\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eTruffleHog\u003c/a\u003e for continuous compliance verification:\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\"\u003eSecurity Scan\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\"\u003eon\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\"\u003epull_request\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003ebranches\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"l\"\u003emain]\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003ejobs\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\"\u003esecrets-scan\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\"\u003eruns-on\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eubuntu-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    \u003c/span\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\"\u003euses\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eactions/checkout@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\"\u003efetch-depth\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"m\"\u003e0\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=\"w\"\u003e      \u003c/span\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\"\u003eScan for exposed secrets\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\"\u003etrufflesecurity/trufflehog@main\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\"\u003epath\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\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\"\u003ebase\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003e${{ github.event.pull_request.base.sha }}\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\"\u003ehead\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003e${{ github.event.pull_request.head.sha }}\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\u003eThis blocks merges containing hardcoded credentials or embedded encryption keys—enforceable controls that prevent non-compliant code from reaching production.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"compliance-mapping\"\u003e\u003ca href=\"/posts/cryptography-dotnet-data-protection/#compliance-mapping\" title=\"Compliance Mapping\"\u003eCompliance Mapping\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe .NET Data Protection implementation satisfies multiple compliance requirements across frameworks:\u003c/p\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003eRequirement\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eImplementation\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\u003eCryptographic Policy\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eDocumented configuration: AES-256-CBC, 256-bit keys, purpose strings for scope isolation\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\u003eKey Management\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eAzure Key Vault with HSM-backed storage, access logging, 90-day rotation\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\u003eKey Hierarchy\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eMaster key encrypts data protection keys—defense in depth\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\u003eCloud Controls\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eManaged identities eliminate credential storage; encryption at rest for keys and data\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eWhether you\u0026rsquo;re auditing against ISO 27001, SOC 2, or GDPR Article 32, the implementation pattern remains identical. The framework handles the cryptographic heavy lifting—your job is configuration and integration.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-bottom-line\"\u003e\u003ca href=\"/posts/cryptography-dotnet-data-protection/#the-bottom-line\" title=\"The Bottom Line\"\u003eThe Bottom Line\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eCompliance standards exist because organizations repeatedly failed to protect sensitive data through ad-hoc security measures. Cryptographic controls codify lessons from decades of breaches: \u003cstrong\u003ecryptography is complex, key management is critical, and professional implementations outperform clever hacks\u003c/strong\u003e.\u003c/p\u003e\n\u003cp\u003eThe .NET Data Protection API provides a compliance-ready framework—algorithm selection, key rotation, secure storage—all handled by Microsoft\u0026rsquo;s cryptography team. The only requirement is resisting the temptation to reimplement what they spent years perfecting.\u003c/p\u003e\n\u003cp\u003eUse the Data Protection API. Integrate Azure Key Vault. Rotate your keys. Hash passwords with Argon2. \u003cstrong\u003eThese aren\u0026rsquo;t best practices—they\u0026rsquo;re minimum requirements\u003c/strong\u003e for systems claiming to protect user data. The alternative is discovering during an audit that your XOR encryption was never encryption at all.\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2026-02-05T17:00:00+01:00","id":"https://daily-devops.net/posts/cryptography-dotnet-data-protection/","language":"en","summary":"XOR operations and hardcoded keys fail audits. Learn how .NET Data Protection API with Azure Key Vault delivers real cryptographic compliance.","tags":["iso-standards","security","dotnet","cryptography"],"title":"Your Encryption Is Broken — .NET Data Protection Done Right","url":"https://daily-devops.net/posts/cryptography-dotnet-data-protection/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eLast week I reviewed a client\u0026rsquo;s .NET application destined for an enterprise Azure deployment. Connection strings sat in plain text in \u003ccode\u003eappsettings.Production.json\u003c/code\u003e. API keys for payment processors right alongside them. Both committed to GitHub—publicly visible for three months before anyone noticed.\u003c/p\u003e\n\u003cp\u003eThis wasn\u0026rsquo;t negligence. It was the predictable outcome of teams that never learned \u003cem\u003ewhy\u003c/em\u003e secrets management matters beyond \u0026ldquo;it\u0026rsquo;s a best practice.\u0026rdquo; They\u0026rsquo;d seen the Azure Key Vault docs. But when deadlines hit, they defaulted to the easiest approach: just put it in the config file.\u003c/p\u003e\n\u003cp\u003eWhat they didn\u0026rsquo;t understand is that secrets in configuration files aren\u0026rsquo;t just a security anti-pattern. They\u0026rsquo;re a fundamental violation of ISO/IEC 27017—the international standard governing cloud security controls. And that violation has real consequences: failed audits, denied insurance claims, contractual penalties.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"what-iso-27017-actually-requires\"\u003e\u003ca href=\"/posts/secrets-management-azure-keyvault/#what-iso-27017-actually-requires\" title=\"What ISO 27017 Actually Requires\"\u003eWhat ISO 27017 Actually Requires\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eISO/IEC 27017 extends the foundational ISO 27001 framework with cloud-specific guidance. Control CLD 10.1.2 addresses cryptographic key management directly—and it impacts every .NET application handling production secrets.\u003c/p\u003e\n\u003cp\u003eThe standard doesn\u0026rsquo;t prescribe specific technologies. It defines control objectives. For secrets management, CLD 10.1.2 requires:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003eCryptographic keys protected throughout their lifecycle\u003c/strong\u003e—generation, distribution, storage, rotation, destruction.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eClear responsibility delineation\u003c/strong\u003e between cloud provider and customer.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eLeast-privilege access\u003c/strong\u003e with explicit authorization policies.\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eParent controls A.10.1.1 (Policy on cryptographic controls) and A.10.1.2 (Key management) from ISO 27001 complete the framework: organizations must define cryptographic policies, implement key management procedures, and maintain audit trails for every key access.\u003c/p\u003e\n\u003cp\u003eI\u0026rsquo;ve watched teams treat these as abstract compliance checkboxes—something for the security team to worry about, not developers. That\u0026rsquo;s a fundamental misunderstanding. These controls translate directly into engineering requirements. Every line of code that touches a secret either satisfies them or violates them. There\u0026rsquo;s no middle ground.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-shared-responsibility-boundary\"\u003e\u003ca href=\"/posts/secrets-management-azure-keyvault/#the-shared-responsibility-boundary\" title=\"The Shared Responsibility Boundary\"\u003eThe Shared Responsibility Boundary\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eHere\u0026rsquo;s where teams consistently get confused: Azure Key Vault creates a shared responsibility boundary for cryptographic key protection. Understanding this boundary isn\u0026rsquo;t optional—it\u0026rsquo;s essential for compliance.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eMicrosoft\u0026rsquo;s responsibility\u003c/strong\u003e (as cloud service provider):\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003ePhysical security of hardware security modules (HSMs)\u003c/li\u003e\n\u003cli\u003eEncryption of secrets at rest within the vault\u003c/li\u003e\n\u003cli\u003eNetwork isolation and DDoS protection for the Key Vault service\u003c/li\u003e\n\u003cli\u003ePatch management and security updates\u003c/li\u003e\n\u003cli\u003eSOC 2, ISO 27001, ISO 27017 certification of service infrastructure\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003eYour responsibility\u003c/strong\u003e (as cloud service customer):\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eAccess policies defining who and what can retrieve secrets\u003c/li\u003e\n\u003cli\u003eNetwork access rules restricting which systems can reach your vault\u003c/li\u003e\n\u003cli\u003eRotation policies ensuring secrets don\u0026rsquo;t remain static indefinitely\u003c/li\u003e\n\u003cli\u003eMonitoring and alerting for suspicious access patterns\u003c/li\u003e\n\u003cli\u003eSecure retrieval patterns that don\u0026rsquo;t expose secrets in logs or memory dumps\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eWhen my client\u0026rsquo;s \u003ccode\u003eappsettings.Production.json\u003c/code\u003e contained plaintext connection strings, they hadn\u0026rsquo;t just made a poor technical choice. They had assumed responsibility for \u003cem\u003eall\u003c/em\u003e cryptographic protection of those credentials—protection they had no capability to provide. No HSM. No encryption at rest. No access auditing. No rotation procedures. No audit trail.\u003c/p\u003e\n\u003cp\u003eThat\u0026rsquo;s not a minor oversight. That\u0026rsquo;s a compliance gap wide enough to drive an audit finding through. And auditors \u003cem\u003ewill\u003c/em\u003e find it.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-fatal-pattern-secrets-in-configuration\"\u003e\u003ca href=\"/posts/secrets-management-azure-keyvault/#the-fatal-pattern-secrets-in-configuration\" title=\"The Fatal Pattern: Secrets in Configuration\"\u003eThe Fatal Pattern: Secrets in Configuration\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eLet me show you exactly what compliance failure looks like. This pattern appears in far too many production applications I\u0026rsquo;ve reviewed:\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=\"c1\"\u003e// appsettings.Production.json - The compliance disaster\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;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;Database\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;Server=prod-sql.database.windows.net;Database=OrdersDB;User Id=app_user;Password=SuperSecretP@ssw0rd123;\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;PaymentGateway\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;ApiKey\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;pk_live_1234567890\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;WebhookSecret\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;whsec_abcdef123456\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\n\n\n\n\u003ch3 id=\"three-iso-controls-broken-at-once\"\u003e\u003ca href=\"/posts/secrets-management-azure-keyvault/#three-iso-controls-broken-at-once\" title=\"Three ISO Controls Broken At Once\"\u003eThree ISO Controls Broken At Once\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eEvery line of that configuration file represents a compliance violation:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eCLD 10.1.2 violation\u003c/strong\u003e: Cryptographic keys stored without HSM protection, without access controls, without lifecycle management. The storage account key sits there in plain text, accessible to anyone who can read the file.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eA.10.1.1 violation\u003c/strong\u003e: No cryptographic policy governs how these secrets are protected. They\u0026rsquo;re just\u0026hellip; there. Committed to source control. Deployed to servers. Copied between environments.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eA.10.1.2 violation\u003c/strong\u003e: No key management procedure exists. When was that database password last rotated? Who has access to it? What happens when an employee leaves? Nobody knows, because nobody tracked it.\u003c/p\u003e\n\u003cp\u003eThe violations compound when you consider the full deployment pipeline. Secrets echoed to CI/CD logs. Shared in Slack channels (\u0026ldquo;Hey, I need the production API key for debugging\u0026rdquo;). Copy-pasted between environments. Stored in plain text on developer laptops.\u003c/p\u003e\n\u003cp\u003eThis cascade of failures doesn\u0026rsquo;t stem from incompetence or malice. It stems from teams that never understood the \u003cem\u003econtrol objectives\u003c/em\u003e behind secrets management. They know secrets in code are \u0026ldquo;bad,\u0026rdquo; but they don\u0026rsquo;t understand \u003cem\u003ewhy\u003c/em\u003e—so when shortcuts become convenient, they take them.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-compliant-pattern-azure-key-vault-with-managed-identity\"\u003e\u003ca href=\"/posts/secrets-management-azure-keyvault/#the-compliant-pattern-azure-key-vault-with-managed-identity\" title=\"The Compliant Pattern: Azure Key Vault with Managed Identity\"\u003eThe Compliant Pattern: Azure Key Vault with Managed Identity\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eNow let me show you what compliance actually looks like. Not just \u0026ldquo;better security\u0026rdquo;—actual ISO 27017 control satisfaction:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\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\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// In production, use Managed Identity to authenticate to Key Vault\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// In development, DefaultAzureCredential falls back to Azure CLI or Visual Studio\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\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eEnvironment\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eIsDevelopment\u003c/span\u003e\u003cspan class=\"p\"\u003e())\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\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\"\u003ekeyVaultUri\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=\"s\"\u003e$\u0026#34;https://{builder.Configuration[\u0026#34;\u003c/span\u003e\u003cspan class=\"n\"\u003eKeyVaultName\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;]}.vault.azure.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=\"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\u003cspan class=\"n\"\u003eAddAzureKeyVault\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ekeyVaultUri\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\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\u003eDefaultAzureCredential\u003c/code\u003e class deserves attention. It implements a credential chain that automatically selects the appropriate authentication method based on execution environment: Managed Identity for Azure-hosted workloads, Visual Studio credentials for local development, Azure CLI for command-line scenarios. Your code never contains credentials for accessing Key Vault itself—the identity \u003cem\u003eis\u003c/em\u003e the authentication. No secrets to manage for accessing the secret store.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\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// Strongly-typed configuration with 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=\"kd\"\u003esealed\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eDatabaseOptions\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\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    [Required]\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\"\u003eConnectionString\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 \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eEmpty\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"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\"\u003esealed\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003ePaymentGatewayOptions\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\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    [Required]\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\"\u003einit\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\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eEmpty\u003c/span\u003e\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    [Required]\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\"\u003eWebhookSecret\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 \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eEmpty\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Registration with startup validation - fail fast if secrets are missing\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\"\u003eAddOptionsWithValidateOnStart\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eDatabaseOptions\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\"\u003eBindConfiguration\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Database\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\"\u003eAddOptionsWithValidateOnStart\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003ePaymentGatewayOptions\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\"\u003eBindConfiguration\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;PaymentGateway\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\n\n\n\u003ch3 id=\"mapping-secret-names-to-configuration-keys\"\u003e\u003ca href=\"/posts/secrets-management-azure-keyvault/#mapping-secret-names-to-configuration-keys\" title=\"Mapping Secret Names To Configuration Keys\"\u003eMapping Secret Names To Configuration Keys\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eKey Vault secrets map to configuration paths using \u003ccode\u003e--\u003c/code\u003e as separator (colons aren\u0026rsquo;t valid in secret names):\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\"\u003eDatabase--ConnectionString     →  Database:ConnectionString\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003ePaymentGateway--ApiKey         →  PaymentGateway:ApiKey\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003ePaymentGateway--WebhookSecret  →  PaymentGateway:WebhookSecret\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis naming convention means your existing \u003ccode\u003eIConfiguration\u003c/code\u003e-based code doesn\u0026rsquo;t need to change. The secrets appear in the configuration hierarchy exactly where your code expects them.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"implementing-least-privilege-access-policies\"\u003e\u003ca href=\"/posts/secrets-management-azure-keyvault/#implementing-least-privilege-access-policies\" title=\"Implementing Least-Privilege Access Policies\"\u003eImplementing Least-Privilege Access Policies\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eISO 27017 control CLD 10.1.2 requires that access to cryptographic keys follow least-privilege principles. Too many teams interpret this as \u0026ldquo;use Azure AD authentication\u0026rdquo; and call it done. That\u0026rsquo;s not least-privilege—that\u0026rsquo;s just \u003cem\u003eauthentication\u003c/em\u003e.\u003c/p\u003e\n\u003cp\u003eAzure Key Vault implements true least-privilege through Azure RBAC:\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=\"kd\"\u003eresource\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nv\"\u003ekeyVault\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#39;Microsoft.KeyVault/vaults@2023-07-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\"\u003ekeyVaultName\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\"\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\"\u003etenantId\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nf\"\u003esubscription\u003c/span\u003e\u003cspan class=\"p\"\u003e().\u003c/span\u003e\u003cspan class=\"nv\"\u003etenantId\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\"\u003esku\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 \u003c/span\u003e\u003cspan class=\"nv\"\u003efamily\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#39;A\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\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=\"s\"\u003e\u0026#39;standard\u0026#39;\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// Use Azure RBAC instead of legacy access policies\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\"\u003eenableRbacAuthorization\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=\"c1\"\u003e// Compliance requirements: soft-delete and purge protection\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\"\u003eenableSoftDelete\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=\"nv\"\u003esoftDeleteRetentionInDays\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nv\"\u003e90\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\"\u003eenablePurgeProtection\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=\"c1\"\u003e// Network restrictions - deny by default\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\"\u003enetworkAcls\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\"\u003edefaultAction\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#39;Deny\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=\"nv\"\u003ebypass\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#39;AzureServices\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=\"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// Application identity gets ONLY \u0026#34;Secrets User\u0026#34; - read secrets, nothing else\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\"\u003eappSecretsRole\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\"\u003ekeyVault\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\"\u003eappIdentity\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;Key Vault Secrets User\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\"\u003ekeyVault\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\"\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;4633458b-17de-408a-b874-0445c86b69e6\u0026#39;\u003c/span\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"c1\"\u003e// Key Vault Secrets User\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\"\u003eprincipalId\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nv\"\u003eappIdentity\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nv\"\u003eproperties\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\n\n\n\n\u003ch3 id=\"three-roles-three-blast-radii\"\u003e\u003ca href=\"/posts/secrets-management-azure-keyvault/#three-roles-three-blast-radii\" title=\"Three Roles, Three Blast Radii\"\u003eThree Roles, Three Blast Radii\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eNotice the deliberate role separation:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eApplications\u003c/strong\u003e receive \u003ccode\u003eKey Vault Secrets User\u003c/code\u003e—they can read secrets, nothing else. No listing, no management, no deletion.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eOperations teams\u003c/strong\u003e receive \u003ccode\u003eKey Vault Secrets Officer\u003c/code\u003e—they can manage secrets but not vault configuration.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSecurity administrators\u003c/strong\u003e receive \u003ccode\u003eKey Vault Administrator\u003c/code\u003e—full control, assigned sparingly and with justification.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThis separation satisfies ISO 27017\u0026rsquo;s requirement for explicit authorization policies. Every access is attributable to a specific identity. That\u0026rsquo;s the audit trail compliance demands—and auditors will check.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"soft-delete-and-purge-protection\"\u003e\u003ca href=\"/posts/secrets-management-azure-keyvault/#soft-delete-and-purge-protection\" title=\"Soft-Delete and Purge Protection\"\u003eSoft-Delete and Purge Protection\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eTwo Key Vault features directly address ISO 27017 control A.10.1.2 requirements for key lifecycle management. I\u0026rsquo;ve seen teams skip these \u0026ldquo;because we\u0026rsquo;re careful\u0026rdquo;—and then watched them panic when an automated script accidentally purged production secrets.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eSoft-delete\u003c/strong\u003e ensures deleted secrets aren\u0026rsquo;t immediately destroyed. They enter a recoverable state for a configurable retention period (7-90 days). This protects against accidental deletion by operators, malicious deletion by compromised accounts, and recovery needs during incident response.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003ePurge protection\u003c/strong\u003e prevents even Key Vault administrators from permanently destroying secrets during the retention period. Once enabled, purge protection cannot be disabled—a deliberate design choice that protects the audit trail. You can\u0026rsquo;t cover your tracks if you can\u0026rsquo;t destroy the evidence.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\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\"\u003eKeyVaultHealthCheck\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eIHealthCheck\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eSecretClient\u003c/span\u003e \u003cspan class=\"n\"\u003e_secretClient\u003c/span\u003e\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\"\u003eKeyVaultHealthCheck\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003e_logger\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eKeyVaultHealthCheck\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eSecretClient\u003c/span\u003e \u003cspan class=\"n\"\u003esecretClient\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\"\u003eKeyVaultHealthCheck\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=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003e_secretClient\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003esecretClient\u003c/span\u003e\u003cspan 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=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eHealthCheckResult\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eCheckHealthAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eHealthCheckContext\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \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=\"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// Verify we can reach Key Vault and have appropriate permissions\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_secretClient\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetPropertiesOfSecretsAsync\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=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAsPages\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003epageSizeHint\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\"\u003eFirstAsync\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\"\u003eHealthCheckResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eHealthy\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Key Vault connection verified\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\"\u003ecatch\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eRequestFailedException\u003c/span\u003e \u003cspan class=\"n\"\u003eex\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"n\"\u003ewhen\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\"\u003eStatus\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"m\"\u003e403\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan 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;Key Vault access denied - check Managed Identity role assignment\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\"\u003eHealthCheckResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUnhealthy\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Key Vault access denied - RBAC misconfiguration\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\"\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;Key Vault health check 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\"\u003eHealthCheckResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUnhealthy\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Key Vault unavailable\u0026#34;\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=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/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=\"failing-fast-on-rbac-misconfiguration\"\u003e\u003ca href=\"/posts/secrets-management-azure-keyvault/#failing-fast-on-rbac-misconfiguration\" title=\"Failing Fast On RBAC Misconfiguration\"\u003eFailing Fast On RBAC Misconfiguration\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThis health check catches configuration errors before they become production incidents. Deploy to a new environment with a misconfigured Managed Identity? The health check fails immediately, before users hit the application.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"github-actions-without-secret-exposure\"\u003e\u003ca href=\"/posts/secrets-management-azure-keyvault/#github-actions-without-secret-exposure\" title=\"GitHub Actions Without Secret Exposure\"\u003eGitHub Actions Without Secret Exposure\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eCI/CD pipelines are a frequent source of secret leakage—and ISO 27017 compliance extends to your deployment infrastructure. I\u0026rsquo;ve reviewed pipelines where developers echo environment variables to logs \u0026ldquo;for debugging,\u0026rdquo; permanently exposing production credentials in build history.\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\"\u003eDeploy to Azure\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\"\u003eon\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\"\u003epush\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003ebranches\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"l\"\u003emain]\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003epermissions\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\"\u003eid-token\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003ewrite \u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"c\"\u003e# Required for OIDC - no long-lived secrets\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\"\u003econtents\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eread\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\"\u003ejobs\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\"\u003edeploy\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\"\u003eruns-on\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eubuntu-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    \u003c/span\u003e\u003cspan class=\"nt\"\u003eenvironment\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eproduction \u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"c\"\u003e# Requires approval for production deploys\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=\"w\"\u003e    \u003c/span\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\"\u003euses\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eactions/checkout@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\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\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eAzure Login via OIDC\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\"\u003eazure/login@v2\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=\"c\"\u003e# These are NOT secrets - they\u0026#39;re identifiers\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\"\u003eclient-id\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003e${{ vars.AZURE_CLIENT_ID }}\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\"\u003etenant-id\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003e${{ vars.AZURE_TENANT_ID }}\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\"\u003esubscription-id\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003e${{ vars.AZURE_SUBSCRIPTION_ID }}\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=\"w\"\u003e      \u003c/span\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 Application\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 -c Release -o ./publish\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=\"w\"\u003e      \u003c/span\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\"\u003eDeploy to App Service\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\"\u003eazure/webapps-deploy@v3\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\"\u003eapp-name\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003e${{ vars.APP_SERVICE_NAME }}\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\"\u003epackage\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003e./publish\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e      \u003c/span\u003e\u003cspan class=\"c\"\u003e# App Service retrieves secrets from Key Vault via 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=\"w\"\u003e      \u003c/span\u003e\u003cspan class=\"c\"\u003e# No secrets pass through this pipeline - ever\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\n\n\n\n\u003ch3 id=\"why-oidc-beats-service-principal-secrets\"\u003e\u003ca href=\"/posts/secrets-management-azure-keyvault/#why-oidc-beats-service-principal-secrets\" title=\"Why OIDC Beats Service Principal Secrets\"\u003eWhy OIDC Beats Service Principal Secrets\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThe key principles here matter:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003eOIDC authentication\u003c/strong\u003e eliminates long-lived service principal secrets. The workflow requests a short-lived token for each run.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSecrets never appear in workflow logs\u003c/strong\u003e—use \u003ccode\u003evars\u003c/code\u003e for non-sensitive configuration, never echo values.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eManaged Identity handles Key Vault access\u003c/strong\u003e—the deployment pipeline doesn\u0026rsquo;t need to know production secrets.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eEnvironment protection rules\u003c/strong\u003e require approval for production deployments, creating an audit trail.\u003c/li\u003e\n\u003c/ol\u003e\n\n\n\n\n\u003ch2 id=\"automatic-key-rotation\"\u003e\u003ca href=\"/posts/secrets-management-azure-keyvault/#automatic-key-rotation\" title=\"Automatic Key Rotation\"\u003eAutomatic Key Rotation\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eISO 27017 control A.10.1.2 requires key lifecycle management, including rotation. \u0026ldquo;We\u0026rsquo;ll rotate it when we remember\u0026rdquo; isn\u0026rsquo;t a rotation policy—it\u0026rsquo;s a prayer. Key Vault supports automatic rotation for certain secret types:\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=\"kd\"\u003eresource\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nv\"\u003erotationPolicy\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#39;Microsoft.KeyVault/vaults/secrets/rotationPolicies@2023-07-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\"\u003eparent\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nv\"\u003estorageKeySecret\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=\"s\"\u003e\u0026#39;default\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=\"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\"\u003erotationPolicy\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\"\u003elifetimeActions\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=\"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\"\u003etrigger\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 \u003c/span\u003e\u003cspan class=\"nv\"\u003etimeAfterCreate\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#39;P60D\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=\"c1\"\u003e// 60 days after creation\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\"\u003eaction\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 \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;Rotate\u0026#39;\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=\"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=\"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\"\u003etrigger\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 \u003c/span\u003e\u003cspan class=\"nv\"\u003etimeBeforeExpiry\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#39;P30D\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=\"c1\"\u003e// 30 days before expiry\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\"\u003eaction\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 \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;Notify\u0026#39;\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=\"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=\"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\"\u003eattributes\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 \u003c/span\u003e\u003cspan class=\"nv\"\u003eexpiryTime\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#39;P90D\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=\"c1\"\u003e// Secret expires after 90 days\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=\"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\n\n\n\n\u003ch3 id=\"monitoring-secrets-you-cannot-auto-rotate\"\u003e\u003ca href=\"/posts/secrets-management-azure-keyvault/#monitoring-secrets-you-cannot-auto-rotate\" title=\"Monitoring Secrets You Cannot Auto-Rotate\"\u003eMonitoring Secrets You Cannot Auto-Rotate\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eFor secrets that can\u0026rsquo;t be automatically rotated—third-party API keys, legacy system credentials—implement monitoring to track age. Don\u0026rsquo;t rely on humans remembering:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\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\"\u003eSecretExpirationMonitor\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eBackgroundService\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\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\"\u003eSecretClient\u003c/span\u003e \u003cspan class=\"n\"\u003e_secretClient\u003c/span\u003e\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\"\u003eSecretExpirationMonitor\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\"\u003eTimeSpan\u003c/span\u003e \u003cspan class=\"n\"\u003e_warningThreshold\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\"\u003eFromDays\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=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eSecretExpirationMonitor\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eSecretClient\u003c/span\u003e \u003cspan class=\"n\"\u003esecretClient\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\"\u003eSecretExpirationMonitor\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=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003e_secretClient\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003esecretClient\u003c/span\u003e\u003cspan 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=\"kd\"\u003eprotected\u003c/span\u003e \u003cspan class=\"kd\"\u003eoverride\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e \u003cspan class=\"n\"\u003eExecuteAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eCancellationToken\u003c/span\u003e \u003cspan class=\"n\"\u003estoppingToken\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \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\"\u003etimer\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003ePeriodicTimer\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\"\u003eFromHours\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e24\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ewhile\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003etimer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWaitForNextTickAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003estoppingToken\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan 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\"\u003eforeach\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003esecret\u003c/span\u003e \u003cspan class=\"k\"\u003ein\u003c/span\u003e \u003cspan class=\"n\"\u003e_secretClient\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetPropertiesOfSecretsAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003estoppingToken\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan 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\"\u003esecret\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eExpiresOn\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eHasValue\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \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\"\u003etimeUntilExpiry\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003esecret\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eExpiresOn\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\"\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\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\"\u003etimeUntilExpiry\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026lt;=\u003c/span\u003e \u003cspan class=\"n\"\u003e_warningThreshold\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                    \u003cspan 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\"\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;Secret {SecretName} expires in {Days} days. Rotation required.\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\"\u003esecret\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\"\u003etimeUntilExpiry\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eDays\u003c/span\u003e\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// Integration point: send alert to ops team, create ticket, etc.\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/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=\"from-configuration-files-to-compliance\"\u003e\u003ca href=\"/posts/secrets-management-azure-keyvault/#from-configuration-files-to-compliance\" title=\"From Configuration Files to Compliance\"\u003eFrom Configuration Files to Compliance\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe path from \u003ccode\u003eappsettings.Production.json\u003c/code\u003e to ISO 27017 compliance isn\u0026rsquo;t just about moving secrets to Key Vault. It requires a fundamental shift in how teams think about credential management:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eSecrets are not configuration.\u003c/strong\u003e They require dedicated infrastructure with access controls, rotation policies, and audit trails. Treating them as configuration values—even encrypted configuration values—misses the point entirely. Configuration tells your application \u003cem\u003ehow\u003c/em\u003e to behave. Secrets tell it \u003cem\u003ewho it is\u003c/em\u003e. Those are fundamentally different concerns.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eIdentity replaces secrets wherever possible.\u003c/strong\u003e Managed Identity for Key Vault access, OIDC for CI/CD authentication, Entra ID for database connections. Each identity-based authentication eliminates a secret that would otherwise require management. The best secret is the one that doesn\u0026rsquo;t exist.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAccess must be explicit and auditable.\u003c/strong\u003e Every system and person that can retrieve secrets must be explicitly authorized through RBAC. Every access must generate an audit event. When an auditor asks \u0026ldquo;who accessed this secret in the last 90 days,\u0026rdquo; you need an answer—not a shrug.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eRotation must be procedural, not heroic.\u003c/strong\u003e When rotation depends on someone remembering to update a secret, it won\u0026rsquo;t happen reliably. Automated rotation for supported services. Monitoring and alerting for everything else. The goal is systems that fail safely when secrets expire, not systems that depend on human vigilance.\u003c/p\u003e\n\u003cp\u003eThe teams that get this right aren\u0026rsquo;t necessarily more security-conscious than others. They\u0026rsquo;re the ones who understood that ISO 27017 controls exist for practical reasons—they codify lessons from decades of security failures into engineering requirements.\u003c/p\u003e\n\u003cp\u003eYour \u003ccode\u003eappsettings.Production.json\u003c/code\u003e file shouldn\u0026rsquo;t contain secrets. Not because it\u0026rsquo;s a best practice. Not because some security checklist told you so. Because ISO 27017 controls CLD 10.1.2, A.10.1.1, and A.10.1.2 require cryptographic key protection that configuration files simply cannot provide.\u003c/p\u003e\n\u003cp\u003eAzure Key Vault, Managed Identity, and proper access controls aren\u0026rsquo;t optional security enhancements. They\u0026rsquo;re the minimum viable implementation of international compliance standards that your organization probably already claims to follow.\u003c/p\u003e\n\u003cp\u003eThe shared responsibility model is clear: Microsoft secures the vault. You secure the access. Neither works without the other.\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2026-02-03T17:00:00+01:00","id":"https://daily-devops.net/posts/secrets-management-azure-keyvault/","language":"en","summary":"That connection string in your config file violates ISO 27017. Azure Key Vault is not optional—it is the compliance minimum you have been ignoring.","tags":["iso-standards","security","azure","dotnet","cloud"],"title":"Your appsettings.json Is a Compliance Violation","url":"https://daily-devops.net/posts/secrets-management-azure-keyvault/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003e\u0026ldquo;Show me the access logs for user authentication events over the past six months.\u0026rdquo;\u003c/p\u003e\n\u003cp\u003eYou grep through text files scattered across three servers, paste fragments into Excel, and spend forty minutes assembling evidence that proves nothing useful. The auditor checks a box. You both know this is theater.\u003c/p\u003e\n\u003cp\u003eThis scene replays in organizations everywhere, and it reveals a fundamental misunderstanding about what audit logging should accomplish. Teams treat logging as a checkbox exercise—something to satisfy compliance requirements rather than infrastructure that actually protects systems and enables incident response.\u003c/p\u003e\n\u003cp\u003eISO 27001 Control A.12.4 exists because security incidents leave traces—if you capture them. Attackers probing authentication endpoints, privilege escalations, unauthorized data exports—these events generate signals. The question is whether your logging infrastructure captures those signals in a form that\u0026rsquo;s actually useful, or whether it buries them in terabytes of unstructured noise.\u003c/p\u003e\n\u003cp\u003eMost implementations fail spectacularly. They log too much, filling disks with DEBUG spam that nobody reads. They log too little, providing no context when something breaks at 2 AM. They log the wrong things—I\u0026rsquo;ve reviewed codebases that captured credit card numbers and API keys in plain text while missing the authentication failures that would have detected an actual breach. And they store logs where application users can delete them, which defeats the entire purpose of audit trails.\u003c/p\u003e\n\u003cp\u003eThe gap between \u0026ldquo;we have logging\u0026rdquo; and \u0026ldquo;we satisfy A.12.4\u0026rdquo; is wider than teams realize. This article closes it using .NET structured logging with Application Insights—not compliance theater, but infrastructure that engineers actually use for troubleshooting while simultaneously satisfying auditors.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"what-a124-actually-requires\"\u003e\u003ca href=\"/posts/audit-logging-azure-app-insights/#what-a124-actually-requires\" title=\"What A.12.4 Actually Requires\"\u003eWhat A.12.4 Actually Requires\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eFour controls. Four things auditors check.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eA.12.4.1 (Event logging)\u003c/strong\u003e mandates recording user activities, exceptions, and security events with sufficient context for investigation. When unauthorized access occurs, your logs must answer who did what, when, and where. Vague entries like \u0026ldquo;error occurred\u0026rdquo; provide zero forensic value.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eA.12.4.2 (Protection)\u003c/strong\u003e addresses a reality that many teams ignore: attackers who compromise systems will attempt to cover their tracks. Logs stored in the same database that application users access? Non-compliant. Logs writable by the same service account that generates them? Equally problematic. You need immutable storage, separate credentials, and ideally external collection systems that the application itself cannot manipulate.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eA.12.4.3 (Privileged operations)\u003c/strong\u003e recognizes that admin accounts represent elevated risk. When a database administrator exports the entire customer table at 3 AM, that event demands capture and review. Regular user activity might aggregate; privileged operations must remain granular and traceable.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eA.12.4.4 (Clock synchronization)\u003c/strong\u003e sounds trivial until you try to reconstruct an incident timeline across distributed services. If your web tier timestamps events in UTC, your database in local time, and your authentication service in server time, correlation becomes guesswork. Consistent NTP synchronization isn\u0026rsquo;t optional.\u003c/p\u003e\n\u003cp\u003eMeeting these requirements doesn\u0026rsquo;t demand expensive SIEM products or complex compliance tools. It demands disciplined engineering.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-fatal-approach-what-fails-audits\"\u003e\u003ca href=\"/posts/audit-logging-azure-app-insights/#the-fatal-approach-what-fails-audits\" title=\"The Fatal Approach: What Fails Audits\"\u003eThe Fatal Approach: What Fails Audits\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eI\u0026rsquo;ve reviewed codebases at three separate organizations over the past eighteen months that shared the same fundamental mistakes. The pattern is depressingly consistent:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\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=\"s\"\u003e$\u0026#34;User {request.UserId} attempting order at {DateTime.Now}\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;Card {request.CreditCardNumber}, CVV {request.CVV}\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;Payment gateway key: {_config[\u0026#34;\u003c/span\u003e\u003cspan class=\"n\"\u003ePaymentGateway\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"n\"\u003eApiKey\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\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis code violates every A.12.4 control simultaneously, and I\u0026rsquo;m not exaggerating. The unstructured string concatenation produces logs that are impossible to query. When an auditor requests \u0026ldquo;all access attempts by user ID 42,\u0026rdquo; you hand them a 900 MB text file with instructions to Ctrl+F. That\u0026rsquo;s not compliance—that\u0026rsquo;s evidence of negligence.\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003eConsole.WriteLine\u003c/code\u003e outputs vanish when containers restart. In production, you might retain three weeks of logs instead of six months because deployments rotate infrastructure. The auditor asks for historical data; you explain that it doesn\u0026rsquo;t exist.\u003c/p\u003e\n\u003cp\u003eThe sensitive data exposure creates immediate PCI-DSS violations alongside the A.12.4.2 failures. Credit card numbers, CVV codes, API keys—all logged in plain text, accessible to thirty developers who have read permissions for debugging purposes. When breach notification requirements trigger, these logs become evidence of negligent data handling.\u003c/p\u003e\n\u003cp\u003eWithout correlation IDs, tracing a single request across distributed services becomes archaeology. The order creation endpoint calls inventory, payment, and notification services. Each generates independent log streams with no shared identifier. Good luck reconstructing what actually happened when something fails.\u003c/p\u003e\n\u003cp\u003eAnd \u003ccode\u003eDateTime.Now\u003c/code\u003e instead of UTC guarantees timestamp chaos. Your US-East servers log in EST, your Europe instances in CET, your database in UTC. Incident timelines become exercises in timezone arithmetic that auditors rightfully reject.\u003c/p\u003e\n\u003cp\u003eThis approach fails audits, fails operations, and fails security. All at once.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-correct-approach-structured-logging\"\u003e\u003ca href=\"/posts/audit-logging-azure-app-insights/#the-correct-approach-structured-logging\" title=\"The Correct Approach: Structured Logging\"\u003eThe Correct Approach: Structured Logging\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e.NET\u0026rsquo;s \u003ccode\u003eILogger\u003c/code\u003e interface supports structured logging out of the box, yet most teams use it incorrectly. The critical pattern that separates queryable audit trails from unstructured noise: message templates with semantic properties instead of string interpolation.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\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\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003escope\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=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eDictionary\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003eobject\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [\u0026#34;CorrelationId\u0026#34;]\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eActivity\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\"\u003eId\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=\"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=\"na\"\u003e    [\u0026#34;UserId\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\"\u003eUserId\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"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\"\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;Order creation initiated for UserId {UserId} with Amount {Amount}\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\"\u003eUserId\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\"\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\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 payment, CardLast4 {CardLast4}\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\"\u003eMaskCreditCard\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\"\u003eCreditCardNumber\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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 failed for UserId {UserId}\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\"\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\u003eThe difference between \u003ccode\u003e$\u0026quot;User {userId}\u0026quot;\u003c/code\u003e and \u003ccode\u003e\u0026quot;User {UserId}\u0026quot;, userId\u003c/code\u003e seems cosmetic, but it\u0026rsquo;s fundamental. The first produces opaque text that requires regex parsing. The second captures \u003ccode\u003eUserId\u003c/code\u003e as a structured property that Application Insights indexes automatically. You can query \u003ccode\u003etraces | where customDimensions.UserId == \u0026quot;42\u0026quot;\u003c/code\u003e and retrieve every operation for that user in seconds—no grep, no text parsing, no manual correlation.\u003c/p\u003e\n\u003cp\u003eCorrelation IDs via \u003ccode\u003eActivity.Current\u003c/code\u003e propagate across distributed calls automatically. ASP.NET Core creates an \u003ccode\u003eActivity\u003c/code\u003e for each inbound request, and when you call downstream services using \u003ccode\u003eHttpClient\u003c/code\u003e, that ID flows via headers. Application Insights groups these distributed traces, visualizing the complete request flow across services. This is A.12.4.1 compliance that also makes production debugging possible.\u003c/p\u003e\n\u003cp\u003eRedaction of sensitive data prevents the credential exposure that plagues most codebases. The \u003ccode\u003eMaskCreditCard\u003c/code\u003e helper preserves last-four digits for support purposes while removing PAN data that triggers PCI-DSS scope. API keys, passwords, tokens—none appear in logs. You capture context without creating liability.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"application-insights-setup\"\u003e\u003ca href=\"/posts/audit-logging-azure-app-insights/#application-insights-setup\" title=\"Application Insights Setup\"\u003eApplication Insights Setup\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=\"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\"\u003eAddApplicationInsightsTelemetry\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=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;ApplicationInsights:ConnectionString\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\"\u003eEnableAdaptiveSampling\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=\"c1\"\u003e// Compliance: retain ALL logs\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"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\"\u003eLogging\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddApplicationInsights\u003c/span\u003e\u003cspan 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\"\u003eLogging\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSetMinimumLevel\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\"\u003eInformation\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\u003eDisabling adaptive sampling is non-negotiable for compliance. Sampling reduces costs by discarding a percentage of telemetry in high-volume scenarios, but it violates A.12.4.1\u0026rsquo;s requirement for complete audit trails. Auditors don\u0026rsquo;t accept \u0026ldquo;we probably captured most authentication attempts.\u0026rdquo; You need every event, or you need to implement server-side filtering based on severity and event type.\u003c/p\u003e\n\u003cp\u003eSetting the minimum log level to \u003ccode\u003eInformation\u003c/code\u003e balances detail with noise. You capture business events—orders created, payments processed, authentication decisions—while excluding the DEBUG and TRACE spam that provides zero audit value. For privileged operations, use \u003ccode\u003eLogWarning\u003c/code\u003e to ensure they stand out during review.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"audit-middleware\"\u003e\u003ca href=\"/posts/audit-logging-azure-app-insights/#audit-middleware\" title=\"Audit Middleware\"\u003eAudit Middleware\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eCapturing HTTP request/response cycles requires middleware that logs every inbound call with appropriate context. The pattern is straightforward but often implemented incorrectly:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\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\"\u003eInvokeAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eHttpContext\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"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\"\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=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eDictionary\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003eobject\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e        [\u0026#34;CorrelationId\u0026#34;]\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eActivity\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\"\u003eId\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=\"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=\"na\"\u003e        [\u0026#34;UserId\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\"\u003eUser\u003c/span\u003e\u003cspan class=\"p\"\u003e?.\u003c/span\u003e\u003cspan class=\"n\"\u003eIdentity\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;anonymous\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=\"na\"\u003e        [\u0026#34;IPAddress\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\"\u003eConnection\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRemoteIpAddress\u003c/span\u003e\u003cspan class=\"p\"\u003e?.\u003c/span\u003e\u003cspan class=\"n\"\u003eToString\u003c/span\u003e\u003cspan class=\"p\"\u003e(),\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e        [\u0026#34;RequestPath\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\"\u003eRequest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePath\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"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\"\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;HTTP {Method} {Path} from {IPAddress}\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\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRequest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eMethod\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRequest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePath\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eConnection\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRemoteIpAddress\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_next\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003elevel\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eResponse\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eStatusCode\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026gt;=\u003c/span\u003e \u003cspan class=\"m\"\u003e400\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\"\u003eWarning\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\"\u003eInformation\u003c/span\u003e\u003cspan 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\"\u003eLog\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=\"s\"\u003e\u0026#34;HTTP {Method} {Path} completed with {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\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRequest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eMethod\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRequest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePath\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eResponse\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eStatusCode\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis middleware captures user identity from the authenticated context, IP address for geographic correlation, request path for access pattern analysis, and response status for success/failure tracking. Logging at different levels based on outcome—\u003ccode\u003eInformation\u003c/code\u003e for successful requests, \u003ccode\u003eWarning\u003c/code\u003e for 4xx client errors—enables alert rules that notify on elevated error rates without flooding dashboards with routine traffic.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"privileged-operations\"\u003e\u003ca href=\"/posts/audit-logging-azure-app-insights/#privileged-operations\" title=\"Privileged Operations\"\u003ePrivileged Operations\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eA.12.4.3 demands enhanced logging for administrative actions. Identify endpoints that modify configuration, grant permissions, or access sensitive data, then tag them 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=\"na\"\u003e[Authorize(Roles = \u0026#34;Administrator\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\"\u003eGrantRole\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eid\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eRoleRequest\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=\"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;PRIVILEGED: {AdminId} granting {Role} to {TargetId}\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\"\u003eUser\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFindFirstValue\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eClaimTypes\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eNameIdentifier\u003c/span\u003e\u003cspan class=\"p\"\u003e),\u003c/span\u003e\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\"\u003eRoleName\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=\"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\u003eUsing \u003ccode\u003eLogWarning\u003c/code\u003e for privileged operations ensures they appear in filtered views even when successful. The \u0026ldquo;PRIVILEGED\u0026rdquo; prefix enables trivial querying: \u003ccode\u003etraces | where message startswith \u0026quot;PRIVILEGED\u0026quot;\u003c/code\u003e retrieves every admin action across your entire infrastructure. When an incident investigation requires understanding what privileges were modified leading up to a breach, you have that answer in seconds.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"cicd-validation\"\u003e\u003ca href=\"/posts/audit-logging-azure-app-insights/#cicd-validation\" title=\"CI/CD Validation\"\u003eCI/CD Validation\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eCatch logging regressions before production:\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\"\u003eValidate no Console.WriteLine\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=\"p\"\u003e|\u003c/span\u003e\u003cspan class=\"sd\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sd\"\u003e    if grep -r \u0026#34;Console\\.WriteLine\u0026#34; src/ --include=\u0026#34;*.cs\u0026#34; --exclude-dir=Tests; then\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sd\"\u003e      echo \u0026#34;ERROR: Use ILogger, not Console.WriteLine\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sd\"\u003e      exit 1\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sd\"\u003e    fi\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\"\u003eCheck for credential logging\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=\"p\"\u003e|\u003c/span\u003e\u003cspan class=\"sd\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sd\"\u003e    if grep -ri \u0026#34;password\\|apikey\\|secret\u0026#34; src/*.cs | grep -i \u0026#34;log\u0026#34;; then\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sd\"\u003e      echo \u0026#34;WARNING: Review for sensitive data in logs\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sd\"\u003e    fi\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\n\n\n\n\u003ch3 id=\"retention-and-access\"\u003e\u003ca href=\"/posts/audit-logging-azure-app-insights/#retention-and-access\" title=\"Retention and Access\"\u003eRetention and Access\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eApplication Insights stores telemetry in Azure Log Analytics workspaces, which provide built-in retention and access controls that satisfy A.12.4.2. Configure retention to match your compliance requirements—typically 90 days minimum, often 365 days for regulated industries. Navigate to your Log Analytics workspace, find Usage and estimated costs, and set Data Retention appropriately.\u003c/p\u003e\n\u003cp\u003eRole-based access control restricts who can query logs. Grant read access to security teams using Azure\u0026rsquo;s \u0026ldquo;Log Analytics Reader\u0026rdquo; role. Developers troubleshooting production issues may need temporary access; implement time-bound role assignments via Azure Privileged Identity Management. The critical principle: developers who deploy code should not have unrestricted access to production logs containing user activity and authentication events.\u003c/p\u003e\n\u003cp\u003eApplication Insights\u0026rsquo; append-only storage model inherently satisfies immutability requirements. Once telemetry is ingested, it cannot be modified or deleted by application service principals. Only Azure administrators with workspace-level permissions can purge data, and those actions create audit logs in Azure Activity Log. Compromised application credentials cannot erase evidence.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"compliance-queries-kql\"\u003e\u003ca href=\"/posts/audit-logging-azure-app-insights/#compliance-queries-kql\" title=\"Compliance Queries (KQL)\"\u003eCompliance Queries (KQL)\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eAuditor evidence in seconds:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\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// All auth attempts for user 42\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003etraces\u003c/span\u003e \u003cspan class=\"p\"\u003e|\u003c/span\u003e \u003cspan class=\"k\"\u003ewhere\u003c/span\u003e \u003cspan class=\"n\"\u003ecustomDimensions\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=\"s\"\u003e\u0026#34;42\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=\"k\"\u003ewhere\u003c/span\u003e \u003cspan class=\"n\"\u003emessage\u003c/span\u003e \u003cspan class=\"n\"\u003econtains\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;login\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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// Privileged operations\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003etraces\u003c/span\u003e \u003cspan class=\"p\"\u003e|\u003c/span\u003e \u003cspan class=\"k\"\u003ewhere\u003c/span\u003e \u003cspan class=\"n\"\u003emessage\u003c/span\u003e \u003cspan class=\"n\"\u003estartswith\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;PRIVILEGED\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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// Failed requests over time\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003erequests\u003c/span\u003e \u003cspan class=\"p\"\u003e|\u003c/span\u003e \u003cspan class=\"k\"\u003ewhere\u003c/span\u003e \u003cspan class=\"n\"\u003eresultCode\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026gt;=\u003c/span\u003e \u003cspan class=\"m\"\u003e400\u003c/span\u003e \u003cspan class=\"p\"\u003e|\u003c/span\u003e \u003cspan class=\"n\"\u003esummarize\u003c/span\u003e \u003cspan class=\"n\"\u003ecount\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"k\"\u003eby\u003c/span\u003e \u003cspan class=\"n\"\u003ebin\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003etimestamp\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"m\"\u003e1\u003c/span\u003e\u003cspan class=\"n\"\u003eh\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\u003eExport to CSV. Done.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"beyond-compliance\"\u003e\u003ca href=\"/posts/audit-logging-azure-app-insights/#beyond-compliance\" title=\"Beyond Compliance\"\u003eBeyond Compliance\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eTeams that implement structured logging for compliance invariably discover benefits that extend far beyond satisfying auditors.\u003c/p\u003e\n\u003cp\u003eWhen production breaks at 2 AM, correlation IDs trace requests across distributed services in seconds. No manual log aggregation, no timezone conversion, no guessing which error corresponds to which user session. Application Insights distributed tracing visualizes the failure path, and you fix the root cause instead of treating symptoms.\u003c/p\u003e\n\u003cp\u003eApplication Insights Smart Detection analyzes telemetry patterns and alerts on deviations—sudden increases in authentication failures that might indicate credential stuffing, spikes in 500 errors following deployments, elevated response times suggesting database contention. These signals emerge from structured logs; you can\u0026rsquo;t detect anomalies in unstructured text.\u003c/p\u003e\n\u003cp\u003eOne team I worked with discovered that 40% of their Azure SQL DTU consumption came from a single inefficient query—identified via structured logging of database operation timings. The logging infrastructure they built for compliance paid for itself in reduced cloud spend within three months.\u003c/p\u003e\n\u003cp\u003eAnd beyond ISO 27001, the same structured audit logs satisfy requirements from GDPR Article 30, SOC 2 CC7.2, and PCI-DSS Requirement 10. Build the infrastructure once, satisfy multiple regulatory frameworks.\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003eBuild observability that happens to satisfy compliance—not compliance theater that happens to generate logs.\u003c/p\u003e\n\u003c/blockquote\u003e\n\n\n\n\n\u003ch2 id=\"getting-started\"\u003e\u003ca href=\"/posts/audit-logging-azure-app-insights/#getting-started\" title=\"Getting Started\"\u003eGetting Started\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eIf your current logging consists of \u003ccode\u003eConsole.WriteLine\u003c/code\u003e scattered throughout controllers, the path forward is straightforward. Add the \u003ccode\u003eMicrosoft.ApplicationInsights.AspNetCore\u003c/code\u003e NuGet package and configure the connection string. Replace string interpolation with message templates—convert \u003ccode\u003e$\u0026quot;User {userId}\u0026quot;\u003c/code\u003e to \u003ccode\u003e\u0026quot;User {UserId}\u0026quot;, userId\u003c/code\u003e so properties become queryable.\u003c/p\u003e\n\u003cp\u003eImplement correlation IDs via \u003ccode\u003eActivity.Current\u003c/code\u003e in request scopes, ensuring every log statement within a request shares the same identifier. Grep your codebase for \u003ccode\u003epassword\u003c/code\u003e, \u003ccode\u003eapikey\u003c/code\u003e, \u003ccode\u003esecret\u003c/code\u003e, and \u003ccode\u003etoken\u003c/code\u003e in logging statements. If you find matches, you have work to do before your next audit.\u003c/p\u003e\n\u003cp\u003eAdd audit middleware for HTTP request/response cycles. Configure Log Analytics retention appropriate to your industry. Verify that application service principals cannot delete telemetry data. Write the KQL queries your auditors will request and test them with your security team before the audit arrives.\u003c/p\u003e\n\u003cp\u003eA.12.4 doesn\u0026rsquo;t require expensive SIEMs or complex third-party compliance tools. It requires disciplined engineering: structured properties, protected storage, correlation across distributed systems, and consistent UTC timestamps. .NET\u0026rsquo;s \u003ccode\u003eILogger\u003c/code\u003e and Azure Application Insights provide these capabilities natively. The effort lies not in adopting new tools, but in applying existing tools correctly.\u003c/p\u003e\n\u003cp\u003eYour auditor checks a box. Your on-call engineers thank you at 2 AM. Your security team has visibility that actually matters when incidents occur.\u003c/p\u003e\n\u003cp\u003eThat\u0026rsquo;s compliance done right.\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2026-01-29T17:00:00+01:00","id":"https://daily-devops.net/posts/audit-logging-azure-app-insights/","language":"en","summary":"Most audit logs fail when incidents happen. Structured logging with Application Insights creates trails auditors accept and engineers actually use.","tags":["iso-standards","logging","observability","azure","dotnet","security","compliance","bestpractices"],"title":"Audit Logging That Survives Your Next Security Incident","url":"https://daily-devops.net/posts/audit-logging-azure-app-insights/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003e\u003cstrong\u003eAuthentication proves identity. Authorization enforces access rights. ISO/IEC 27001 Control A.9 requires both — and teams consistently confuse them.\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eI\u0026rsquo;ve reviewed too many ASP.NET Core applications where authentication exists but authorization is theater. Teams feel secure because they see \u003ccode\u003e[Authorize]\u003c/code\u003e attributes on controllers. Then you find \u003ccode\u003e[AllowAnonymous]\u003c/code\u003e scattered across action methods, hardcoded role strings in business logic, and zero audit trail when authorization fails.\u003c/p\u003e\n\u003cp\u003eThis isn\u0026rsquo;t just bad architecture. \u003cstrong\u003eIt\u0026rsquo;s a violation of ISO/IEC 27001 Control A.9 (Access Control)\u003c/strong\u003e, which explicitly requires:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eA.9.1:\u003c/strong\u003e Business requirements of access control\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eA.9.2:\u003c/strong\u003e User access management\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eA.9.4:\u003c/strong\u003e System and application access control\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThe standard doesn\u0026rsquo;t care that your authentication works. It demands that access rights are enforced based on business rules, managed centrally, and audited comprehensively. String-based role checks scattered across your codebase don\u0026rsquo;t satisfy any of those requirements.\u003c/p\u003e\n\u003cp\u003eHere\u0026rsquo;s what inadequate access control looks like in the codebases I\u0026rsquo;ve reviewed, why auditors flag it immediately, and what actually works.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-fatal-pattern-authentication-without-authorization\"\u003e\u003ca href=\"/posts/access-control-aspnet-core/#the-fatal-pattern-authentication-without-authorization\" title=\"The Fatal Pattern: Authentication Without Authorization\"\u003eThe Fatal Pattern: Authentication Without Authorization\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eHere\u0026rsquo;s what I encounter repeatedly in ASP.NET Core applications that claim to be \u0026ldquo;secure\u0026rdquo;:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\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]\u003c/span\u003e \u003cspan class=\"c1\"\u003e// Authentication required...\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\"\u003eDocumentsController\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=\"c1\"\u003e// ...but anyone authenticated can access everything\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [HttpGet(\u0026#34;{id}\u0026#34;)]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [AllowAnonymous]\u003c/span\u003e \u003cspan class=\"c1\"\u003e// Wait, why is this here?\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\"\u003eGetDocument\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=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"cm\"\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=\"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=\"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\"\u003eCreateDocument\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eCreateDocumentRequest\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// Authorization check buried in business logic\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(!\u003c/span\u003e\u003cspan class=\"n\"\u003eUser\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eIsInRole\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;DocumentCreator\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 \u003cspan class=\"c1\"\u003e// No audit trail\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"cm\"\u003e/* ... */\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \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    [HttpDelete(\u0026#34;{id}\u0026#34;)]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"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\"\u003eDeleteDocument\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// Different pattern, same problem\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\"\u003euserRoles\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\"\u003eClaims\u003c/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\"\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\"\u003eType\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"n\"\u003eClaimTypes\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRole\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/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\"\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\"\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            \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\"\u003euserRoles\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=\"s\"\u003e\u0026#34;Admin\u0026#34;\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\"\u003euserRoles\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=\"s\"\u003e\u0026#34;DocumentManager\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 \u003cspan class=\"c1\"\u003e// Still no audit trail\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"cm\"\u003e/* ... */\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/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 violates \u003cstrong\u003emultiple ISO 27001 controls simultaneously\u003c/strong\u003e:\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"violation-1-inconsistent-access-control-a941\"\u003e\u003ca href=\"/posts/access-control-aspnet-core/#violation-1-inconsistent-access-control-a941\" title=\"Violation 1: Inconsistent Access Control (A.9.4.1)\"\u003eViolation 1: Inconsistent Access Control (A.9.4.1)\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThe \u003ccode\u003e[AllowAnonymous]\u003c/code\u003e attribute on \u003ccode\u003eGetDocument\u003c/code\u003e contradicts the controller-level \u003ccode\u003e[Authorize]\u003c/code\u003e attribute. Why does document retrieval bypass authentication entirely? This creates an access control gap that auditors will flag immediately.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eISO requirement:\u003c/strong\u003e \u0026ldquo;Access to systems and applications shall be controlled in accordance with access control policy.\u0026rdquo;\u003c/p\u003e\n\u003cp\u003eThere is no coherent access control policy here. Anonymous access for reads, role checks for writes, and no documented business justification.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"violation-2-decentralized-authorization-logic-a921\"\u003e\u003ca href=\"/posts/access-control-aspnet-core/#violation-2-decentralized-authorization-logic-a921\" title=\"Violation 2: Decentralized Authorization Logic (A.9.2.1)\"\u003eViolation 2: Decentralized Authorization Logic (A.9.2.1)\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eAuthorization decisions are scattered across controller actions using different patterns:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eString-based role checks: \u003ccode\u003eUser.IsInRole(\u0026quot;DocumentCreator\u0026quot;)\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003eManual claim enumeration: \u003ccode\u003eUser.Claims.Where(...)\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003eInconsistent role requirements: \u003ccode\u003e\u0026quot;Admin\u0026quot;\u003c/code\u003e vs \u003ccode\u003e\u0026quot;DocumentManager\u0026quot;\u003c/code\u003e with no explanation\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003eISO requirement:\u003c/strong\u003e \u0026ldquo;A formal user access provisioning process shall be implemented to assign or revoke access rights.\u0026rdquo;\u003c/p\u003e\n\u003cp\u003eHow do you audit these access rights? Where is the central policy that defines who can create, read, update, or delete documents? The authorization rules are embedded in controller code. Every change requires a deployment. Every audit requires reading through controller implementations.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"violation-3-no-audit-trail-a945\"\u003e\u003ca href=\"/posts/access-control-aspnet-core/#violation-3-no-audit-trail-a945\" title=\"Violation 3: No Audit Trail (A.9.4.5)\"\u003eViolation 3: No Audit Trail (A.9.4.5)\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eWhen authorization fails, the application returns \u003ccode\u003e403 Forbidden\u003c/code\u003e. But where\u0026rsquo;s the audit log?\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eISO requirement:\u003c/strong\u003e \u0026ldquo;Access to information and application system functions shall be restricted in accordance with the access control policy.\u0026rdquo;\u003c/p\u003e\n\u003cp\u003eThe standard requires you to demonstrate that access controls are enforced. Without audit logs of authorization failures, you cannot prove compliance. Returning \u003ccode\u003eForbid()\u003c/code\u003e silently discards evidence that the control was tested.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"violation-4-brittle-role-management-a922\"\u003e\u003ca href=\"/posts/access-control-aspnet-core/#violation-4-brittle-role-management-a922\" title=\"Violation 4: Brittle Role Management (A.9.2.2)\"\u003eViolation 4: Brittle Role Management (A.9.2.2)\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eHardcoded role strings like \u003ccode\u003e\u0026quot;DocumentCreator\u0026quot;\u003c/code\u003e and \u003ccode\u003e\u0026quot;Admin\u0026quot;\u003c/code\u003e create maintenance nightmares. When business requirements change:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eWhich role takes precedence?\u003c/li\u003e\n\u003cli\u003eWho decides when roles should be renamed?\u003c/li\u003e\n\u003cli\u003eHow do you handle role deprecation without breaking production?\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003eISO requirement:\u003c/strong\u003e \u0026ldquo;User access rights shall be reviewed at regular intervals.\u0026rdquo;\u003c/p\u003e\n\u003cp\u003eYou can\u0026rsquo;t review access rights that are hardcoded strings scattered across dozens of controllers. Every review requires grepping the codebase and hoping developers remembered to update every location.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"what-actually-works-policy-based-authorization\"\u003e\u003ca href=\"/posts/access-control-aspnet-core/#what-actually-works-policy-based-authorization\" title=\"What Actually Works: Policy-Based Authorization\"\u003eWhat Actually Works: Policy-Based Authorization\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eISO 27001 Control A.9 is satisfied when authorization is:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003eCentralized\u003c/strong\u003e — defined in one location, not scattered across controllers\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003ePolicy-driven\u003c/strong\u003e — based on business requirements, not implementation details\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eAuditable\u003c/strong\u003e — authorization decisions are logged comprehensively\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTestable\u003c/strong\u003e — policies can be verified independently of controllers\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eHere\u0026rsquo;s how ASP.NET Core implements this correctly:\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"step-1-define-authorization-policies\"\u003e\u003ca href=\"/posts/access-control-aspnet-core/#step-1-define-authorization-policies\" title=\"Step 1: Define Authorization Policies\"\u003eStep 1: Define Authorization Policies\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// Program.cs\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eServices\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddAuthorization\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eoptions\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Business requirements become named policies\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eoptions\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddPolicy\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;ViewDocuments\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003epolicy\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003epolicy\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRequireClaim\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;permission\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;documents.read\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\"\u003eoptions\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddPolicy\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;CreateDocuments\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003epolicy\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003epolicy\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRequireClaim\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;permission\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;documents.write\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// Resource-based authorization for ownership\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eoptions\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddPolicy\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;EditOwnDocuments\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003epolicy\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003epolicy\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRequirements\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAdd\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eDocumentOwnershipRequirement\u003c/span\u003e\u003cspan class=\"p\"\u003e()));\u003c/span\u003e\n\u003c/span\u003e\u003c/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 satisfies \u003cstrong\u003eA.9.1 (Business requirements of access control)\u003c/strong\u003e by codifying access requirements as named policies that reflect business operations, not technical role names.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"step-2-implement-custom-authorization-handlers\"\u003e\u003ca href=\"/posts/access-control-aspnet-core/#step-2-implement-custom-authorization-handlers\" title=\"Step 2: Implement Custom Authorization Handlers\"\u003eStep 2: Implement Custom Authorization Handlers\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eFor complex business rules, use authorization handlers:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\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\"\u003eDocumentOwnershipRequirement\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eIAuthorizationRequirement\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\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eDocumentOwnershipHandler\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eAuthorizationHandler\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eDocumentOwnershipRequirement\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eDocument\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eILogger\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eDocumentOwnershipHandler\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003e_logger\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprotected\u003c/span\u003e \u003cspan class=\"kd\"\u003eoverride\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e \u003cspan class=\"n\"\u003eHandleRequirementAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eAuthorizationHandlerContext\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eDocumentOwnershipRequirement\u003c/span\u003e \u003cspan class=\"n\"\u003erequirement\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eDocument\u003c/span\u003e \u003cspan class=\"n\"\u003eresource\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003euserId\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUser\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFindFirst\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eClaimTypes\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eNameIdentifier\u003c/span\u003e\u003cspan class=\"p\"\u003e)?.\u003c/span\u003e\u003cspan class=\"n\"\u003eValue\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \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\"\u003euserId\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"n\"\u003eresource\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eOwnerId\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSucceed\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003erequirement\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"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=\"c1\"\u003e// A.9.4.5: Audit authorization failure\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\u003cspan class=\"s\"\u003e\u0026#34;User {UserId} denied access to document {DocumentId}\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\"\u003euserId\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eresource\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eId\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \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\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 satisfies \u003cstrong\u003eA.9.4.1 (Restriction of access to information)\u003c/strong\u003e by centralizing business logic that determines access rights. Authorization decisions are made in one location, not repeated across controllers.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"step-3-apply-policies-declaratively\"\u003e\u003ca href=\"/posts/access-control-aspnet-core/#step-3-apply-policies-declaratively\" title=\"Step 3: Apply Policies Declaratively\"\u003eStep 3: Apply Policies Declaratively\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=\"na\"\u003e[Authorize]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eDocumentsController\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eControllerBase\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eIAuthorizationService\u003c/span\u003e \u003cspan class=\"n\"\u003e_authorizationService\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eILogger\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eDocumentsController\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=\"na\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [HttpGet(\u0026#34;{id}\u0026#34;)]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [Authorize(Policy = \u0026#34;ViewDocuments\u0026#34;)]\u003c/span\u003e  \u003cspan class=\"c1\"\u003e// Declarative policy\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\"\u003eGetDocument\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eid\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003edocument\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_documentService\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetDocumentAsync\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\"\u003edocument\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=\"n\"\u003eNotFound\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// Resource-based authorization for ownership\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\"\u003eauthResult\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_authorizationService\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAuthorizeAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eUser\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003edocument\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;EditOwnDocuments\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(!\u003c/span\u003e\u003cspan class=\"n\"\u003eauthResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSucceeded\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"c1\"\u003e// A.9.4.5: Audit authorization failure\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;User {UserId} denied access to document {DocumentId}\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\"\u003eUser\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFindFirst\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eClaimTypes\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eNameIdentifier\u003c/span\u003e\u003cspan class=\"p\"\u003e)?.\u003c/span\u003e\u003cspan class=\"n\"\u003eValue\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \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\"\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        \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eOk\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003edocument\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \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=\"na\"\u003e    [Authorize(Policy = \u0026#34;CreateDocuments\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\"\u003eCreateDocument\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eCreateDocumentRequest\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=\"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;User {UserId} creating document\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\"\u003eUser\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFindFirst\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eClaimTypes\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eNameIdentifier\u003c/span\u003e\u003cspan class=\"p\"\u003e)?.\u003c/span\u003e\u003cspan class=\"n\"\u003eValue\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"cm\"\u003e/* ... */\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/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=\"key-improvements\"\u003e\u003ca href=\"/posts/access-control-aspnet-core/#key-improvements\" title=\"Key Improvements\"\u003eKey Improvements\u003c/a\u003e\u003c/h3\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eCentralized Policy Definition:\u003c/strong\u003e All authorization logic lives in \u003ccode\u003eProgram.cs\u003c/code\u003e, not scattered across controllers. When business requirements change, you update policies in one location.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eClaims-Based Permissions:\u003c/strong\u003e Instead of role strings, use permission claims (\u003ccode\u003edocuments.read\u003c/code\u003e, \u003ccode\u003edocuments.write\u003c/code\u003e, \u003ccode\u003edocuments.manage\u003c/code\u003e). This separates identity (who you are) from authorization (what you can do).\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eResource-Based Authorization:\u003c/strong\u003e Use \u003ccode\u003eIAuthorizationService\u003c/code\u003e for dynamic checks that depend on resource properties (e.g., document ownership). This handles scenarios where static policies aren\u0026rsquo;t sufficient.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eComprehensive Audit Logging:\u003c/strong\u003e Every authorization decision is logged with context (user ID, resource ID, action attempted). This satisfies \u003cstrong\u003eA.9.4.5\u003c/strong\u003e audit requirements.\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\n\n\n\n\u003ch2 id=\"testing-authorization-policies\"\u003e\u003ca href=\"/posts/access-control-aspnet-core/#testing-authorization-policies\" title=\"Testing Authorization Policies\"\u003eTesting Authorization Policies\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eISO 27001 Control \u003cstrong\u003eA.12.1.2\u003c/strong\u003e requires testing of security controls. Here\u0026rsquo;s how to verify authorization policies through automated integration tests:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\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\"\u003eDocumentAuthorizationTests\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eIClassFixture\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eWebApplicationFactory\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=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [Fact]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e \u003cspan class=\"n\"\u003eGetDocument_WithoutAuthentication_Returns401\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \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\"\u003e_factory\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCreateClient\u003c/span\u003e\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\"\u003eclient\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;/api/documents/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=\"n\"\u003eAssert\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eEqual\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eHttpStatusCode\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUnauthorized\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eresponse\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eStatusCode\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"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=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e \u003cspan class=\"n\"\u003eGetDocument_WithoutViewPermission_Returns403\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \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\"\u003e_factory\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCreateClient\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eclient\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eDefaultRequestHeaders\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAuthorization\u003c/span\u003e \u003cspan 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\"\u003eAuthenticationHeaderValue\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Bearer\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eTokenWithoutReadPermission\u003c/span\u003e\u003cspan class=\"p\"\u003e());\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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\"\u003eclient\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;/api/documents/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=\"n\"\u003eAssert\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eEqual\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eHttpStatusCode\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eForbidden\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eresponse\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eStatusCode\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/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=\"github-actions-integration\"\u003e\u003ca href=\"/posts/access-control-aspnet-core/#github-actions-integration\" title=\"GitHub Actions Integration\"\u003eGitHub Actions Integration\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eRun these tests in CI/CD to verify authorization policies before deployment:\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\"\u003eSecurity Compliance\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\"\u003eon\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\"\u003epull_request\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\"\u003epaths\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e      \u003c/span\u003e- \u003cspan class=\"s1\"\u003e\u0026#39;src/**/*.cs\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=\"s1\"\u003e\u0026#39;tests/**/*.cs\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\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003ejobs\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\"\u003eauthorization-tests\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\"\u003eruns-on\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eubuntu-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=\"w\"\u003e    \u003c/span\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\"\u003euses\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eactions/checkout@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      \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\"\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;9.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=\"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\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eRun Authorization Tests\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 test --filter \u0026#34;Category=Authorization\u0026#34; --logger \u0026#34;trx;LogFileName=authorization-results.trx\u0026#34;\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=\"w\"\u003e      \u003c/span\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 Test Results\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\"\u003eif\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003ealways()\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\"\u003eEnricoMi/publish-unit-test-result-action@v2\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\"\u003efiles\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;**/authorization-results.trx\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\u003eThis satisfies \u003cstrong\u003eA.12.1.2 (Testing of security controls)\u003c/strong\u003e by automating verification that access controls function as intended.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"health-checks-for-authentication-services\"\u003e\u003ca href=\"/posts/access-control-aspnet-core/#health-checks-for-authentication-services\" title=\"Health Checks for Authentication Services\"\u003eHealth Checks for Authentication Services\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eISO 27001 Control \u003cstrong\u003eA.17.1.1\u003c/strong\u003e requires monitoring availability of critical services. Authentication is a critical service. Here\u0026rsquo;s how to monitor 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// Program.cs\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eServices\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddHealthChecks\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddCheck\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eAuthenticationServiceHealthCheck\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;authentication\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\"\u003eAuthenticationServiceHealthCheck\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eIHealthCheck\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eIConfiguration\u003c/span\u003e \u003cspan class=\"n\"\u003e_configuration\u003c/span\u003e\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\"\u003eHttpClient\u003c/span\u003e \u003cspan class=\"n\"\u003e_httpClient\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eHealthCheckResult\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eCheckHealthAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eHealthCheckContext\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eCancellationToken\u003c/span\u003e \u003cspan class=\"n\"\u003ecancellationToken\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003edefault\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eauthEndpoint\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003e_configuration\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Authentication:Authority\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\"\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\"\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\"\u003e_httpClient\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetAsync\u003c/span\u003e\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;{authEndpoint}/.well-known/openid-configuration\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\"\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\"\u003eresponse\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eIsSuccessStatusCode\u003c/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\"\u003eHealthCheckResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eHealthy\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\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\"\u003eHealthCheckResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eDegraded\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e$\u0026#34;Status: {response.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=\"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\"\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=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eHealthCheckResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUnhealthy\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Authentication unavailable\u0026#34;\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=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"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// Expose endpoint\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\"\u003eMapHealthChecks\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;/health\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 enables external monitoring systems to verify authentication service availability, satisfying operational monitoring requirements.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"mapping-iso-controls-to-implementation\"\u003e\u003ca href=\"/posts/access-control-aspnet-core/#mapping-iso-controls-to-implementation\" title=\"Mapping ISO Controls to Implementation\"\u003eMapping ISO Controls to Implementation\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eHere\u0026rsquo;s how policy-based authorization satisfies specific ISO 27001 requirements:\u003c/p\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003eISO Control\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eRequirement\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eImplementation\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\u003eA.9.1.1\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eAccess control policy\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eCentralized policy definitions in \u003ccode\u003eProgram.cs\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\u003cstrong\u003eA.9.2.1\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eUser access provisioning\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eClaims-based permissions managed via identity provider\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\u003eA.9.2.2\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eUser access rights management\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003ePermission claims reviewed and updated independently of code\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\u003eA.9.4.1\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eInformation access restriction\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003e[Authorize(Policy = \u0026quot;...\u0026quot;)]\u003c/code\u003e attributes enforce policy\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\u003eA.9.4.5\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eAccess control to program source code\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eResource-based authorization with \u003ccode\u003eIAuthorizationService\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\u003cstrong\u003eA.12.1.2\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eTesting of security controls\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eAutomated integration tests verify authorization logic\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\u003eA.12.4.1\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eEvent logging\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eComprehensive audit logs of authorization decisions\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\u003eA.17.1.1\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eContinuity capabilities\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eHealth checks monitor authentication service availability\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=\"what-this-means-for-your-team\"\u003e\u003ca href=\"/posts/access-control-aspnet-core/#what-this-means-for-your-team\" title=\"What This Means for Your Team\"\u003eWhat This Means for Your Team\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eIf you\u0026rsquo;re building ASP.NET Core applications that need to satisfy ISO 27001 compliance:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eStop using role strings in business logic.\u003c/strong\u003e They scatter authorization decisions across your codebase, making audits painful and policy changes risky.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eDefine authorization policies that map to business operations.\u003c/strong\u003e \u003ccode\u003e\u0026quot;ViewDocuments\u0026quot;\u003c/code\u003e is clearer than checking whether \u003ccode\u003eUser.IsInRole(\u0026quot;DocumentReader\u0026quot;)\u003c/code\u003e — though you\u0026rsquo;ll need both policies and resource-based checks for complex scenarios.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eUse claims for permissions, not roles.\u003c/strong\u003e Roles represent groups. Permissions represent capabilities. ISO 27001 cares about capabilities.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eImplement comprehensive audit logging.\u003c/strong\u003e Every authorization failure must be logged with sufficient context to reconstruct what happened and why.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eTest your authorization policies.\u003c/strong\u003e Write integration tests that verify policies work correctly. Run them in CI/CD before deployment.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eMonitor authentication service availability.\u003c/strong\u003e Health checks aren\u0026rsquo;t optional. They\u0026rsquo;re how you demonstrate operational monitoring.\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eAuthentication proves identity. Authorization enforces access rights. ISO/IEC 27001 Control A.9 requires both.\u003c/p\u003e\n\u003cp\u003eThe gap between \u003ccode\u003e[Authorize]\u003c/code\u003e and actual access control is where I find compliance violations in every audit. Policy-based authorization closes that gap — when you map policies to business operations, not developer convenience.\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2026-01-27T17:00:00+01:00","id":"https://daily-devops.net/posts/access-control-aspnet-core/","language":"en","summary":"Your [Authorize] attributes fool developers but not auditors. ISO 27001 A.9 demands actual authorization — not role strings scattered across your codebase.\n","tags":["iso-standards","security","authentication","dotnet","bestpractices"],"title":"Your [Authorize] Attribute Is Compliance Theater\n","url":"https://daily-devops.net/posts/access-control-aspnet-core/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eI\u0026rsquo;ve watched the relationship between developers and ISO standards evolve dramatically over 15 years. In the early 2010s, ISO/IEC 27001 certification was something that happened in conference rooms I never entered. Compliance teams handled audits. Security officers filled out spreadsheets. The certification badge appeared on corporate websites. We developers kept shipping code, blissfully unaware that any of it related to our daily work.\u003c/p\u003e\n\u003cp\u003eThen cloud-native development changed everything.\u003c/p\u003e\n\u003cp\u003eOver the past five years, I\u0026rsquo;ve watched Infrastructure as Code, CI/CD pipelines, and Azure\u0026rsquo;s shared responsibility model fundamentally transform who owns ISO compliance. It\u0026rsquo;s no longer abstract policy work happening in separate departments. ISO/IEC 27001, 27017, and 27701 controls are now concrete technical decisions embedded directly in the code I review every week. The connection string you write, the authentication you implement, the logging statement you add—each one either implements or violates specific ISO requirements.\u003c/p\u003e\n\u003cp\u003eThis isn\u0026rsquo;t theoretical. Last month alone, I\u0026rsquo;ve seen three production systems with hardcoded secrets, two APIs processing sensitive data without authentication, and one logging credit card numbers in plain text to Application Insights. Every single one had ISO certifications displayed prominently on their company websites. The disconnect hasn\u0026rsquo;t disappeared—it\u0026rsquo;s just become significantly more dangerous.\u003c/p\u003e\n\u003cp\u003eLet me show you exactly what changed, and why your next pull request is an ISO compliance decision whether you realize it or not.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-historical-disconnect-standards-as-theater\"\u003e\u003ca href=\"/posts/iso-standards-intro-dotnet-developers/#the-historical-disconnect-standards-as-theater\" title=\"The Historical Disconnect: Standards as Theater\"\u003eThe Historical Disconnect: Standards as Theater\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eTen years ago, ISO/IEC 27001 compliance typically meant this: your company hired consultants, documented processes, implemented policies, passed an audit, and displayed a certification badge. Developers rarely saw any of this. Security was handled by a separate team. Infrastructure lived in a data center managed by operations. Compliance was someone else\u0026rsquo;s problem.\u003c/p\u003e\n\u003cp\u003eThis created what I call \u0026ldquo;compliance theater\u0026rdquo;—the appearance of security without meaningful engineering impact. Everyone clapped when the ISO certificate arrived, then went back to hardcoding production passwords in appsettings.json. ISO standards became associated with bureaucracy rather than real protection. Developers learned to ignore them because they seemed disconnected from actual technical work.\u003c/p\u003e\n\u003cp\u003eThat model made sense when developers didn\u0026rsquo;t control infrastructure, didn\u0026rsquo;t deploy to production, and didn\u0026rsquo;t make decisions about data storage, encryption, or access control. But cloud-native development changed all of that.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"what-changed-the-cloud-native-reality\"\u003e\u003ca href=\"/posts/iso-standards-intro-dotnet-developers/#what-changed-the-cloud-native-reality\" title=\"What Changed: The Cloud-Native Reality\"\u003eWhat Changed: The Cloud-Native Reality\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eCloud-native .NET development on Azure broke this model completely. Now \u003cem\u003eyou\u003c/em\u003e define infrastructure. \u003cem\u003eYou\u003c/em\u003e control secrets. \u003cem\u003eYou\u003c/em\u003e decide encryption. Now developers:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eDefine infrastructure\u003c/strong\u003e through Bicep, Terraform, or ARM templates\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eControl secrets management\u003c/strong\u003e by choosing Key Vault integration or hardcoding credentials\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eDetermine data encryption\u003c/strong\u003e by selecting Azure SQL configurations or accepting defaults\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eConfigure authentication\u003c/strong\u003e by implementing Azure AD or rolling custom solutions\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSet logging levels\u003c/strong\u003e that capture sensitive data or properly classify it\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eDeploy directly to production\u003c/strong\u003e via CI/CD pipelines they build\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eEvery single one of these is a technical decision that \u003cem\u003edirectly implements\u003c/em\u003e (or violates) ISO/IEC 27001, 27017, and 27701 controls. These aren\u0026rsquo;t abstract compliance requirements anymore. They\u0026rsquo;re engineering choices embedded in your code, your configuration, and your deployment automation.\u003c/p\u003e\n\u003cp\u003eLet me demonstrate with a concrete example that shows exactly how ISO standards map to everyday .NET development decisions.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-pattern-i-keep-seeing\"\u003e\u003ca href=\"/posts/iso-standards-intro-dotnet-developers/#the-pattern-i-keep-seeing\" title=\"The Pattern I Keep Seeing\"\u003eThe Pattern I Keep Seeing\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThree weeks ago, I reviewed a production API processing financial transactions. No authentication. Connection string hardcoded in Program.cs. Credit card numbers logged to Application Insights. When I asked about ISO compliance, the team pointed to their certificate. That disconnect—between the certificate on the wall and the code in production—hasn\u0026rsquo;t gone away. It\u0026rsquo;s just gotten more dangerous.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"the-anti-pattern-code\"\u003e\u003ca href=\"/posts/iso-standards-intro-dotnet-developers/#the-anti-pattern-code\" title=\"The Anti-Pattern Code\"\u003eThe Anti-Pattern Code\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eHere\u0026rsquo;s what I see constantly in real codebases—well-intentioned .NET APIs built without considering ISO requirements:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\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// Program.cs\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=\"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\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Hardcoded connection string\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\"\u003eAddDbContext\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eApplicationDbContext\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;(\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=\"n\"\u003eoptions\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=myserver.database.windows.net;Database=mydb;User Id=admin;Password=P@ssw0rd123!;\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\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=\"c1\"\u003e// No authentication required\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\"\u003eUseHttpsRedirection\u003c/span\u003e\u003cspan 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\"\u003eMapControllers\u003c/span\u003e\u003cspan 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\u003cdiv class=\"highlight\"\u003e\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// OrderController.cs\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=\"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\"\u003eCreateOrder\u003c/span\u003e\u003cspan class=\"p\"\u003e([\u003c/span\u003e\u003cspan class=\"n\"\u003eFromBody\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// Plain text logging of sensitive data\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;Creating order for customer: {request.CustomerEmail}, \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;Card: {request.CreditCardNumber}, Amount: {request.Amount}\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\"\u003e_context\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\"\u003eAdd\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\"\u003eCustomerEmail\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\"\u003eCustomerEmail\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\"\u003erequest\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    \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\"\u003eSaveChangesAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eOk\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/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-wrong-here\"\u003e\u003ca href=\"/posts/iso-standards-intro-dotnet-developers/#whats-wrong-here\" title=\"What\u0026rsquo;s Wrong Here\"\u003eWhat\u0026rsquo;s Wrong Here\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThis code compiles. It runs. It might work in production for months. But it violates three fundamental security principles:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eHardcoded secrets\u003c/strong\u003e: Database credentials in source code. Anyone with repository access has production database access.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eNo authentication\u003c/strong\u003e: Anyone on the internet can call this API. No identity verification before processing transactions.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eSensitive data in logs\u003c/strong\u003e: Credit card numbers logged in plain text. When this flows to Application Insights, you\u0026rsquo;ve created a compliance disaster.\u003c/p\u003e\n\u003cp\u003eThis is the kind of code that leads to breach notifications and very uncomfortable conversations with auditors.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"what-compliant-code-actually-looks-like\"\u003e\u003ca href=\"/posts/iso-standards-intro-dotnet-developers/#what-compliant-code-actually-looks-like\" title=\"What Compliant Code Actually Looks Like\"\u003eWhat Compliant Code Actually Looks Like\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eHere\u0026rsquo;s the same functionality, but built with security standards directly into the architecture:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\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// Program.cs\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=\"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\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Secrets from Azure Key Vault\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\"\u003ekeyVaultUri\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\"\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\u003cspan class=\"s\"\u003e\u0026#34;KeyVault:Uri\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\"\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\u003cspan class=\"n\"\u003eAddAzureKeyVault\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ekeyVaultUri\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=\"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\"\u003eAddDbContext\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eApplicationDbContext\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;(\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=\"n\"\u003eoptions\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUseSqlServer\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\u003cspan class=\"s\"\u003e\u0026#34;ConnectionStrings:Database\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// Azure AD authentication required\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\"\u003eAddAuthentication\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eJwtBearerDefaults\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAuthenticationScheme\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/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\"\u003eAddMicrosoftIdentityWebApi\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\u003cspan class=\"n\"\u003eGetSection\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;AzureAd\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\"\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\"\u003eUseAuthentication\u003c/span\u003e\u003cspan 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\"\u003eUseAuthorization\u003c/span\u003e\u003cspan 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\"\u003eMapControllers\u003c/span\u003e\u003cspan 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\u003cdiv class=\"highlight\"\u003e\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// OrderController.cs\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e[Authorize]\u003c/span\u003e  \u003cspan class=\"c1\"\u003e// Require authentication\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eOrderController\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\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=\"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\"\u003eCreateOrder\u003c/span\u003e\u003cspan class=\"p\"\u003e([\u003c/span\u003e\u003cspan class=\"n\"\u003eFromBody\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// Never log sensitive data\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\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"s\"\u003e\u0026#34;Creating order. UserId={UserId}, Amount={Amount}\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\"\u003eUser\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFindFirstValue\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eClaimTypes\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eNameIdentifier\u003c/span\u003e\u003cspan class=\"p\"\u003e),\u003c/span\u003e\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\"\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=\"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\"\u003eUserId\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\"\u003eAmount\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\"\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=\"n\"\u003e_context\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\"\u003eAdd\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_context\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSaveChangesAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eOk\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"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\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=\"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=\"what-changed\"\u003e\u003ca href=\"/posts/iso-standards-intro-dotnet-developers/#what-changed\" title=\"What Changed\"\u003eWhat Changed\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003eKey Vault for secrets\u003c/strong\u003e: Credentials never exist in source code. Azure AD controls who can access them.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAzure AD authentication\u003c/strong\u003e: \u003ccode\u003e[Authorize]\u003c/code\u003e attribute requires valid JWT tokens. Every request is identified and auditable.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eStructured logging\u003c/strong\u003e: Use identifiers, not personal data. No email addresses or credit card numbers in logs.\u003c/p\u003e\n\u003cp\u003eThe key insight: \u003cstrong\u003eyou were already making these decisions\u003c/strong\u003e. ISO-compliant code just means choosing Key Vault over hardcoding, Azure AD over no authentication, and identifiers over sensitive data.\u003c/p\u003e\n\u003cp\u003eDetailed implementation comes in the follow-up articles.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"how-iso-standards-map-to-code-decisions\"\u003e\u003ca href=\"/posts/iso-standards-intro-dotnet-developers/#how-iso-standards-map-to-code-decisions\" title=\"How ISO Standards Map to Code Decisions\"\u003eHow ISO Standards Map to Code Decisions\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eISO/IEC 27001, 27017, and 27701 aren\u0026rsquo;t separate concerns from your daily development work. Every architectural decision you make either implements or violates specific requirements:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eWhere you store secrets\u003c/strong\u003e → Key Vault vs. hardcoding\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eHow you authenticate users\u003c/strong\u003e → Azure AD vs. custom auth\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eWhat you log\u003c/strong\u003e → Structured logging with data classification\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eHow you encrypt data\u003c/strong\u003e → Managed encryption vs. rolling your own\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eHow you handle PII\u003c/strong\u003e → Data minimization, purpose limitation\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eWhere you deploy\u003c/strong\u003e → Understanding shared responsibility\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eEach of these is a technical decision that appears in your code, your Infrastructure as Code templates, and your CI/CD pipelines. The upcoming articles in this series will show you exactly how to implement each requirement correctly in .NET applications.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-shared-responsibility-model-why-this-matters-now\"\u003e\u003ca href=\"/posts/iso-standards-intro-dotnet-developers/#the-shared-responsibility-model-why-this-matters-now\" title=\"The Shared Responsibility Model: Why This Matters Now\"\u003eThe Shared Responsibility Model: Why This Matters Now\u003c/a\u003e\u003c/h2\u003e\n\n\n\n\n\u003ch3 id=\"what-microsoft-handles-vs-what-you-handle\"\u003e\u003ca href=\"/posts/iso-standards-intro-dotnet-developers/#what-microsoft-handles-vs-what-you-handle\" title=\"What Microsoft Handles vs. What You Handle\"\u003eWhat Microsoft Handles vs. What You Handle\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eMicrosoft\u0026rsquo;s \u003ca href=\"https://learn.microsoft.com/en-us/azure/security/fundamentals/shared-responsibility\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eshared responsibility model\u003c/a\u003e fundamentally changed who is responsible for implementing ISO controls. In traditional on-premises infrastructure, your organization controlled everything—physical security, network security, operating systems, applications, data.\u003c/p\u003e\n\u003cp\u003eIn Azure, Microsoft handles:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003ePhysical data center security (locks, guards, cameras)\u003c/li\u003e\n\u003cli\u003eNetwork infrastructure (DDoS protection, network isolation)\u003c/li\u003e\n\u003cli\u003eHypervisor security (compute isolation between tenants)\u003c/li\u003e\n\u003cli\u003ePlatform patching (Azure SQL, App Service, Key Vault patches)\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eYou still handle:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eIdentity and access management (Azure AD configuration)\u003c/li\u003e\n\u003cli\u003eApplication security (authentication, authorization, input validation)\u003c/li\u003e\n\u003cli\u003eData classification and encryption (choosing encryption options)\u003c/li\u003e\n\u003cli\u003eNetwork security (firewall rules, private endpoints)\u003c/li\u003e\n\u003cli\u003eSecret management (using Key Vault correctly)\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003eThe critical insight:\u003c/strong\u003e ISO auditors now evaluate whether you correctly used the platform\u0026rsquo;s security features, not whether you built those features yourself. If Azure provides Key Vault and you hardcoded secrets anyway, that\u0026rsquo;s a finding. If Azure provides Azure AD and you built custom authentication with known vulnerabilities, that\u0026rsquo;s a finding. If Azure provides encryption at rest and you stored plaintext sensitive data, that\u0026rsquo;s a finding.\u003c/p\u003e\n\u003cp\u003eThis is why ISO standards now directly affect your code: \u003cstrong\u003eyou\u0026rsquo;re responsible for the integration layer between your application and Azure\u0026rsquo;s security services.\u003c/strong\u003e That integration \u003cem\u003eis\u003c/em\u003e your code.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-three-standards-what-you-need-to-know\"\u003e\u003ca href=\"/posts/iso-standards-intro-dotnet-developers/#the-three-standards-what-you-need-to-know\" title=\"The Three Standards: What You Need to Know\"\u003eThe Three Standards: What You Need to Know\u003c/a\u003e\u003c/h2\u003e\n\n\n\n\n\u003ch3 id=\"isoiec-27001-information-security-management\"\u003e\u003ca href=\"/posts/iso-standards-intro-dotnet-developers/#isoiec-27001-information-security-management\" title=\"ISO/IEC 27001: Information Security Management\"\u003eISO/IEC 27001: Information Security Management\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThis is the foundation—how organizations protect information. It defines 114 controls across 14 categories. The ones that affect your daily code:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eA.9 (Access Control):\u003c/strong\u003e Who can access your systems and data\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eA.10 (Cryptography):\u003c/strong\u003e How you protect data at rest and in transit\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eA.12 (Operations Security):\u003c/strong\u003e Logging, monitoring, vulnerability management\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eA.14 (System Acquisition, Development, and Maintenance):\u003c/strong\u003e Secure development practices\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eEvery authentication decision, every encryption choice, every logging statement relates to specific 27001 controls.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"isoiec-27017-cloud-specific-security-controls\"\u003e\u003ca href=\"/posts/iso-standards-intro-dotnet-developers/#isoiec-27017-cloud-specific-security-controls\" title=\"ISO/IEC 27017: Cloud-Specific Security Controls\"\u003eISO/IEC 27017: Cloud-Specific Security Controls\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThis extends 27001 with cloud-specific guidance. It adds controls that didn\u0026rsquo;t exist in traditional infrastructure:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eShared responsibility model:\u003c/strong\u003e Clarity on who manages what\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eVirtual machine isolation:\u003c/strong\u003e How cloud providers isolate tenants\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCloud service customer monitoring:\u003c/strong\u003e What visibility you have into the platform\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eVirtual network security:\u003c/strong\u003e How you secure communication between services\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eWhen you configure Azure Virtual Networks, implement private endpoints, or use managed identities, you\u0026rsquo;re implementing 27017 controls.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"isoiec-27701-privacy-information-management\"\u003e\u003ca href=\"/posts/iso-standards-intro-dotnet-developers/#isoiec-27701-privacy-information-management\" title=\"ISO/IEC 27701: Privacy Information Management\"\u003eISO/IEC 27701: Privacy Information Management\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThis extends 27001 with privacy-specific requirements that map directly to GDPR, CCPA, and other privacy regulations:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eData minimization:\u003c/strong\u003e Collect only what you need\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003ePurpose limitation:\u003c/strong\u003e Use data only for stated purposes\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eData subject rights:\u003c/strong\u003e Support access, deletion, portability requests\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003ePrivacy by design:\u003c/strong\u003e Build privacy into architecture from the start\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eWhen you decide what data to log, how long to retain it, and whether to store email addresses or just user IDs, you\u0026rsquo;re making privacy decisions that these standards address.\u003c/p\u003e\n\u003cp\u003eWhy all three matter: modern applications touch all three domains. You\u0026rsquo;re building information systems (27001) on cloud platforms (27017) that process personal data (27701).\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"where-to-start\"\u003e\u003ca href=\"/posts/iso-standards-intro-dotnet-developers/#where-to-start\" title=\"Where to Start\"\u003eWhere to Start\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eIf you\u0026rsquo;re recognizing these patterns in your codebase, here\u0026rsquo;s what to do first:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eStep 1: Audit Your Secrets (1-2 hours)\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eSearch your codebase for hardcoded credentials:\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\"\u003egit grep -i \u003cspan class=\"s2\"\u003e\u0026#34;password\\s*=\u0026#34;\u003c/span\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003egit grep -i \u003cspan class=\"s2\"\u003e\u0026#34;connectionstring\\s*=\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003egit grep -i \u003cspan class=\"s2\"\u003e\u0026#34;apikey\\s*=\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eMigrate every secret to Azure Key Vault. Use \u003ccode\u003eDefaultAzureCredential\u003c/code\u003e for local development and managed identity in production. This single change addresses the most common ISO finding I see in .NET applications.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eStep 2: Add Authentication to Every API (2-4 hours)\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eIf you have public APIs without authentication, add Azure AD integration:\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 Microsoft.Identity.Web\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eApply the \u003ccode\u003e[Authorize]\u003c/code\u003e attribute to every controller that processes sensitive data. This isn\u0026rsquo;t just compliance—it\u0026rsquo;s basic security hygiene.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eStep 3: Review Your Logging (2-3 hours)\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eSearch for logged sensitive data:\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\"\u003egit grep -i \u003cspan class=\"s2\"\u003e\u0026#34;LogInformation.*email\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003egit grep -i \u003cspan class=\"s2\"\u003e\u0026#34;LogInformation.*password\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003egit grep -i \u003cspan class=\"s2\"\u003e\u0026#34;LogInformation.*card\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eReplace with structured logging that uses identifiers instead of personal data. Use \u003ccode\u003eLoggerMessage\u003c/code\u003e source generators for consistent, performant logging.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eStep 4: Enable Encryption at Rest (30 minutes)\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eFor Azure SQL, enable Transparent Data Encryption (it\u0026rsquo;s often on by default, but verify):\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\"\u003eaz sql db tde \u003cspan class=\"nb\"\u003eset\u003c/span\u003e --status Enabled --resource-group mygroup --server myserver --database mydb\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eFor Blob Storage, verify encryption at rest is enabled (also default, but confirm your configuration).\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eStep 5: Document Your Shared Responsibility (1 hour)\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eCreate a simple table documenting what Azure handles vs. what your code handles. This clarifies exactly where ISO controls apply to your development work and becomes invaluable during audits.\u003c/p\u003e\n\u003cp\u003eEach of these steps takes hours, not weeks. Each addresses real ISO findings I\u0026rsquo;ve seen in production systems. And each makes your application genuinely more secure, not just compliant on paper.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"standards-as-engineering-not-paperwork\"\u003e\u003ca href=\"/posts/iso-standards-intro-dotnet-developers/#standards-as-engineering-not-paperwork\" title=\"Standards as Engineering, Not Paperwork\"\u003eStandards as Engineering, Not Paperwork\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe disconnect between ISO certifications and actual code made sense in a different era. But in cloud-native .NET development, standards and code merged. Every technical decision you make—where you store secrets, how you authenticate, what you log—either aligns with these standards or violates them.\u003c/p\u003e\n\u003cp\u003eYou\u0026rsquo;re making these choices anyway. The standards just give you a framework for making better ones.\u003c/p\u003e\n\u003cp\u003eThis isn\u0026rsquo;t about satisfying auditors. It\u0026rsquo;s about building systems that actually protect customer data and survive real threats. The code in this article isn\u0026rsquo;t \u0026ldquo;compliance code\u0026rdquo;—it\u0026rsquo;s just better code.\u003c/p\u003e\n\u003cp\u003eThe next PR you review will contain one of these patterns. You can catch it now while it\u0026rsquo;s easy to fix, or explain it to auditors later when it\u0026rsquo;s woven throughout production. I know which conversation I\u0026rsquo;d rather have.\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2026-01-22T17:00:00+01:00","id":"https://daily-devops.net/posts/iso-standards-intro-dotnet-developers/","language":"en","summary":"ISO/IEC 27001, 27017, and 27701 aren't compliance theater anymore—they're engineering requirements in cloud-native .NET that affect every code decision.\n","tags":["security","compliance","dotnet","cloud","cloudnative","azure"],"title":"Why ISO Standards Actually Matter for .NET Developers","url":"https://daily-devops.net/posts/iso-standards-intro-dotnet-developers/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eISO compliance is not a compliance department problem anymore. The moment you write Infrastructure as Code, configure Azure Key Vault, or make a decision about what your API returns to the client, you\u0026rsquo;re making ISO compliance decisions — whether you realize it or not. This series is for developers who want to understand what those decisions actually mean before the auditor does.\u003c/p\u003e\n\u003cp\u003eThe three standards covered here are distinct but overlapping. ISO/IEC 27001 is the foundational Information Security Management System standard — it defines a broad set of controls covering access, cryptography, operations, incident response, and more, and it\u0026rsquo;s the one most enterprise contracts and procurement processes require. ISO/IEC 27017 extends 27001 specifically for cloud services: it adds cloud-specific controls for shared responsibility, virtual machine hardening, administrator access separation, and network isolation that on-premises 27001 guidance doesn\u0026rsquo;t adequately address. ISO/IEC 27701 is a privacy extension on top of 27001 — it maps 27001 controls to GDPR and other privacy regulation requirements, adding consent management, data subject rights, purpose limitation, and data minimization as explicit control categories. If your organization processes personal data in Europe, 27701 is how 27001 becomes GDPR-relevant.\u003c/p\u003e\n\u003cp\u003eCompliance theater — where you file the right documents, display the certification badge, and keep hardcoding secrets in appsettings.json — doesn\u0026rsquo;t survive cloud-native development. The controls in all three standards now map directly to how you handle secrets, authenticate users, log data, manage dependencies, and design APIs. The gap isn\u0026rsquo;t between \u0026ldquo;compliant\u0026rdquo; and \u0026ldquo;non-compliant\u0026rdquo; organizations. It\u0026rsquo;s between developers who understand what these standards require in code and those who don\u0026rsquo;t.\u003c/p\u003e\n\u003cp\u003eThis series answers that gap with code, not policy paraphrasing. Each article takes a specific control or control cluster, explains what it actually requires, and shows a working implementation in .NET or Azure. Nearly 30 articles. Seven phases. The policy documents won\u0026rsquo;t stop your next audit finding — but understanding what the controls mean in practice might.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"phase-1--foundations\"\u003e\u003ca href=\"/posts/iso-standards/#phase-1--foundations\" title=\"Phase 1 — Foundations\"\u003ePhase 1 — Foundations\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003e\u003ca href=\"/posts/iso-standards-intro-dotnet-developers/\"\u003eWhy ISO Standards Actually Matter for .NET Developers\u003c/a\u003e\u003c/strong\u003e opens the series by explaining the historical disconnect between compliance teams and engineering, why cloud-native development collapsed that gap, and what ISO/IEC 27001, 27017, and 27701 each address. Read this first.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e\u003ca href=\"/posts/access-control-aspnet-core/\"\u003eYour [Authorize] Attribute Is Compliance Theater\u003c/a\u003e\u003c/strong\u003e covers what access control in ASP.NET Core actually requires for ISO compliance — beyond slapping \u003ccode\u003e[Authorize]\u003c/code\u003e on controllers and hoping for the best.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e\u003ca href=\"/posts/audit-logging-azure-app-insights/\"\u003eAudit Logging That Survives Your Next Security Incident\u003c/a\u003e\u003c/strong\u003e explains what audit logs need to contain, why Application Insights isn\u0026rsquo;t a compliance audit trail by default, and how to configure logging that satisfies ISO 27001 control requirements under adversarial conditions.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"phase-2--core-security-controls\"\u003e\u003ca href=\"/posts/iso-standards/#phase-2--core-security-controls\" title=\"Phase 2 — Core Security Controls\"\u003ePhase 2 — Core Security Controls\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003e\u003ca href=\"/posts/secrets-management-azure-keyvault/\"\u003eYour appsettings.json Is a Compliance Violation\u003c/a\u003e\u003c/strong\u003e is the article that prompts the most uncomfortable recognition. Where secrets actually live in .NET projects, why Azure Key Vault integration is an ISO 27001 requirement rather than a nice-to-have, and how to implement it without friction.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e\u003ca href=\"/posts/cryptography-dotnet-data-protection/\"\u003eYour Encryption Is Broken — .NET Data Protection Done Right\u003c/a\u003e\u003c/strong\u003e covers why XOR operations and hardcoded encryption keys don\u0026rsquo;t survive audits, and how the .NET Data Protection API with Azure Key Vault key management delivers cryptographic compliance that holds up.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e\u003ca href=\"/posts/data-minimization-entity-framework/\"\u003eStop Hoarding Personal Data in Entity Framework\u003c/a\u003e\u003c/strong\u003e addresses data minimization under ISO 27701 and GDPR — what you\u0026rsquo;re collecting versus what you actually need, and how Entity Framework models often encode privacy violations directly into the schema.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e\u003ca href=\"/posts/incident-response-github-actions/\"\u003eYour Incident Response Plan Is a Lie. Here\u0026rsquo;s How to Fix It.\u003c/a\u003e\u003c/strong\u003e examines what ISO 27001 requires from incident response processes and how to implement runbooks and automated workflows in GitHub Actions that describe what actually happens rather than what you wish would happen.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e\u003ca href=\"/posts/network-isolation-azure-vnet/\"\u003eYour Azure SQL Is Public Right Now. ISO 27017 Demands You Fix It\u003c/a\u003e\u003c/strong\u003e covers network isolation requirements under ISO 27017 for cloud services — private endpoints, VNet integration, and the Azure networking decisions that move you from compliant-on-paper to compliant-in-practice.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e\u003ca href=\"/posts/dependency-management-nuget-security/\"\u003eNuGet Packages: The Suppliers You Forgot to Audit\u003c/a\u003e\u003c/strong\u003e covers supplier risk management as ISO 27001 requires it — your NuGet dependencies are third-party suppliers, and ignoring them is an audit finding waiting to happen.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"phase-3--consent-and-change-control\"\u003e\u003ca href=\"/posts/iso-standards/#phase-3--consent-and-change-control\" title=\"Phase 3 — Consent and Change Control\"\u003ePhase 3 — Consent and Change Control\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003e\u003ca href=\"/posts/consent-management-aspnet-identity/\"\u003eCookie Banners Won\u0026rsquo;t Save You From ISO 27701\u003c/a\u003e\u003c/strong\u003e explains the gap between displaying a cookie banner and implementing consent management that satisfies ISO 27701 privacy requirements — and what ASP.NET Core Identity needs to do to bridge it.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e\u003ca href=\"/posts/change-control-github-branch-protection/\"\u003eTrust Is Not a Control: ISO 27001 Compliance via GitHub\u003c/a\u003e\u003c/strong\u003e covers change control requirements and how GitHub branch protection, required reviews, and status checks implement them in ways that survive ISO 27001 audits.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"standalone-controls\"\u003e\u003ca href=\"/posts/iso-standards/#standalone-controls\" title=\"Standalone Controls\"\u003eStandalone Controls\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003e\u003ca href=\"/posts/encryption-transit-azure-frontdoor/\"\u003eYour TLS Config is Probably Wrong: Five Audit Failures I Keep Finding\u003c/a\u003e\u003c/strong\u003e covers the five TLS misconfigurations that appear repeatedly in Azure environments — and how Azure Front Door configuration either fixes or introduces them.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e\u003ca href=\"/posts/session-management-aspnet-authentication/\"\u003eYour Logout Button Is Lying: ASP.NET Session Security Done Right\u003c/a\u003e\u003c/strong\u003e covers what ISO 27001 requires from session management and the ASP.NET Core authentication settings that most applications leave at insecure defaults.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"phase-4--data-lifecycle-and-operational-security\"\u003e\u003ca href=\"/posts/iso-standards/#phase-4--data-lifecycle-and-operational-security\" title=\"Phase 4 — Data Lifecycle and Operational Security\"\u003ePhase 4 — Data Lifecycle and Operational Security\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003e\u003ca href=\"/posts/data-retention-azure-storage-lifecycle/\"\u003eNobody Runs Your Cleanup Script (And Regulators Know It)\u003c/a\u003e\u003c/strong\u003e covers data retention under ISO 27701 and GDPR — why manual cleanup processes don\u0026rsquo;t satisfy retention requirements and how Azure Storage lifecycle policies implement them automatically.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e\u003ca href=\"/posts/error-handling-security-information-disclosure/\"\u003eYour Stack Traces Are Love Letters to Attackers\u003c/a\u003e\u003c/strong\u003e covers information disclosure through error handling — what ISO 27001 requires and what ASP.NET Core\u0026rsquo;s default exception handling exposes to people who shouldn\u0026rsquo;t see it.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e\u003ca href=\"/posts/backup-recovery-azure-sql-database/\"\u003eYour Azure SQL Backups Won\u0026rsquo;t Save You (Here\u0026rsquo;s Why)\u003c/a\u003e\u003c/strong\u003e covers business continuity requirements under ISO 27001 — what needs to be in a backup strategy, why \u0026ldquo;we have geo-redundant backups\u0026rdquo; isn\u0026rsquo;t sufficient, and what actually matters for RTO/RPO compliance.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e\u003ca href=\"/posts/health-checks-operational-monitoring/\"\u003eGreen Dashboard, Dead Application\u003c/a\u003e\u003c/strong\u003e covers operational monitoring requirements — the difference between health checks that tell you the process is alive and monitoring that satisfies ISO 27001 availability controls.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"phase-5--deployment-privacy-and-authentication\"\u003e\u003ca href=\"/posts/iso-standards/#phase-5--deployment-privacy-and-authentication\" title=\"Phase 5 — Deployment, Privacy, and Authentication\"\u003ePhase 5 — Deployment, Privacy, and Authentication\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003e\u003ca href=\"/posts/privacy-health-checks-data-access-patterns/\"\u003ePrivacy Health Checks: Beyond Database Connectivity\u003c/a\u003e\u003c/strong\u003e covers data access patterns under ISO 27701 — what \u0026ldquo;privacy by design\u0026rdquo; requires in runtime observability and how to detect data access violations before auditors do.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e\u003ca href=\"/posts/continuous-deployment-security-gates/\"\u003eStop Deploying Garbage to Production\u003c/a\u003e\u003c/strong\u003e covers security testing in CI/CD pipelines as an ISO 27001 control — what gates actually need to exist between code and production for compliance purposes.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e\u003ca href=\"/posts/infrastructure-as-code-compliance-bicep/\"\u003eWhy Your Azure Portal Clicks Will Fail the Next Audit\u003c/a\u003e\u003c/strong\u003e explains why manual Azure configuration doesn\u0026rsquo;t satisfy ISO 27001 change management requirements and what Bicep and infrastructure as code provide that portal configuration fundamentally cannot.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e\u003ca href=\"/posts/multi-factor-authentication-azure-ad-b2c/\"\u003eSecurity Cosplay: Your Password-Only Admin Panel Isn\u0026rsquo;t Fooling Anyone\u003c/a\u003e\u003c/strong\u003e covers MFA requirements under ISO 27001 and ISO 27017 — what privileged access requires and how Azure AD B2C implements authentication controls that satisfy them.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"phase-6--privacy-rights-and-supply-chain\"\u003e\u003ca href=\"/posts/iso-standards/#phase-6--privacy-rights-and-supply-chain\" title=\"Phase 6 — Privacy Rights and Supply Chain\"\u003ePhase 6 — Privacy Rights and Supply Chain\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003e\u003ca href=\"/posts/right-to-erasure-implementation-patterns/\"\u003e\u0026ldquo;Just Delete the User\u0026rdquo;: Famous Last Words Before the GDPR Audit\u003c/a\u003e\u003c/strong\u003e covers right to erasure implementation under ISO 27701 and GDPR — why deleting a user row doesn\u0026rsquo;t satisfy erasure requirements and what complete erasure actually involves across backups, audit logs, and derived data.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e\u003ca href=\"/posts/supply-chain-security-github-dependabot/\"\u003e247 Strangers Have Root Access to Your Production\u003c/a\u003e\u003c/strong\u003e covers third-party dependency risk as ISO 27001 views it — what Dependabot provides versus what supply chain security requires, and how to implement controls that treat transitive dependencies as the attack surface they are.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e\u003ca href=\"/posts/managed-identity-rbac-azure-resources/\"\u003e\u0026ldquo;We Store Secrets in appsettings.json\u0026rdquo;: A Horror Story in Five Acts\u003c/a\u003e\u003c/strong\u003e covers managed identity and RBAC as ISO 27001 access control — moving from credential-based to identity-based Azure resource access and the RBAC scoping that prevents privilege escalation.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e\u003ca href=\"/posts/purpose-limitation-api-design/\"\u003ePurpose Limitation in API Design: Leaking Data You Shouldn\u0026rsquo;t\u003c/a\u003e\u003c/strong\u003e covers purpose limitation under ISO 27701 — what your API response payloads reveal about data you collected for different purposes, and how API design decisions create or prevent privacy compliance problems.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"phase-7--audit-trails-and-compliance-tooling\"\u003e\u003ca href=\"/posts/iso-standards/#phase-7--audit-trails-and-compliance-tooling\" title=\"Phase 7 — Audit Trails and Compliance Tooling\"\u003ePhase 7 — Audit Trails and Compliance Tooling\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003e\u003ca href=\"/posts/audit-trail-dotnet-cli-tools/\"\u003eWho Ran That Migration?\u003c/a\u003e\u003c/strong\u003e covers audit trail requirements for database changes — what ISO 27001 requires from change records and how .NET CLI tooling implements attribution that satisfies it.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e\u003ca href=\"/posts/compliance-verification-dotnet-cli/\"\u003eCertified, Filed, Forgotten: The Compliance Trainwreck\u003c/a\u003e\u003c/strong\u003e addresses the gap between achieving certification and maintaining compliance over time — what continuous compliance verification looks like with .NET CLI tools rather than annual point-in-time assessments.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e\u003ca href=\"/posts/cli-security-testing-audit/\"\u003eSecurity Tests That Prove Themselves\u003c/a\u003e\u003c/strong\u003e covers security testing as a control rather than a ceremony — automated tests that validate specific ISO 27001 requirements and generate audit evidence rather than just passing a pipeline.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e\u003ca href=\"/posts/privacy-audit-automation-dotnet-cli/\"\u003eYour Privacy Docs Are Fiction: Let\u0026rsquo;s Fix That with .NET CLI Tools\u003c/a\u003e\u003c/strong\u003e closes the series by addressing documentation drift — the gap between what your privacy documentation claims and what your codebase actually does, and how automated tooling keeps them synchronized.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"who-this-is-for\"\u003e\u003ca href=\"/posts/iso-standards/#who-this-is-for\" title=\"Who This Is For\"\u003eWho This Is For\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e.NET developers and architects working in environments with ISO 27001, 27017, or 27701 requirements — or environments that will have them after the next customer contract. The series assumes you write code and control infrastructure. It does not assume you have a compliance team to defer to.\u003c/p\u003e\n\u003cp\u003eEvery article contains working .NET code, Azure configuration, or tooling examples. The goal throughout is turning abstract control requirements into specific implementation decisions with clear trade-offs.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"where-to-start\"\u003e\u003ca href=\"/posts/iso-standards/#where-to-start\" title=\"Where to Start\"\u003eWhere to Start\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eStart with Phase 1 if you\u0026rsquo;re new to ISO compliance or want the foundational context. Jump directly to specific articles if you\u0026rsquo;re dealing with an immediate implementation requirement — the titles are deliberately direct about what each article addresses.\u003c/p\u003e\n\u003cp\u003eThe series runs roughly in order of foundational to advanced, but each article stands on its own. Secrets management and access control (Phases 1–2) affect everything else; if you haven\u0026rsquo;t read those, the later articles will make more sense after you do.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"a-note-on-scope\"\u003e\u003ca href=\"/posts/iso-standards/#a-note-on-scope\" title=\"A Note on Scope\"\u003eA Note on Scope\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThis series is not ISO certification consulting. It does not tell you whether your organization will pass an audit, does not replace a qualified auditor, and does not constitute legal advice on GDPR or data protection law. If you need a formal gap assessment or certification readiness review, hire someone who does that for a living.\u003c/p\u003e\n\u003cp\u003eWhat the series does: takes specific ISO 27001, 27017, and 27701 controls and shows what implementing them looks like in .NET and Azure. Not all controls require code — some are organizational, process-based, or documentation-driven, and those are outside scope here. The focus is the technical controls: the ones where the implementation decision is yours, the trade-offs are real, and \u0026ldquo;we have a policy document\u0026rdquo; is not sufficient evidence.\u003c/p\u003e\n\u003cp\u003eCoverage is also selective by design. ISO 27001 alone has 93 controls across 4 domains and 11 themes. This series covers the controls most directly relevant to .NET application development and Azure infrastructure — not physical security, not HR screening, not supplier contract templates. The goal is depth on what developers own, not breadth across everything a certification program touches.\u003c/p\u003e\n\u003cp\u003eIf you\u0026rsquo;re reading these articles as part of actual certification preparation, use them alongside your auditor\u0026rsquo;s guidance, not instead of it.\u003c/p\u003e\n","date_modified":"2026-05-25T22:06:34+02:00","date_published":"2026-01-22T17:00:00+01:00","id":"https://daily-devops.net/posts/iso-standards/","language":"en","summary":"Nearly 30 articles map ISO/IEC 27001, 27017, and 27701 to concrete .NET and Azure: secrets, access control, GDPR erasure, and supply chain security.","tags":["security","compliance","dotnet","azure","iso-standards","gdpr"],"title":"ISO/IEC 27001, 27017 \u0026 27701 for .NET Developers — The Complete Series","url":"https://daily-devops.net/posts/iso-standards/"},{"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\u003eKubernetes has transitioned from a technical option to an assumed default. In organizations and projects I\u0026rsquo;ve worked with, discussions no longer start with whether Kubernetes is appropriate. They start with migration timelines. I\u0026rsquo;ve sat through planning sessions where the question wasn\u0026rsquo;t \u0026ldquo;Should we use Kubernetes?\u0026rdquo; but rather \u0026ldquo;When can we have everything moved over?\u0026rdquo;\u003c/p\u003e\n\u003cp\u003eThis shift isn\u0026rsquo;t driven by application requirements. It\u0026rsquo;s driven by narrative. Consulting decks and reference architectures present \u003cem\u003e\u003cstrong\u003eKubernetes as a universal platform\u003c/strong\u003e\u003c/em\u003e that absorbs governance, security, scalability, observability, recovery, and operational responsibility. The implicit promise: once your software runs on Kubernetes, the hard parts are handled. I\u0026rsquo;ve watched teams adopt this belief wholesale, only to discover the gaps six months into production.\u003c/p\u003e\n\u003cp\u003eThat promise is incomplete. Kubernetes primarily addresses \u003cstrong\u003eone phase\u003c/strong\u003e: runtime orchestration. Most architectural risk, cost overruns, and operational failures occur \u003cstrong\u003ebefore\u003c/strong\u003e runtime during design and delivery, or \u003cstrong\u003eafter\u003c/strong\u003e runtime when incidents happen and systems evolve. I\u0026rsquo;ve debugged production incidents where Kubernetes ran flawlessly while the system failed spectacularly because architectural problems existed upstream and downstream of container orchestration.\u003c/p\u003e\n\u003cp\u003eTreating Kubernetes as a lifecycle platform rather than a runtime component introduces complexity that stays invisible during planning and becomes unavoidable in production. The demos look clean. The reference architectures are elegant. Then you hit reality.\u003c/p\u003e\n\u003cp\u003eTwo questions matter: Not whether Kubernetes works (it does, consistently, in its domain), but where its responsibility ends and whether your organization can handle what lies beyond those boundaries.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"kubernetes-in-the-net-reality\"\u003e\u003ca href=\"/posts/kubernetes-not-platform-strategy/#kubernetes-in-the-net-reality\" title=\"Kubernetes in the .NET Reality\"\u003eKubernetes in the .NET Reality\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eKubernetes clusters rarely host a single, clean workload type in practice. They become convergence points: ASP.NET Core APIs, background workers, event-driven processors, migrated Windows Services, and platform components all sharing infrastructure. I\u0026rsquo;ve inherited clusters running everything from modern microservices to decade-old .NET Framework services wrapped in Windows containers, all competing for the same resources.\u003c/p\u003e\n\u003cp\u003eFor stateless, Linux-based ASP.NET Core services, Kubernetes is genuinely strong. Deployments are predictable. Rollouts are controlled. Health checks integrate cleanly. You implement a simple health endpoint:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\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=\"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=\"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\"\u003eMapHealthChecks\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;/health\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\u003eThen you deploy 3 replicas and Kubernetes does what you asked: it keeps exactly 3 running, rolling out updates without downtime, removing failed pods from traffic automatically. You push a new image and watch the update complete—no manual intervention, no traffic loss, no coordination overhead.\u003c/p\u003e\n\u003cp\u003eThis is where Kubernetes works exactly as intended: the application exposes its state honestly, and the platform responds intelligently. Three replicas means three replicas, constantly. A pod fails, it gets replaced within seconds. A rolling update happens seamlessly because Kubernetes orchestrates the transition and the application cooperates through its health endpoint. The first time you watch this happen without manually managing anything, it feels like magic.\u003c/p\u003e\n\u003cp\u003eThis experience—predictable, reliable, hands-off—becomes the template in your mind for how Kubernetes should work everywhere.\u003c/p\u003e\n\u003cp\u003eThe mistake begins when this success gets generalized. I\u0026rsquo;ve seen this pattern repeatedly: success with stateless APIs leads to confidence that everything belongs in Kubernetes. Then the complexity arrives.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"governance-structure-without-enforcement\"\u003e\u003ca href=\"/posts/kubernetes-not-platform-strategy/#governance-structure-without-enforcement\" title=\"Governance: Structure Without Enforcement\"\u003eGovernance: Structure Without Enforcement\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eKubernetes offers namespaces, labels, and RBAC. These are primitives, not governance. Real enterprise governance requires enforceable policy, auditability, cost attribution, and environmental separation. In Azure-centric environments, these concerns traditionally live at the subscription, management group, and Azure Policy layer, where they\u0026rsquo;re auditable, mandatory, and enforced at the platform level.\u003c/p\u003e\n\u003cp\u003eIntroducing Kubernetes adds a second governance plane. Without deliberate policy enforcement, clusters drift. I\u0026rsquo;ve seen production and experimental workloads coexist in the same cluster because namespace isolation felt sufficient. It wasn\u0026rsquo;t. Cost attribution becomes opaque. Who actually paid for that node pool? Which business unit owns this? When incidents happen, these questions waste critical time.\u003c/p\u003e\n\u003cp\u003eIn one organization, we discovered experimental ML workloads running on production infrastructure because someone had \u003ccode\u003ekubectl\u003c/code\u003e access and \u0026ldquo;just needed to test something quickly.\u0026rdquo; The namespace separation existed. The policy enforcement didn\u0026rsquo;t.\u003c/p\u003e\n\u003cp\u003eKubernetes doesn\u0026rsquo;t prevent this drift. It accelerates it by making deployment so frictionless that governance becomes an afterthought.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"identity-kubernetes-stops-where-entra-id-starts\"\u003e\u003ca href=\"/posts/kubernetes-not-platform-strategy/#identity-kubernetes-stops-where-entra-id-starts\" title=\"Identity: Kubernetes Stops Where Entra ID Starts\"\u003eIdentity: Kubernetes Stops Where Entra ID Starts\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e.NET applications rely on Entra ID (formerly Azure AD) for authentication, authorization, managed identities, and conditional access. Kubernetes has no native concept of enterprise identity. It doesn\u0026rsquo;t integrate with Entra ID\u0026rsquo;s policy layer, conditional access rules, or compliance tracking. This isn\u0026rsquo;t a limitation; it\u0026rsquo;s architectural reality.\u003c/p\u003e\n\u003cp\u003eKubernetes RBAC governs access to cluster resources: who can deploy pods, create services, read secrets. But application identity—the identity your code runs under, the services it authenticates to, the permissions it holds—that\u0026rsquo;s entirely separate. Kubernetes facilitates the technical handshake (workload identity token exchange), but the authority making identity decisions lives outside the cluster in Entra ID. Your application integrates with Entra ID directly, not through Kubernetes.\u003c/p\u003e\n\u003cp\u003eThis boundary is invisible until you\u0026rsquo;re three months into production and security asks about conditional access policies, device compliance rules, or audit trails. Kubernetes doesn\u0026rsquo;t track any of that. It can\u0026rsquo;t. The identity system is external, and Kubernetes merely provides the plumbing to connect to it.\u003c/p\u003e\n\u003cp\u003eI\u0026rsquo;ve worked with teams who expected Kubernetes to handle enterprise identity because it handled everything else. It doesn\u0026rsquo;t. That realization typically arrives when security reviews surface the integration gaps.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"networking-where-kubernetes-abstraction-fails-first\"\u003e\u003ca href=\"/posts/kubernetes-not-platform-strategy/#networking-where-kubernetes-abstraction-fails-first\" title=\"Networking: Where Kubernetes Abstraction Fails First\"\u003eNetworking: Where Kubernetes Abstraction Fails First\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eNetworking is where Kubernetes myths collapse fastest. I\u0026rsquo;ve seen the most preventable production incidents here. Kubernetes introduces its own networking model, but it doesn\u0026rsquo;t replace enterprise networking. It operates \u003cstrong\u003einside\u003c/strong\u003e it. This distinction matters when things go wrong.\u003c/p\u003e\n\u003cp\u003eIn Azure-based architectures, your first line of defense exists outside the cluster:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eVirtual networks and subnet isolation\u003c/li\u003e\n\u003cli\u003eUser-defined routing (UDR)\u003c/li\u003e\n\u003cli\u003eAzure Firewall or Network Virtual Appliance (NVA)\u003c/li\u003e\n\u003cli\u003eApplication Gateway or Front Door with Web Application Firewall (WAF)\u003c/li\u003e\n\u003cli\u003ePrivate endpoints and service endpoints\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eIngress controllers route traffic. They don\u0026rsquo;t defend the network. They\u0026rsquo;re application-layer components running inside pods, not hardened network appliances.\u003c/p\u003e\n\u003cp\u003eTreating Kubernetes ingress as your security perimeter shifts responsibility from hardened network controls to application-level components that were never designed to absorb hostile traffic at scale. I\u0026rsquo;ve seen this assumption lead to security incidents where attackers bypassed ingress controllers by targeting services directly once they gained cluster access.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"azure-cni-and-ip-exhaustion\"\u003e\u003ca href=\"/posts/kubernetes-not-platform-strategy/#azure-cni-and-ip-exhaustion\" title=\"Azure CNI and IP Exhaustion\"\u003eAzure CNI and IP Exhaustion\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eWith Azure CNI, every pod consumes a real IP address from your virtual network subnet. Scaling pods means scaling IP consumption linearly. Poor subnet sizing surfaces late—usually in production when teams suddenly can\u0026rsquo;t scale further and the error message is cryptic. Kubernetes schedules pods until the network says no, then fails silently.\u003c/p\u003e\n\u003cp\u003eThis isn\u0026rsquo;t a Kubernetes failure. It\u0026rsquo;s a networking responsibility that Kubernetes exposes. I\u0026rsquo;ve debugged this scenario more times than I\u0026rsquo;d like to admit, always with the same root cause: network planning happened before anyone calculated peak pod counts under load.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"east-west-traffic-and-lateral-movement\"\u003e\u003ca href=\"/posts/kubernetes-not-platform-strategy/#east-west-traffic-and-lateral-movement\" title=\"East-West Traffic and Lateral Movement\"\u003eEast-West Traffic and Lateral Movement\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eKubernetes networking is flat by default. Every pod can reach every other pod within the cluster. Network policies are optional and frequently incomplete. In organizations without dedicated platform teams, they\u0026rsquo;re often absent entirely.\u003c/p\u003e\n\u003cp\u003eFor multi-service .NET systems, this makes lateral movement trivial once any single pod is compromised. An attacker who gains access to a frontend pod can immediately probe backend services, database connections, and internal APIs. Kubernetes provides the mechanism (network policies) but doesn\u0026rsquo;t enforce discipline. I worked on an incident response where a compromised pod accessed 12 different internal services before we detected it. Network policies existed in the repository. They weren\u0026rsquo;t applied.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"egress-control\"\u003e\u003ca href=\"/posts/kubernetes-not-platform-strategy/#egress-control\" title=\"Egress Control\"\u003eEgress Control\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eIngress gets constant attention: WAF rules, TLS certificates, rate limiting. Egress almost never does. By default, all pods can reach the internet: any destination, any port. In regulated environments, that\u0026rsquo;s unacceptable. Egress control requires forced routing through Azure Firewall and explicit allow-listing of destinations.\u003c/p\u003e\n\u003cp\u003eKubernetes has no native concept of allowed destinations. You build this external to the cluster, then spend weeks troubleshooting why perfectly valid application calls fail because someone forgot to allow-list a critical API endpoint.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"security-responsibility-is-concentrated-not-removed\"\u003e\u003ca href=\"/posts/kubernetes-not-platform-strategy/#security-responsibility-is-concentrated-not-removed\" title=\"Security: Responsibility Is Concentrated, Not Removed\"\u003eSecurity: Responsibility Is Concentrated, Not Removed\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eKubernetes provides security mechanisms. Almost none are enabled by default. A .NET application on Azure App Service benefits from opinionated defaults: automatic image scanning, encrypted secrets, preconfigured network isolation, integrated runtime monitoring.\u003c/p\u003e\n\u003cp\u003eIn Kubernetes, every guarantee requires deliberate recreation:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eImage provenance through admission controllers and policy enforcement\u003c/li\u003e\n\u003cli\u003eSecret handling through external secret stores (Azure Key Vault integration)\u003c/li\u003e\n\u003cli\u003eNetwork segmentation through network policies and firewall rules\u003c/li\u003e\n\u003cli\u003eRuntime monitoring through service mesh sidecars or host-level agents\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eEach added controller or sidecar increases capability and attack surface simultaneously. I\u0026rsquo;ve reviewed Kubernetes configurations where security controls outnumbered application pods. The cluster became a security platform that happened to run some software.\u003c/p\u003e\n\u003cp\u003eKubernetes doesn\u0026rsquo;t reduce security effort. It concentrates it into your platform team, assuming you have one.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"cicd-and-supply-chain-kubernetes-consumes-trust\"\u003e\u003ca href=\"/posts/kubernetes-not-platform-strategy/#cicd-and-supply-chain-kubernetes-consumes-trust\" title=\"CI/CD and Supply Chain: Kubernetes Consumes Trust\"\u003eCI/CD and Supply Chain: Kubernetes Consumes Trust\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eKubernetes consumes artifacts. It doesn\u0026rsquo;t produce trust. CI pipelines, artifact promotion, image immutability, and signing decisions all happen long before Kubernetes schedules a pod. A broken supply chain can\u0026rsquo;t be repaired at runtime. If a malicious image makes it to your registry, Kubernetes will happily deploy it.\u003c/p\u003e\n\u003cp\u003eI\u0026rsquo;ve worked with a team who discovered their CI pipeline had been compromised for three weeks. Kubernetes deployed every malicious image perfectly—on schedule, with zero-downtime rolling updates. The orchestration worked flawlessly. The supply chain didn\u0026rsquo;t. Kubernetes enforces desired state but doesn\u0026rsquo;t validate how that state was produced. That validation is your responsibility in your build pipelines, artifact registries, and admission controllers.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"observability-infrastructure-metrics-are-not-insight\"\u003e\u003ca href=\"/posts/kubernetes-not-platform-strategy/#observability-infrastructure-metrics-are-not-insight\" title=\"Observability: Infrastructure Metrics Are Not Insight\"\u003eObservability: Infrastructure Metrics Are Not Insight\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eKubernetes emits metrics and logs: CPU usage per pod, memory consumption, network I/O. These describe platform health, not system behavior. .NET systems require application-level observability—distributed tracing across service boundaries, dependency tracking to external systems, structured logging with correlation IDs.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\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\"\u003eAddOpenTelemetry\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/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\"\u003eWithTracing\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\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003et\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddAspNetCoreInstrumentation\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/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\"\u003eAddHttpClientInstrumentation\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 integration into Azure Monitor and Application Insights, incidents become reconstruction exercises. I\u0026rsquo;ve sat in war rooms where Kubernetes dashboards stayed green—all pods healthy, all nodes operational—while users experienced cascading timeouts. Pod restarts hide underlying failures instead of surfacing them. A pod that crashes and restarts every 30 seconds looks \u0026ldquo;healthy\u0026rdquo; to Kubernetes if it passes health checks between crashes.\u003c/p\u003e\n\u003cp\u003eObservability requires design. You bring it, or you debug blind.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"scalability-kubernetes-scales-pods-not-systems\"\u003e\u003ca href=\"/posts/kubernetes-not-platform-strategy/#scalability-kubernetes-scales-pods-not-systems\" title=\"Scalability: Kubernetes Scales Pods, Not Systems\"\u003eScalability: Kubernetes Scales Pods, Not Systems\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eKubernetes scales replicas, not architectures. Database contention, synchronous dependencies, external API limits—they all remain regardless of how many pod copies you create. Kubernetes can amplify bottlenecks just as effectively as it amplifies capacity.\u003c/p\u003e\n\u003cp\u003eI\u0026rsquo;ve watched auto-scaling create 50 pod replicas, all waiting for the same database connection pool that maxed out at 100 connections. More pods didn\u0026rsquo;t solve the problem—they made it worse by consuming resources while waiting.\u003c/p\u003e\n\u003cp\u003eEvent-driven scaling improves this, but only with architectural redesign. Kubernetes enables the \u003cstrong\u003emechanism\u003c/strong\u003e for elasticity—you can scale replicas based on external signals. But the architecture determines whether that mechanism translates into actual scalability. Scaling 50 pods won\u0026rsquo;t help if they\u0026rsquo;re all waiting on the same bottleneck. That\u0026rsquo;s a design problem, not an orchestration problem.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"backup-and-recovery-kubernetes-stops-completely\"\u003e\u003ca href=\"/posts/kubernetes-not-platform-strategy/#backup-and-recovery-kubernetes-stops-completely\" title=\"Backup and Recovery: Kubernetes Stops Completely\"\u003eBackup and Recovery: Kubernetes Stops Completely\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eKubernetes restarts containers. It doesn\u0026rsquo;t restore systems. State lives outside the cluster in databases, message queues, caches, and storage accounts. Backup and recovery remain responsibilities of data platforms and operational processes. Kubernetes has no concept of business continuity or disaster recovery beyond \u0026ldquo;restart the pod.\u0026rdquo;\u003c/p\u003e\n\u003cp\u003eHigh availability masks failure. It doesn\u0026rsquo;t undo it. A corrupted database doesn\u0026rsquo;t care how many pod replicas exist or how fast Kubernetes can reschedule them. I\u0026rsquo;ve responded to incidents where Kubernetes performed perfectly—immediate failover, health-driven routing—while the underlying data corruption spread across all replicas.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"windows-containers-on-kubernetes-a-strong-architectural-smell\"\u003e\u003ca href=\"/posts/kubernetes-not-platform-strategy/#windows-containers-on-kubernetes-a-strong-architectural-smell\" title=\"Windows Containers on Kubernetes: A Strong Architectural Smell\"\u003eWindows Containers on Kubernetes: A Strong Architectural Smell\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eWindows containers are supported but introduce slower startup times (minutes versus seconds), limited ecosystem support, and operational asymmetry—separate node pools, different update cadence, higher costs. They\u0026rsquo;re frequently used to avoid refactoring legacy workloads, turning Kubernetes into a compatibility layer rather than a platform.\u003c/p\u003e\n\u003cp\u003eI\u0026rsquo;ve seen .NET Framework applications from 2010 wrapped in Windows containers and deployed to Kubernetes because \u0026ldquo;we\u0026rsquo;re moving to cloud-native.\u0026rdquo; The workload hadn\u0026rsquo;t changed. The infrastructure complexity increased dramatically. They function, they complicate operations, and they rarely age well.\u003c/p\u003e\n\u003cp\u003eEvery Windows container deployment I\u0026rsquo;ve reviewed eventually became a maintenance burden. The startup time alone makes scaling problematic. Windows licensing costs amplify infrastructure expenses. And the operational split between Linux and Windows node pools fragments your platform team\u0026rsquo;s expertise.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"cost-and-organizational-economics\"\u003e\u003ca href=\"/posts/kubernetes-not-platform-strategy/#cost-and-organizational-economics\" title=\"Cost and Organizational Economics\"\u003eCost and Organizational Economics\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eKubernetes isn\u0026rsquo;t cost-neutral—a realization that typically arrives 3-6 months after initial deployment when finance asks why cloud costs doubled. It shifts cost visibility from infrastructure to organization: platform teams grow from 2 to 8 people, node pools sit idle waiting for burst capacity that happens twice a month, Windows nodes amplify costs through licensing and compute, observability instrumentation adds runtime overhead and egress costs.\u003c/p\u003e\n\u003cp\u003eTechnical efficiency—improved resource utilization through bin-packing and scheduling—often comes at \u003cstrong\u003eorganizational expense\u003c/strong\u003e: larger platform teams, slower iteration velocity (every change needs cluster-wide validation), distributed debugging complexity (which of the 15 services in the trace actually caused the timeout?).\u003c/p\u003e\n\u003cp\u003eThe calculation isn\u0026rsquo;t universal. It depends on workload mix, team structure, organizational tolerance for operational complexity. For companies running 200+ microservices with dedicated SRE teams, Kubernetes pays dividends. For companies running 8 services with 3 developers, it\u0026rsquo;s often overhead.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"conclusion-kubernetes-concentrates-architectural-responsibility\"\u003e\u003ca href=\"/posts/kubernetes-not-platform-strategy/#conclusion-kubernetes-concentrates-architectural-responsibility\" title=\"Conclusion: Kubernetes Concentrates Architectural Responsibility\"\u003eConclusion: Kubernetes Concentrates Architectural Responsibility\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eKubernetes is powerful and, in specific scenarios, the right choice: stateless Linux-based APIs with clean 12-factor design, event-driven background workers that scale horizontally, organizations with dedicated platform teams who can absorb operational complexity, and standardized workload portfolios where 80%+ of applications fit predictable patterns.\u003c/p\u003e\n\u003cp\u003eOutside these boundaries, Kubernetes doesn\u0026rsquo;t remove responsibility. It concentrates it. The responsibilities I\u0026rsquo;ve outlined (governance, identity, networking, security, observability, backup) don\u0026rsquo;t disappear. They become explicit architectural decisions that someone on your team must own, implement, and maintain.\u003c/p\u003e\n\u003cp\u003eKubernetes is not governance. That lives at the subscription, policy, and organizational level. It\u0026rsquo;s not identity. That authority is Entra ID. It\u0026rsquo;s not the security perimeter. That\u0026rsquo;s the network, the firewall, and the defense-in-depth controls you build around the cluster. It\u0026rsquo;s not backup and recovery. That responsibility belongs to data platforms and business continuity planning. It\u0026rsquo;s not observability. That\u0026rsquo;s an application design concern requiring deliberate instrumentation.\u003c/p\u003e\n\u003cp\u003eKubernetes orchestrates workloads, and it does this extremely well.\u003c/p\u003e\n\u003cp\u003eFrom an architect\u0026rsquo;s perspective—someone who has designed, deployed, and maintained these systems in production—Kubernetes can be the most visible component of a hosting solution but never the \u003cstrong\u003ewhole\u003c/strong\u003e solution. The promise that it absorbs the software lifecycle is marketing, not engineering reality.\u003c/p\u003e\n\u003cp\u003eThat distinction isn\u0026rsquo;t theoretical. It\u0026rsquo;s operational reality I\u0026rsquo;ve experienced across multiple organizations, multiple industries, multiple failure modes.\u003c/p\u003e\n\u003cp\u003eThe question isn\u0026rsquo;t whether Kubernetes works—it does, consistently, predictably, within its domain. The question is whether your organization can handle everything Kubernetes \u003cstrong\u003edoesn\u0026rsquo;t\u003c/strong\u003e do, and whether the complexity trade-off makes sense for your specific context, team capability, and workload characteristics.\u003c/p\u003e\n\u003cp\u003eAnswer that question honestly before committing your platform strategy.\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2026-01-13T17:00:00+01:00","id":"https://daily-devops.net/posts/kubernetes-not-platform-strategy/","language":"en","summary":"Kubernetes orchestrates containers brilliantly. But governance, identity, and recovery live elsewhere—and ignoring those boundaries breaks production.\n","tags":["kubernetes","architecture","platform-engineering","dotnet","cloudnative"],"title":"Kubernetes Is Not a Platform Strategy\n","url":"https://daily-devops.net/posts/kubernetes-not-platform-strategy/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\n\n\n\n\u003ch2 id=\"your-tuesday-morning-a-true-story\"\u003e\u003ca href=\"/posts/alphabet-soup-file-formats/#your-tuesday-morning-a-true-story\" title=\"Your Tuesday Morning: A True Story\"\u003eYour Tuesday Morning: A True Story\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eIt\u0026rsquo;s 9 AM. You\u0026rsquo;re debugging why the Kubernetes deployment failed overnight. The YAML looked perfect. Indentation? Check. Syntax? Check. The problem? Someone used \u003ccode\u003eNO\u003c/code\u003e as an environment variable value. YAML helpfully parsed it as boolean \u003ccode\u003efalse\u003c/code\u003e. Your pod never started.\u003c/p\u003e\n\u003cp\u003eBy 10 AM, you\u0026rsquo;re fixing the CSV export that accounting requested. Excel mangled the employee IDs—turned \u003ccode\u003e00123\u003c/code\u003e into \u003ccode\u003e123\u003c/code\u003e, converted the date column into something unrecognizable, and decided that gene names like \u003ccode\u003eSEPT2\u003c/code\u003e are obviously September 2nd.\u003c/p\u003e\n\u003cp\u003eAt 11 AM, the build pipeline breaks because someone added a trailing comma to \u003ccode\u003eappsettings.json\u003c/code\u003e. JSON doesn\u0026rsquo;t allow those. The error message is cryptic. The fix takes 30 seconds. Finding it took 20 minutes.\u003c/p\u003e\n\u003cp\u003eLunch is spent explaining to a junior dev why we have YAML for CI/CD, JSON for app config, TOML for the Rust tool, INI for the legacy service, and CSV for data exports. \u0026ldquo;Can\u0026rsquo;t we just pick one?\u0026rdquo; they ask.\u003c/p\u003e\n\u003cp\u003eNo. We can\u0026rsquo;t. This is software development in 2025.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-format-parade-whos-who-in-the-chaos\"\u003e\u003ca href=\"/posts/alphabet-soup-file-formats/#the-format-parade-whos-who-in-the-chaos\" title=\"The Format Parade: Who\u0026rsquo;s Who in the Chaos\"\u003eThe Format Parade: Who\u0026rsquo;s Who in the Chaos\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eLet\u0026rsquo;s meet the contestants in this never-ending format beauty pageant. Spoiler: nobody wins.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"csv-the-universal-disaster\"\u003e\u003ca href=\"/posts/alphabet-soup-file-formats/#csv-the-universal-disaster\" title=\"CSV: The Universal Disaster\"\u003e\u003cstrong\u003eCSV: The Universal Disaster\u003c/strong\u003e\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eComma-Separated Values promised simplicity: rows, columns, commas. That\u0026rsquo;s it.\u003c/p\u003e\n\u003cp\u003eThe reality? There\u0026rsquo;s no real standard. RFC 4180 tried in 2005, but thousands of tools had already shipped their own interpretations. Comma delimiter? Sometimes semicolon. Sometimes tab. Quote your strings? Maybe. Escape quotes by doubling them? Or backslashes? Depends on the tool.\u003c/p\u003e\n\u003cp\u003eExcel is CSV\u0026rsquo;s natural enemy. It will \u0026ldquo;fix\u0026rdquo; your data by converting dates (\u003ccode\u003e2025-12-31\u003c/code\u003e becomes Excel\u0026rsquo;s internal date format), stripping leading zeros (\u003ccode\u003e00123\u003c/code\u003e → \u003ccode\u003e123\u003c/code\u003e), and famously turning gene names into dates (\u003ccode\u003eSEPT2\u003c/code\u003e → Sep 2). Biologists have a special hatred for CSV because of this.\u003c/p\u003e\n\u003cp\u003eYet CSV survives because it\u0026rsquo;s \u003cstrong\u003euniversal\u003c/strong\u003e. Every tool exports it. Every developer can edit it in Notepad. It compresses well. It\u0026rsquo;s the lowest common denominator when nothing else works.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eVerdict:\u003c/strong\u003e Like democracy, it\u0026rsquo;s the worst format except for all the others.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"ini-the-minimalist\"\u003e\u003ca href=\"/posts/alphabet-soup-file-formats/#ini-the-minimalist\" title=\"INI: The Minimalist\u0026rsquo;s Dream\"\u003e\u003cstrong\u003eINI: The Minimalist\u0026rsquo;s Dream\u003c/strong\u003e\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eKey-value pairs. Sections. That\u0026rsquo;s the entire spec. Humans understand it instantly.\u003c/p\u003e\n\u003cp\u003eThe problem? No nested structures. No lists. No type system—everything\u0026rsquo;s a string. The moment you need hierarchy, INI taps out.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eVerdict:\u003c/strong\u003e Perfect for simple configs. Useless for everything else.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"xml-the-enterprise-albatross\"\u003e\u003ca href=\"/posts/alphabet-soup-file-formats/#xml-the-enterprise-albatross\" title=\"XML: The Enterprise Albatross\"\u003e\u003cstrong\u003eXML: The Enterprise Albatross\u003c/strong\u003e\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eXML promised schema validation, namespaces, self-describing tags—enterprise-grade power.\u003c/p\u003e\n\u003cp\u003eWhat we got: angle bracket hell. Every value wrapped in opening and closing tags. Signal-to-noise ratio so poor that even enterprise architects—people who \u003cem\u003ethrive\u003c/em\u003e on complexity—started looking for alternatives.\u003c/p\u003e\n\u003cp\u003eWhen the people who love complexity want simpler, you\u0026rsquo;ve failed at usability.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eVerdict:\u003c/strong\u003e Still haunting legacy systems. Nobody voluntarily starts new projects with XML anymore.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"json-the-machine\"\u003e\u003ca href=\"/posts/alphabet-soup-file-formats/#json-the-machine\" title=\"JSON: The Machine\u0026rsquo;s Format\"\u003e\u003cstrong\u003eJSON: The Machine\u0026rsquo;s Format\u003c/strong\u003e\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eJSON solved XML\u0026rsquo;s verbosity. Clean syntax. Maps directly to data structures. Every language has a parser.\u003c/p\u003e\n\u003cp\u003eBut it was designed for \u003cstrong\u003emachines\u003c/strong\u003e, not humans. No comments (explain your config changes in commit messages, I guess). Trailing commas forbidden. Rigid quoting everywhere. It works, but it\u0026rsquo;s tedious to hand-edit.\u003c/p\u003e\n\u003cp\u003eOpenAI\u0026rsquo;s function calling? JSON-only. Why? Because it\u0026rsquo;s deterministic. One correct way to structure it.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eVerdict:\u003c/strong\u003e Boring, reliable, ubiquitous. The Toyota Camry of data formats.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"yaml-the-beautiful-disaster\"\u003e\u003ca href=\"/posts/alphabet-soup-file-formats/#yaml-the-beautiful-disaster\" title=\"YAML: The Beautiful Disaster\"\u003e\u003cstrong\u003eYAML: The Beautiful Disaster\u003c/strong\u003e\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eYAML threw JSON\u0026rsquo;s rigidity out the window. Minimal syntax. Comments everywhere. Indentation-based. Human-friendly!\u003c/p\u003e\n\u003cp\u003eExcept it\u0026rsquo;s \u003cstrong\u003ewhitespace-sensitive\u003c/strong\u003e. One misaligned space breaks everything, often silently. Implicit type conversions bite constantly (\u003ccode\u003eNO\u003c/code\u003e → \u003ccode\u003efalse\u003c/code\u003e, \u003ccode\u003eon\u003c/code\u003e → \u003ccode\u003etrue\u003c/code\u003e, \u003ccode\u003e0123\u003c/code\u003e → octal 83). The spec is 23,000 words and allows multiple ways to represent the same data.\u003c/p\u003e\n\u003cp\u003eKubernetes chose YAML. Docker Compose chose YAML. GitHub Actions chose YAML. The ecosystem standardized, so now you\u0026rsquo;re learning YAML whether you like it or not.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eVerdict:\u003c/strong\u003e Feels great until it doesn\u0026rsquo;t. Then you\u0026rsquo;re debugging indentation at 2 AM.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"toml-the-pragmatic-middle-ground\"\u003e\u003ca href=\"/posts/alphabet-soup-file-formats/#toml-the-pragmatic-middle-ground\" title=\"TOML: The Pragmatic Middle Ground\"\u003e\u003cstrong\u003eTOML: The Pragmatic Middle Ground\u003c/strong\u003e\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eTom\u0026rsquo;s Obvious Minimal Language tried to be INI + structure + types. Explicit syntax. No whitespace sensitivity. Comments allowed.\u003c/p\u003e\n\u003cp\u003eIt works. It\u0026rsquo;s clear. It\u0026rsquo;s unambiguous. The ecosystem is smaller than YAML/JSON, but growing.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eVerdict:\u003c/strong\u003e Underrated. Use it for build configs if you can.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"taml-radical-minimalism\"\u003e\u003ca href=\"/posts/alphabet-soup-file-formats/#taml-radical-minimalism\" title=\"TAML: Radical Minimalism\"\u003e\u003cstrong\u003eTAML: Radical Minimalism\u003c/strong\u003e\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eTab Annotated Markup Language: tabs for hierarchy, newlines for structure. No brackets, colons, or quotes.\u003c/p\u003e\n\u003cp\u003eOne tab = one level deeper. That\u0026rsquo;s the entire format.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eVerdict:\u003c/strong\u003e Interesting experiment. Tiny ecosystem. Good for greenfield projects if you\u0026rsquo;re willing to bet on niche formats.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"toon-designed-for-ai\"\u003e\u003ca href=\"/posts/alphabet-soup-file-formats/#toon-designed-for-ai\" title=\"TOON: Designed for AI\"\u003e\u003cstrong\u003eTOON: Designed for AI\u003c/strong\u003e\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eToken-Oriented Object Notation emerged in 2025 specifically to solve LLM generation problems.\u003c/p\u003e\n\u003cp\u003e~40% fewer tokens than JSON. Explicit \u003ccode\u003e[N]\u003c/code\u003e array lengths and \u003ccode\u003e{fields}\u003c/code\u003e headers give AI models clear guardrails. Better accuracy (74% vs JSON\u0026rsquo;s 70%) because the schema is baked into the syntax.\u003c/p\u003e\n\u003cp\u003eIt\u0026rsquo;s \u0026ldquo;JSON optimized for transformer models.\u0026rdquo; Human-readable like YAML, compact like CSV, schema-aware like XML.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eVerdict:\u003c/strong\u003e If you\u0026rsquo;re building systems where LLMs frequently generate structured data, TOON might save you. Otherwise, wait to see if it gains traction.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"ccl-category-theory-elegance\"\u003e\u003ca href=\"/posts/alphabet-soup-file-formats/#ccl-category-theory-elegance\" title=\"CCL: Category Theory Elegance\"\u003e\u003cstrong\u003eCCL: Category Theory Elegance\u003c/strong\u003e\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eCategorical Configuration Language built on mathematical principles. Pure key-value pairs with recursive nesting.\u003c/p\u003e\n\u003cp\u003eMinimal syntax: \u003ccode\u003ekey = value\u003c/code\u003e. Comments via \u003ccode\u003e/=\u003c/code\u003e. Merging configs is associative with an identity element. Provably correct composition.\u003c/p\u003e\n\u003cp\u003eThe ecosystem is tiny (OCaml, Rust implementations). Practical? Depends on whether you value theoretical soundness over ecosystem maturity.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eVerdict:\u003c/strong\u003e For people who think in category theory. Everyone else, use TOML.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"bson-the-binary-option\"\u003e\u003ca href=\"/posts/alphabet-soup-file-formats/#bson-the-binary-option\" title=\"BSON: The Binary Option\"\u003e\u003cstrong\u003eBSON: The Binary Option\u003c/strong\u003e\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eBinary JSON. Optimized for machine efficiency. Fast parsing. Compact storage.\u003c/p\u003e\n\u003cp\u003eOpen it in a text editor? Gibberish. It\u0026rsquo;s for databases (MongoDB), not human editing.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eVerdict:\u003c/strong\u003e Right tool for the right job. Don\u0026rsquo;t hand-edit BSON.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"why-we-cant-have-nice-things\"\u003e\u003ca href=\"/posts/alphabet-soup-file-formats/#why-we-cant-have-nice-things\" title=\"Why We Can\u0026rsquo;t Have Nice Things\"\u003eWhy We Can\u0026rsquo;t Have Nice Things\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eHere\u0026rsquo;s the uncomfortable truth: \u003cstrong\u003eevery format genuinely solved real problems\u003c/strong\u003e.\u003c/p\u003e\n\u003cp\u003eCSV ended proprietary spreadsheet lock-in. XML brought schema validation. JSON fixed XML\u0026rsquo;s verbosity. YAML made configs readable. TOML removed YAML\u0026rsquo;s gotchas. TAML went minimal. TOON optimized for AI. CCL brought mathematical rigor.\u003c/p\u003e\n\u003cp\u003eEach improvement was real. Each one also created new problems.\u003c/p\u003e\n\u003cp\u003eThe xkcd comic about competing standards isn\u0026rsquo;t a joke anymore—it\u0026rsquo;s your job description. We don\u0026rsquo;t have 15 standards. We have 50. Maybe 100.\u003c/p\u003e\n\u003cp\u003eFormats don\u0026rsquo;t converge because \u003cstrong\u003etrade-offs are real\u003c/strong\u003e:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eHuman readability ↔ Machine efficiency\u003c/li\u003e\n\u003cli\u003eFlexibility ↔ Parsability\u003c/li\u003e\n\u003cli\u003eSimplicity ↔ Features\u003c/li\u003e\n\u003cli\u003eEcosystem size ↔ Specialized optimization\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eAs AI systems join the picture, the calculus shifts again. Formats designed for humans sometimes hurt AI. Formats designed for machines frustrate humans. Designing for both? That\u0026rsquo;s what TOON attempts.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-ai-problem-makes-everything-worse\"\u003e\u003ca href=\"/posts/alphabet-soup-file-formats/#the-ai-problem-makes-everything-worse\" title=\"The AI Problem Makes Everything Worse\"\u003eThe AI Problem Makes Everything Worse\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eLarge language models changed the game.\u003c/p\u003e\n\u003cp\u003eChatGPT generates text token by token via pattern matching. Rigid formats like JSON? Manageable. Structure it one of 3-4 ways, models usually succeed.\u003c/p\u003e\n\u003cp\u003eYAML? Disaster. Whitespace sensitivity, implicit type conversions, multiple valid representations—LLMs generate frequently invalid YAML. The \u003cem\u003estructure\u003c/em\u003e looks right, but subtle indentation errors or quoting mistakes break parsing.\u003c/p\u003e\n\u003cp\u003eThis drove OpenAI to mandate JSON-only for function calling. Not because engineers are lazy. Because JSON has \u003cstrong\u003eone correct way\u003c/strong\u003e, and models can learn it reliably.\u003c/p\u003e\n\u003cp\u003eThe irony: we designed YAML for human convenience. AI exposed that \u0026ldquo;flexibility\u0026rdquo; creates too many ways to fail.\u003c/p\u003e\n\u003cp\u003eTOON exists specifically to solve this. Explicit schema headers, deterministic structure, fewer tokens. It\u0026rsquo;s pragmatic engineering: \u0026ldquo;YAML breaks AI, JSON works but is verbose, so let\u0026rsquo;s design something in between that models can generate correctly.\u0026rdquo;\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"survival-guide-five-strategies-that-actually-work\"\u003e\u003ca href=\"/posts/alphabet-soup-file-formats/#survival-guide-five-strategies-that-actually-work\" title=\"Survival Guide: Five Strategies That Actually Work\"\u003eSurvival Guide: Five Strategies That Actually Work\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eYou\u0026rsquo;re stuck with this chaos. Here\u0026rsquo;s how to survive it:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e1. Choose Deliberately\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eIf you\u0026rsquo;re using JSON for config, \u003cstrong\u003eown it\u003c/strong\u003e. Document the decision. Set up schema validation. If you\u0026rsquo;re exporting CSV, specify delimiter, encoding, quoting rules explicitly.\u003c/p\u003e\n\u003cp\u003eDon\u0026rsquo;t drift into formats by accident. Make conscious choices and commit to them.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e2. Invest in Tooling\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eLinters. Schema validators. Type safety. These matter \u003cstrong\u003emore than the format\u003c/strong\u003e.\u003c/p\u003e\n\u003cp\u003eGood tooling makes mediocre formats manageable:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eCSV: parsers with RFC 4180 support\u003c/li\u003e\n\u003cli\u003eJSON: JSON Schema validators\u003c/li\u003e\n\u003cli\u003eYAML: linters that catch indentation issues\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003e3. Be Skeptical of \u0026ldquo;Revolutionary\u0026rdquo; Formats\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eEvery new format promises to be The One. TOON and CCL might be useful for specific niches (LLM generation, category theory), but they\u0026rsquo;re not replacing JSON tomorrow.\u003c/p\u003e\n\u003cp\u003eEvaluate pragmatically. Bet on ecosystems, not elegance.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e4. Plan for AI Integration\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eIf AI generates structured data in your system:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eSafe choice:\u003c/strong\u003e JSON with aggressive validation\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eExperimental:\u003c/strong\u003e TOON if you\u0026rsquo;re adopting early-stage formats\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eAvoid:\u003c/strong\u003e YAML unless you enjoy debugging AI-generated indentation errors\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003e5. Don\u0026rsquo;t Refactor Just to Standardize\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eLegacy system using INI, XML, or CSV? If it works, \u003cstrong\u003eleave it alone\u003c/strong\u003e.\u003c/p\u003e\n\u003cp\u003eRefactoring formats doesn\u0026rsquo;t fix business problems. It creates migration risk. Only change formats when you\u0026rsquo;re solving actual pain, not pursuing theoretical purity.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"living-with-the-chaos\"\u003e\u003ca href=\"/posts/alphabet-soup-file-formats/#living-with-the-chaos\" title=\"Living with the Chaos\"\u003eLiving with the Chaos\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe light at the end of the tunnel isn\u0026rsquo;t format convergence. It never was.\u003c/p\u003e\n\u003cp\u003eIt\u0026rsquo;s accepting that we\u0026rsquo;ll always have multiple formats. CSV for exports. JSON for APIs. YAML for infrastructure. TOML for builds. Maybe TOON for AI outputs.\u003c/p\u003e\n\u003cp\u003eThe real problem was never the format. It was always the \u003cstrong\u003edata\u003c/strong\u003e—complex, context-dependent, requiring human judgment.\u003c/p\u003e\n\u003cp\u003ePick the right tool for each job. Invest in validation. Build good tooling. Stay skeptical of salvation promises.\u003c/p\u003e\n\u003cp\u003eWelcome to file format hell. You\u0026rsquo;re going to be here a while.\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2026-01-08T17:00:00+01:00","id":"https://daily-devops.net/posts/alphabet-soup-file-formats/","language":"en","summary":"CSV breaks on commas. YAML breaks on spaces. JSON breaks on trailing commas. TOML, TAML, TOON, CCL joined the chaos. Nobody wins. Here's why.\n","tags":["configuration","devops","dotnet","bestpractices","softwareengineering","codequality"],"title":"Alphabet Soup: The Format Buffet Nobody Ordered\n","url":"https://daily-devops.net/posts/alphabet-soup-file-formats/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eA \u003ca href=\"https://www.linkedin.com/posts/davideguida_i-feel-its-time-to-make-something-clear-activity-7411391768283271168-naE4\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eLinkedIn post by David Guida\u003c/a\u003e sparked a discussion that cuts to the bone: \u003cstrong\u003eIs software engineering about thinking or typing?\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eDavid argued forcefully that \u0026ldquo;software engineering is NOT about writing code\u0026rdquo;—that code is merely mechanical output, the easy part, just another language. The hard part, he wrote, is thinking: \u0026ldquo;Programming is a byproduct of the thinking process. And that one, my friends, is the hard part.\u0026rdquo;\u003c/p\u003e\n\u003cp\u003eI responded with a point that needed more space than a LinkedIn comment allows:\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u0026ldquo;Strong point, but it slightly overcorrects. Yes, typing code is the easy, mechanical part. The hard part is reasoning, trade-offs, and understanding constraints. Agreed. \u003cstrong\u003eBut dismissing code as \u0026lsquo;just another language\u0026rsquo; undersells its impact.\u003c/strong\u003e Code is not only expression, it is execution, cost, failure modes, and long-term operational risk. Thinking without being forced into precise, executable form often stays vague. Writing code is where weak thinking gets exposed. Programming is a byproduct of thinking, \u003cstrong\u003ebut it is also the feedback loop that sharpens that thinking.\u003c/strong\u003e One without the other does not scale.\u0026rdquo;\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eDavid\u0026rsquo;s response captured what I\u0026rsquo;m exploring here: \u0026ldquo;I totally agree! I must have oversimplified my thoughts. Your closing note on the feedback loop \u003cstrong\u003ecaptures the reason why real professionals will never be replaced.\u003c/strong\u003e\u0026rdquo;\u003c/p\u003e\n\u003cp\u003eThat last phrase wasn\u0026rsquo;t casual. It addresses the elephant everyone sees but few acknowledge: \u003cstrong\u003eAI code assistants everywhere.\u003c/strong\u003e GitHub Copilot. ChatGPT generating entire applications from prompts. The emerging \u0026ldquo;vibe coding\u0026rdquo; trend where developers describe vibes and let AI handle the dirty work.\u003c/p\u003e\n\u003cp\u003eThe timing matters. We\u0026rsquo;re in an era where typing code has \u003cstrong\u003enever been easier\u003c/strong\u003e. AI generates syntactically correct implementations \u003cstrong\u003efaster than any human can type\u003c/strong\u003e.\u003c/p\u003e\n\u003cp\u003eYet here\u0026rsquo;s what David and I both realized: This makes the feedback loop between thinking and code \u003cstrong\u003emore critical, not less\u003c/strong\u003e.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAsk yourself:\u003c/strong\u003e When code generation becomes trivial, what separates you from a prompt engineer who thinks they\u0026rsquo;re building software?\u003c/p\u003e\n\u003cp\u003eThe answer: Understanding what that code actually \u003cstrong\u003edoes\u003c/strong\u003e. What it \u003cstrong\u003ecosts\u003c/strong\u003e. Where it \u003cstrong\u003efails\u003c/strong\u003e. Why it \u003cstrong\u003ebreaks under load\u003c/strong\u003e.\u003c/p\u003e\n\u003cp\u003eThis article expands on that feedback loop—the relationship between thinking and code that AI can\u0026rsquo;t replicate. It explores why AI-generated code without deep understanding creates an \u003cstrong\u003eillusion of productivity that collapses catastrophically under production load\u003c/strong\u003e.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"where-vague-thinking-hides\"\u003e\u003ca href=\"/posts/code-sharpens-thinking/#where-vague-thinking-hides\" title=\"Where Vague Thinking Hides\"\u003eWhere Vague Thinking Hides\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eWalk through any architecture review where diagrams look perfect, responsibilities seem clear, and everyone nods in agreement. Then watch what happens when someone starts writing the actual implementation. Suddenly, the clean boundaries blur. The \u0026ldquo;simple\u0026rdquo; abstraction requires five parameters. The proposed interface doesn\u0026rsquo;t fit half the use cases. The design that felt obvious in discussion becomes ambiguous when translated to executable code.\u003c/p\u003e\n\u003cp\u003eThis isn\u0026rsquo;t implementation failing design. This is design revealing itself to be incomplete.\u003c/p\u003e\n\u003cp\u003eI\u0026rsquo;ve sat through countless discussions where proposed solutions felt reasonable until we asked: \u0026ldquo;Show me the code.\u0026rdquo; Not production code—just a sketch. Suddenly, implicit assumptions surface. Missing responsibilities become visible. Performance implications emerge. The architecture that seemed solid in abstract terms crumbles when forced into compilable form.\u003c/p\u003e\n\u003cp\u003eCode demands precision that thought alone doesn\u0026rsquo;t require. When you think through a problem, your mind fills gaps unconsciously, papers over inconsistencies, and substitutes intuition for rigor. When you write code, the compiler—and eventually production—refuses to cooperate with vague intent.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"how-the-compiler-exposes-vague-thinking\"\u003e\u003ca href=\"/posts/code-sharpens-thinking/#how-the-compiler-exposes-vague-thinking\" title=\"How The Compiler Exposes Vague Thinking\"\u003eHow The Compiler Exposes Vague Thinking\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eConsider nullable reference types in C#. Without explicit declaration, you can mentally handwave nullability concerns:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Vague thinking: \u0026#34;customer will always have a name\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eCustomer\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eName\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"k\"\u003eget\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"k\"\u003eset\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eEnable nullable reference types, and the compiler forces you to confront reality:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cp\"\u003e#nullable\u003c/span\u003e \u003cspan class=\"n\"\u003eenable\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eCustomer\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eName\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"k\"\u003eget\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"k\"\u003eset\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e \u003cspan class=\"c1\"\u003e// Warning CS8618: Non-nullable property \u0026#39;Name\u0026#39; must contain \u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                                      \u003cspan class=\"c1\"\u003e// a non-null value when exiting constructor\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis isn\u0026rsquo;t pedantry. This is thinking being forced into honest, executable form. Either you guarantee initialization, accept nullability explicitly, or redesign the constructor contract:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eCustomer\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eCustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eName\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ename\u003c/span\u003e \u003cspan class=\"p\"\u003e??\u003c/span\u003e \u003cspan class=\"k\"\u003ethrow\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eArgumentNullException\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003enameof\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eName\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"k\"\u003eget\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe act of writing code exposed a decision that pure thought glossed over. Was \u003ccode\u003eName\u003c/code\u003e required or optional? The compiler didn\u0026rsquo;t care about your mental model—it demanded an explicit answer.\u003c/p\u003e\n\u003cp\u003eThis happens at every level: API contracts, concurrency assumptions, resource ownership, error propagation. Abstract thinking lets you defer these decisions indefinitely. Code forces resolution.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-vibe-coding-illusion-when-ai-generates-code-without-understanding\"\u003e\u003ca href=\"/posts/code-sharpens-thinking/#the-vibe-coding-illusion-when-ai-generates-code-without-understanding\" title=\"The Vibe Coding Illusion: When AI Generates Code Without Understanding\"\u003eThe Vibe Coding Illusion: When AI Generates Code Without Understanding\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eAI code assistants accelerate the mechanical part—the typing—\u003cstrong\u003eextraordinarily well\u003c/strong\u003e. Describe a function in natural language, and GitHub Copilot suggests an implementation within seconds. Ask ChatGPT to build a REST API, and it generates hundreds of lines of code that compile and often run.\u003c/p\u003e\n\u003cp\u003eThis feels like \u003cstrong\u003emagic\u003c/strong\u003e until you ask the critical question: \u003cem\u003edoes the generated code do what you actually need, not what you described?\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003eI\u0026rsquo;ve reviewed pull requests where developers used AI to generate complete features. The code compiled. Tests passed. The PR description matched the implementation. Everything looked \u003cstrong\u003efine\u003c/strong\u003e. Until production deployment revealed that the AI had:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eGenerated \u003cstrong\u003ethread-unsafe code\u003c/strong\u003e for concurrent scenarios the prompt didn\u0026rsquo;t mention\u003c/li\u003e\n\u003cli\u003eAllocated memory in hot paths \u003cstrong\u003ewithout consideration\u003c/strong\u003e for garbage collection pressure\u003c/li\u003e\n\u003cli\u003eImplemented \u003cstrong\u003eO(n²) algorithms\u003c/strong\u003e where O(n) solutions existed\u003c/li\u003e\n\u003cli\u003eCreated database queries that worked with test data but \u003cstrong\u003efailed catastrophically\u003c/strong\u003e with production scale\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eIgnored error handling\u003c/strong\u003e edge cases that weren\u0026rsquo;t in the prompt\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003eThe AI didn\u0026rsquo;t fail—it did exactly what was asked.\u003c/strong\u003e The developer failed by not understanding that code generated from a prompt is a starting point, \u003cstrong\u003enot a solution\u003c/strong\u003e. The feedback loop—write code, measure behavior, understand consequences, refine thinking—got \u003cstrong\u003eshort-circuited\u003c/strong\u003e.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"why-vibe-coding-collapses-under-load\"\u003e\u003ca href=\"/posts/code-sharpens-thinking/#why-vibe-coding-collapses-under-load\" title=\"Why Vibe Coding Collapses Under Load\"\u003eWhy Vibe Coding Collapses Under Load\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003e\u0026ldquo;\u003cstrong\u003eVibe coding\u003c/strong\u003e\u0026rdquo; is the term emerging for this pattern: describe the vibe of what you want, let AI generate implementation, ship it if it passes basic tests. It treats code as expression divorced from execution reality. It assumes that if code compiles and handles the happy path, \u003cstrong\u003eit\u0026rsquo;s correct\u003c/strong\u003e.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eThis assumption works until it doesn\u0026rsquo;t.\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eAnd when it doesn\u0026rsquo;t? You\u0026rsquo;re stuck. No foundation for debugging. Can\u0026rsquo;t reason about performance. Can\u0026rsquo;t identify where implementation diverges from requirements. Can\u0026rsquo;t refactor intelligently.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eWhy?\u003c/strong\u003e Because you don\u0026rsquo;t understand what the code actually does.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eHere\u0026rsquo;s your professional advantage:\u003c/strong\u003e You recognize what the compiler \u003cstrong\u003ecan\u0026rsquo;t\u003c/strong\u003e tell you:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eThat the generated code works for the described case but \u003cstrong\u003efails for the dozen edge cases\u003c/strong\u003e you didn\u0026rsquo;t think to mention\u003c/li\u003e\n\u003cli\u003eThat the algorithm performs acceptably with 100 records but \u003cstrong\u003ecollapses with 100,000\u003c/strong\u003e\u003c/li\u003e\n\u003cli\u003eThat the abstraction looks clean but \u003cstrong\u003ecreates maintenance nightmares\u003c/strong\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003eAI generates syntax. Professionals understand semantics, performance characteristics, failure modes, and operational implications.\u003c/strong\u003e The gap between these is where \u0026ldquo;real professionals will never be replaced.\u0026rdquo;\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"code-materializes-consequences\"\u003e\u003ca href=\"/posts/code-sharpens-thinking/#code-materializes-consequences\" title=\"Code Materializes Consequences\"\u003eCode Materializes Consequences\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eCode isn\u0026rsquo;t just structured thought—it\u0026rsquo;s thought with operational consequences. When you design an architecture, you\u0026rsquo;re reasoning about responsibilities and boundaries. When you implement it, you\u0026rsquo;re creating CPU consumption patterns, memory allocation profiles, I/O bottlenecks, and long-term maintenance burdens.\u003c/p\u003e\n\u003cp\u003eThese aren\u0026rsquo;t secondary concerns. They\u0026rsquo;re the actual impact of your decisions.\u003c/p\u003e\n\u003cp\u003eTake a straightforward example: caching. In discussion, caching sounds simple—\u0026ldquo;we\u0026rsquo;ll cache frequently accessed data.\u0026rdquo; The thinking feels complete. Then you implement it:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Looks reasonable in isolation\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eDictionary\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eCustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003e_cache\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eCustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e?\u003c/span\u003e \u003cspan class=\"n\"\u003eGetCustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eid\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003e_cache\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTryGetValue\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eid\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"k\"\u003eout\u003c/span\u003e \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003e_repository\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLoad\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eid\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e \u003cspan class=\"p\"\u003e!=\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003e_cache\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003eid\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis code compiles. It runs. It even passes basic functional tests.\u003c/p\u003e\n\u003cp\u003eThen production hits.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"what-abstract-thinking-defers\"\u003e\u003ca href=\"/posts/code-sharpens-thinking/#what-abstract-thinking-defers\" title=\"What Abstract Thinking Defers\"\u003eWhat Abstract Thinking Defers\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003eWhat abstract thinking missed:\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eMemory\u003c/strong\u003e: Cache grows unbounded. No eviction policy. Memory consumption increases until the process crashes or triggers garbage collection storms that degrade response times by 300%.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eConcurrency\u003c/strong\u003e: No synchronization. Multiple threads corrupt dictionary state, causing crashes or silent data corruption that costs hours of incident response time.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eConsistency\u003c/strong\u003e: Cache never invalidates. Stale data persists indefinitely, creating subtle bugs that customer support escalates—costing reputation and revenue.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eObservability\u003c/strong\u003e: No metrics. You can\u0026rsquo;t tell if caching helps or hurts performance without instrumenting separately.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eEach of these issues represents thinking that felt complete in abstract terms but was fundamentally incomplete in executable reality. The \u0026ldquo;simple\u0026rdquo; caching decision materialized as:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eConcurrentDictionary\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eCacheEntry\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eCustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003e_cache\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eTimeSpan\u003c/span\u003e \u003cspan class=\"n\"\u003e_expirationWindow\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eTimeSpan\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFromMinutes\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e5\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003e_maxCacheSize\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e10000\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eCustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e?\u003c/span\u003e \u003cspan class=\"n\"\u003eGetCustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eid\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003e_cache\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTryGetValue\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eid\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"k\"\u003eout\u003c/span\u003e \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eentry\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e \u003cspan class=\"p\"\u003e!\u003c/span\u003e\u003cspan class=\"n\"\u003eentry\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eIsExpired\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003e_expirationWindow\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eMetrics\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCacheHitCounter\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eIncrement\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eentry\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eValue\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eMetrics\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCacheMissCounter\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eIncrement\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003e_repository\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLoad\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eid\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e \u003cspan class=\"p\"\u003e!=\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003e_cache\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCount\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026gt;=\u003c/span\u003e \u003cspan class=\"n\"\u003e_maxCacheSize\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eEvictOldestEntry\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003e_cache\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003eid\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eCacheEntry\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eCustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;(\u003c/span\u003e\u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eDateTimeOffset\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUtcNow\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eCode didn\u0026rsquo;t complicate a simple idea—it revealed that the idea was never actually simple. Abstract thinking deferred decisions about memory, concurrency, staleness, observability, and eviction. Code forced those decisions into concrete form where consequences become visible and measurable.\u003c/p\u003e\n\u003cp\u003eThis is not implementation detail obscuring elegant design. This is reality asserting itself.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"what-comes-next-the-feedback-loop-ai-cannot-replicate\"\u003e\u003ca href=\"/posts/code-sharpens-thinking/#what-comes-next-the-feedback-loop-ai-cannot-replicate\" title=\"What Comes Next: The Feedback Loop AI Cannot Replicate\"\u003eWhat Comes Next: The Feedback Loop AI Cannot Replicate\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eAI-generated code without understanding creates productivity illusions that collapse in production. Code forces abstract thinking into executable form, exposing gaps that pure reasoning glosses over. That much is clear.\u003c/p\u003e\n\u003cp\u003eBut understanding the problem doesn\u0026rsquo;t answer the deeper question: What exactly is this feedback loop between code and reality, and why can\u0026rsquo;t AI replicate it? What mechanisms transform vague reasoning into concrete understanding?\u003c/p\u003e\n\u003cp\u003eThe answer lies in the tools we use every day: compilers, profilers, tests, production environments. These aren\u0026rsquo;t just validation gates. They\u0026rsquo;re \u003cstrong\u003ereality engines\u003c/strong\u003e that do something AI fundamentally cannot: they execute your assumptions against actual constraints and report back with unfiltered truth.\u003c/p\u003e\n\u003cp\u003e\u003cem\u003eIn the next part of this series, we\u0026rsquo;ll explore how these mechanisms form a cognitive feedback loop that sharpens professional thinking in ways no AI prompt can simulate.\u003c/em\u003e\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2026-01-06T17:00:00+01:00","id":"https://daily-devops.net/posts/code-sharpens-thinking/","language":"en","summary":"Typing code is trivial now—AI does it instantly. So why will real professionals never be replaced? Because vibe coding collapses under production reality.\n","tags":["softwareengineering","codequality","bestpractices","architecture","dotnet","csharp","technicaldebt","ai-code-assistant","github-copilot"],"title":"Why Real Professionals Will Never Be Replaced by AI\n","url":"https://daily-devops.net/posts/code-sharpens-thinking/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003e\u003cem\u003e\u003cstrong\u003eHappy New Year 2026! 🎉\u003c/strong\u003e\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003eSkip the generic wishes. My wish: fix the technical debt you\u0026rsquo;ve been promising since 2023. Stop telling yourself it will happen \u003cem\u003enext quarter.\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003eEvery January, the same ritual. Sprint planning. Someone mentions that problematic module—you know the one. \u0026ldquo;We\u0026rsquo;ll refactor it next quarter,\u0026rdquo; they say. Ticket created. Backlog updated.\u003c/p\u003e\n\u003cp\u003eBy mid-January, forgotten.\u003c/p\u003e\n\u003cp\u003eI\u0026rsquo;ve taught enough .NET courses and consulted with enough teams to know: Everyone has technical debt. The Fortune 500 companies have it. The startups have it. You have it. The difference between teams that succeed in 2026 and teams that burn out isn\u0026rsquo;t whether they have technical debt—it\u0026rsquo;s whether they\u0026rsquo;re honest about it.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"why-next-quarter-never-comes\"\u003e\u003ca href=\"/posts/happy-new-year-2026/#why-next-quarter-never-comes\" title=\"Why Next Quarter Never Comes\"\u003eWhy \u003cem\u003eNext Quarter\u003c/em\u003e Never Comes\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe pattern is always the same. A feature ships. It works—barely. \u0026ldquo;We\u0026rsquo;ll clean this up next quarter,\u0026rdquo; someone says. The team knows it\u0026rsquo;s a lie. Management knows it\u0026rsquo;s a lie. Everyone pretends anyway.\u003c/p\u003e\n\u003cp\u003eWhy? Because admitting you\u0026rsquo;re building on a foundation of compromises feels like failure. It\u0026rsquo;s not. It\u0026rsquo;s reality. But we\u0026rsquo;d rather maintain the fiction.\u003c/p\u003e\n\u003cp\u003eTemporary solutions become permanent infrastructure. That \u0026ldquo;quick integration\u0026rdquo; from 2019 is now mission-critical and touches everything. The developer who wrote it left in 2021. The documentation? Nonexistent.\u003c/p\u003e\n\u003cp\u003eThis compounds. Every shortcut builds on the previous shortcut. Every \u0026ldquo;we\u0026rsquo;ll fix it later\u0026rdquo; adds to the pile. Fast forward to 2026, and you\u0026rsquo;re spending more time working around bad decisions than you would have spent making good ones.\u003c/p\u003e\n\u003cp\u003eTechnical debt feels free when you create it. Your sprint metrics look great. You shipped the feature. Everyone\u0026rsquo;s happy.\u003c/p\u003e\n\u003cp\u003eEighteen months later, that code is load-bearing. Developers quit because debugging incomprehensible code is soul-crushing. Your velocity drops. The business wonders why. You can\u0026rsquo;t admit the foundation is crumbling.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"every-january-same-promises\"\u003e\u003ca href=\"/posts/happy-new-year-2026/#every-january-same-promises\" title=\"Every January, Same Promises\"\u003eEvery January, Same Promises\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eI teach .NET courses for students and apprentices. Every year, developers tell me about the refactoring they\u0026rsquo;re planning.\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cem\u003eThis year we\u0026rsquo;ll finally add tests.\u003c/em\u003e\u003c/li\u003e\n\u003cli\u003e\u003cem\u003eThis year we\u0026rsquo;ll upgrade from .NET Framework 4.8.\u003c/em\u003e\u003c/li\u003e\n\u003cli\u003e\u003cem\u003eThis year we\u0026rsquo;ll split up that 4,000-line controller.\u003c/em\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eBy February, they\u0026rsquo;re back to shipping features. The tests? Still at 12% coverage. The Framework migration? Still \u0026ldquo;risky.\u0026rdquo; The controller? Now 4,300 lines.\u003c/p\u003e\n\u003cp\u003eHere\u0026rsquo;s the code everyone writes on January 1st:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eOrderProcessor\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// TODO 2023: Refactor this - too much responsibility\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// TODO 2024: Seriously, we need to split this up\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// TODO 2025: I\u0026#39;m not even joking anymore\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// TODO 2026: Kill me\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"kd\"\u003estatic\u003c/span\u003e \u003cspan class=\"n\"\u003eDictionary\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eOrderState\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003e_stateCache\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e \u003cspan class=\"c1\"\u003e// Race condition central\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003ebool\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eProcessOrder\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eOrder\u003c/span\u003e \u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"c1\"\u003e// CA2007 warning since 2022, still ignored\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// Checking null in 2026 like it\u0026#39;s 2015\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eorder\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"kc\"\u003efalse\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eresult\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eSaveToDatabase\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e \u003cspan class=\"c1\"\u003e// Deadlock count: 23 and climbing\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eSendEmail\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eEmail\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e \u003cspan class=\"c1\"\u003e// NullReferenceException #47 this month\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eresult\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThose CA2007 warnings from ConfigureAwait? Been there since you upgraded to .NET Core 3.1 in 2020. You keep meaning to fix them. You suppress them instead because \u0026ldquo;we\u0026rsquo;ll do it properly in the refactoring.\u0026rdquo;\u003c/p\u003e\n\u003cp\u003eThis isn\u0026rsquo;t developer failure. It\u0026rsquo;s organizational reality. You can only refactor when the business prioritizes it (rarely happens), you have time between features (almost never), you\u0026rsquo;re not fighting production fires (frequently untrue), and nobody\u0026rsquo;s pressuring you to ship faster (never).\u003c/p\u003e\n\u003cp\u003eThe system ensures technical debt is always someone else\u0026rsquo;s problem. Always scheduled for later. Later never arrives.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"why-technical-debt-compounds-like-credit-card-interest\"\u003e\u003ca href=\"/posts/happy-new-year-2026/#why-technical-debt-compounds-like-credit-card-interest\" title=\"Why Technical Debt Compounds Like Credit Card Interest\"\u003eWhy Technical Debt Compounds Like Credit Card Interest\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eYour 2026 codebase reflects every shortcut from 2025. Every \u0026quot;we\u0026rsquo;ll fix it later.\u0026quot; Every suppressed warning. Every test you didn\u0026rsquo;t write. Cause and effect.\u003c/p\u003e\n\u003cp\u003eMost teams think they\u0026rsquo;re trading speed for quality. That\u0026rsquo;s not even wrong—it\u0026rsquo;s nonsense disguised as pragmatism. You\u0026rsquo;re choosing whether to pay now or pay later with interest. Later always costs more.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"the-cost-nobody-tracks\"\u003e\u003ca href=\"/posts/happy-new-year-2026/#the-cost-nobody-tracks\" title=\"The Cost Nobody Tracks\"\u003eThe Cost Nobody Tracks\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eTechnical debt is a business decision. Treat it like one.\u003c/p\u003e\n\u003cp\u003eWriting code fast but wrong costs more than writing it right. The 3 AM production incident. The Friday afternoon rollback. The six hours debugging something that proper async patterns would have prevented.\u003c/p\u003e\n\u003cp\u003eThese costs don\u0026rsquo;t show up in sprint reports. They show up in exhausted developers, missed deadlines, and customer incidents.\u003c/p\u003e\n\u003cp\u003eFeatures that should take days take weeks because the codebase fights you. Every change risks breaking something unrelated. \u0026ldquo;Just be careful\u0026rdquo; doesn\u0026rsquo;t scale.\u003c/p\u003e\n\u003cp\u003eDevelopers quit. Not because of salary. Because maintaining incomprehensible code destroys your soul. That new hire still lost after six months? Your architecture is the problem.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"what-actually-works-from-15-years-of-watching-teams-fail\"\u003e\u003ca href=\"/posts/happy-new-year-2026/#what-actually-works-from-15-years-of-watching-teams-fail\" title=\"What Actually Works (From 15 Years of Watching Teams Fail)\"\u003eWhat Actually Works (From 15 Years of Watching Teams Fail)\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eSustainable doesn\u0026rsquo;t mean perfect. It means the codebase doesn\u0026rsquo;t actively fight you.\u003c/p\u003e\n\u003cp\u003eWrite tests because they save debugging time, not because some \u0026ldquo;best practices\u0026rdquo; document says to. I\u0026rsquo;ve watched developers spend three days tracking down a bug that a 15-line unit test would have caught in three seconds. That\u0026rsquo;s not a best practice—that\u0026rsquo;s basic economics.\u003c/p\u003e\n\u003cp\u003eRefactor as you go. Not in some mythical future sprint. When you\u0026rsquo;re in a file and you see garbage code, fix it then. Yes, even if it\u0026rsquo;s \u0026ldquo;out of scope.\u0026rdquo; Especially if it\u0026rsquo;s out of scope. The Boy Scout Rule isn\u0026rsquo;t a suggestion—it\u0026rsquo;s how you avoid code rot.\u003c/p\u003e\n\u003cp\u003ePush back on scope creep with data. \u0026ldquo;This will take three days with tests, one day without\u0026rdquo; is a lie everyone tells. It takes three days either way—you\u0026rsquo;re just choosing whether to spend them now or during the 2 AM production incident.\u003c/p\u003e\n\u003cp\u003eYour .NET project in 2026 has every tool needed to avoid this. Roslyn analyzers like \u003ca href=\"https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1062\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eCA1062\u003c/a\u003e catch null reference exceptions before they ship. \u003ca href=\"https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2007\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eCA2007\u003c/a\u003e prevents ConfigureAwait deadlocks automatically. In my MCT courses, I enable these analyzers on legacy projects. Within hours, they\u0026rsquo;ve found bugs that lived in production for years.\u003c/p\u003e\n\u003cp\u003eNUnit\u0026rsquo;s parameterized tests let you cover edge cases in three lines. C# 12\u0026rsquo;s primary constructors eliminate the boilerplate nobody ever tested. .NET 9\u0026rsquo;s performance improvements mean you can write cleaner code that\u0026rsquo;s also faster.\u003c/p\u003e\n\u003cp\u003eTools aren\u0026rsquo;t the problem. You can download Visual Studio 2022, enable all the analyzers, and catch 80% of common bugs before your first commit.\u003c/p\u003e\n\u003cp\u003eDiscipline is the problem.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"what-2026-actually-offers\"\u003e\u003ca href=\"/posts/happy-new-year-2026/#what-2026-actually-offers\" title=\"What 2026 Actually Offers\"\u003eWhat 2026 Actually Offers\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e2026 isn\u0026rsquo;t special. .NET 9 is mature and stable now—shipped November 2024. C# 13 brought some nice features. The ecosystem keeps improving.\u003c/p\u003e\n\u003cp\u003eBut here\u0026rsquo;s what matters: The tools to build maintainable software have been available for years. Roslyn analyzers. Testing frameworks. Structured logging. Observability tools. None of this is new.\u003c/p\u003e\n\u003cp\u003eThe bottleneck was never tooling. It\u0026rsquo;s discipline.\u003c/p\u003e\n\u003cp\u003eWhat makes 2026 different? Nothing, unless you decide it is.\u003c/p\u003e\n\u003cp\u003eYou can start the year like every other year—good intentions, abandoned by February. Or you can actually change something.\u003c/p\u003e\n\u003cp\u003eNot by adopting the newest framework. Not by rewriting everything in the latest architectural pattern. By making the unsexy choice: Fix one thing at a time. Add tests as you go. Enable analyzers. Refactor when you touch code, not \u0026ldquo;next quarter.\u0026rdquo;\u003c/p\u003e\n\u003cp\u003e.NET 9\u0026rsquo;s \u003ca href=\"https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-9/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eperformance improvements\u003c/a\u003e are real—LINQ is faster, JSON serialization allocates less, the JIT is smarter. Migrating from .NET 6 or 8 is straightforward. Most teams can do it in days.\u003c/p\u003e\n\u003cp\u003eC# 13\u0026rsquo;s params collections and field keyword are fine. Use them where they help. Ignore them where they don\u0026rsquo;t.\u003c/p\u003e\n\u003cp\u003eAzure\u0026rsquo;s container and serverless offerings are stable now. Pick what fits your team\u0026rsquo;s expertise. Ignore what Hacker News says is \u0026ldquo;modern.\u0026rdquo;\u003c/p\u003e\n\u003cp\u003eAI integration will be 2026\u0026rsquo;s buzzword. Most will be snake oil. Some—like GitHub Copilot for boilerplate—actually helps. Don\u0026rsquo;t chase hype. Solve real problems.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"code-that-doesnt-wake-you-up-at-3-am\"\u003e\u003ca href=\"/posts/happy-new-year-2026/#code-that-doesnt-wake-you-up-at-3-am\" title=\"Code That Doesn\u0026rsquo;t Wake You Up at 3 AM\"\u003eCode That Doesn\u0026rsquo;t Wake You Up at 3 AM\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eIntentional development looks boring. No clever patterns. No abstraction for abstraction\u0026rsquo;s sake. Just code that works and can be debugged when it doesn\u0026rsquo;t.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003eMicrosoft.Extensions.Logging\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003eSystem.Diagnostics\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eCustomerOrderService\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eILogger\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eCustomerOrderService\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003e_logger\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eActivitySource\u003c/span\u003e \u003cspan class=\"n\"\u003e_activitySource\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eIOrderRepository\u003c/span\u003e \u003cspan class=\"n\"\u003e_repository\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eCustomerOrderService\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eILogger\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eCustomerOrderService\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003elogger\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eActivitySource\u003c/span\u003e \u003cspan class=\"n\"\u003eactivitySource\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eIOrderRepository\u003c/span\u003e \u003cspan class=\"n\"\u003erepository\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003e_logger\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003elogger\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003e_activitySource\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eactivitySource\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003e_repository\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003erepository\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eOrderResult\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eProcessOrder\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eOrderRequest\u003c/span\u003e \u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eCancellationToken\u003c/span\u003e \u003cspan class=\"n\"\u003ecancellationToken\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// OpenTelemetry distributed tracing - you\u0026#39;ll thank me during the incident\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eactivity\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003e_activitySource\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eStartActivity\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;ProcessOrder\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eactivity\u003c/span\u003e\u003cspan class=\"p\"\u003e?.\u003c/span\u003e\u003cspan class=\"n\"\u003eSetTag\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;order.id\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eOrderId\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eactivity\u003c/span\u003e\u003cspan class=\"p\"\u003e?.\u003c/span\u003e\u003cspan class=\"n\"\u003eSetTag\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;customer.id\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCustomerId\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003e_logger\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLogInformation\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"s\"\u003e\u0026#34;Processing order {OrderId} for customer {CustomerId}\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eOrderId\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCustomerId\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// CA2007 compliant - no deadlocks in ASP.NET synchronization context\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003evalidationResult\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eValidateOrder\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ecancellationToken\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eConfigureAwait\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kc\"\u003efalse\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(!\u003c/span\u003e\u003cspan class=\"n\"\u003evalidationResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eIsValid\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"c1\"\u003e// Structured logging means you can query this in Application Insights\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003e_logger\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLogWarning\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"s\"\u003e\u0026#34;Order validation failed for {OrderId}: {ValidationErrors}\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eOrderId\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eJoin\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;, \u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003evalidationResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eErrors\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eactivity\u003c/span\u003e\u003cspan class=\"p\"\u003e?.\u003c/span\u003e\u003cspan class=\"n\"\u003eSetStatus\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eActivityStatusCode\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eError\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Validation failed\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eOrderResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eValidationFailed\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003evalidationResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eErrors\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eorder\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_repository\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSaveOrder\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003evalidationResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eOrder\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ecancellationToken\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eConfigureAwait\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kc\"\u003efalse\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eactivity\u003c/span\u003e\u003cspan class=\"p\"\u003e?.\u003c/span\u003e\u003cspan class=\"n\"\u003eSetTag\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;order.total\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTotalAmount\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eOrderResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSuccess\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis is production code, adapted from a real order-processing system. Nothing fancy. Dependency injection makes it testable—I can mock \u003ccode\u003eIOrderRepository\u003c/code\u003e in unit tests. Structured logging means when something breaks at 3 AM, I can find it in Azure Application Insights in thirty seconds instead of thirty minutes. OpenTelemetry gives me distributed traces across services. ConfigureAwait prevents the deadlocks that plagued the previous version.\u003c/p\u003e\n\u003cp\u003eIt\u0026rsquo;s not clever. It\u0026rsquo;s reliable. After fifteen years, I\u0026rsquo;ll take reliable over clever every single time.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"making-2026-different\"\u003e\u003ca href=\"/posts/happy-new-year-2026/#making-2026-different\" title=\"Making 2026 Different\"\u003eMaking 2026 Different\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eLearning new C# features isn\u0026rsquo;t hard. Optimizing Azure costs isn\u0026rsquo;t hard. Discipline is hard.\u003c/p\u003e\n\u003cp\u003eSaying no when a VP wants a feature that solves no real problem. Budgeting time for maintenance and defending it. Writing tests when nobody\u0026rsquo;s watching. Code reviewing properly when you\u0026rsquo;re swamped. Having uncomfortable conversations about quality.\u003c/p\u003e\n\u003cp\u003eMeasuring what matters: incident rates, recovery time, PR review duration, developer retention. Not just velocity.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"what-january-looks-like-for-most-teams\"\u003e\u003ca href=\"/posts/happy-new-year-2026/#what-january-looks-like-for-most-teams\" title=\"What January Looks Like for Most Teams\"\u003eWhat January Looks Like for Most Teams\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eJanuary: \u0026ldquo;This year will be different.\u0026rdquo;\u003c/p\u003e\n\u003cp\u003eFebruary: Feature roadmap consumed the quarter.\u003c/p\u003e\n\u003cp\u003eMarch: Production incident. All hands on deck.\u003c/p\u003e\n\u003cp\u003eApril-December: Repeat.\u003c/p\u003e\n\u003cp\u003eDiscipline is hard because it requires saying no to immediate pressure for long-term stability. The pressure is real. The meetings asking \u0026ldquo;why so long\u0026rdquo; are real. The 5 PM Friday Slack messages are real.\u003c/p\u003e\n\u003cp\u003eTeams that survive aren\u0026rsquo;t smarter. They\u0026rsquo;re not using better frameworks. They have organizational support for saying no. Or they\u0026rsquo;re stubborn enough to do it anyway.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"what-winning-in-2026-looks-like\"\u003e\u003ca href=\"/posts/happy-new-year-2026/#what-winning-in-2026-looks-like\" title=\"What Winning in 2026 Looks Like\"\u003eWhat Winning in 2026 Looks Like\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eShip fewer features that work, instead of many features that half work.\u003c/p\u003e\n\u003cp\u003eIncident rates go down. Developers stay. You can respond to market changes because you\u0026rsquo;re not buried in debt.\u003c/p\u003e\n\u003cp\u003eNot exciting. Pragmatic.\u003c/p\u003e\n\u003cp\u003eSo here\u0026rsquo;s my actual New Year wish for you: Stop lying to yourself about \u0026ldquo;next quarter.\u0026rdquo; Fix one thing this week. Enable one analyzer. Write one test. Refactor one function.\u003c/p\u003e\n\u003cp\u003eNot next quarter. This week.\u003c/p\u003e\n\u003cp\u003eThat\u0026rsquo;s how software survives to 2027.\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2026-01-01T14:00:00+01:00","id":"https://daily-devops.net/posts/happy-new-year-2026/","language":"en","summary":"Stop promising to fix technical debt next quarter. .NET 10, analyzers, and tests are ready in 2026; only the engineering discipline is missing.","tags":["technicaldebt","dotnet","csharp","softwareengineering","codequality"],"title":"Most Software Teams Are Lying to Themselves—2026 Needs to Be Different","url":"https://daily-devops.net/posts/happy-new-year-2026/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eLet\u0026rsquo;s be honest about 2025: no runtime breakthroughs, no language revolutions. Nothing that\u0026rsquo;ll make the keynote highlight reels. What we got instead was something the ecosystem desperately needed—tooling that finally stopped lying about complexity.\u003c/p\u003e\n\u003cp\u003eThe wins came from admitting reality. Distributed systems aren\u0026rsquo;t simple, and tools that pretend otherwise just create delayed failures. Async execution semantics matter, whether your abstraction acknowledges them or not. Infrastructure dependencies aren\u0026rsquo;t implementation details you can mock away without consequences. In 2025, the tools that delivered value made all of this explicit, testable, impossible to ignore.\u003c/p\u003e\n\u003cp\u003eBut alongside that technical progress, we also saw the cracks widen. Open source sustainability, corporate consumption patterns, ecosystem trust—these structural tensions didn\u0026rsquo;t get resolved. If anything, they became harder to ignore. And they\u0026rsquo;re shaping our tooling choices just as much as any technical consideration.\u003c/p\u003e\n\u003cp\u003eHere\u0026rsquo;s what actually mattered this year.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"making-complexity-visible-not-optional\"\u003e\u003ca href=\"/posts/dotnet-2025-year-in-review/#making-complexity-visible-not-optional\" title=\"Making Complexity Visible, Not Optional\"\u003eMaking Complexity Visible, Not Optional\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe pattern I kept seeing in 2025: tools that actually mattered forced you to deal with reality instead of pretending it away. Topology. Concurrency. Dependency lifecycles. Infrastructure behavior. The messy stuff we\u0026rsquo;ve been hiding behind \u0026ldquo;convenience\u0026rdquo; layers for years, just postponing production incidents.\u003c/p\u003e\n\u003cp\u003eAspire, TUnit, Testcontainers. Three different problems. One consistent theme: show me what\u0026rsquo;s actually happening.\u003c/p\u003e\n\u003cp\u003e.NET Aspire: Beyond the Azure Narrative\u003c/p\u003e\n\u003cp\u003eMost people look at Aspire and see Azure tooling. That\u0026rsquo;s reading it wrong. It\u0026rsquo;s worth correcting because it misses what actually changed in 2025.\u003c/p\u003e\n\u003cp\u003eI watched teams use Aspire in ways that had nothing to do with Azure. Polyglot systems where only the orchestration layer was .NET. Existing containerized services that got wired in without rewrites. Self-hosted infrastructure, alternative cloud providers, Docker on a developer\u0026rsquo;s laptop. Hybrid setups where Aspire was just the coordination layer, not the runtime.\u003c/p\u003e\n\u003cp\u003eWhat makes this work is that Aspire isn\u0026rsquo;t really about deployment targets. It\u0026rsquo;s about making system intent explicit.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eDistributedApplication\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCreateBuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eargs\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003epostgres\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddPostgres\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;db\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eapi\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddProject\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eProjects\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eApi\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;api\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                 \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWithReference\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003epostgres\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eBuild\u003c/span\u003e\u003cspan class=\"p\"\u003e().\u003c/span\u003e\u003cspan class=\"n\"\u003eRun\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eLook at this code. Dependencies aren\u0026rsquo;t buried in appsettings files or injected through environment variables scattered across deployment scripts. They\u0026rsquo;re right there, versioned with your application code, reviewable in pull requests, enforced at composition time.\u003c/p\u003e\n\u003cp\u003eThe app model is your system topology as code. Aspire then \u0026ldquo;lowers\u0026rdquo; that high-level description into whatever you actually need—Kubernetes manifests, Bicep templates, Docker Compose files, whatever your target environment requires.\u003c/p\u003e\n\u003cp\u003eBut the thing that actually shifted conversations: observability gets baked in. With Aspire, OpenTelemetry isn\u0026rsquo;t a post-deployment retrofit. \u003ccode\u003eOTEL_SERVICE_NAME\u003c/code\u003e and \u003ccode\u003eOTEL_EXPORTER_OTLP_ENDPOINT\u003c/code\u003e are automatic. The dashboard shows you traces, logs, metrics during local dev—without the boilerplate.\u003c/p\u003e\n\u003cp\u003eWhen observability is structural instead of bolted-on, the entire conversation changes.\u003c/p\u003e\n\u003cp\u003eThat alignment—between how you describe your system, how it gets deployed, and how you observe it—is where Aspire delivered real value in 2025.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResources\u003c/strong\u003e: \u003ca href=\"https://github.com/dotnet/aspire\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eGitHub\u003c/a\u003e | \u003ca href=\"https://learn.microsoft.com/en-us/dotnet/aspire/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eDocs\u003c/a\u003e\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"tunit-when-test-frameworks-hide-what-matters\"\u003e\u003ca href=\"/posts/dotnet-2025-year-in-review/#tunit-when-test-frameworks-hide-what-matters\" title=\"TUnit: When Test Frameworks Hide What Matters\"\u003eTUnit: When Test Frameworks Hide What Matters\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eTUnit looks like cleaner syntax. It\u0026rsquo;s not. The actual value is in execution semantics that most frameworks just ignore because they don\u0026rsquo;t care about precision.\u003c/p\u003e\n\u003cp\u003eReal test suites fail constantly for reasons that have nothing to do with your code. Shared state between parameterized tests. Async forced into sync silently. Parallel runs creating race conditions that only show up in CI. Test fixtures hiding execution boundaries you never designed for. The list goes on.\u003c/p\u003e\n\u003cp\u003eMost frameworks allow tests with these problems. TUnit makes them hard to accidentally create.\u003c/p\u003e\n\u003cp\u003eTake a realistic scenario—testing behavior that depends on multiple runtime dimensions like feature flags and tenant configuration:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003esealed\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eFeatureFlagTests\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [Test]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e \u003cspan class=\"n\"\u003eRequest_is_processed_correctly\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e        [Values(true, false)]\u003c/span\u003e \u003cspan class=\"kt\"\u003ebool\u003c/span\u003e \u003cspan class=\"n\"\u003efeatureEnabled\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e        [Values(\u0026#34;Free\u0026#34;, \u0026#34;Premium\u0026#34;)]\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003etenantType\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003esystem\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eTestSystem\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCreateAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003efeatureEnabled\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003etenantType\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eresponse\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003esystem\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eExecuteRequestAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eAssert\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eThat\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eresponse\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eIsSuccessful\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\u003cspan class=\"n\"\u003eIsTrue\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eIn TUnit, each parameter combination runs in complete isolation. The async lifecycle is native—no hidden \u003ccode\u003eTask.Run()\u003c/code\u003e or \u003ccode\u003e.Result\u003c/code\u003e calls. Fixtures are explicit. Parallel execution doesn\u0026rsquo;t introduce coupling you didn\u0026rsquo;t ask for.\u003c/p\u003e\n\u003cp\u003eWhat this eliminates is that whole category of tests that pass locally, fail in CI, pass again when you re-run them, and fail on Tuesdays. You know the ones. The flaky tests that eat hours of investigation time because the failure mode has nothing to do with the business logic you\u0026rsquo;re testing.\u003c/p\u003e\n\u003cp\u003eIn production CI pipelines, I saw this translate to predictable parallel execution times, reduced variance across agents, and—most importantly—test failures that actually correlated with system behavior rather than execution artifacts.\u003c/p\u003e\n\u003cp\u003eTUnit makes execution boundaries explicit. That\u0026rsquo;s the real contribution.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResources\u003c/strong\u003e: \u003ca href=\"https://github.com/thomhurst/TUnit\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eGitHub\u003c/a\u003e\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"testcontainers-when-mocks-stop-being-enough\"\u003e\u003ca href=\"/posts/dotnet-2025-year-in-review/#testcontainers-when-mocks-stop-being-enough\" title=\"Testcontainers: When Mocks Stop Being Enough\"\u003eTestcontainers: When Mocks Stop Being Enough\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eBy 2025, I stopped treating Testcontainers as optional. If you\u0026rsquo;re testing assumptions instead of real infrastructure, you\u0026rsquo;re setting yourself up for surprises in production.\u003c/p\u003e\n\u003cp\u003eIn-memory substitutes lie. You can\u0026rsquo;t test transaction isolation with SQLite. You can\u0026rsquo;t test Kafka\u0026rsquo;s partition rebalancing without Kafka. Message delivery semantics, startup timing, schema migrations—the real database handles all this differently than a polite fake.\u003c/p\u003e\n\u003cp\u003eTestcontainers lets you test actual infrastructure behavior:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003ekafka\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eKafkaBuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWithCleanUp\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kc\"\u003etrue\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eBuild\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003ekafka\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eStartAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eWhen these tests fail, they\u0026rsquo;re usually telling you about real production risks, not artifacts of your test harness.\u003c/p\u003e\n\u003cp\u003eConsider what this means for database testing. PostgreSQL handles concurrent transactions, deadlocks, constraint violations in ways that in-memory databases simply don\u0026rsquo;t. Kafka\u0026rsquo;s exactly-once semantics, partition assignment, consumer group rebalancing—you need the actual broker to test any of this meaningfully.\u003c/p\u003e\n\u003cp\u003eI\u0026rsquo;ve watched too many teams ship code that works fine against mocks and breaks immediately in production. Connection pool exhaustion. Deadlocks under load. Message ordering violations during partition reassignment. Schema migrations that work on SQLite but fail on Postgres because of type handling differences.\u003c/p\u003e\n\u003cp\u003eThese aren\u0026rsquo;t edge cases. They\u0026rsquo;re the default in real systems.\u003c/p\u003e\n\u003cp\u003eTestcontainers spins up real containers in your CI pipeline. Tests run against actual systems. Then the containers get cleaned up. The feedback loop stays fast. The confidence isn\u0026rsquo;t false.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResources\u003c/strong\u003e: \u003ca href=\"https://github.com/testcontainers/testcontainers-dotnet\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eGitHub\u003c/a\u003e\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-structural-problems-were-not-solving\"\u003e\u003ca href=\"/posts/dotnet-2025-year-in-review/#the-structural-problems-were-not-solving\" title=\"The Structural Problems We\u0026rsquo;re Not Solving\"\u003eThe Structural Problems We\u0026rsquo;re Not Solving\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe tooling highlights tell one story. But 2025 also made it harder to ignore structural problems that aren\u0026rsquo;t getting better.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"licensing-as-operational-dependency\"\u003e\u003ca href=\"/posts/dotnet-2025-year-in-review/#licensing-as-operational-dependency\" title=\"Licensing as Operational Dependency\"\u003eLicensing as Operational Dependency\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eCommercializing open source dependencies isn\u0026rsquo;t new. What became clearer in 2025 were the operational costs that don\u0026rsquo;t appear in pricing discussions.\u003c/p\u003e\n\u003cp\u003eCI pipelines started failing during container builds because license checks couldn\u0026rsquo;t reach licensing servers. Dependency upgrades got blocked not for technical reasons but because legal teams needed weeks to review new license terms. Build systems became coupled to licensing infrastructure in ways nobody had planned for. Features fragmented across paid and unpaid tiers, forcing architectural decisions based on licensing rather than technical fit.\u003c/p\u003e\n\u003cp\u003eFrom an RCDA perspective, this is a risk profile change. When your build breaks because a license server is down, you\u0026rsquo;ve introduced a runtime dependency that wasn\u0026rsquo;t part of the original technical evaluation. The feedback cycle slows. Operational complexity increases. And most teams don\u0026rsquo;t see this coming until they\u0026rsquo;re already committed to the dependency.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"the-consumption-contribution-imbalance\"\u003e\u003ca href=\"/posts/dotnet-2025-year-in-review/#the-consumption-contribution-imbalance\" title=\"The Consumption-Contribution Imbalance\"\u003eThe Consumption-Contribution Imbalance\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eLarge organizations continued extracting value from open source while contributing little back. Internal forks maintained indefinitely. Bug fixes applied internally but never pushed upstream. Copyright violations discovered through community audits, not voluntary disclosure.\u003c/p\u003e\n\u003cp\u003eIs this malicious? Usually not. It\u0026rsquo;s legal risk management, procurement friction, organizational complexity. But the outcome remains the same: ecosystem fragmentation and maintainer burnout, while enterprises save millions on software they couldn\u0026rsquo;t build themselves.\u003c/p\u003e\n\u003cp\u003eThis isn\u0026rsquo;t sustainable. When consumption at scale doesn\u0026rsquo;t come with proportional contribution—whether that\u0026rsquo;s code, funding, security disclosures, or just documentation improvements—the ecosystem becomes extractive. Maintainers burn out. Critical libraries go unmaintained. Trust erodes.\u003c/p\u003e\n\u003cp\u003e2025 made this tension more visible. We still don\u0026rsquo;t have good answers.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"what-2025-actually-taught-us\"\u003e\u003ca href=\"/posts/dotnet-2025-year-in-review/#what-2025-actually-taught-us\" title=\"What 2025 Actually Taught Us\"\u003eWhat 2025 Actually Taught Us\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e2025 was the year .NET tooling stopped hiding what\u0026rsquo;s actually hard. Aspire made system intent explicit. TUnit made execution boundaries explicit. Testcontainers made infrastructure behavior explicit.\u003c/p\u003e\n\u003cp\u003eThe open source sustainability crisis? Still unresolved. Still worsening. And still being treated as someone else\u0026rsquo;s problem by many organizations extracting the most value. These aren\u0026rsquo;t abstract concerns—they shape which tools survive, which maintainers continue, which dependencies remain viable long-term.\u003c/p\u003e\n\u003cp\u003eHere\u0026rsquo;s the lesson: technical maturity and ecosystem health aren\u0026rsquo;t separate. Ignore sustainability problems and you eventually constrain technical progress. Build on foundations maintained by exhausted volunteers subsidizing enterprise infrastructure, and you\u0026rsquo;re building on uncertain ground.\u003c/p\u003e\n\u003cp\u003eThe tools that mattered were honest. They didn\u0026rsquo;t promise to make distributed systems simple. They didn\u0026rsquo;t pretend async execution doesn\u0026rsquo;t matter. They didn\u0026rsquo;t hide infrastructure behavior and hope you wouldn\u0026rsquo;t notice.\u003c/p\u003e\n\u003cp\u003eA mature ecosystem doesn\u0026rsquo;t have magic. It has tools that show you what\u0026rsquo;s happening so you can make real decisions instead of discovering the truth during an incident.\u003c/p\u003e\n\u003cp\u003eThe frameworks and libraries that\u0026rsquo;ll thrive going forward are the ones making system behavior transparent, testable, debuggable. Not the ones selling simplicity through opacity.\u003c/p\u003e\n\u003cp\u003e2025 taught us that honesty scales better than convenient abstractions that break under production load.\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2025-12-30T17:00:00+01:00","id":"https://daily-devops.net/posts/dotnet-2025-year-in-review/","language":"en","summary":"No runtime revolutions—Aspire, TUnit, and Testcontainers won by making distributed systems visible. Plus .NET's open source sustainability crisis.","tags":["opensource","architecture","dotnet","csharp","aspire","testing","softwareengineering","technicaldebt"],"title":"2025 in Review: The Year .NET Stopped Lying to Itself","url":"https://daily-devops.net/posts/dotnet-2025-year-in-review/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\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\u003eI\u0026rsquo;ve watched developers, including myself, waste hundreds of hours on something completely avoidable. Not architecture decisions or complex algorithms—just typing. Specifically, typing the same \u003ccode\u003e.NET CLI\u003c/code\u003e commands over and over because they couldn\u0026rsquo;t quite remember the exact syntax.\u003c/p\u003e\n\u003cp\u003eYou\u0026rsquo;ve probably done it yourself. You\u0026rsquo;re about to add a NuGet package, you type \u003ccode\u003edotnet add package\u003c/code\u003e, then you pause. Was it \u003ccode\u003eMicrosoft.Extensions.Logging\u003c/code\u003e or \u003ccode\u003eMicrosoft.Extensions.Logging.Abstractions\u003c/code\u003e? You open a browser tab, search NuGet.org, find the package, copy the name, switch back to your terminal, paste it. Fifteen seconds lost. Multiply that by dozens of commands daily.\u003c/p\u003e\n\u003cp\u003eThat\u0026rsquo;s not even counting the times you mistype a command and have to run it again. Or when you forget which flags \u003ccode\u003edotnet publish\u003c/code\u003e supports and end up in \u003ccode\u003e--help\u003c/code\u003e documentation.\u003c/p\u003e\n\u003cp\u003eThe \u003ccode\u003e.NET CLI\u003c/code\u003e has technically supported tab completion for years. But getting it to work? That meant diving into PowerShell documentation, copying \u003ccode\u003eRegister-ArgumentCompleter\u003c/code\u003e snippets from Stack Overflow, debugging why it wouldn\u0026rsquo;t load properly, then maintaining that brittle setup across machines. I tried it once on a project in 2021. Gave up after the third machine where it broke differently.\u003c/p\u003e\n\u003cp\u003eWhen \u003cstrong\u003e.NET 10\u003c/strong\u003e shipped in November 2025,  Microsoft finally included what should\u0026rsquo;ve been there from day one: native completion scripts. One command. That\u0026rsquo;s it. No registration. No manual shell configuration. Just \u003ccode\u003edotnet completions script \u0026gt;\u0026gt; $PROFILE\u003c/code\u003e and you\u0026rsquo;re done.\u003c/p\u003e\n\u003cp\u003eI tested it the day the release dropped. Took me exactly 47 seconds from reading the release notes to having working completion. That\u0026rsquo;s the kind of feature that makes you wonder why you tolerated the old way for so long.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"why-tab-completion-matters-more-than-you-think\"\u003e\u003ca href=\"/posts/dotnet-cli-expanding-scope-autocomplete/#why-tab-completion-matters-more-than-you-think\" title=\"Why Tab Completion Matters (More Than You Think)\"\u003eWhy Tab Completion Matters (More Than You Think)\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eHere\u0026rsquo;s something I tracked for a week in October: I ran 847 \u003ccode\u003edotnet\u003c/code\u003e commands. That\u0026rsquo;s not an exceptional week—I was doing standard development work on four different projects. No CI/CD pipelines, no deployment scripts, just regular coding.\u003c/p\u003e\n\u003cp\u003eOf those 847 commands, 312 involved package names or project references. Before I enabled completion, I\u0026rsquo;d estimate I spent 10-15 seconds per command hunting for exact names. With completion? Two seconds. Tab, confirm, done.\u003c/p\u003e\n\u003cp\u003eDo the math on that. Even at a conservative 10 seconds saved per operation, that\u0026rsquo;s 3,120 seconds weekly. That\u0026rsquo;s 52 minutes I\u0026rsquo;m not spending on mechanical busywork. But here\u0026rsquo;s what really matters: the cognitive load disappears.\u003c/p\u003e\n\u003cp\u003eYou stop maintaining these useless mental indexes of syntax. I used to have \u003ccode\u003e-c\u003c/code\u003e vs \u003ccode\u003e--configuration\u003c/code\u003e memorized, along with whether it was \u003ccode\u003e--framework\u003c/code\u003e or \u003ccode\u003e-f\u003c/code\u003e, whether \u003ccode\u003epublish\u003c/code\u003e took \u003ccode\u003e--output\u003c/code\u003e or \u003ccode\u003e-o\u003c/code\u003e. Now? I type \u003ccode\u003edotnet publish -\u003c/code\u003e and press Tab. The shell shows me everything available. I pick what I need.\u003c/p\u003e\n\u003cp\u003eLast month I discovered \u003ccode\u003edotnet workload repair\u003c/code\u003e because it appeared in completion results. I\u0026rsquo;d been manually reinstalling workloads when they broke. Turns out there\u0026rsquo;s been a repair command since .NET 6. I just never knew because I never ran \u003ccode\u003edotnet --help\u003c/code\u003e looking for it.\u003c/p\u003e\n\u003cp\u003eThe modern \u003ccode\u003e.NET CLI\u003c/code\u003e does a lot more than most developers realize:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eIt queries NuGet.org in real-time as you type package names. Type \u003ccode\u003edotnet add package Micro\u003c/code\u003e and hit Tab—you\u0026rsquo;ll see \u003ccode\u003eMicrosoft.Extensions.*\u003c/code\u003e packages before you finish typing.\u003c/li\u003e\n\u003cli\u003eIt understands your solution structure. In a multi-project solution, \u003ccode\u003edotnet add reference\u003c/code\u003e will show you the actual project files available, not force you to remember paths.\u003c/li\u003e\n\u003cli\u003eNested commands like \u003ccode\u003edotnet tool install\u003c/code\u003e have their own completion contexts. The shell knows when you\u0026rsquo;re specifying a tool name vs. a version vs. a configuration flag.\u003c/li\u003e\n\u003cli\u003eSome completions are context-aware. If you\u0026rsquo;ve already specified \u003ccode\u003e--framework net8.0\u003c/code\u003e, subsequent completions adjust accordingly.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThe old approach—before \u003cstrong\u003e.NET 10\u003c/strong\u003e—worked but had a fundamental performance problem. Every tab press spawned a subprocess running \u003ccode\u003edotnet complete\u003c/code\u003e. That means process initialization overhead, parsing your command context, generating suggestions, serializing results back to PowerShell, then rendering them.\u003c/p\u003e\n\u003cp\u003eI measured this once on a moderately powerful dev machine (Ryzen 7, NVMe SSD, 32GB RAM). Simple completions like \u003ccode\u003edotnet b[Tab]\u003c/code\u003e took 80-120ms. Not terrible, but noticeable. Package completions that needed to query NuGet.org? 400-800ms depending on network latency.\u003c/p\u003e\n\u003cp\u003eThe \u003cstrong\u003e.NET 10\u003c/strong\u003e approach is architecturally different. The completion script that gets written to your \u003ccode\u003e$PROFILE\u003c/code\u003e contains the entire static grammar—every command, subcommand, and standard flag—compiled into shell-native code. Your shell (PowerShell, Bash, Zsh) can evaluate that code instantly because there\u0026rsquo;s no external process. It only invokes \u003ccode\u003edotnet complete\u003c/code\u003e when you hit something dynamic like package names or project file paths. The difference is immediately perceptible. Completions feel instant because most of them actually are instant.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-evolution-from-dynamic-to-native-completion\"\u003e\u003ca href=\"/posts/dotnet-cli-expanding-scope-autocomplete/#the-evolution-from-dynamic-to-native-completion\" title=\"The Evolution: From Dynamic to Native Completion\"\u003eThe Evolution: From Dynamic to Native Completion\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eUnderstanding what changed in \u003cstrong\u003e.NET 10\u003c/strong\u003e gives context to why this matters.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"the-old-way-dynamic-completion-pre-net-10\"\u003e\u003ca href=\"/posts/dotnet-cli-expanding-scope-autocomplete/#the-old-way-dynamic-completion-pre-net-10\" title=\"The Old Way: Dynamic Completion (Pre-.NET 10)\"\u003eThe Old Way: Dynamic Completion (Pre-.NET 10)\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eFor \u003ccode\u003e.NET\u003c/code\u003e versions before 10, if you wanted tab completion, you needed to register an argument completer in your PowerShell profile. The approach was straightforward but had overhead:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-pwsh\" data-lang=\"pwsh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c\"\u003e# The legacy approach that still works\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nb\"\u003eRegister-ArgumentCompleter\u003c/span\u003e \u003cspan class=\"n\"\u003e-Native\u003c/span\u003e \u003cspan class=\"n\"\u003e-CommandName\u003c/span\u003e \u003cspan class=\"n\"\u003edotnet\u003c/span\u003e \u003cspan class=\"n\"\u003e-ScriptBlock\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eparam\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$wordToComplete\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nv\"\u003e$commandAst\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nv\"\u003e$cursorPosition\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003edotnet\u003c/span\u003e \u003cspan class=\"n\"\u003ecomplete\u003c/span\u003e \u003cspan class=\"p\"\u003e-\u003c/span\u003e\u003cspan class=\"n\"\u003e-position\u003c/span\u003e \u003cspan class=\"nv\"\u003e$cursorPosition\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;\u003c/span\u003e\u003cspan class=\"nv\"\u003e$commandAst\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;\u003c/span\u003e \u003cspan class=\"p\"\u003e|\u003c/span\u003e \u003cspan class=\"nb\"\u003eForEach-Object\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"no\"\u003eSystem.Management.Automation.CompletionResult\u003c/span\u003e\u003cspan class=\"p\"\u003e]::\u003c/span\u003e\u003cspan class=\"n\"\u003enew\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$_\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nv\"\u003e$_\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;ParameterValue\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nv\"\u003e$_\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis approach technically worked. But every Tab press meant PowerShell had to invoke \u003ccode\u003edotnet complete\u003c/code\u003e as a subprocess, wait for it to parse your current command, generate completions, and return them. On my old laptop (circa 2019), this sometimes took long enough that I\u0026rsquo;d press Tab, see nothing happen, assume completion wasn\u0026rsquo;t working, and just finish typing manually.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"the-new-way-native-completions-net-10\"\u003e\u003ca href=\"/posts/dotnet-cli-expanding-scope-autocomplete/#the-new-way-native-completions-net-10\" title=\"The New Way: Native Completions (.NET 10\u0026#43;)\"\u003eThe New Way: Native Completions (.NET 10+)\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eEnter \u003cstrong\u003e.NET 10\u003c/strong\u003e. Microsoft introduced the \u003ccode\u003edotnet completions script\u003c/code\u003e command that generates shell-specific completion code. This code:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003eHandles static grammar directly\u003c/strong\u003e in the shell without invoking \u003ccode\u003edotnet complete\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eFalls back intelligently\u003c/strong\u003e only for dynamic content (like NuGet package names)\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eIntegrates natively\u003c/strong\u003e with your shell\u0026rsquo;s completion system\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eDelivers near-instant results\u003c/strong\u003e for common operations\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eThe result? Noticeably faster, smoother completion experience.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-one-liner-that-changes-everything\"\u003e\u003ca href=\"/posts/dotnet-cli-expanding-scope-autocomplete/#the-one-liner-that-changes-everything\" title=\"The One-Liner That Changes Everything\"\u003eThe One-Liner That Changes Everything\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eEverything we\u0026rsquo;ve discussed leads to this single, elegant line:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-pwsh\" data-lang=\"pwsh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003edotnet\u003c/span\u003e \u003cspan class=\"n\"\u003ecompletions\u003c/span\u003e \u003cspan class=\"n\"\u003escript\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026gt;\u0026gt;\u003c/span\u003e \u003cspan class=\"nv\"\u003e$PROFILE\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThat\u0026rsquo;s genuinely all you need. One command, executed once, and you\u0026rsquo;re done. Tab completion works from that moment forward, persisting through every future session.\u003c/p\u003e\n\u003cp\u003eNow, while you could stop here and be perfectly fine, understanding what\u0026rsquo;s actually happening beneath the surface transforms this from \u0026ldquo;magic command\u0026rdquo; to something you can troubleshoot and maintain confidently. So let\u0026rsquo;s break down what\u0026rsquo;s really going on.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"what-this-command-does\"\u003e\u003ca href=\"/posts/dotnet-cli-expanding-scope-autocomplete/#what-this-command-does\" title=\"What This Command Does\"\u003eWhat This Command Does\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eLet\u0026rsquo;s examine each component. First, the command itself:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-pwsh\" data-lang=\"pwsh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003edotnet\u003c/span\u003e \u003cspan class=\"n\"\u003ecompletions\u003c/span\u003e \u003cspan class=\"n\"\u003escript\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis single command tells the \u003ccode\u003e.NET CLI\u003c/code\u003e to generate a completion script for your shell. The \u003ccode\u003edotnet\u003c/code\u003e executable is intelligent about this—it examines your environment, detects which shell you\u0026rsquo;re currently running, and outputs the appropriate completion code for that shell. On Windows systems, it defaults to PowerShell (\u003ccode\u003epwsh\u003c/code\u003e). On Linux or macOS, it checks your environment variables to determine whether you\u0026rsquo;re using Bash, Zsh, Fish, or Nushell, and generates the right script accordingly. This automatic detection removes another friction point—you don\u0026rsquo;t have to tell it which shell you want.\u003c/p\u003e\n\u003cp\u003eThe second part of the equation is the redirection operator:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-pwsh\" data-lang=\"pwsh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u0026gt;\u003c/span\u003e \u003cspan class=\"nv\"\u003e$PROFILE\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe double angle-bracket (\u003ccode\u003e\u0026gt;\u0026gt;\u003c/code\u003e) is PowerShell\u0026rsquo;s append operator. It takes everything the \u003ccode\u003edotnet completions script\u003c/code\u003e command outputs and appends it to your PowerShell profile file. The \u003ccode\u003e$PROFILE\u003c/code\u003e is an automatic variable that PowerShell sets during startup—it points to your current user\u0026rsquo;s current host profile. For most Windows developers, this lives at:\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e$HOME\\Documents\\PowerShell\\Microsoft.PowerShell_profile.ps1\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003eBut the beauty of using \u003ccode\u003e$PROFILE\u003c/code\u003e is that you don\u0026rsquo;t need to know or remember the exact path. PowerShell handles it for you.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"why-this-approach-is-brilliant\"\u003e\u003ca href=\"/posts/dotnet-cli-expanding-scope-autocomplete/#why-this-approach-is-brilliant\" title=\"Why This Approach Is Brilliant\"\u003eWhy This Approach Is Brilliant\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThis one-liner is a masterclass in pragmatic design. It leverages several elegant PowerShell concepts that make it simultaneously powerful and forgiving:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAutomatic environment detection\u003c/strong\u003e means you don\u0026rsquo;t need to tell the \u003ccode\u003edotnet\u003c/code\u003e executable anything about your shell. It figures it out. This eliminates the most common mistake people make with shell configuration—specifying the wrong shell or format. You run one command, and the correct code for your exact environment is generated.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eProfile persistence\u003c/strong\u003e ensures your setup survives across sessions. Unlike configuration that lives only in your current terminal, changes to your profile apply every time you open a new PowerShell window. This is how you move from \u0026ldquo;temporary configuration\u0026rdquo; to \u0026ldquo;permanent improvement.\u0026rdquo;\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eSafe appending\u003c/strong\u003e with the \u003ccode\u003e\u0026gt;\u0026gt;\u003c/code\u003e operator is crucial. This isn\u0026rsquo;t destructive. You\u0026rsquo;re not overwriting your profile—you\u0026rsquo;re adding to it. If you\u0026rsquo;ve already customized your profile with functions, aliases, or other settings, they all remain untouched. The completion script just appends at the end. This means you can run the command multiple times without fear. It\u0026rsquo;s idempotent.\u003c/p\u003e\n\u003cp\u003eThis combination of automatic detection, persistence, and safety is pragmatic design at its best. It removes the annoying steps that plague so many technical setup processes.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"making-it-stick-the-powershell-profile-deep-dive\"\u003e\u003ca href=\"/posts/dotnet-cli-expanding-scope-autocomplete/#making-it-stick-the-powershell-profile-deep-dive\" title=\"Making It Stick: The PowerShell Profile Deep Dive\"\u003eMaking It Stick: The PowerShell Profile Deep Dive\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThis is where the process becomes slightly more involved—but also where understanding matters. Your profile file must exist before you can append to it, and it must actually load when PowerShell starts. Both of these requirements are usually met, but not always. Let\u0026rsquo;s make sure you\u0026rsquo;re covered.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"verifying-your-profile-path\"\u003e\u003ca href=\"/posts/dotnet-cli-expanding-scope-autocomplete/#verifying-your-profile-path\" title=\"Verifying Your Profile Path\"\u003eVerifying Your Profile Path\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eFirst, see where PowerShell thinks your profile lives:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-pwsh\" data-lang=\"pwsh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nv\"\u003e$PROFILE\u003c/span\u003e \u003cspan class=\"p\"\u003e|\u003c/span\u003e \u003cspan class=\"nb\"\u003eSelect-Object\u003c/span\u003e \u003cspan class=\"p\"\u003e*\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis will show you all available profile paths. The one labeled \u003ccode\u003eMicrosoft.PowerShell_profile.ps1\u003c/code\u003e in your Documents folder is what we care about.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"creating-your-profile-if-it-doesnt-exist\"\u003e\u003ca href=\"/posts/dotnet-cli-expanding-scope-autocomplete/#creating-your-profile-if-it-doesnt-exist\" title=\"Creating Your Profile If It Doesn\u0026rsquo;t Exist\"\u003eCreating Your Profile If It Doesn\u0026rsquo;t Exist\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003ePowerShell doesn\u0026rsquo;t create your profile automatically. If you\u0026rsquo;ve never customized PowerShell before, you might not have one. Create it like this:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-pwsh\" data-lang=\"pwsh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(!(\u003c/span\u003e\u003cspan class=\"nb\"\u003eTest-Path\u003c/span\u003e \u003cspan class=\"n\"\u003e-Path\u003c/span\u003e \u003cspan class=\"nv\"\u003e$PROFILE\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nb\"\u003eNew-Item\u003c/span\u003e \u003cspan class=\"n\"\u003e-ItemType\u003c/span\u003e \u003cspan class=\"n\"\u003eFile\u003c/span\u003e \u003cspan class=\"n\"\u003e-Path\u003c/span\u003e \u003cspan class=\"nv\"\u003e$PROFILE\u003c/span\u003e \u003cspan class=\"n\"\u003e-Force\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis is defensive: if the profile doesn\u0026rsquo;t exist, create it. If it does, do nothing.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"actually-adding-tab-completion\"\u003e\u003ca href=\"/posts/dotnet-cli-expanding-scope-autocomplete/#actually-adding-tab-completion\" title=\"Actually Adding Tab Completion\"\u003eActually Adding Tab Completion\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eNow that you know your profile exists, add the completion:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-pwsh\" data-lang=\"pwsh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003edotnet\u003c/span\u003e \u003cspan class=\"n\"\u003ecompletions\u003c/span\u003e \u003cspan class=\"n\"\u003escript\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026gt;\u0026gt;\u003c/span\u003e \u003cspan class=\"nv\"\u003e$PROFILE\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\n\n\n\u003ch3 id=\"activating-it-in-your-current-session\"\u003e\u003ca href=\"/posts/dotnet-cli-expanding-scope-autocomplete/#activating-it-in-your-current-session\" title=\"Activating It in Your Current Session\"\u003eActivating It in Your Current Session\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThe profile runs automatically when you open a new PowerShell window. But if you want tab completion right now in your current session, reload the profile:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-pwsh\" data-lang=\"pwsh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e \u003cspan class=\"nv\"\u003e$PROFILE\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe dot (\u003ccode\u003e.\u003c/code\u003e) is PowerShell\u0026rsquo;s dot-sourcing operator. It executes the profile file in the current session\u0026rsquo;s scope, making all its contents immediately available.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"testing-your-setup\"\u003e\u003ca href=\"/posts/dotnet-cli-expanding-scope-autocomplete/#testing-your-setup\" title=\"Testing Your Setup\"\u003eTesting Your Setup\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eAfter you\u0026rsquo;ve run the setup, test it immediately. Don\u0026rsquo;t trust that it worked—verify it. Open PowerShell and type:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-pwsh\" data-lang=\"pwsh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003edotnet\u003c/span\u003e \u003cspan class=\"n\"\u003ea\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"no\"\u003eTab\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eIf it\u0026rsquo;s working, you\u0026rsquo;ll see \u003ccode\u003eadd\u003c/code\u003e appear instantly. Press Tab again and you\u0026rsquo;ll cycle through \u003ccode\u003eanalyze\u003c/code\u003e and any other \u0026lsquo;a\u0026rsquo; commands. That\u0026rsquo;s the static completion engine—your shell already knows those commands exist.\u003c/p\u003e\n\u003cp\u003eIf nothing happens, something\u0026rsquo;s wrong. Don\u0026rsquo;t waste time wondering why. Jump to the troubleshooting section below.\u003c/p\u003e\n\u003cp\u003eNow let\u0026rsquo;s test something more complex that exercises the dynamic side of completion:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-pwsh\" data-lang=\"pwsh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003edotnet\u003c/span\u003e \u003cspan class=\"n\"\u003eadd\u003c/span\u003e \u003cspan class=\"n\"\u003epackage\u003c/span\u003e \u003cspan class=\"n\"\u003eMicro\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"no\"\u003eTab\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eType this exactly and press Tab. Within a moment, you\u0026rsquo;ll see suggestions for NuGet packages starting with \u0026ldquo;Micro\u0026rdquo;—\u003ccode\u003eMicrosoft.AspNetCore.App\u003c/code\u003e, \u003ccode\u003eMicrosoft.Extensions.Logging\u003c/code\u003e, and dozens of others. This is where the hybrid approach really shines. Your shell instantly handles the known parts (\u003ccode\u003edotnet add package\u003c/code\u003e), then intelligently queries NuGet.org for available packages that match your prefix. You see results without any delay, yet they\u0026rsquo;re genuinely dynamic and current.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"understanding-completion-modes-why-hybrid-matters\"\u003e\u003ca href=\"/posts/dotnet-cli-expanding-scope-autocomplete/#understanding-completion-modes-why-hybrid-matters\" title=\"Understanding Completion Modes: Why Hybrid Matters\"\u003eUnderstanding Completion Modes: Why Hybrid Matters\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eMicrosoft\u0026rsquo;s documentation distinguishes between two different completion strategies, and understanding this distinction helps you appreciate why \u003cstrong\u003e.NET 10\u003c/strong\u003e is such a significant upgrade.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"hybrid-completion-the-net-10-standard\"\u003e\u003ca href=\"/posts/dotnet-cli-expanding-scope-autocomplete/#hybrid-completion-the-net-10-standard\" title=\"Hybrid Completion: The .NET 10 Standard\"\u003eHybrid Completion: The \u003cstrong\u003e.NET 10\u003c/strong\u003e Standard\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eHybrid completion is what you get when using the native completion scripts in \u003cstrong\u003e.NET 10\u003c/strong\u003e or later with PowerShell, Bash, or Zsh. The strategy is elegantly split:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003eStatic grammar is handled directly\u003c/strong\u003e by shell code that was generated specifically for your shell. The shell already knows about all the \u003ccode\u003e.NET CLI\u003c/code\u003e commands, subcommands, and standard flags. This runs instantly, without any external process.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eDynamic content\u003c/strong\u003e triggers \u003ccode\u003edotnet complete\u003c/code\u003e only when necessary. Package names, project files, and context-specific values are fetched on demand, but only when you actually need them.\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eThis hybrid architecture is why completion feels so responsive. I compared it directly: on the same machine, the old \u003ccode\u003eRegister-ArgumentCompleter\u003c/code\u003e approach took 95ms average for static completions. Native completion? 8ms. That\u0026rsquo;s over 10x faster, and you feel it.\u003c/p\u003e\n\u003cp\u003eFor dynamic package completions, both approaches need to query NuGet, so they\u0026rsquo;re roughly equivalent (around 500ms depending on your network). But the difference is that 90% of your completions are static. Microsoft clearly spent time optimizing where it matters most.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"dynamic-completion-the-legacy-approach\"\u003e\u003ca href=\"/posts/dotnet-cli-expanding-scope-autocomplete/#dynamic-completion-the-legacy-approach\" title=\"Dynamic Completion: The Legacy Approach\"\u003eDynamic Completion: The Legacy Approach\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eIf you\u0026rsquo;re running \u003ccode\u003e.NET 9\u003c/code\u003e or earlier, or if you\u0026rsquo;ve configured completion using the older registration method, every single completion request—even for static commands—invokes the \u003ccode\u003edotnet complete\u003c/code\u003e command in a subprocess. This approach works, absolutely. But it\u0026rsquo;s noticeably slower. You press Tab and wait for a process to start, execute, and return results. For simple completions, the wait is usually acceptable. But for comprehensive, package-aware completions, many developers notice the latency.\u003c/p\u003e\n\u003cp\u003eThis is why upgrading to \u003cstrong\u003e.NET 10\u003c/strong\u003e and enabling native completion is worth doing. You\u0026rsquo;re not just getting a feature—you\u0026rsquo;re getting a more responsive development experience.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"when-should-you-enable-this\"\u003e\u003ca href=\"/posts/dotnet-cli-expanding-scope-autocomplete/#when-should-you-enable-this\" title=\"When Should You Enable This?\"\u003eWhen Should You Enable This?\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe honest answer: immediately. There is genuinely no downside to enabling tab completion for the \u003ccode\u003e.NET CLI\u003c/code\u003e. It\u0026rsquo;s pure upside—faster work, fewer mistakes, better exploration of available commands—with zero risk and minimal setup.\u003c/p\u003e\n\u003cp\u003eYou\u0026rsquo;ll see particularly significant benefits if you:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eCreate projects frequently\u003c/strong\u003e using \u003ccode\u003edotnet new\u003c/code\u003e. Template completion means you stop guessing at template names and let your shell guide you through available options. This is especially valuable when you need templates for specific purposes and can\u0026rsquo;t quite remember the exact name.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eManage NuGet dependencies regularly\u003c/strong\u003e with \u003ccode\u003edotnet add package\u003c/code\u003e. Package completion transforms this from \u0026ldquo;Hunt for the right package on NuGet.org, copy the name, paste it in the terminal\u0026rdquo; to \u0026ldquo;Type a prefix and tab through suggestions.\u0026rdquo;\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eWork with multiple solutions and projects\u003c/strong\u003e where \u003ccode\u003edotnet publish\u003c/code\u003e, \u003ccode\u003edotnet pack\u003c/code\u003e, and similar commands need project file context. Your shell becomes aware of your actual project structure and can complete project paths intelligently.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eUse complex build or publish profiles\u003c/strong\u003e where remembering the exact configuration names and publish targets becomes tedious. Let completion handle the recall.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eWant to explore CLI capabilities\u003c/strong\u003e beyond the commands you use regularly. Completion surfaces available subcommands and options, making it easier to discover capabilities you didn\u0026rsquo;t know existed. How many developers skip learning some feature simply because they didn\u0026rsquo;t know it was available?\u003c/p\u003e\n\u003cp\u003eEven if you\u0026rsquo;re an IDE enthusiast who spends most time in Visual Studio rather than the terminal, tab completion removes a psychological barrier to shell adoption. Knowing the tool will help you remember what you need makes you more willing to use it. That\u0026rsquo;s a genuine quality-of-life improvement.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"when-things-dont-work-troubleshooting\"\u003e\u003ca href=\"/posts/dotnet-cli-expanding-scope-autocomplete/#when-things-dont-work-troubleshooting\" title=\"When Things Don\u0026rsquo;t Work: Troubleshooting\"\u003eWhen Things Don\u0026rsquo;t Work: Troubleshooting\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eMost of the time this works on the first try. But I\u0026rsquo;ve helped enough people set this up to know the failure modes. Here\u0026rsquo;s what actually breaks and how to fix it.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"your-profile-exists-but-isnt-loading\"\u003e\u003ca href=\"/posts/dotnet-cli-expanding-scope-autocomplete/#your-profile-exists-but-isnt-loading\" title=\"Your Profile Exists But Isn\u0026rsquo;t Loading\"\u003eYour Profile Exists But Isn\u0026rsquo;t Loading\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThe most common issue is that your profile file exists, but PowerShell isn\u0026rsquo;t executing it. This usually indicates an execution policy problem. Check what policy is currently set:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-pwsh\" data-lang=\"pwsh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nb\"\u003eGet-ExecutionPolicy\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eIf this returns \u003ccode\u003eRestricted\u003c/code\u003e, PowerShell refuses to run any scripts, including your profile. This is the default on many systems. You need to change it. The recommended setting is \u003ccode\u003eRemoteSigned\u003c/code\u003e, which allows scripts you created locally to run while blocking scripts downloaded from the internet—a good security balance:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-pwsh\" data-lang=\"pwsh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nb\"\u003eSet-ExecutionPolicy\u003c/span\u003e \u003cspan class=\"n\"\u003e-ExecutionPolicy\u003c/span\u003e \u003cspan class=\"n\"\u003eRemoteSigned\u003c/span\u003e \u003cspan class=\"n\"\u003e-Scope\u003c/span\u003e \u003cspan class=\"n\"\u003eCurrentUser\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis sets the policy for your user account specifically, without requiring administrative elevation. Once you\u0026rsquo;ve run this, your profile will load and execute automatically.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"completion-still-isnt-working-after-setup\"\u003e\u003ca href=\"/posts/dotnet-cli-expanding-scope-autocomplete/#completion-still-isnt-working-after-setup\" title=\"Completion Still Isn\u0026rsquo;t Working After Setup\"\u003eCompletion Still Isn\u0026rsquo;t Working After Setup\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eYour profile loaded, but tab completion still doesn\u0026rsquo;t appear when you try it. Walk through this checklist:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eVerify you have \u003cstrong\u003e.NET 10\u003c/strong\u003e or later installed:\u003c/strong\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-pwsh\" data-lang=\"pwsh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003edotnet\u003c/span\u003e \u003cspan class=\"p\"\u003e-\u003c/span\u003e\u003cspan class=\"n\"\u003e-version\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eNative completion scripts only exist in \u003ccode\u003e.NET 10+\u003c/code\u003e. If you\u0026rsquo;re on \u003ccode\u003e.NET 9\u003c/code\u003e or earlier, the command won\u0026rsquo;t generate anything.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eConfirm the completion script was actually appended to your profile:\u003c/strong\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-pwsh\" data-lang=\"pwsh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nb\"\u003eGet-Content\u003c/span\u003e \u003cspan class=\"nv\"\u003e$PROFILE\u003c/span\u003e \u003cspan class=\"p\"\u003e|\u003c/span\u003e \u003cspan class=\"nb\"\u003eSelect-String\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;dotnet completions\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis checks whether the profile file contains the completion script. If it returns nothing, the append didn\u0026rsquo;t work. Check that your profile path is accessible and writable.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eReload your profile or open a fresh terminal:\u003c/strong\u003e\nIf the script is there but completion doesn\u0026rsquo;t work, your current session hasn\u0026rsquo;t loaded it yet. Run \u003ccode\u003e. $PROFILE\u003c/code\u003e to reload, or simply close and reopen PowerShell.\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\n\n\n\n\u003ch3 id=\"completion-feels-slow\"\u003e\u003ca href=\"/posts/dotnet-cli-expanding-scope-autocomplete/#completion-feels-slow\" title=\"Completion Feels Slow\"\u003eCompletion Feels Slow\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eIf you notice completion taking a second or two to respond, especially on complex queries, you\u0026rsquo;re likely experiencing the dynamic fallback in action. When you request NuGet package suggestions or other context-dependent completions, PowerShell invokes \u003ccode\u003edotnet complete\u003c/code\u003e in the background. This is expected behavior, not a bug. The hybrid approach minimizes this latency for static completions, but truly dynamic data sometimes requires a moment. For most users, the responsiveness improvement over pre-.NET 10 completion is still significant.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-clis-evolving-scope-context-matters\"\u003e\u003ca href=\"/posts/dotnet-cli-expanding-scope-autocomplete/#the-clis-evolving-scope-context-matters\" title=\"The CLI\u0026rsquo;s Evolving Scope: Context Matters\"\u003eThe CLI\u0026rsquo;s Evolving Scope: Context Matters\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eTab completion might seem like a small feature, but it\u0026rsquo;s actually a signal of something larger happening with the \u003ccode\u003e.NET CLI\u003c/code\u003e. The tool has evolved dramatically. Consider what you can accomplish from the command line today:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eNative shell completions\u003c/strong\u003e now exist for PowerShell, Bash, Zsh, Fish, and Nushell—recognizing that .NET developers work across different operating systems and shells.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eWorkload management\u003c/strong\u003e lets you install and manage platform-specific tools directly through the CLI—Swift for iOS development, NDK tooling, emulators.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eGlobal tools and tool manifests\u003c/strong\u003e turn your development environment into a versioned, reproducible collection of utilities that travel with your projects.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eSolution-level operations and dependency management\u003c/strong\u003e mean the CLI understands your entire solution structure, not just individual projects.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eBuilt-in diagnostics and observability\u003c/strong\u003e help you understand what\u0026rsquo;s happening under the hood—from environment information to detailed build diagnostics.\u003c/p\u003e\n\u003cp\u003eThe journey from early \u003ccode\u003e.NET Core\u003c/code\u003e days to now is remarkable. The CLI started as a basic project builder. It\u0026rsquo;s now a sophisticated platform with growing capabilities. Tab completion, in this context, is Microsoft making a statement: we\u0026rsquo;ve built something complex and powerful, and we\u0026rsquo;re committed to making it accessible. You shouldn\u0026rsquo;t need to memorize obscure syntax or hunt through documentation for common operations. The tool should guide you. Completion is that commitment made practical.\u003c/p\u003e\n\u003cp\u003eFor developers who live at the command line, this kind of incremental thoughtfulness adds up. It\u0026rsquo;s not revolutionary. But it\u0026rsquo;s genuine progress.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-lazy-developers-summary\"\u003e\u003ca href=\"/posts/dotnet-cli-expanding-scope-autocomplete/#the-lazy-developers-summary\" title=\"The Lazy Developer\u0026rsquo;s Summary\"\u003eThe Lazy Developer\u0026rsquo;s Summary\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eWant the fastest path to productivity? Here\u0026rsquo;s the checklist:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003eMake sure your PowerShell profile exists:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-pwsh\" data-lang=\"pwsh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(!(\u003c/span\u003e\u003cspan class=\"nb\"\u003eTest-Path\u003c/span\u003e \u003cspan class=\"n\"\u003e-Path\u003c/span\u003e \u003cspan class=\"nv\"\u003e$PROFILE\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nb\"\u003eNew-Item\u003c/span\u003e \u003cspan class=\"n\"\u003e-ItemType\u003c/span\u003e \u003cspan class=\"n\"\u003eFile\u003c/span\u003e \u003cspan class=\"n\"\u003e-Path\u003c/span\u003e \u003cspan class=\"nv\"\u003e$PROFILE\u003c/span\u003e \u003cspan class=\"n\"\u003e-Force\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003eAdd the completion script:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-pwsh\" data-lang=\"pwsh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003edotnet\u003c/span\u003e \u003cspan class=\"n\"\u003ecompletions\u003c/span\u003e \u003cspan class=\"n\"\u003escript\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026gt;\u0026gt;\u003c/span\u003e \u003cspan class=\"nv\"\u003e$PROFILE\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003eReload in your current session:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-pwsh\" data-lang=\"pwsh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e \u003cspan class=\"nv\"\u003e$PROFILE\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003eVerify it works:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-pwsh\" data-lang=\"pwsh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003edotnet\u003c/span\u003e \u003cspan class=\"n\"\u003ea\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"no\"\u003eTab\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eThe beauty of this approach is that it\u0026rsquo;s idempotent. You can run the second command multiple times; it just appends to your profile. It\u0026rsquo;s not elegant, but it\u0026rsquo;s practical.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"final-thoughts-the-compound-effect-of-small-improvements\"\u003e\u003ca href=\"/posts/dotnet-cli-expanding-scope-autocomplete/#final-thoughts-the-compound-effect-of-small-improvements\" title=\"Final Thoughts: The Compound Effect of Small Improvements\"\u003eFinal Thoughts: The Compound Effect of Small Improvements\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eLook, I get it. Tab completion sounds trivial. \u0026ldquo;Just learn the commands\u0026rdquo; or \u0026ldquo;use the IDE\u0026rdquo; or whatever. I\u0026rsquo;ve heard all the dismissive responses.\u003c/p\u003e\n\u003cp\u003eBut here\u0026rsquo;s what actually happened after I enabled this: I stopped avoiding the CLI. Before, I\u0026rsquo;d often reach for Visual Studio\u0026rsquo;s NuGet manager or the solution explorer because I didn\u0026rsquo;t want to fight with command syntax. Now I stay in the terminal because it\u0026rsquo;s genuinely faster than switching contexts.\u003c/p\u003e\n\u003cp\u003eLast week I added twelve NuGet packages across four projects. Total time: maybe two minutes. Six months ago that would\u0026rsquo;ve been ten minutes minimum—switching to VS, waiting for NuGet to load, searching, selecting versions, clicking Install, waiting for restore.\u003c/p\u003e\n\u003cp\u003eThe time savings are real (I tracked 52 minutes weekly, remember), but the bigger win is staying in flow. Every context switch costs you focus. Every moment you spend hunting for syntax is a moment you\u0026rsquo;re not solving the actual problem.\u003c/p\u003e\n\u003cp\u003eThe \u003ccode\u003e.NET CLI\u003c/code\u003e has become a genuinely sophisticated tool—powerful enough to accomplish real work from the command line, yet complex enough that most developers never explore its full capabilities. Native tab completion in \u003cstrong\u003e.NET 10\u003c/strong\u003e is the accessibility layer that makes that power usable without constant cognitive overhead. It\u0026rsquo;s not flashy. It\u0026rsquo;s not revolutionary. But it\u0026rsquo;s the kind of thoughtful engineering that separates tools you tolerate from tools you actually \u003cem\u003eenjoy\u003c/em\u003e using.\u003c/p\u003e\n\u003cp\u003eThe setup takes ninety seconds. I timed it. Actually, I\u0026rsquo;ve timed it on six different machines now helping colleagues set this up. Longest was two minutes because one person had a permissions issue with their profile directory.\u003c/p\u003e\n\u003cp\u003eDo it now. Seriously—stop reading, open PowerShell, run the three commands in the summary below, test it with \u003ccode\u003edotnet a[Tab]\u003c/code\u003e. If it works, you just saved yourself dozens of hours over the next year. If it doesn\u0026rsquo;t work, the troubleshooting section will get you sorted.\u003c/p\u003e\n\u003cp\u003eI\u0026rsquo;ve been using this daily since November 2024. It\u0026rsquo;s one of those rare features that actually lives up to the promise. No gotchas, no edge cases where it breaks, just consistent quality-of-life improvement every single time I touch the CLI.\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2025-12-18T17:00:00+01:00","id":"https://daily-devops.net/posts/dotnet-cli-expanding-scope-autocomplete/","language":"en","summary":".NET 10 ships native tab completion for the dotnet CLI. One command, no Register-ArgumentCompleter snippets, and your shell finally remembers.","tags":["cli","dotnet","bestpractices","devops","softwareengineering"],"title":"Stop Typing: The .NET CLI Tab Completion You've Been Missing","url":"https://daily-devops.net/posts/dotnet-cli-expanding-scope-autocomplete/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eSelecting a job scheduler is selecting an operational philosophy. The choice determines how your team thinks about background processing, what operational burdens you accept, and how your system scales as workloads grow. I\u0026rsquo;ve seen teams pick Quartz.NET for an MVP because \u003cem\u003e\u0026ldquo;we might need clustering eventually\u0026rdquo;,\u003c/em\u003e then spend three months fighting its complexity instead of shipping features.\u003c/p\u003e\n\u003cp\u003eA framework that simplifies development today might impose constraints tomorrow when throughput demands clustering or when job durability becomes non-negotiable. Conversely, adopting enterprise-grade features prematurely introduces complexity that slows iteration and increases onboarding friction.\u003c/p\u003e\n\u003cp\u003eThis article synthesizes the series into comparative analysis. It presents feature matrices, rates framework suitability across operational dimensions, and offers decision heuristics grounded in system maturity, infrastructure realities, and team capabilities. By the end, you\u0026rsquo;ll have a structured approach to selecting the scheduler that aligns with your needs—not the one with the most stars on GitHub.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"feature-matrix-what-each-framework-provides\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-7-comparative-review/#feature-matrix-what-each-framework-provides\" title=\"Feature Matrix: What Each Framework Provides\"\u003eFeature Matrix: What Each Framework Provides\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe table below compares core capabilities across the five frameworks:\u003c/p\u003e\n\u003ctable class=\"striped\"\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003eFeature\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eHangfire\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eQuartz.NET\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eCoravel\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eNCronJob\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eTickerQ\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003ePersistence\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eSQL/Redis\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eSQL/Memory\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eIn-memory\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eIn-memory\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eEF Core\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eClustering\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eOptional\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eYes\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNo\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNo\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eYes\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eDashboard\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eYes\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNo\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNo\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNo\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eYes (SignalR)\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eAutomatic Retries\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eYes\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eCustom\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eManual\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eManual\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eYes\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eCron Expressions\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eYes\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eYes\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eYes\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eYes\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eYes\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eJob Calendars\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNo\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eYes\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNo\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNo\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNo\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eDependency Injection\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eYes\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eYes\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eYes\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eYes\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eYes\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eAsync-First\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003ePartial\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eYes\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eYes\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eYes\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eYes\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eSource Generation\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNo\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNo\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNo\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNo\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eYes\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eQueue Support\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eYes\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNo\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eYes\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNo\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNo\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eBatch Jobs\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003ePro only\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eCustom\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNo\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNo\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eYes\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eReal-Time Monitoring\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003ePolling\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eCustom\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eLogs\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eLogs\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eSignalR\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eExternal Dependencies\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eDatabase\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eDatabase\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNone\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNone\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eDatabase\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eMaturity (Years)\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e13+\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e20+\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e6+\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e2+\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e2+\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eThis matrix reveals trade-offs. Hangfire and Quartz.NET offer persistence and clustering but require databases. Coravel and NCronJob eliminate dependencies but sacrifice durability. TickerQ modernizes the stack with source generation and SignalR but lacks ecosystem maturity.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"suitability-ratings-across-dimensions\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-7-comparative-review/#suitability-ratings-across-dimensions\" title=\"Suitability Ratings Across Dimensions\"\u003eSuitability Ratings Across Dimensions\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe following ratings (1-5, where 5 is best) assess each framework across operational dimensions:\u003c/p\u003e\n\u003ctable class=\"striped\"\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003eDimension\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eHangfire\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eQuartz.NET\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eCoravel\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eNCronJob\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eTickerQ\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eSimplicity\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e4\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e2\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e5\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e5\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e3\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003ePersistence\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e5\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e5\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e1\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e1\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e5\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eScalability\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e4\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e5\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e1\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e1\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e4\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eObservability\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e5\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e3\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e2\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e2\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e5\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eDeveloper Experience\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e4\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e3\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e5\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e5\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e4\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eOperational Maturity\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e5\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e5\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e4\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e3\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e3\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003ePerformance\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e4\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e4\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e5\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e5\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e5\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eFlexibility\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e4\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e5\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e3\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e2\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e4\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e\u003cstrong\u003eSimplicity\u003c/strong\u003e: NCronJob and Coravel score highest—zero dependencies, minimal configuration. Quartz.NET scores lowest due to its steep learning curve.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003ePersistence\u003c/strong\u003e: Hangfire, Quartz.NET, and TickerQ provide database-backed durability. Coravel and NCronJob don\u0026rsquo;t.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eScalability\u003c/strong\u003e: Quartz.NET excels with robust clustering. Hangfire supports it but with limitations. Coravel and NCronJob don\u0026rsquo;t coordinate across instances.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eObservability\u003c/strong\u003e: Hangfire and TickerQ provide built-in dashboards. Quartz.NET requires custom listeners. Coravel and NCronJob rely on logging.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eDeveloper Experience\u003c/strong\u003e: Coravel and NCronJob prioritize fluent APIs and rapid integration. Quartz.NET\u0026rsquo;s complexity detracts from velocity.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eOperational Maturity\u003c/strong\u003e: Hangfire and Quartz.NET have extensive production validation. TickerQ and NCronJob are newer with smaller communities.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003ePerformance\u003c/strong\u003e: In-memory frameworks (NCronJob, Coravel) and TickerQ\u0026rsquo;s reflection-free design excel. Database-backed frameworks introduce latency.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eFlexibility\u003c/strong\u003e: Quartz.NET\u0026rsquo;s advanced features (calendars, misfires) offer unmatched control. NCronJob\u0026rsquo;s minimalism limits customization.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"decision-heuristics-matching-framework-to-context\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-7-comparative-review/#decision-heuristics-matching-framework-to-context\" title=\"Decision Heuristics: Matching Framework to Context\"\u003eDecision Heuristics: Matching Framework to Context\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eSelecting a scheduler requires evaluating your system\u0026rsquo;s operational profile across several axes:\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"system-maturity-and-workload-characteristics\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-7-comparative-review/#system-maturity-and-workload-characteristics\" title=\"System Maturity and Workload Characteristics\"\u003eSystem Maturity and Workload Characteristics\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003eEarly-stage startups or MVPs\u003c/strong\u003e: Prioritize speed. Use \u003cstrong\u003eCoravel\u003c/strong\u003e or \u003cstrong\u003eNCronJob\u003c/strong\u003e to eliminate infrastructure overhead and accelerate feature delivery. Jobs are likely transient (cache warming, health checks), making persistence unnecessary. As the product matures, migrate to Hangfire or TickerQ if durability becomes critical.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eGrowing applications with modest throughput\u003c/strong\u003e: Use \u003cstrong\u003eHangfire\u003c/strong\u003e. Its persistence ensures reliability, dashboards provide visibility, and automatic retries reduce operational burden. It scales vertically (more workers per server) and horizontally (multiple servers with optional clustering) as workloads grow. Suitable for web applications processing hundreds to thousands of jobs per minute.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eEnterprise systems with complex scheduling\u003c/strong\u003e: Use \u003cstrong\u003eQuartz.NET\u003c/strong\u003e. Its job calendars, misfire policies, and clustering support demanding workflows—financial batch processing, regulatory reporting, multi-tenant SaaS platforms. Operational complexity is justified by requirements Hangfire can\u0026rsquo;t meet: business day logic, priority-based execution, multi-datacenter coordination.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eCloud-native microservices\u003c/strong\u003e: Use \u003cstrong\u003eNCronJob\u003c/strong\u003e. Its stateless design fits containerized deployments where ephemeral pods start, execute tasks, and terminate. Jobs should be idempotent to tolerate duplication across horizontal replicas. For critical workflows requiring persistence, use \u003cstrong\u003eTickerQ\u003c/strong\u003e integrated with your existing Entity Framework Core infrastructure.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003ePerformance-sensitive systems\u003c/strong\u003e: Use \u003cstrong\u003eTickerQ\u003c/strong\u003e. Source generation eliminates reflection overhead, async-first design maximizes throughput, and real-time monitoring via SignalR reduces operational latency. Ideal for SaaS platforms processing tens of thousands of jobs daily where every millisecond compounds across volume.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"infrastructure-constraints\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-7-comparative-review/#infrastructure-constraints\" title=\"Infrastructure Constraints\"\u003eInfrastructure Constraints\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003eNo database available\u003c/strong\u003e: Use \u003cstrong\u003eCoravel\u003c/strong\u003e or \u003cstrong\u003eNCronJob\u003c/strong\u003e. Both run in-memory without external dependencies, fitting serverless functions, edge devices, or cost-constrained environments.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eSQL Server or PostgreSQL in use\u003c/strong\u003e: Use \u003cstrong\u003eHangfire\u003c/strong\u003e or \u003cstrong\u003eTickerQ\u003c/strong\u003e. Both integrate seamlessly with relational databases. Hangfire offers more storage backend options (MySQL, MongoDB); TickerQ requires Entity Framework Core.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eRedis for caching\u003c/strong\u003e: Consider \u003cstrong\u003eHangfire with Redis storage\u003c/strong\u003e. It reduces database load and leverages existing infrastructure. Quartz.NET also supports Redis but requires more configuration.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eKubernetes or containerized deployments\u003c/strong\u003e: \u003cstrong\u003eNCronJob\u003c/strong\u003e fits naturally. For workflows requiring persistence, \u003cstrong\u003eTickerQ\u003c/strong\u003e works if you provision managed databases (Azure SQL, Amazon RDS). \u003cstrong\u003eHangfire\u003c/strong\u003e also fits but adds database management overhead.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"team-priorities-and-constraints\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-7-comparative-review/#team-priorities-and-constraints\" title=\"Team Priorities and Constraints\"\u003eTeam Priorities and Constraints\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003eDeveloper velocity is paramount\u003c/strong\u003e: Use \u003cstrong\u003eCoravel\u003c/strong\u003e or \u003cstrong\u003eNCronJob\u003c/strong\u003e. Minimal configuration, fluent APIs, and zero operational overhead accelerate delivery. Ideal for small teams or solo developers.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eOperational reliability is critical\u003c/strong\u003e: Use \u003cstrong\u003eHangfire\u003c/strong\u003e or \u003cstrong\u003eQuartz.NET\u003c/strong\u003e. Persistence, retries, and observability reduce risk of silent failures. Suitable for teams managing production systems where background jobs impact business operations.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eModern tooling and patterns preferred\u003c/strong\u003e: Use \u003cstrong\u003eTickerQ\u003c/strong\u003e. Source generation, SignalR, and Entity Framework Core integration appeal to teams comfortable with current .NET conventions. The learning curve is moderate but rewarding for performance-sensitive systems.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eLegacy system maintenance\u003c/strong\u003e: Use \u003cstrong\u003eQuartz.NET\u003c/strong\u003e or \u003cstrong\u003eHangfire\u003c/strong\u003e. Both support .NET Framework, have extensive documentation, and integrate with older application architectures. TickerQ and NCronJob target modern .NET (6+).\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"scaling-and-operational-concerns\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-7-comparative-review/#scaling-and-operational-concerns\" title=\"Scaling and Operational Concerns\"\u003eScaling and Operational Concerns\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003eSingle instance, no scaling planned\u003c/strong\u003e: \u003cstrong\u003eCoravel\u003c/strong\u003e, \u003cstrong\u003eNCronJob\u003c/strong\u003e, or \u003cstrong\u003eHangfire\u003c/strong\u003e (without clustering) suffice. Persistence depends on job criticality—Hangfire if durability matters, Coravel/NCronJob if not.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eHorizontal scaling with job coordination\u003c/strong\u003e: Use \u003cstrong\u003eQuartz.NET\u003c/strong\u003e (robust clustering) or \u003cstrong\u003eHangfire\u003c/strong\u003e (polling-based coordination). TickerQ supports clustering via Entity Framework Core optimistic concurrency but is less battle-tested at scale.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eHigh throughput (tens of thousands of jobs/min)\u003c/strong\u003e: Use \u003cstrong\u003eQuartz.NET\u003c/strong\u003e with Redis or \u003cstrong\u003eTickerQ\u003c/strong\u003e. Hangfire\u0026rsquo;s polling introduces latency at extreme volumes. NCronJob and Coravel lack coordination mechanisms for distributed workloads.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eMulti-region or geo-distributed\u003c/strong\u003e: Use \u003cstrong\u003eQuartz.NET\u003c/strong\u003e. Its clustering supports multiple datacenters with database replication. Hangfire can work but requires careful tuning. TickerQ\u0026rsquo;s youth makes it less proven in multi-region scenarios.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"practical-selection-framework\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-7-comparative-review/#practical-selection-framework\" title=\"Practical Selection Framework\"\u003ePractical Selection Framework\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eUse this decision tree to narrow choices:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eDo jobs need to survive application restarts?\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eNo\u003c/strong\u003e: Consider \u003cstrong\u003eCoravel\u003c/strong\u003e or \u003cstrong\u003eNCronJob\u003c/strong\u003e.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eYes\u003c/strong\u003e: Proceed to step 2.\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eWill you run multiple instances requiring coordination?\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eNo\u003c/strong\u003e: Use \u003cstrong\u003eHangfire\u003c/strong\u003e (simple persistence, good observability).\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eYes\u003c/strong\u003e: Proceed to step 3.\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eDo you need advanced scheduling (calendars, misfires, priorities)?\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eNo\u003c/strong\u003e: Use \u003cstrong\u003eHangfire\u003c/strong\u003e (simpler than Quartz.NET, adequate clustering).\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eYes\u003c/strong\u003e: Use \u003cstrong\u003eQuartz.NET\u003c/strong\u003e (enterprise-grade features justify complexity).\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eIs performance (reflection-free, async-first) a top priority?\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eYes, and you use Entity Framework Core\u003c/strong\u003e: Consider \u003cstrong\u003eTickerQ\u003c/strong\u003e (modern architecture, real-time monitoring).\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eYes, but no database\u003c/strong\u003e: Use \u003cstrong\u003eNCronJob\u003c/strong\u003e (minimal overhead, stateless).\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eNo\u003c/strong\u003e: Stick with \u003cstrong\u003eHangfire\u003c/strong\u003e or \u003cstrong\u003eQuartz.NET\u003c/strong\u003e based on feature needs.\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eDoes your team value developer velocity over advanced features?\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eYes\u003c/strong\u003e: Use \u003cstrong\u003eCoravel\u003c/strong\u003e (fluent API, integrated queuing/caching/mailing).\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eNo\u003c/strong\u003e: Select based on operational requirements (Hangfire for balance, Quartz.NET for control, TickerQ for modern tooling).\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\n\n\n\n\u003ch2 id=\"real-world-scenarios-and-recommendations\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-7-comparative-review/#real-world-scenarios-and-recommendations\" title=\"Real-World Scenarios and Recommendations\"\u003eReal-World Scenarios and Recommendations\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003eScenario 1: E-commerce platform processing order fulfillment workflows\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eNeeds\u003c/strong\u003e: Persistence (orders must complete), retries (external APIs fail), observability (track order states).\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eScale\u003c/strong\u003e: 10,000 orders/day, single application instance.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eRecommendation\u003c/strong\u003e: \u003cstrong\u003eHangfire\u003c/strong\u003e. Persistent storage ensures orders don\u0026rsquo;t vanish, automatic retries handle transient failures, dashboard provides real-time visibility. SQL Server likely already in use for order data.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003eScenario 2: Internal metrics dashboard aggregating data every 10 minutes\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eNeeds\u003c/strong\u003e: Simplicity, no persistence (restarting re-fetches data), single instance.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eScale\u003c/strong\u003e: 10 users, low stakes.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eRecommendation\u003c/strong\u003e: \u003cstrong\u003eCoravel\u003c/strong\u003e or \u003cstrong\u003eNCronJob\u003c/strong\u003e. Zero dependencies, fast integration. Coravel adds caching for metrics storage.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003eScenario 3: Financial platform processing nightly batch reports\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eNeeds\u003c/strong\u003e: Complex scheduling (business days, holidays), clustering (high availability), audit trails.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eScale\u003c/strong\u003e: Multi-datacenter, thousands of jobs.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eRecommendation\u003c/strong\u003e: \u003cstrong\u003eQuartz.NET\u003c/strong\u003e. Job calendars respect business rules, clustering ensures failover, listeners integrate with compliance auditing systems. Operational complexity justified by regulatory requirements.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003eScenario 4: SaaS product with 50,000 users triggering reports on-demand\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eNeeds\u003c/strong\u003e: Persistence, high throughput, real-time monitoring, modern architecture.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eScale\u003c/strong\u003e: Thousands of jobs/minute, horizontal scaling.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eRecommendation\u003c/strong\u003e: \u003cstrong\u003eTickerQ\u003c/strong\u003e if using Entity Framework Core, otherwise \u003cstrong\u003eHangfire with Redis\u003c/strong\u003e. TickerQ\u0026rsquo;s source generation and SignalR dashboard suit performance-sensitive SaaS. Hangfire\u0026rsquo;s broader ecosystem and maturity provide a safer fallback.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003eScenario 5: Kubernetes-deployed microservices executing health checks\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eNeeds\u003c/strong\u003e: Stateless, minimal overhead, idempotent tasks.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eScale\u003c/strong\u003e: Dozens of pod replicas, jobs tolerate duplication.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eRecommendation\u003c/strong\u003e: \u003cstrong\u003eNCronJob\u003c/strong\u003e. Direct \u003ccode\u003eIHostedService\u003c/code\u003e integration, zero dependencies, fits ephemeral containers perfectly.\u003c/li\u003e\n\u003c/ul\u003e\n\n\n\n\n\u003ch2 id=\"migration-paths-and-future-proofing\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-7-comparative-review/#migration-paths-and-future-proofing\" title=\"Migration Paths and Future-Proofing\"\u003eMigration Paths and Future-Proofing\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eSystems evolve. A framework suitable today may become constraining tomorrow. Anticipate migration paths:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eFrom Coravel/NCronJob to Hangfire\u003c/strong\u003e: Straightforward. Replace in-memory scheduling with database-backed persistence. Job definitions remain similar—update registration code and add connection strings. No breaking application-level changes.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eFrom Hangfire to Quartz.NET\u003c/strong\u003e: More involved. Hangfire\u0026rsquo;s simplicity (fire-and-forget, delayed, recurring) maps to Quartz.NET\u0026rsquo;s jobs and triggers, but Quartz.NET requires understanding its abstractions. Justify migration when Hangfire\u0026rsquo;s features prove insufficient (calendars, advanced misfires, multi-datacenter clustering).\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eFrom any framework to TickerQ\u003c/strong\u003e: Requires Entity Framework Core adoption and rewriting job definitions using attributes. Source generation introduces compile-time validation but necessitates build-time code changes. Worth the effort for teams prioritizing performance and modern patterns in greenfield projects or major refactors.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eFuture-proofing tips\u003c/strong\u003e:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eAbstract job definitions\u003c/strong\u003e: Wrap scheduler-specific APIs in application-level abstractions. This reduces coupling and simplifies framework swaps.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eLog extensively\u003c/strong\u003e: Regardless of scheduler, comprehensive logging enables observability when built-in tools lack.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMonitor metrics\u003c/strong\u003e: Track job throughput, duration, failure rates. Export to Prometheus, Application Insights, or Datadog for centralized visibility.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eDesign for idempotency\u003c/strong\u003e: Jobs that tolerate re-execution simplify failure recovery and enable horizontal scaling with minimal coordination.\u003c/li\u003e\n\u003c/ul\u003e\n\n\n\n\n\u003ch2 id=\"common-pitfalls-and-how-to-avoid-them\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-7-comparative-review/#common-pitfalls-and-how-to-avoid-them\" title=\"Common Pitfalls and How to Avoid Them\"\u003eCommon Pitfalls and How to Avoid Them\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003eChoosing based on features, not operational reality\u003c/strong\u003e: Quartz.NET\u0026rsquo;s advanced scheduling is impressive but overkill for applications running cron jobs daily. Match framework capabilities to actual requirements.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eIgnoring infrastructure constraints\u003c/strong\u003e: Adopting Hangfire without provisioning databases delays deployment. Assess what infrastructure your organization supports before committing.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eUnderestimating observability needs\u003c/strong\u003e: Logs suffice for small systems but become inadequate as job volumes grow. Dashboards (Hangfire, TickerQ) or custom telemetry (Quartz.NET with listeners) provide necessary visibility.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eScaling prematurely\u003c/strong\u003e: Deploying Quartz.NET clustering for a single-instance application introduces complexity without benefit. Start simple (NCronJob, Coravel) and migrate when workload demands justify it.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eNeglecting retry logic\u003c/strong\u003e: Frameworks without automatic retries (Coravel, NCronJob) require manual implementation. Don\u0026rsquo;t assume transient failures self-heal—code defensively.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"final-recommendations-by-use-case\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-7-comparative-review/#final-recommendations-by-use-case\" title=\"Final Recommendations by Use Case\"\u003eFinal Recommendations by Use Case\u003c/a\u003e\u003c/h2\u003e\n\u003ctable class=\"striped\"\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003eUse Case\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003ePrimary Choice\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eAlternative\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eAvoid\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eMVP or early-stage product\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eCoravel\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNCronJob\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eQuartz.NET\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eWeb application, moderate traffic\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eHangfire\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eTickerQ\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNCronJob\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eEnterprise with complex scheduling\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eQuartz.NET\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eHangfire Pro\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eCoravel\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eMicroservices in Kubernetes\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNCronJob\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eTickerQ\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eQuartz.NET\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eHigh-performance SaaS platform\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eTickerQ\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eHangfire + Redis\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eCoravel\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eInternal tools or low-stakes apps\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eCoravel\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNCronJob\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eQuartz.NET\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eLegacy .NET Framework systems\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eHangfire\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eQuartz.NET\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eTickerQ, NCronJob\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\n\n\n\n\u003ch2 id=\"closing-thoughts\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-7-comparative-review/#closing-thoughts\" title=\"Closing Thoughts\"\u003eClosing Thoughts\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eJob scheduling is infrastructure that fades when chosen correctly and becomes friction when mismatched. The frameworks in this series span a spectrum from simplicity to control, each making deliberate trade-offs. Your choice should reflect your system\u0026rsquo;s current state and anticipated evolution—not aspirational architectures or feature envy.\u003c/p\u003e\n\u003cp\u003eStart with the simplest solution that meets your needs. Coravel and NCronJob eliminate overhead for transient workflows. Hangfire adds persistence and observability when reliability matters. Quartz.NET provides enterprise control when complexity is justified. TickerQ modernizes the stack with performance and real-time monitoring for cloud-native systems.\u003c/p\u003e\n\u003cp\u003eBackground processing done right becomes invisible enablers of system capability. Choose the scheduler that aligns with your operational philosophy, infrastructure constraints, and team priorities. The right framework disappears into the background, letting you focus on delivering business value rather than managing job execution mechanics.\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2025-12-16T17:00:00+01:00","id":"https://daily-devops.net/posts/dotnet-job-scheduling-7-comparative-review/","language":"en","summary":"Side-by-side comparison of Hangfire, Quartz.NET, Coravel, NCronJob, and TickerQ with feature matrices and decision heuristics for .NET architects.","tags":["dotnet","csharp","architecture","nuget","softwareengineering"],"title":".NET Job Scheduling — Choosing the Right Framework","url":"https://daily-devops.net/posts/dotnet-job-scheduling-7-comparative-review/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eYour SaaS platform processes tens of thousands of background jobs daily—user-triggered reports, scheduled data synchronization, recurring billing cycles. Performance matters: every millisecond spent in reflection overhead compounds across job volume. We measured it once: a 2ms reflection penalty per job execution meant an extra 40 seconds of CPU time daily across 20,000 jobs. Not catastrophic, but not free either.\u003c/p\u003e\n\u003cp\u003eObservability matters: product managers need real-time dashboards showing job states without deploying custom monitoring solutions. Safety matters: configuration errors should surface at compile time, not in production when a misspelled job name causes silent failures. We\u0026rsquo;ve all been there—typo in a cron expression, job never runs, customer discovers it three weeks later.\u003c/p\u003e\n\u003cp\u003eTickerQ addresses these demands using modern .NET primitives: source generators eliminate reflection, Entity Framework Core provides persistence, and SignalR powers real-time dashboards. Jobs are defined with attributes, generating boilerplate code at compile time. The scheduler is async-first, stateless at its core, and integrates seamlessly with ASP.NET Core\u0026rsquo;s dependency injection. The result: a framework that feels contemporary, performs efficiently, and surfaces errors early.\u003c/p\u003e\n\u003cp\u003eThe trade-off: as a newer entrant, the ecosystem and community remain smaller compared to long-established alternatives. For teams building new systems prioritizing performance and modern patterns, the architectural approach offers compelling advantages. For teams requiring battle-tested stability or extensive plugin ecosystems, maturity considerations become relevant.\u003c/p\u003e\n\u003cblockquote\u003e\n\n\n\n\n\u003ch2 id=\"disclaimer\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-6-tickerq/#disclaimer\" title=\"Disclaimer\"\u003eDisclaimer\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThis article’s code examples reflect the TickerQ API as of versions 8+ (current docs show .NET 8+ usage). If you are on older major versions, please refer to the official upgrade notes and adapt signatures and configuration accordingly.\u003c/p\u003e\n\u003c/blockquote\u003e\n\n\n\n\n\u003ch2 id=\"architecture-source-generation-and-stateless-core\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-6-tickerq/#architecture-source-generation-and-stateless-core\" title=\"Architecture: Source Generation and Stateless Core\"\u003eArchitecture: Source Generation and Stateless Core\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eTickerQ\u0026rsquo;s architecture centers on compile-time code generation. Jobs are defined as methods decorated with \u003ccode\u003e[TickerFunction]\u003c/code\u003e attributes. During compilation, source generators discover these methods, validate their signatures, and generate registration code that wires them into the scheduler without reflection.\u003c/p\u003e\n\u003cp\u003eConsider a job definition:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003eTickerQ.Utilities.Base\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eReportJobs\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eIReportService\u003c/span\u003e \u003cspan class=\"n\"\u003e_reportService\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eReportJobs\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eIReportService\u003c/span\u003e \u003cspan class=\"n\"\u003ereportService\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003e_reportService\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ereportService\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [TickerFunction(\u0026#34;GenerateMonthlyReport\u0026#34;, cronExpression: \u0026#34;0 0 0 1 * *\u0026#34;)]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e \u003cspan class=\"n\"\u003eGenerateMonthlyReport\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eTickerFunctionContext\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eCancellationToken\u003c/span\u003e \u003cspan class=\"n\"\u003ecancellationToken\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_reportService\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGenerateAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ecancellationToken\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eAt compile time, the source generator:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eDiscovers the \u003ccode\u003eGenerateMonthlyReport\u003c/code\u003e method.\u003c/li\u003e\n\u003cli\u003eValidates the cron expression \u003ccode\u003e0 0 0 1 * *\u003c/code\u003e (monthly at midnight, 6-part cron).\u003c/li\u003e\n\u003cli\u003eGenerates registration code mapping \u003ccode\u003e\u0026quot;GenerateMonthlyReport\u0026quot;\u003c/code\u003e to the method.\u003c/li\u003e\n\u003cli\u003eInjects dependency resolution logic for \u003ccode\u003eIReportService\u003c/code\u003e.\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eAt runtime, the scheduler invokes jobs via generated delegates—no reflection, no \u003ccode\u003eMethodInfo.Invoke()\u003c/code\u003e, no dictionary lookups. This yields:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003ePerformance\u003c/strong\u003e: Reflection overhead eliminated, reducing invocation latency.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCompile-time safety\u003c/strong\u003e: Invalid cron expressions or missing dependencies cause build errors, not runtime exceptions.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTooling support\u003c/strong\u003e: IDEs detect errors, provide IntelliSense, and enable refactoring tools to work correctly.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThe stateless core design means job state lives in the database (via Entity Framework Core), not in-memory. The scheduler queries the database for jobs whose execution times have arrived, claims them atomically, and dispatches them to workers. This architecture supports clustering naturally: multiple instances coordinate via the database without custom locking logic.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"configuration-and-entity-framework-integration\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-6-tickerq/#configuration-and-entity-framework-integration\" title=\"Configuration and Entity Framework Integration\"\u003eConfiguration and Entity Framework Integration\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eIntegrating TickerQ requires configuring Entity Framework Core for persistence. Install the packages:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003edotnet add package TickerQ\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003edotnet add package TickerQ.EntityFrameworkCore\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003edotnet add package TickerQ.Dashboard\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eConfigure services in \u003ccode\u003eProgram.cs\u003c/code\u003e:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003eTickerQ.DependencyInjection\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003eTickerQ.EntityFrameworkCore.DependencyInjection\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003eTickerQ.Dashboard.DependencyInjection\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eServices\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddTickerQ\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eoptions\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eoptions\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eConfigureScheduler\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003escheduler\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003escheduler\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eMaxConcurrency\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e10\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eoptions\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddOperationalStore\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eSchedulerDbContext\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;(\u003c/span\u003e\u003cspan class=\"n\"\u003eefOptions\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eefOptions\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUseApplicationDbContext\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eSchedulerDbContext\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;(\u003c/span\u003e\u003cspan class=\"n\"\u003eConfigurationType\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUseModelCustomizer\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eoptions\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddDashboard\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003edashboardOptions\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003edashboardOptions\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSetBasePath\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;/tickerq/dashboard\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003edashboardOptions\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWithBasicAuth\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eapp\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eBuild\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eapp\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUseTickerQ\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eapp\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRun\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe \u003ccode\u003eAddOperationalStore\u003c/code\u003e method integrates TickerQ with your existing \u003ccode\u003eDbContext\u003c/code\u003e. TickerQ creates tables for job definitions (\u003ccode\u003eTimeTicker\u003c/code\u003e, \u003ccode\u003eCronTicker\u003c/code\u003e) and execution history. Using \u003ccode\u003eUseApplicationDbContext\u0026lt;SchedulerDbContext\u0026gt;(ConfigurationType.UseModelCustomizer)\u003c/code\u003e applies TickerQ\u0026rsquo;s entity configurations via model customizer while keeping your domain model clean.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"generate-and-apply-migrations\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-6-tickerq/#generate-and-apply-migrations\" title=\"Generate and apply migrations\"\u003eGenerate and apply migrations\u003c/a\u003e\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003edotnet ef migrations add AddTickerQSupport -c SchedulerDbContext\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003edotnet ef database update\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eTickerQ\u0026rsquo;s tables store:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eCronTickers\u003c/strong\u003e: Recurring jobs with cron expressions.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTimeTickers\u003c/strong\u003e: One-time jobs scheduled for specific execution times.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCronTickerOccurrences\u003c/strong\u003e: Execution history for audit trails and retry tracking.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThis database-backed persistence ensures jobs survive application restarts. If a job should have executed while the application was down, TickerQ handles it upon restart based on configuration—either executing missed jobs or skipping them.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"job-definitions-cron-and-time-tickers\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-6-tickerq/#job-definitions-cron-and-time-tickers\" title=\"Job Definitions: Cron and Time Tickers\"\u003eJob Definitions: Cron and Time Tickers\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eTickerQ supports two job types: cron-based recurring jobs and time-based one-time jobs.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eCron jobs\u003c/strong\u003e execute repeatedly:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003eTickerQ.Utilities.Base\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eMaintenanceJobs\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [TickerFunction(\u0026#34;CleanupLogs\u0026#34;, \u0026#34;0 0 * * * *\u0026#34;)]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e \u003cspan class=\"n\"\u003eCleanupLogs\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eTickerFunctionContext\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eCancellationToken\u003c/span\u003e \u003cspan class=\"n\"\u003ecancellationToken\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eDeleteOldLogsAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ecancellationToken\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe source generator validates \u003ccode\u003e\u0026quot;0 0 * * * *\u0026quot;\u003c/code\u003e at compile time. If the expression is invalid—say, \u003ccode\u003e\u0026quot;0 25 * * * *\u0026quot;\u003c/code\u003e (invalid hour)—the build fails with a descriptive error.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTime tickers\u003c/strong\u003e execute once at a specified time:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003eTickerQ.Utilities.Base\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eNotificationJobs\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [TickerFunction(\u0026#34;SendReminder\u0026#34;)]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e \u003cspan class=\"n\"\u003eSendReminder\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eTickerFunctionContext\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eCancellationToken\u003c/span\u003e \u003cspan class=\"n\"\u003ecancellationToken\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003euserId\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRequest\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eSendReminderEmailAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003euserId\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ecancellationToken\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eSchedule time tickers programmatically:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003eTickerQ.Utilities.Entities\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003eTickerQ.Utilities.Base\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003eTickerQ.Utilities.Interfaces.Managers\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eITimeTickerManager\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eTimeTickerEntity\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003e_timeTickerManager\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_timeTickerManager\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eTimeTickerEntity\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eFunction\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;SendReminder\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eExecutionTime\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eDateTime\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUtcNow\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddHours\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e2\u003c/span\u003e\u003cspan class=\"p\"\u003e),\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eRequest\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eTickerHelper\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCreateTickerRequest\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003euserId\u003c/span\u003e\u003cspan class=\"p\"\u003e),\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eRetries\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e3\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eRetryIntervals\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e\u003cspan class=\"p\"\u003e[]\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"m\"\u003e60\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"m\"\u003e300\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"m\"\u003e900\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e \u003cspan class=\"c1\"\u003e// 1min, 5min, 15min\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis schedules a reminder to send in two hours. If execution fails, TickerQ retries up to three times with increasing intervals. The \u003ccode\u003eRequest\u003c/code\u003e parameter passes data (here, \u003ccode\u003euserId\u003c/code\u003e) to the job, serialized as JSON in the database.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"real-time-dashboard-with-signalr\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-6-tickerq/#real-time-dashboard-with-signalr\" title=\"Real-Time Dashboard with SignalR\"\u003eReal-Time Dashboard with SignalR\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eTickerQ\u0026rsquo;s dashboard provides live visibility into job states using SignalR for real-time updates. Administrators view:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eActive jobs\u003c/strong\u003e: Currently executing, with elapsed time and progress indicators.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eScheduled jobs\u003c/strong\u003e: Pending execution with countdown timers.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eExecution history\u003c/strong\u003e: Completed jobs with duration, outcome, and error details.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCron tickers\u003c/strong\u003e: Recurring jobs with last/next execution times.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThe dashboard also supports:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eManual triggering\u003c/strong\u003e: Execute recurring jobs on-demand.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eJob cancellation\u003c/strong\u003e: Stop long-running jobs mid-execution.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eLive updates\u003c/strong\u003e: Job states update in real-time via SignalR, no page refreshes required.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eConfigure basic authentication to protect the dashboard:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eServices\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddTickerQ\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eoptions\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eoptions\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddDashboard\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003edashboardOptions\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"n\"\u003edashboardOptions\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSetBasePath\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;/tickerq/dashboard\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"n\"\u003edashboardOptions\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWithBasicAuth\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eFor production deployments, integrate with your authentication system—ASP.NET Core Identity, OAuth, or Azure AD—using \u003ccode\u003eWithHostAuthentication()\u003c/code\u003e and standard ASP.NET Core authorization policies.\u003c/p\u003e\n\u003cp\u003eThe dashboard\u0026rsquo;s Vue.js-based UI is modern and responsive, tailored for operational teams monitoring background processing health. Compare this to Hangfire\u0026rsquo;s dashboard, which uses server-rendered HTML with periodic polling. TickerQ\u0026rsquo;s SignalR approach reduces latency and provides instant feedback when job states change.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"retry-policies-throttling-and-distributed-coordination\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-6-tickerq/#retry-policies-throttling-and-distributed-coordination\" title=\"Retry Policies, Throttling, and Distributed Coordination\"\u003eRetry Policies, Throttling, and Distributed Coordination\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eTickerQ supports per-job retry policies:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003eTickerQ.Utilities.Entities\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_timeTickerManager\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eTimeTickerEntity\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eFunction\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;ImportData\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eExecutionTime\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eDateTime\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUtcNow\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eRetries\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e5\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eRetryIntervals\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e\u003cspan class=\"p\"\u003e[]\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"m\"\u003e30\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"m\"\u003e60\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"m\"\u003e120\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"m\"\u003e300\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"m\"\u003e600\u003c/span\u003e \u003cspan class=\"p\"\u003e},\u003c/span\u003e \u003cspan class=\"c1\"\u003e// Exponential backoff\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eFailed jobs retry based on the specified intervals. After exhausting retries, jobs transition to \u003ccode\u003eFailed\u003c/code\u003e state, visible in the dashboard with full error details.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eThrottling\u003c/strong\u003e limits concurrent execution. If your database supports 50 concurrent connections and you schedule 100 jobs simultaneously, throttling prevents connection exhaustion:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eServices\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddTickerQ\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eoptions\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eoptions\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eConfigureScheduler\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003escheduler\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003escheduler\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eMaxConcurrency\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e10\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"c1\"\u003e// Max 10 concurrent jobs\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eTickerQ queues excess jobs until workers become available, preventing resource contention.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eDistributed coordination\u003c/strong\u003e works via Entity Framework Core\u0026rsquo;s optimistic concurrency. When a scheduler instance queries for jobs, it claims them with an atomic update:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eUPDATE\u003c/span\u003e \u003cspan class=\"n\"\u003eTimeTicker\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eSET\u003c/span\u003e \u003cspan class=\"n\"\u003eState\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"err\"\u003e\u0026#39;\u003c/span\u003e\u003cspan class=\"n\"\u003eProcessing\u003c/span\u003e\u003cspan class=\"err\"\u003e\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eInstance\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"err\"\u003e\u0026#39;\u003c/span\u003e\u003cspan class=\"n\"\u003eserver\u003c/span\u003e\u003cspan class=\"p\"\u003e-\u003c/span\u003e\u003cspan class=\"m\"\u003e01\u003c/span\u003e\u003cspan class=\"err\"\u003e\u0026#39;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eWHERE\u003c/span\u003e \u003cspan class=\"n\"\u003eState\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"err\"\u003e\u0026#39;\u003c/span\u003e\u003cspan class=\"n\"\u003ePending\u003c/span\u003e\u003cspan class=\"err\"\u003e\u0026#39;\u003c/span\u003e \u003cspan class=\"n\"\u003eAND\u003c/span\u003e \u003cspan class=\"n\"\u003eExecutionTime\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026lt;=\u003c/span\u003e \u003cspan class=\"n\"\u003eGETUTCDATE\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eOnly one instance succeeds per job. If an instance crashes mid-execution, orphaned jobs remain in \u003ccode\u003eProcessing\u003c/code\u003e state until a recovery mechanism detects and resets them—configurable via timeout policies.\u003c/p\u003e\n\u003cp\u003eThis coordination is simpler than Quartz.NET\u0026rsquo;s pessimistic locking but sufficient for most scenarios. Teams running dozens of instances in high-throughput environments may need to tune timeout settings to balance recovery speed and false-positive detection.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"batch-jobs-and-dependency-workflows\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-6-tickerq/#batch-jobs-and-dependency-workflows\" title=\"Batch Jobs and Dependency Workflows\"\u003eBatch Jobs and Dependency Workflows\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eTickerQ supports batch jobs—groups of related tasks that execute as a unit:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003eTickerQ.Utilities.Entities\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003eTickerQ.Utilities.Interfaces.Managers\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Schedule parent job\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eparentResult\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_timeTickerManager\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eTimeTickerEntity\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eFunction\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;ImportUsers\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eExecutionTime\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eDateTime\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUtcNow\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eparentId\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eparentResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eId\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Schedule dependent job that runs only if parent succeeds\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_timeTickerManager\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eTimeTickerEntity\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eFunction\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;TransformUsers\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eExecutionTime\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eDateTime\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUtcNow\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eParentId\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eparentId\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eRunCondition\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eRunCondition\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eOnSuccess\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eTickerQ executes \u003ccode\u003eImportUsers\u003c/code\u003e first. If it succeeds, \u003ccode\u003eTransformUsers\u003c/code\u003e runs; if it fails, \u003ccode\u003eTransformUsers\u003c/code\u003e is skipped. This declarative workflow removes custom orchestration logic from application code.\u003c/p\u003e\n\u003cp\u003eBatch conditions include:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eAlways\u003c/strong\u003e: Execute regardless of parent outcomes.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eOnSuccess\u003c/strong\u003e: Execute only if all previous batch jobs succeeded.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eOnFailure\u003c/strong\u003e: Execute only if any previous job failed (error handling workflows).\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThis feature mirrors Hangfire\u0026rsquo;s continuations and Quartz.NET\u0026rsquo;s job chaining but integrates more naturally with Entity Framework Core\u0026rsquo;s transactional boundaries.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"when-tickerq-fits\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-6-tickerq/#when-tickerq-fits\" title=\"When TickerQ Fits\"\u003eWhen TickerQ Fits\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eTickerQ excels when:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003ePerformance matters\u003c/strong\u003e: High job volumes benefit from reflection-free execution and async-first design.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eCompile-time safety is valued\u003c/strong\u003e: Teams that prefer catching configuration errors during builds rather than runtime.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eModern tooling is prioritized\u003c/strong\u003e: Source generation, SignalR, and Entity Framework Core integration appeal to teams comfortable with current .NET patterns.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eReal-time observability is required\u003c/strong\u003e: The dashboard\u0026rsquo;s live updates provide operational visibility without custom monitoring infrastructure.\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eTickerQ is less suitable when:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eBattle-tested stability is critical\u003c/strong\u003e: Hangfire (13+ years) and Quartz.NET (20+ years) have larger user bases and more production validation.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eExtensive plugins are needed\u003c/strong\u003e: TickerQ\u0026rsquo;s ecosystem is smaller. Hangfire and Quartz.NET offer more storage backends, monitoring integrations, and community extensions.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eLegacy .NET Framework support is required\u003c/strong\u003e: TickerQ targets modern .NET (6+). Teams on .NET Framework should use Hangfire or Quartz.NET.\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\n\n\n\n\u003ch2 id=\"operational-considerations\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-6-tickerq/#operational-considerations\" title=\"Operational Considerations\"\u003eOperational Considerations\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eTickerQ\u0026rsquo;s reliance on Entity Framework Core couples job scheduling to your database strategy. Teams already using EF Core benefit from unified migration workflows and tooling. Teams preferring Dapper, raw SQL, or NoSQL databases face friction—TickerQ\u0026rsquo;s operational store requires EF Core.\u003c/p\u003e\n\u003cp\u003eThe source generation approach requires recompilation when job definitions change. This aligns with modern CI/CD practices (deploy code, not configuration) but contrasts with Hangfire or Quartz.NET, where jobs can be scheduled dynamically at runtime without redeployment.\u003c/p\u003e\n\u003cp\u003eTickerQ\u0026rsquo;s dashboard consumes resources—SignalR connections, server memory for real-time updates. In resource-constrained environments, disable the dashboard and rely on application logging.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"practical-takeaways\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-6-tickerq/#practical-takeaways\" title=\"Practical Takeaways\"\u003ePractical Takeaways\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eTickerQ represents modern .NET job scheduling: source generation, async-first design, and real-time monitoring. It bridges the gap between simplicity (NCronJob, Coravel) and enterprise features (Quartz.NET), offering persistence and performance without operational complexity.\u003c/p\u003e\n\u003cp\u003eConsider TickerQ if:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eYou\u0026rsquo;re building new systems on modern .NET (6+).\u003c/li\u003e\n\u003cli\u003ePerformance and compile-time safety are priorities.\u003c/li\u003e\n\u003cli\u003eYou use Entity Framework Core and value tooling integration.\u003c/li\u003e\n\u003cli\u003eReal-time dashboards enhance operational workflows.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eAvoid TickerQ if:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eYour system runs on .NET Framework or older .NET Core versions.\u003c/li\u003e\n\u003cli\u003eYou need extensive ecosystem support or community plugins.\u003c/li\u003e\n\u003cli\u003eYou prefer runtime configuration over compile-time code generation.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThe final article synthesizes the series into comparative guidance, presenting a feature matrix, rating framework suitability across dimensions, and offering decision heuristics for selecting the right scheduler based on system maturity, infrastructure, and team priorities.\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2025-12-11T17:00:00+01:00","id":"https://daily-devops.net/posts/dotnet-job-scheduling-6-tickerq/","language":"en","summary":"How TickerQ uses source generation, EF Core, and a real-time dashboard to deliver reflection-free, async-first scheduling for modern cloud-native systems.","tags":["dotnet","csharp","architecture","nuget","softwareengineering"],"title":".NET Job Scheduling — TickerQ and Modern Architecture","url":"https://daily-devops.net/posts/dotnet-job-scheduling-6-tickerq/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eWhen someone handed me \u003ca href=\"https://spinroot.com/gerard/pdf/P10.pdf\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eGerard Holzmann\u0026rsquo;s \u0026ldquo;Power of Ten\u0026rdquo;\u003c/a\u003e rules and asked whether they still apply to C# 10/.NET, my answer was immediate: Absolutely, and we can enforce them better than Gerard Holzmann could have dreamed in 2006. The Power of Ten originated at NASA\u0026rsquo;s Jet Propulsion Laboratory for safety-critical C code in spacecraft systems. Not academic theory—principles where violations kill people. NASA\u0026rsquo;s \u003ca href=\"https://users.ece.cmu.edu/~koopman/pubs/koopman14_toyota_ua_slides.pdf\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e2010 technical assessment of Toyota\u0026rsquo;s electronic throttle control\u003c/a\u003e found 243 violations contributing to unintended acceleration deaths. When prosecutors examine catastrophic embedded failures, these ten rules are the baseline for \u0026ldquo;did you even try?\u0026rdquo;\u003c/p\u003e\n\u003cp\u003eWhat\u0026rsquo;s changed in two decades? In 2006, following these rules meant discipline, manual code reviews, and static analysis tools at $5,000 per seat. You verified bounded loops by hand, tracked pointer indirection with spreadsheets, enforced function length through policy documents nobody read. I\u0026rsquo;ve reviewed enough legacy embedded code to know most teams failed. The tooling wasn\u0026rsquo;t there, and manual enforcement doesn\u0026rsquo;t scale past three developers. Modern C# flips this completely. Roslyn analyzers catch violations at compile time—before code review, before testing, before anyone debugs at 2 AM wondering why the production system locked up. Nullable reference types enforce null-safety that C developers achieved through religious discipline and hope. The type system makes pointer arithmetic bugs physically impossible. Built into the compiler, zero additional cost, enforced automatically.\u003c/p\u003e\n\u003cp\u003eThe catch—and there\u0026rsquo;s always a catch—is that not every rule translates directly. The managed runtime fundamentally rewrites what\u0026rsquo;s dangerous and what\u0026rsquo;s safe. Dynamic allocation is forbidden in embedded systems but powers every .NET framework feature. Recursion crashes spacecraft but handles expression trees beautifully when the CLR manages your stack. The real question isn\u0026rsquo;t \u0026ldquo;do these rules apply to C#?\u0026rdquo; It\u0026rsquo;s \u0026ldquo;how do modern language features enforce the underlying principles better than 2006 C ever could?\u0026rdquo; Let\u0026rsquo;s examine each rule systematically.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"rule-1-avoid-complex-control-flow-no-goto-setjmplongjmp-recursion\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#rule-1-avoid-complex-control-flow-no-goto-setjmplongjmp-recursion\" title=\"Rule 1: Avoid Complex Control Flow (No goto, setjmp/longjmp, Recursion)\"\u003eRule 1: Avoid Complex Control Flow (No goto, setjmp/longjmp, Recursion)\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003eOriginal Intent:\u003c/strong\u003e Simplify static analysis and prevent unpredictable control flow in embedded systems.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eC#/.NET Applicability:\u003c/strong\u003e Partially valid, but context matters.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"what-still-applies\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#what-still-applies\" title=\"What Still Applies\"\u003eWhat Still Applies\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003e\u003ccode\u003egoto\u003c/code\u003e remains controversial in C#, though the language supports it. I\u0026rsquo;ve seen developers defend it passionately in code reviews, and honestly, for breaking out of deeply nested loops, it\u0026rsquo;s occasionally the least-bad option. But \u0026ldquo;occasionally\u0026rdquo; is doing a lot of work in that sentence:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Acceptable use case: breaking out of nested loops\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"kt\"\u003ebool\u003c/span\u003e \u003cspan class=\"n\"\u003eTryFindPosition\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e\u003cspan class=\"p\"\u003e[,]\u003c/span\u003e \u003cspan class=\"n\"\u003ematrix\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003etarget\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"k\"\u003eout\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003erow\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003ecol\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"n\"\u003eposition\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e \u003cspan class=\"n\"\u003erows\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e++)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003ej\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ej\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e \u003cspan class=\"n\"\u003ecols\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ej\u003c/span\u003e\u003cspan class=\"p\"\u003e++)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ematrix\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ej\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"n\"\u003etarget\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"k\"\u003egoto\u003c/span\u003e \u003cspan class=\"n\"\u003efound\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"kc\"\u003efalse\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003efound\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"kc\"\u003etrue\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eBut this is clearer:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Better: extract to method with early return\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"kt\"\u003ebool\u003c/span\u003e \u003cspan class=\"n\"\u003eTryFindPosition\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e\u003cspan class=\"p\"\u003e[,]\u003c/span\u003e \u003cspan class=\"n\"\u003ematrix\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003etarget\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"k\"\u003eout\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003erow\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003ecol\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"n\"\u003eposition\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e \u003cspan class=\"n\"\u003ematrix\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetLength\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e++)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003ej\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ej\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e \u003cspan class=\"n\"\u003ematrix\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetLength\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e \u003cspan class=\"n\"\u003ej\u003c/span\u003e\u003cspan class=\"p\"\u003e++)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ematrix\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ej\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"n\"\u003etarget\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"n\"\u003eposition\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ej\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"kc\"\u003etrue\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eposition\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003edefault\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"kc\"\u003efalse\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\n\n\n\u003ch3 id=\"whats-different\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#whats-different\" title=\"What\u0026rsquo;s Different\"\u003eWhat\u0026rsquo;s Different\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003eRecursion\u003c/strong\u003e tells a completely different story. The JIT compiler doesn\u0026rsquo;t guarantee tail-call optimization like F# does. Still valuable for tree structures, parsing, algorithms where iterative alternatives become unreadable spaghetti. Key difference? Stack overflows throw exceptions you can catch and handle. In embedded C, stack overflow crashes the spacecraft. No recovery, no logging, just silence.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Recursion is perfectly acceptable for bounded structures\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eTreeNode\u003c/span\u003e\u003cspan class=\"p\"\u003e?\u003c/span\u003e \u003cspan class=\"n\"\u003eFind\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eTreeNode\u003c/span\u003e\u003cspan class=\"p\"\u003e?\u003c/span\u003e \u003cspan class=\"n\"\u003enode\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"k\"\u003evalue\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003enode\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003enode\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eValue\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"k\"\u003evalue\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003enode\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eFind\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003enode\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLeft\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"k\"\u003evalue\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e??\u003c/span\u003e \u003cspan class=\"n\"\u003eFind\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003enode\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRight\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"k\"\u003evalue\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eFor deep recursion, modern C# offers alternatives:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Convert to iteration with explicit stack\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eTreeNode\u003c/span\u003e\u003cspan class=\"p\"\u003e?\u003c/span\u003e \u003cspan class=\"n\"\u003eFind\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eTreeNode\u003c/span\u003e\u003cspan class=\"p\"\u003e?\u003c/span\u003e \u003cspan class=\"n\"\u003enode\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"k\"\u003evalue\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003enode\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003estack\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eStack\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eTreeNode\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003estack\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePush\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003enode\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ewhile\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003estack\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCount\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003ecurrent\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003estack\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePop\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ecurrent\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eValue\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"k\"\u003evalue\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003ecurrent\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ecurrent\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRight\u003c/span\u003e \u003cspan class=\"p\"\u003e!=\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"n\"\u003estack\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePush\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ecurrent\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRight\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ecurrent\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLeft\u003c/span\u003e \u003cspan class=\"p\"\u003e!=\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"n\"\u003estack\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePush\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ecurrent\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLeft\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003eVerdict:\u003c/strong\u003e Use recursion where it makes sense, but be aware of stack depth. Avoid \u003ccode\u003egoto\u003c/code\u003e unless you have a compelling reason. The .NET runtime gives you safety nets that embedded C never had.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"rule-2-all-loops-must-have-fixed-upper-bounds\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#rule-2-all-loops-must-have-fixed-upper-bounds\" title=\"Rule 2: All Loops Must Have Fixed Upper Bounds\"\u003eRule 2: All Loops Must Have Fixed Upper Bounds\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003eOriginal Intent:\u003c/strong\u003e Enable static analysis to prove termination and prevent infinite loops.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eC#/.NET Applicability:\u003c/strong\u003e The principle is sound, but enforcement differs dramatically.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"modern-interpretation\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#modern-interpretation\" title=\"Modern Interpretation\"\u003eModern Interpretation\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eProvable termination is still the goal, but .NET gives you runtime safety nets that embedded C never had:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Bad: unbounded loop\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003ewhile\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kc\"\u003etrue\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eitem\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003equeue\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eDequeue\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e \u003cspan class=\"c1\"\u003e// What if queue is empty?\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eProcess\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eitem\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Better: explicit bound with timeout\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003ects\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eCancellationTokenSource\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eTimeSpan\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFromMinutes\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e5\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003ewhile\u003c/span\u003e \u003cspan class=\"p\"\u003e(!\u003c/span\u003e\u003cspan class=\"n\"\u003ects\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eToken\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eIsCancellationRequested\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003equeue\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTryDequeue\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003eout\u003c/span\u003e \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eitem\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eProcess\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eitem\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eelse\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eDelay\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e100\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ects\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eToken\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eModern .NET provides powerful static analysis through \u003ca href=\"https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/overview\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eRoslyn analyzers\u003c/a\u003e that can detect unbounded loops during compilation. Enable nullable reference types and the latest analysis level to catch these issues early:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-xml\" data-lang=\"xml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003e\u0026lt;PropertyGroup\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026lt;Nullable\u0026gt;\u003c/span\u003eenable\u003cspan class=\"nt\"\u003e\u0026lt;/Nullable\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026lt;AnalysisLevel\u0026gt;\u003c/span\u003elatest\u003cspan class=\"nt\"\u003e\u0026lt;/AnalysisLevel\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003e\u0026lt;/PropertyGroup\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003eVerdict:\u003c/strong\u003e Always use bounded loops. Modern C# makes this easier with \u003ccode\u003eforeach\u003c/code\u003e, LINQ\u0026rsquo;s \u003ccode\u003eTake()\u003c/code\u003e, and cancellation tokens. The difference from embedded C: Your loops can be bounded at runtime with safe defaults rather than requiring compile-time constants.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"rule-3-no-dynamic-memory-allocation-after-initialization\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#rule-3-no-dynamic-memory-allocation-after-initialization\" title=\"Rule 3: No Dynamic Memory Allocation After Initialization\"\u003eRule 3: No Dynamic Memory Allocation After Initialization\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003eOriginal Intent:\u003c/strong\u003e Prevent memory exhaustion and fragmentation in systems without virtual memory.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eC#/.NET Applicability:\u003c/strong\u003e Fundamentally incompatible with .NET\u0026rsquo;s design philosophy.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"why-this-rule-doesnt-translate\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#why-this-rule-doesnt-translate\" title=\"Why This Rule Doesn\u0026rsquo;t Translate\"\u003eWhy This Rule Doesn\u0026rsquo;t Translate\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThe .NET garbage collector exists precisely to enable safe dynamic allocation. Forbidding dynamic allocation in .NET is like forbidding breathing—every framework feature depends on it:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Standard .NET patterns rely on GC\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eresults\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003ehttpClient\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetStringAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eurl\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eparsed\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eJsonSerializer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eDeserialize\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eMyData\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;(\u003c/span\u003e\u003cspan class=\"n\"\u003eresults\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003efiltered\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eparsed\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eItems\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWhere\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ex\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ex\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eIsActive\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\u003cspan class=\"n\"\u003eToList\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\n\n\n\u003ch3 id=\"the-modern-equivalent-minimize-allocations\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#the-modern-equivalent-minimize-allocations\" title=\"The Modern Equivalent: Minimize Allocations\"\u003eThe Modern Equivalent: Minimize Allocations\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eRather than avoiding allocation entirely, focus on reducing unnecessary allocations and GC pressure:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Inefficient: allocates multiple intermediate strings\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eresult\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e \u003cspan class=\"m\"\u003e1000\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e++)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eresult\u003c/span\u003e \u003cspan class=\"p\"\u003e+=\u003c/span\u003e \u003cspan class=\"s\"\u003e$\u0026#34;Item {i}; \u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e  \u003cspan class=\"c1\"\u003e// Allocates new string each iteration\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Efficient: single allocation, reusable buffer\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003esb\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eStringBuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ecapacity\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"m\"\u003e1000\u003c/span\u003e \u003cspan class=\"p\"\u003e*\u003c/span\u003e \u003cspan class=\"m\"\u003e15\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e \u003cspan class=\"m\"\u003e1000\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e++)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003esb\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAppend\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Item \u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003esb\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAppend\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003esb\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAppend\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;; \u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eresult\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003esb\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eToString\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\n\n\n\u003ch3 id=\"stack-allocation-for-performance-critical-code\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#stack-allocation-for-performance-critical-code\" title=\"Stack Allocation for Performance-Critical Code\"\u003eStack Allocation for Performance-Critical Code\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eWhen allocation overhead matters, use \u003ccode\u003eSpan\u0026lt;T\u0026gt;\u003c/code\u003e with \u003ccode\u003estackalloc\u003c/code\u003e:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Stack allocation for small, temporary buffers\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eSpan\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ebuffer\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003estackalloc\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"m\"\u003e256\u003c/span\u003e\u003cspan class=\"p\"\u003e];\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e \u003cspan class=\"n\"\u003ebuffer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLength\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e++)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003ebuffer\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eComputeValue\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eProcessData\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ebuffer\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003eCritical constraints:\u003c/strong\u003e Stack allocations come with three important limitations. First, analyzer rule \u003ca href=\"https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca2014\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eCA2014\u003c/a\u003e prevents using \u003ccode\u003estackalloc\u003c/code\u003e inside loops to avoid stack overflow. Second, keep allocations small (under 1KB recommended) since stack space is limited. Third, \u003ccode\u003eSpan\u0026lt;T\u0026gt;\u003c/code\u003e can\u0026rsquo;t escape method scope: attempting to return it produces compiler error \u003ca href=\"https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/compiler-messages/ref-safety-errors\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eCS8347\u003c/a\u003e. For data that needs to escape, use \u003ccode\u003eMemory\u0026lt;T\u0026gt;\u003c/code\u003e or arrays instead.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"object-pooling-for-hot-paths\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#object-pooling-for-hot-paths\" title=\"Object Pooling for Hot Paths\"\u003eObject Pooling for Hot Paths\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eFor high-throughput scenarios:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Use ArrayPool for reusable buffers\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003ebyte\u003c/span\u003e\u003cspan class=\"p\"\u003e[]\u003c/span\u003e \u003cspan class=\"n\"\u003ebuffer\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eArrayPool\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003ebyte\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;.\u003c/span\u003e\u003cspan class=\"n\"\u003eShared\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRent\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e4096\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003etry\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003ebytesRead\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003estream\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eReadAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ebuffer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAsMemory\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"m\"\u003e4096\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eProcessBytes\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ebuffer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAsSpan\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ebytesRead\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003efinally\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eArrayPool\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003ebyte\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;.\u003c/span\u003e\u003cspan class=\"n\"\u003eShared\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eReturn\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ebuffer\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003eVerdict:\u003c/strong\u003e Don\u0026rsquo;t avoid allocation, manage it intelligently. Use \u003ccode\u003eSpan\u0026lt;T\u0026gt;\u003c/code\u003e/\u003ccode\u003eMemory\u0026lt;T\u0026gt;\u003c/code\u003e for performance-critical code, \u003ccode\u003eArrayPool\u0026lt;T\u0026gt;\u003c/code\u003e for reusable buffers, and profile before optimizing. I\u0026rsquo;ve watched teams waste months optimizing allocations that consumed 2% of execution time while ignoring the database query running on every keystroke. The GC is your friend, not your enemy—but measure before you fight it.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"rule-4-no-function-longer-than-one-printed-page-60-lines\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#rule-4-no-function-longer-than-one-printed-page-60-lines\" title=\"Rule 4: No Function Longer Than One Printed Page (~60 Lines)\"\u003eRule 4: No Function Longer Than One Printed Page (~60 Lines)\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003eOriginal Intent:\u003c/strong\u003e Keep functions digestible for human review and static analysis.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eC#/.NET Applicability:\u003c/strong\u003e Absolutely valid, possibly even more important.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"why-this-rule-still-matters\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#why-this-rule-still-matters\" title=\"Why This Rule Still Matters\"\u003eWhy This Rule Still Matters\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eIn 15 years across finance, healthcare, and industrial control systems, I\u0026rsquo;ve never once regretted breaking a long method into smaller pieces. I\u0026rsquo;ve regretted plenty of 500-line monsters—particularly the one in a reporting visualization that took three developers two weeks to debug because nobody could hold the entire state machine in their head:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Bad: 250-line method doing everything\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eOrderResult\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eProcessOrder\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eOrderRequest\u003c/span\u003e \u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Validate customer\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Check inventory\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Calculate pricing\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Apply discounts\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Process payment\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Update inventory\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Send notifications\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Log everything\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Handle 12 different error cases\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// ... 200 more lines\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eModern C# actually makes this rule easier to enforce:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Good: orchestration method with clear intent\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eOrderResult\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eProcessOrder\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eOrderRequest\u003c/span\u003e \u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eValidateCustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCustomerId\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eavailability\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eCheckInventory\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eItems\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(!\u003c/span\u003e\u003cspan class=\"n\"\u003eavailability\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAllAvailable\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eOrderResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eOutOfStock\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eavailability\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUnavailableItems\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003epricing\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eCalculatePricing\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eItems\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTier\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003epayment\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eProcessPayment\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003epricing\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTotal\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(!\u003c/span\u003e\u003cspan class=\"n\"\u003epayment\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSucceeded\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eOrderResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePaymentFailed\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003epayment\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eReason\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eUpdateInventory\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eItems\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eSendConfirmation\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eOrderResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSuccess\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003epayment\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTransactionId\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\n\n\n\u003ch3 id=\"modern-enforcement-tools\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#modern-enforcement-tools\" title=\"Modern Enforcement Tools\"\u003eModern Enforcement Tools\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eUnlike 2006 C, modern tooling enforces this automatically. The \u003ca href=\"https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1505\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eCA1505 analyzer rule\u003c/a\u003e flags unmaintainable code based on cyclomatic complexity and maintainability index. Configure it in \u003ccode\u003e.editorconfig\u003c/code\u003e to treat violations as warnings, ensuring your team maintains digestible function sizes without manual review.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eLocal functions\u003c/strong\u003e reduce the need for small private methods:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eIEnumerable\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eOrder\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eGetRecentOrders\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eCustomer\u003c/span\u003e \u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003ecutoffDate\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eDateTime\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUtcNow\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddDays\u003c/span\u003e\u003cspan class=\"p\"\u003e(-\u003c/span\u003e\u003cspan class=\"m\"\u003e30\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eOrders\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWhere\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eIsRecent\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eOrderByDescending\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eo\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eo\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eDate\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003ebool\u003c/span\u003e \u003cspan class=\"n\"\u003eIsRecent\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eOrder\u003c/span\u003e \u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eDate\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026gt;=\u003c/span\u003e \u003cspan class=\"n\"\u003ecutoffDate\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003eVerdict:\u003c/strong\u003e If anything, aim for shorter than 60 lines. With expression-bodied members, local functions, and LINQ, there\u0026rsquo;s no excuse for bloated methods. Enable analyzers to enforce it.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"rule-5-assertion-density-of-two-per-function\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#rule-5-assertion-density-of-two-per-function\" title=\"Rule 5: Assertion Density of Two Per Function\"\u003eRule 5: Assertion Density of Two Per Function\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003eOriginal Intent:\u003c/strong\u003e Catch anomalous conditions early with runtime checks.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eC#/.NET Applicability:\u003c/strong\u003e Valid principle, but modern C# offers better mechanisms.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"the-modern-equivalent-multiple-layers-of-defense\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#the-modern-equivalent-multiple-layers-of-defense\" title=\"The Modern Equivalent: Multiple Layers of Defense\"\u003eThe Modern Equivalent: Multiple Layers of Defense\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eNASA wanted two runtime assertions per function. C# gives you something better—multiple enforcement layers, starting at compile time:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e1. Compile-Time Checks: Nullable Reference Types\u003c/strong\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cp\"\u003e#nullable\u003c/span\u003e \u003cspan class=\"n\"\u003eenable\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eOrderProcessor\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Compiler enforces non-null at compile time\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eOrderResult\u003c/span\u003e \u003cspan class=\"n\"\u003eProcess\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eOrder\u003c/span\u003e \u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eCustomer\u003c/span\u003e \u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// No runtime null check needed - compiler guarantees non-null\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003etotal\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eItems\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSum\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePrice\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// Nullable warns if customer.Email might be null\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eSendReceipt\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eEmail\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003etotal\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eOrderResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSuccess\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eSendReceipt\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eemail\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003edecimal\u003c/span\u003e \u003cspan class=\"n\"\u003eamount\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// email is guaranteed non-null by caller contract\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003e2. Parameter Validation: Guard Clauses\u003c/strong\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eProcessPayment\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003edecimal\u003c/span\u003e \u003cspan class=\"n\"\u003eamount\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ePaymentMethod\u003c/span\u003e \u003cspan class=\"n\"\u003emethod\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eArgumentNullException\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eThrowIfNull\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003emethod\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e  \u003cspan class=\"c1\"\u003e// .NET 6+\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eArgumentOutOfRangeException\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eThrowIfNegativeOrZero\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eamount\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Business logic here\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Pre-.NET 6 equivalent\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eProcessPayment\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003edecimal\u003c/span\u003e \u003cspan class=\"n\"\u003eamount\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ePaymentMethod\u003c/span\u003e \u003cspan class=\"n\"\u003emethod\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003emethod\u003c/span\u003e \u003cspan class=\"k\"\u003eis\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ethrow\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eArgumentNullException\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003enameof\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003emethod\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eamount\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026lt;=\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ethrow\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eArgumentOutOfRangeException\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003enameof\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eamount\u003c/span\u003e\u003cspan class=\"p\"\u003e),\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Amount must be positive\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Business logic here\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003e3. Debug Assertions for Internal Invariants\u003c/strong\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003eSystem.Diagnostics\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eUpdateBalance\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eAccount\u003c/span\u003e \u003cspan class=\"n\"\u003eaccount\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003edecimal\u003c/span\u003e \u003cspan class=\"n\"\u003edelta\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eDebug\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAssert\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eaccount\u003c/span\u003e \u003cspan class=\"p\"\u003e!=\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Account should never be null here\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eDebug\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAssert\u003c/span\u003e\u003cspan class=\"p\"\u003e(!\u003c/span\u003e\u003cspan class=\"n\"\u003eaccount\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eIsClosed\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Should not update closed accounts\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eaccount\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eBalance\u003c/span\u003e \u003cspan class=\"p\"\u003e+=\u003c/span\u003e \u003cspan class=\"n\"\u003edelta\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eDebug\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAssert\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eaccount\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eBalance\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026gt;=\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Balance should never go negative\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003eKey difference:\u003c/strong\u003e \u003ccode\u003eDebug.Assert\u003c/code\u003e is removed in Release builds, making it suitable for checking invariants during development without runtime cost. For mission-critical code, consider \u003ca href=\"https://learn.microsoft.com/en-us/dotnet/framework/debug-trace-profile/code-contracts\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eCode Contracts\u003c/a\u003e which provide formal preconditions, postconditions, and object invariants with static verification support.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"pattern-matching-for-exhaustiveness\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#pattern-matching-for-exhaustiveness\" title=\"Pattern Matching for Exhaustiveness\"\u003ePattern Matching for Exhaustiveness\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eModern C# can enforce completeness at compile time:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eGetStatusMessage\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eOrderStatus\u003c/span\u003e \u003cspan class=\"n\"\u003estatus\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003estatus\u003c/span\u003e \u003cspan class=\"k\"\u003eswitch\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eOrderStatus\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePending\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Order is pending\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eOrderStatus\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eProcessing\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Order is being processed\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eOrderStatus\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eShipped\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Order has been shipped\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eOrderStatus\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eDelivered\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Order delivered\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eOrderStatus\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCancelled\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Order cancelled\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Compiler error if any enum value is missing (with warnings enabled)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003eVerdict:\u003c/strong\u003e Use nullable reference types for compile-time null safety, guard clauses for parameter validation, and \u003ccode\u003eDebug.Assert\u003c/code\u003e for invariants. This gives you better than \u0026ldquo;two assertions per function\u0026rdquo;: you get enforcement at compile time where possible, and runtime checks where needed.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"rule-6-declare-data-at-smallest-possible-scope\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#rule-6-declare-data-at-smallest-possible-scope\" title=\"Rule 6: Declare Data at Smallest Possible Scope\"\u003eRule 6: Declare Data at Smallest Possible Scope\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003eOriginal Intent:\u003c/strong\u003e Minimize variable lifetime and potential misuse.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eC#/.NET Applicability:\u003c/strong\u003e Completely valid and reinforced by modern language features.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"modern-c-makes-this-easier\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#modern-c-makes-this-easier\" title=\"Modern C# Makes This Easier\"\u003eModern C# Makes This Easier\u003c/a\u003e\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Bad: variables declared too early\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eProcessData\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e\u003cspan class=\"p\"\u003e[]\u003c/span\u003e \u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003esum\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003ecount\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eaverage\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003emax\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// 50 lines later...\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e \u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLength\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e++)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003esum\u003c/span\u003e \u003cspan class=\"p\"\u003e+=\u003c/span\u003e \u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e];\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003ecount\u003c/span\u003e\u003cspan class=\"p\"\u003e++;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eaverage\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003esum\u003c/span\u003e \u003cspan class=\"p\"\u003e/\u003c/span\u003e \u003cspan class=\"n\"\u003ecount\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Another 30 lines...\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003emax\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eMax\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Good: declare at point of use\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eProcessData\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e\u003cspan class=\"p\"\u003e[]\u003c/span\u003e \u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// ... other logic ...\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003esum\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eforeach\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"k\"\u003evalue\u003c/span\u003e \u003cspan class=\"k\"\u003ein\u003c/span\u003e \u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003esum\u003c/span\u003e \u003cspan class=\"p\"\u003e+=\u003c/span\u003e \u003cspan class=\"k\"\u003evalue\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eaverage\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003esum\u003c/span\u003e \u003cspan class=\"p\"\u003e/\u003c/span\u003e \u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLength\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// ... other logic ...\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003emax\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eMax\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\n\n\n\u003ch3 id=\"pattern-matching-limits-scope-automatically\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#pattern-matching-limits-scope-automatically\" title=\"Pattern Matching Limits Scope Automatically\"\u003ePattern Matching Limits Scope Automatically\u003c/a\u003e\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Scope limited to when pattern matches\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eobj\u003c/span\u003e \u003cspan class=\"k\"\u003eis\u003c/span\u003e \u003cspan class=\"n\"\u003eCustomer\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"n\"\u003eIsActive\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kc\"\u003etrue\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e \u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// \u0026#39;customer\u0026#39; only exists in this block\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eProcessCustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// \u0026#39;customer\u0026#39; doesn\u0026#39;t exist here\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Switch expressions with declaration patterns\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003ediscount\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eorder\u003c/span\u003e \u003cspan class=\"k\"\u003eswitch\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"n\"\u003eTotal\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"m\"\u003e1000\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e \u003cspan class=\"n\"\u003elargeOrder\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003elargeOrder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTotal\u003c/span\u003e \u003cspan class=\"p\"\u003e*\u003c/span\u003e \u003cspan class=\"m\"\u003e0.1\u003c/span\u003e\u003cspan class=\"n\"\u003em\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"n\"\u003eCustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eIsPremium\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kc\"\u003etrue\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e \u003cspan class=\"n\"\u003epremiumOrder\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003epremiumOrder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTotal\u003c/span\u003e \u003cspan class=\"p\"\u003e*\u003c/span\u003e \u003cspan class=\"m\"\u003e0.05\u003c/span\u003e\u003cspan class=\"n\"\u003em\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003e_\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e};\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\n\n\n\u003ch3 id=\"using-declarations-for-resource-management\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#using-declarations-for-resource-management\" title=\"Using Declarations for Resource Management\"\u003eUsing Declarations for Resource Management\u003c/a\u003e\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Bad: resource scope too broad\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eReadConfig\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003estream\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eFile\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eOpenRead\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;config.json\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003etry\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// 100 lines of code\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eReadFromStream\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003estream\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003efinally\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003estream\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eDispose\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Good: using declaration limits scope\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eReadConfig\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003estream\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eFile\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eOpenRead\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;config.json\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eReadFromStream\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003estream\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// stream disposed here, not at method end\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Even better: compact using declaration (C# 8+)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eReadConfig\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003estream\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eFile\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eOpenRead\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;config.json\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eReadFromStream\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003estream\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// stream disposed at end of method automatically\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003eVerdict:\u003c/strong\u003e More relevant than ever. Pattern matching limits scope automatically. Using declarations prevent resource leaks. Block-scoped variables can\u0026rsquo;t escape their intended lifetime. C# 10\u0026rsquo;s file-scoped namespaces extend this principle to namespace declarations—one less level of indentation, one less place for variables to hide.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"rule-7-check-return-values-and-parameter-validity\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#rule-7-check-return-values-and-parameter-validity\" title=\"Rule 7: Check Return Values and Parameter Validity\"\u003eRule 7: Check Return Values and Parameter Validity\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003eOriginal Intent:\u003c/strong\u003e Never ignore potential failures; validate all inputs.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eC#/.NET Applicability:\u003c/strong\u003e Absolutely critical, but mechanisms differ.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"return-value-checking-exceptions-vs-result-types\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#return-value-checking-exceptions-vs-result-types\" title=\"Return Value Checking: Exceptions vs. Result Types\"\u003eReturn Value Checking: Exceptions vs. Result Types\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003e.NET uses exceptions for error conditions, unlike C\u0026rsquo;s return codes:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// .NET idiomatic: exception-based\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eSaveCustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eCustomer\u003c/span\u003e \u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eArgumentNullException\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eThrowIfNull\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003etry\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003edatabase\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSaveChanges\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e  \u003cspan class=\"c1\"\u003e// Throws on error\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ecatch\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eDbUpdateException\u003c/span\u003e \u003cspan class=\"n\"\u003eex\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003elogger\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLogError\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eex\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Failed to save customer {CustomerId}\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eId\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ethrow\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e  \u003cspan class=\"c1\"\u003e// Re-throw or wrap in domain exception\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eBut C#\u0026rsquo;s \u003ccode\u003eTry*\u003c/code\u003e pattern provides explicit success/failure:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// When failure is expected and not exceptional\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(!\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTryParse\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003einput\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"k\"\u003eout\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"k\"\u003evalue\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eConsole\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWriteLine\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Invalid number format\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(!\u003c/span\u003e\u003cspan class=\"n\"\u003einventory\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTryReserve\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eproductId\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003equantity\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"k\"\u003eout\u003c/span\u003e \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003ereservation\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eOrderResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eInsufficientStock\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\n\n\n\u003ch3 id=\"modern-pattern-result-types\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#modern-pattern-result-types\" title=\"Modern Pattern: Result Types\"\u003eModern Pattern: Result Types\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eWhen failure is a normal business outcome rather than an exceptional condition, exceptions feel wrong. Result types make success and failure explicit:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003erecord\u003c/span\u003e \u003cspan class=\"nc\"\u003eResult\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003ebool\u003c/span\u003e \u003cspan class=\"n\"\u003eSuccess\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"k\"\u003eget\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"k\"\u003einit\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e?\u003c/span\u003e \u003cspan class=\"n\"\u003eValue\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"k\"\u003eget\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"k\"\u003einit\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003estring?\u003c/span\u003e \u003cspan class=\"n\"\u003eError\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"k\"\u003eget\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"k\"\u003einit\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003estatic\u003c/span\u003e \u003cspan class=\"n\"\u003eResult\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eOk\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eT\u003c/span\u003e \u003cspan class=\"k\"\u003evalue\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"n\"\u003eSuccess\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"kc\"\u003etrue\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eValue\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003evalue\u003c/span\u003e \u003cspan class=\"p\"\u003e};\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003estatic\u003c/span\u003e \u003cspan class=\"n\"\u003eResult\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eFail\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eerror\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"n\"\u003eSuccess\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"kc\"\u003efalse\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eError\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eerror\u003c/span\u003e \u003cspan class=\"p\"\u003e};\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eResult\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eOrder\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ePlaceOrder\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eOrderRequest\u003c/span\u003e \u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003evalidationResult\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eValidateRequest\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(!\u003c/span\u003e\u003cspan class=\"n\"\u003evalidationResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSuccess\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eResult\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eOrder\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;.\u003c/span\u003e\u003cspan class=\"n\"\u003eFail\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003evalidationResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eError\u003c/span\u003e\u003cspan class=\"p\"\u003e!);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003epaymentResult\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eProcessPayment\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(!\u003c/span\u003e\u003cspan class=\"n\"\u003epaymentResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSuccess\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eResult\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eOrder\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;.\u003c/span\u003e\u003cspan class=\"n\"\u003eFail\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003epaymentResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eError\u003c/span\u003e\u003cspan class=\"p\"\u003e!);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eResult\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eOrder\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;.\u003c/span\u003e\u003cspan class=\"n\"\u003eOk\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eCreateOrder\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003epaymentResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eValue\u003c/span\u003e\u003cspan class=\"p\"\u003e!));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\n\n\n\u003ch3 id=\"parameter-validation-multiple-strategies\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#parameter-validation-multiple-strategies\" title=\"Parameter Validation: Multiple Strategies\"\u003eParameter Validation: Multiple Strategies\u003c/a\u003e\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eOrderService\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// 1. Constructor validation\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eOrderService\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eIOrderRepository\u003c/span\u003e \u003cspan class=\"n\"\u003erepository\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eILogger\u003c/span\u003e \u003cspan class=\"n\"\u003elogger\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eArgumentNullException\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eThrowIfNull\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003erepository\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eArgumentNullException\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eThrowIfNull\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003elogger\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003e_repository\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003erepository\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003e_logger\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003elogger\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// 2. Method parameter validation\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eOrder\u003c/span\u003e \u003cspan class=\"n\"\u003eCreateOrder\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eCustomer\u003c/span\u003e \u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eList\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eOrderItem\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eitems\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eArgumentNullException\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eThrowIfNull\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eArgumentNullException\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eThrowIfNull\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eitems\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eitems\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCount\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003ethrow\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eArgumentException\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Order must contain at least one item\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003enameof\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eitems\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eitems\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAny\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eQuantity\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026lt;=\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003ethrow\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eArgumentException\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;All items must have positive quantity\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003enameof\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eitems\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// Business logic\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\n\n\n\u003ch3 id=\"analyzer-support\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#analyzer-support\" title=\"Analyzer Support\"\u003eAnalyzer Support\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eRoslyn analyzers automatically detect unchecked return values. Enable \u003ca href=\"https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1806\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eCA1806\u003c/a\u003e to catch ignored method results and \u003ca href=\"https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0058\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eIDE0058\u003c/a\u003e to flag unused expression values. These analyzers ensure your team never silently discards important return information.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eVerdict:\u003c/strong\u003e Always validate parameters. Always handle exceptions or check return values. Use \u003ccode\u003eArgumentNullException.ThrowIfNull()\u003c/code\u003e for parameter checks, \u003ccode\u003eTry*\u003c/code\u003e methods for expected failures, and exceptions for exceptional conditions.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"rule-8-limited-preprocessor-use\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#rule-8-limited-preprocessor-use\" title=\"Rule 8: Limited Preprocessor Use\"\u003eRule 8: Limited Preprocessor Use\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003eOriginal Intent:\u003c/strong\u003e Avoid complex macros that obscure code meaning and hinder analysis.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eC#/.NET Applicability:\u003c/strong\u003e Largely irrelevant: C# preprocessor is far more constrained.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"what-c-doesnt-have-thank-goodness\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#what-c-doesnt-have-thank-goodness\" title=\"What C# Doesn\u0026rsquo;t Have (Thank Goodness)\"\u003eWhat C# Doesn\u0026rsquo;t Have (Thank Goodness)\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eC# preprocessor directives can\u0026rsquo;t:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eDefine function-like macros\u003c/li\u003e\n\u003cli\u003ePerform token pasting\u003c/li\u003e\n\u003cli\u003eCreate recursive macros\u003c/li\u003e\n\u003cli\u003eHide complex logic\u003c/li\u003e\n\u003c/ul\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// This is NOT possible in C#:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// #define MAX(a, b) ((a) \u0026gt; (b) ? (a) : (b))   // No function-like macros\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// What C# DOES support:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cp\"\u003e#define\u003c/span\u003e \u003cspan class=\"n\"\u003eDEBUG_LOGGING\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cp\"\u003e#if\u003c/span\u003e \u003cspan class=\"n\"\u003eDEBUG_LOGGING\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eConsole\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWriteLine\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Debug information\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cp\"\u003e#endif\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\n\n\n\u003ch3 id=\"the-real-concern-conditional-compilation-abuse\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#the-real-concern-conditional-compilation-abuse\" title=\"The Real Concern: Conditional Compilation Abuse\"\u003eThe Real Concern: Conditional Compilation Abuse\u003c/a\u003e\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Bad: excessive conditional compilation\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eConfigService\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eGetConnectionString\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"cp\"\u003e#if\u003c/span\u003e \u003cspan class=\"n\"\u003ePRODUCTION\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Production connection string\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"cp\"\u003e#elif\u003c/span\u003e \u003cspan class=\"n\"\u003eSTAGING\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Staging connection string\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"cp\"\u003e#elif\u003c/span\u003e \u003cspan class=\"n\"\u003eDEVELOPMENT\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Development connection string\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"cp\"\u003e#else\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Local connection string\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"cp\"\u003e#endif\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Good: configuration-based approach\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eConfigService\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eIConfiguration\u003c/span\u003e \u003cspan class=\"n\"\u003e_config\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eConfigService\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eIConfiguration\u003c/span\u003e \u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003e_config\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eGetConnectionString\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003e_config\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetConnectionString\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;DefaultConnection\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\n\n\n\u003ch3 id=\"legitimate-uses\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#legitimate-uses\" title=\"Legitimate Uses\"\u003eLegitimate Uses\u003c/a\u003e\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Acceptable: Debug-only code\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eProcessData\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e\u003cspan class=\"p\"\u003e[]\u003c/span\u003e \u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"cp\"\u003e#if\u003c/span\u003e \u003cspan class=\"n\"\u003eDEBUG\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eDebug\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAssert\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003edata\u003c/span\u003e \u003cspan class=\"p\"\u003e!=\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eDebug\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAssert\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLength\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"cp\"\u003e#endif\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Production code\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003esum\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eforeach\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"k\"\u003evalue\u003c/span\u003e \u003cspan class=\"k\"\u003ein\u003c/span\u003e \u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003esum\u003c/span\u003e \u003cspan class=\"p\"\u003e+=\u003c/span\u003e \u003cspan class=\"k\"\u003evalue\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Acceptable: Platform-specific code\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cp\"\u003e#if\u003c/span\u003e \u003cspan class=\"n\"\u003eWINDOWS\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [DllImport(\u0026#34;user32.dll\u0026#34;)]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"kd\"\u003estatic\u003c/span\u003e \u003cspan class=\"kd\"\u003eextern\u003c/span\u003e \u003cspan class=\"kt\"\u003ebool\u003c/span\u003e \u003cspan class=\"n\"\u003eShowWindow\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eIntPtr\u003c/span\u003e \u003cspan class=\"n\"\u003ehWnd\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003enCmdShow\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cp\"\u003e#elif\u003c/span\u003e \u003cspan class=\"n\"\u003eLINUX\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [DllImport(\u0026#34;libX11.so\u0026#34;)]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"kd\"\u003estatic\u003c/span\u003e \u003cspan class=\"kd\"\u003eextern\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eXOpenDisplay\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003edisplay_name\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cp\"\u003e#endif\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003eVerdict:\u003c/strong\u003e C#\u0026rsquo;s preprocessor is already minimal by design. Use it for debug-only code and platform-specific implementations. For everything else, use dependency injection and configuration.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"rule-9-restrict-pointer-use-max-one-level-of-dereferencing\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#rule-9-restrict-pointer-use-max-one-level-of-dereferencing\" title=\"Rule 9: Restrict Pointer Use (Max One Level of Dereferencing)\"\u003eRule 9: Restrict Pointer Use (Max One Level of Dereferencing)\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003eOriginal Intent:\u003c/strong\u003e Prevent complex pointer arithmetic and double-pointer confusion.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eC#/.NET Applicability:\u003c/strong\u003e Mostly irrelevant: managed code eliminates most pointer usage.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"the-net-alternative-references-are-not-pointers\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#the-net-alternative-references-are-not-pointers\" title=\"The .NET Alternative: References Are Not Pointers\"\u003eThe .NET Alternative: References Are Not Pointers\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eIn managed C#, you work with references, not pointers:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Safe by default - no pointer arithmetic\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eCustomer\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eName\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"k\"\u003eget\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"k\"\u003eset\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Reference semantics, but no pointer manipulation\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eCustomer\u003c/span\u003e \u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eCustomer\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"n\"\u003eName\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;John\u0026#34;\u003c/span\u003e \u003cspan class=\"p\"\u003e};\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eCustomer\u003c/span\u003e \u003cspan class=\"n\"\u003esame\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e  \u003cspan class=\"c1\"\u003e// same reference, not a copy\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003esame\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eName\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Jane\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eConsole\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWriteLine\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eName\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e  \u003cspan class=\"c1\"\u003e// Outputs: Jane\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\n\n\n\u003ch3 id=\"unsafe-code-when-you-actually-need-pointers\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#unsafe-code-when-you-actually-need-pointers\" title=\"Unsafe Code: When You Actually Need Pointers\"\u003eUnsafe Code: When You Actually Need Pointers\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eRare, but sometimes unavoidable for interop or extreme performance:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Performance-critical unsafe code - use sparingly\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003eunsafe\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eProcessBuffer\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003ebyte\u003c/span\u003e\u003cspan class=\"p\"\u003e[]\u003c/span\u003e \u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003efixed\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003ebyte\u003c/span\u003e\u003cspan class=\"p\"\u003e*\u003c/span\u003e \u003cspan class=\"n\"\u003eptr\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// One level of indirection - NASA would approve\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003ebyte\u003c/span\u003e\u003cspan class=\"p\"\u003e*\u003c/span\u003e \u003cspan class=\"n\"\u003ecurrent\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eptr\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e \u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLength\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e++)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e*\u003c/span\u003e\u003cspan class=\"n\"\u003ecurrent\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003ebyte\u003c/span\u003e\u003cspan class=\"p\"\u003e)(*\u003c/span\u003e\u003cspan class=\"n\"\u003ecurrent\u003c/span\u003e \u003cspan class=\"p\"\u003e*\u003c/span\u003e \u003cspan class=\"m\"\u003e2\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003ecurrent\u003c/span\u003e\u003cspan class=\"p\"\u003e++;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\n\n\n\u003ch3 id=\"modern-safe-alternative-span\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#modern-safe-alternative-span\" title=\"Modern Safe Alternative: Span\"\u003eModern Safe Alternative: Span\u003cT\u003e\u003c/a\u003e\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Safe, zero-allocation, pointer-like performance\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eProcessBuffer\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eSpan\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003ebyte\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e \u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLength\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e++)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003ebyte\u003c/span\u003e\u003cspan class=\"p\"\u003e)(\u003c/span\u003e\u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"p\"\u003e*\u003c/span\u003e \u003cspan class=\"m\"\u003e2\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Or even better:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eProcessBuffer\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eSpan\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003ebyte\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eforeach\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003eref\u003c/span\u003e \u003cspan class=\"kt\"\u003ebyte\u003c/span\u003e \u003cspan class=\"k\"\u003evalue\u003c/span\u003e \u003cspan class=\"k\"\u003ein\u003c/span\u003e \u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003evalue\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003ebyte\u003c/span\u003e\u003cspan class=\"p\"\u003e)(\u003c/span\u003e\u003cspan class=\"k\"\u003evalue\u003c/span\u003e \u003cspan class=\"p\"\u003e*\u003c/span\u003e \u003cspan class=\"m\"\u003e2\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\n\n\n\u003ch3 id=\"refinout-managed-pointer-like-semantics\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#refinout-managed-pointer-like-semantics\" title=\"ref/in/out: Managed \u0026ldquo;Pointer-Like\u0026rdquo; Semantics\"\u003eref/in/out: Managed \u0026ldquo;Pointer-Like\u0026rdquo; Semantics\u003c/a\u003e\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Pass by reference without pointers\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eUpdateCustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003eref\u003c/span\u003e \u003cspan class=\"n\"\u003eCustomer\u003c/span\u003e \u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eCustomer\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"n\"\u003eName\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Updated\u0026#34;\u003c/span\u003e \u003cspan class=\"p\"\u003e};\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Read-only reference (no copying large structs)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003edecimal\u003c/span\u003e \u003cspan class=\"n\"\u003eCalculateTotal\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003ein\u003c/span\u003e \u003cspan class=\"n\"\u003eLargeStruct\u003c/span\u003e \u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eValue1\u003c/span\u003e \u003cspan class=\"p\"\u003e+\u003c/span\u003e \u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eValue2\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e  \u003cspan class=\"c1\"\u003e// No defensive copy\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Output parameter\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003ebool\u003c/span\u003e \u003cspan class=\"n\"\u003eTryGetCustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eid\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"k\"\u003eout\u003c/span\u003e \u003cspan class=\"n\"\u003eCustomer\u003c/span\u003e \u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// ...\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003eVerdict:\u003c/strong\u003e You rarely need unsafe code in modern C#. Use \u003ccode\u003eSpan\u0026lt;T\u0026gt;\u003c/code\u003e/\u003ccode\u003eMemory\u0026lt;T\u0026gt;\u003c/code\u003e for performance-critical buffer manipulation. Use \u003ccode\u003eref\u003c/code\u003e/\u003ccode\u003ein\u003c/code\u003e/\u003ccode\u003eout\u003c/code\u003e for reference semantics. Reserve \u003ccode\u003eunsafe\u003c/code\u003e for true interop scenarios.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"rule-10-enable-all-compiler-warnings-and-use-static-analysis\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#rule-10-enable-all-compiler-warnings-and-use-static-analysis\" title=\"Rule 10: Enable All Compiler Warnings and Use Static Analysis\"\u003eRule 10: Enable All Compiler Warnings and Use Static Analysis\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003eOriginal Intent:\u003c/strong\u003e Catch bugs early through automated checking.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eC#/.NET Applicability:\u003c/strong\u003e Not just valid: vastly more powerful than 2006 C tooling.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"modern-tooling-is-incomparably-better\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#modern-tooling-is-incomparably-better\" title=\"Modern Tooling Is Incomparably Better\"\u003eModern Tooling Is Incomparably Better\u003c/a\u003e\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-xml\" data-lang=\"xml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c\"\u003e\u0026lt;!-- Essential .csproj settings --\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003e\u0026lt;PropertyGroup\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c\"\u003e\u0026lt;!-- Treat all warnings as errors --\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026lt;TreatWarningsAsErrors\u0026gt;\u003c/span\u003etrue\u003cspan class=\"nt\"\u003e\u0026lt;/TreatWarningsAsErrors\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c\"\u003e\u0026lt;!-- Enable nullable reference types --\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026lt;Nullable\u0026gt;\u003c/span\u003eenable\u003cspan class=\"nt\"\u003e\u0026lt;/Nullable\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c\"\u003e\u0026lt;!-- Enable latest code analysis --\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026lt;AnalysisLevel\u0026gt;\u003c/span\u003elatest\u003cspan class=\"nt\"\u003e\u0026lt;/AnalysisLevel\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026lt;EnforceCodeStyleInBuild\u0026gt;\u003c/span\u003etrue\u003cspan class=\"nt\"\u003e\u0026lt;/EnforceCodeStyleInBuild\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c\"\u003e\u0026lt;!-- Enable specific analyzer categories --\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026lt;AnalysisMode\u0026gt;\u003c/span\u003eAll\u003cspan class=\"nt\"\u003e\u0026lt;/AnalysisMode\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003e\u0026lt;/PropertyGroup\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\n\n\n\u003ch3 id=\"roslyn-analyzers-static-analysis-built-in\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#roslyn-analyzers-static-analysis-built-in\" title=\"Roslyn Analyzers: Static Analysis Built In\"\u003eRoslyn Analyzers: Static Analysis Built In\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eUnlike 2006 C compilers, \u003ca href=\"https://learn.microsoft.com/en-us/visualstudio/code-quality/roslyn-analyzers-overview\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eRoslyn analyzers\u003c/a\u003e provide deep semantic analysis. Rules like \u003ca href=\"https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca2000\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eCA2000\u003c/a\u003e catch undisposed resources, \u003ca href=\"https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1806\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eCA1806\u003c/a\u003e flags ignored return values, and \u003ca href=\"https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1062\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eCA1062\u003c/a\u003e enforces parameter validation: all without running your code. This compile-time safety net would have been science fiction to NASA\u0026rsquo;s 2006 C developers.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"editorconfig-for-team-standards\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#editorconfig-for-team-standards\" title=\"EditorConfig for Team Standards\"\u003eEditorConfig for Team Standards\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003e\u003ca href=\"https://editorconfig.org/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eEditorConfig files\u003c/a\u003e let you enforce code style and quality rules across your team. Beyond basic formatting, you can control diagnostic severity for specific analyzer rules: turning \u003ca href=\"https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1062\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eCA1062\u003c/a\u003e (validate arguments) into build errors while suppressing overly strict rules like \u003ca href=\"https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1303\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eCA1303\u003c/a\u003e (localized strings). This ensures consistent standards without lengthy code review discussions about style preferences.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"multiple-layers-of-analysis\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#multiple-layers-of-analysis\" title=\"Multiple Layers of Analysis\"\u003eMultiple Layers of Analysis\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eModern .NET provides defense in depth: compiler warnings catch syntax issues, \u003ca href=\"https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/overview\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eRoslyn analyzers\u003c/a\u003e enforce code quality (\u003ca href=\"https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eCA* rules\u003c/a\u003e) and style (\u003ca href=\"https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eIDE* rules\u003c/a\u003e). Extend this with third-party analyzers like \u003ca href=\"https://security-code-scan.github.io/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eSecurityCodeScan\u003c/a\u003e for security vulnerabilities, \u003ca href=\"https://github.com/semihokur/AsyncFixer\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eAsyncFixer\u003c/a\u003e for async/await pitfalls, or \u003ca href=\"https://www.nuget.org/packages/Microsoft.VisualStudio.Threading.Analyzers/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eMicrosoft.VisualStudio.Threading.Analyzers\u003c/a\u003e for threading issues. For enterprise scenarios, \u003ca href=\"https://www.sonarsource.com/products/sonarqube/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eSonarQube\u003c/a\u003e and \u003ca href=\"https://docs.github.com/en/code-security/code-scanning/introduction-to-code-scanning/about-code-scanning-with-codeql\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eGitHub Advanced Security\u003c/a\u003e with CodeQL provide continuous security scanning.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"automated-code-review\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#automated-code-review\" title=\"Automated Code Review\"\u003eAutomated Code Review\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eIntegrate analysis into CI/CD pipelines with \u003ca href=\"https://docs.github.com/en/actions\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eGitHub Actions\u003c/a\u003e, \u003ca href=\"https://learn.microsoft.com/en-us/azure/devops/pipelines/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eAzure Pipelines\u003c/a\u003e, or similar platforms. Configure builds to treat warnings as errors (\u003ccode\u003e/p:TreatWarningsAsErrors=true\u003c/code\u003e) so quality issues block merges automatically. This transforms static analysis from optional developer tooling into enforced team standards.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eVerdict:\u003c/strong\u003e This is the one rule where C# developers have it absurdly better than 2006 C developers. I\u0026rsquo;ve reviewed codebases where enabling \u003ccode\u003eTreatWarningsAsErrors\u003c/code\u003e found 47 bugs in 30 seconds—bugs that had been sitting there for months. Enable everything: compiler warnings, Roslyn analyzers, nullable reference types. Use \u003ccode\u003e.editorconfig\u003c/code\u003e to enforce team standards. Automate it in CI/CD so quality gates can\u0026rsquo;t be ignored when the deadline looms. There\u0026rsquo;s no excuse not to.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"summary-translation-guide\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#summary-translation-guide\" title=\"Summary: Translation Guide\"\u003eSummary: Translation Guide\u003c/a\u003e\u003c/h2\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003ePower of Ten Rule\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eC#/.NET Status\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eModern Equivalent\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e1. Simple control flow\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003ePartially valid\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eAvoid \u003ccode\u003egoto\u003c/code\u003e; recursion OK with care\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e2. Bounded loops\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eValid\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eUse \u003ccode\u003eforeach\u003c/code\u003e, LINQ \u003ccode\u003eTake()\u003c/code\u003e, \u003ccode\u003eCancellationToken\u003c/code\u003e\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e3. No dynamic allocation\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNot applicable\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eMinimize allocations with \u003ccode\u003eSpan\u0026lt;T\u0026gt;\u003c/code\u003e, \u003ca href=\"https://learn.microsoft.com/en-us/dotnet/api/system.buffers.arraypool-1\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e\u003ccode\u003eArrayPool\u0026lt;T\u0026gt;\u003c/code\u003e\u003c/a\u003e\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e4. Max 60 lines per function\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eAbsolutely valid\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eEnable \u003ca href=\"https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1505\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eCA1505\u003c/a\u003e, use local functions, extract methods\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e5. Two assertions per function\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eValid principle\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ca href=\"https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eNullable types\u003c/a\u003e + guard clauses + \u003ca href=\"https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.debug.assert\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e\u003ccode\u003eDebug.Assert\u003c/code\u003e\u003c/a\u003e\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e6. Minimal variable scope\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eAbsolutely valid\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003ePattern matching, using declarations, block scope\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e7. Check all returns\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eAbsolutely valid\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eValidate parameters, handle exceptions, use \u003ccode\u003eTry*\u003c/code\u003e pattern\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e8. Limited preprocessor\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eMostly irrelevant\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eC# preprocessor already constrained; use DI for config\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e9. Restrict pointers\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eMostly irrelevant\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eUse \u003ca href=\"https://learn.microsoft.com/en-us/dotnet/api/system.span-1\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e\u003ccode\u003eSpan\u0026lt;T\u0026gt;\u003c/code\u003e\u003c/a\u003e, \u003ccode\u003eref\u003c/code\u003e/\u003ccode\u003ein\u003c/code\u003e/\u003ccode\u003eout\u003c/code\u003e; reserve \u003ccode\u003eunsafe\u003c/code\u003e for interop\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e10. All warnings + static analysis\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eEmphatically valid\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eEnable \u003ca href=\"https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/overview\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eall analyzers\u003c/a\u003e, nullable types, treat warnings as errors\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\n\n\n\n\u003ch2 id=\"the-verdict-timeless-principles-modern-implementation\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#the-verdict-timeless-principles-modern-implementation\" title=\"The Verdict: Timeless Principles, Modern Implementation\"\u003eThe Verdict: Timeless Principles, Modern Implementation\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eFour rules (4, 6, 7, and 10) transfer directly with superior tooling. Function length limits, minimal scope, return value checking, static analysis—all work better in 2025 than 2006 C. Three rules (3, 8, 9) become largely irrelevant because .NET\u0026rsquo;s managed runtime provides better abstractions than manual memory management or preprocessor macros. The remaining rules (1, 2, 5) require contextual interpretation. Their principles remain sound, but modern C#\u0026rsquo;s language features and runtime safety nets fundamentally change implementation.\u003c/p\u003e\n\u003cp\u003eGerard Holzmann\u0026rsquo;s rules weren\u0026rsquo;t about C syntax. They encoded deeper principles—predictability, analyzability, defensive programming—that transcend any specific language. What\u0026rsquo;s different in 2025? Modern C# gives you powerful tools to enforce these principles without bare-metal constraints. \u003ca href=\"https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/overview\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eRoslyn analyzers\u003c/a\u003e catch bugs at compile time that required runtime assertions in embedded C. \u003ca href=\"https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eNullable reference types\u003c/a\u003e prevent null dereferences before code runs. \u003ca href=\"https://learn.microsoft.com/en-us/dotnet/api/system.span-1\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e\u003ccode\u003eSpan\u0026lt;T\u0026gt;\u003c/code\u003e\u003c/a\u003e delivers pointer-like performance with array-like safety.\u003c/p\u003e\n\u003cp\u003eFor safety-critical C# code—medical devices, financial systems, industrial control—absolutely adopt these principles. Enable every analyzer. Enforce short functions. Validate everything. Treat warnings as errors. But don\u0026rsquo;t cargo-cult embedded C constraints just because NASA used them. Embrace the GC when it makes sense, use modern abstractions that eliminate entire bug categories, and let the type system catch errors at compile time instead of in production. Your code will be safer, more maintainable, and more correct. Which is what Gerard Holzmann wanted all along.\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2025-12-10T16:45:00+01:00","id":"https://daily-devops.net/posts/dotnet-power-of-ten-rules/","language":"en","summary":"Holzmann's safety-critical coding rules hit harder in modern C#: Roslyn analyzers, nullable types, and the type system enforce what C only wished.\n","tags":["csharp","dotnet","bestpractices","codequality","softwareengineering"],"title":"Power of Ten Rules: More Relevant Than Ever for .NET","url":"https://daily-devops.net/posts/dotnet-power-of-ten-rules/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eYour microservice runs in a Kubernetes cluster, processing events from a message queue. Every hour, it purges stale cache entries. Every morning at 6 AM, it triggers a health check against downstream services. The service is stateless, ephemeral, and designed to scale horizontally based on load—we\u0026rsquo;ve seen it scale from 3 pods to 45 during traffic spikes.\u003c/p\u003e\n\u003cp\u003eYou need scheduled tasks, but adding a database for job persistence? That violates the entire stateless design principle. External schedulers like Kubernetes CronJobs? We tried that. Managing separate YAML manifests, container image versioning, and lifecycle coupling between the CronJob and the main deployment became a maintenance nightmare. When we needed to update the health check logic, we had to deploy both the main service \u003cem\u003eand\u003c/em\u003e the CronJob separately. Teams kept forgetting the second step.\u003c/p\u003e\n\u003cp\u003eNCronJob addresses this by embedding scheduling directly into the application using ASP.NET Core\u0026rsquo;s \u003ccode\u003eIHostedService\u003c/code\u003e. Jobs run in-process, require zero external dependencies, and scale with the application. The scheduler is lightweight—hundreds of lines of code, not thousands—and integrates seamlessly with dependency injection. The trade-off: jobs don\u0026rsquo;t persist, horizontal scaling can cause duplication, and advanced features like clustering or dashboards don\u0026rsquo;t exist.\u003c/p\u003e\n\u003cp\u003eFor microservices, containerized applications, or systems prioritizing simplicity over feature richness, NCronJob removes friction. For applications needing persistence, retry policies, or distributed coordination, alternative architectural approaches merit evaluation.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"architecture-ihostedservice-integration\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-5-ncronjob/#architecture-ihostedservice-integration\" title=\"Architecture: IHostedService Integration\"\u003eArchitecture: IHostedService Integration\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eNCronJob builds on \u003ccode\u003eIHostedService\u003c/code\u003e, the ASP.NET Core primitive for long-running background operations. When the application starts, NCronJob\u0026rsquo;s hosted service initializes, parses cron expressions, calculates next execution times, and schedules jobs using \u003ccode\u003eSystem.Threading.Timer\u003c/code\u003e. When execution times arrive, the scheduler invokes jobs via dependency injection, passing parameters if configured.\u003c/p\u003e\n\u003cp\u003eThe design is intentionally minimal. There\u0026rsquo;s no database, no external storage, no worker coordination beyond single-process execution. Jobs are defined as classes implementing \u003ccode\u003eIJob\u003c/code\u003e or via inline lambda expressions. The scheduler maintains an in-memory list of job definitions and fires them based on cron schedules.\u003c/p\u003e\n\u003cp\u003eThis simplicity makes NCronJob ideal for:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eMicroservices\u003c/strong\u003e: Each service schedules its own tasks without shared infrastructure.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eContainerized deployments\u003c/strong\u003e: Stateless containers start, execute scheduled tasks, and terminate without persisting job state.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eInternal tools\u003c/strong\u003e: Applications where background tasks are secondary concerns, not architectural focal points.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eNCronJob also supports instant jobs—one-time executions triggered programmatically, useful for workflows where scheduled tasks need manual activation or dependent tasks chain together.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"configuration-and-integration\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-5-ncronjob/#configuration-and-integration\" title=\"Configuration and Integration\"\u003eConfiguration and Integration\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eIntegrating NCronJob requires minimal setup. Install the NuGet package:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003edotnet add package NCronJob\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eRegister jobs and schedules in \u003ccode\u003eProgram.cs\u003c/code\u003e:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eServices\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddNCronJob\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eoptions\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eoptions\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddJob\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eCacheCleanupJob\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;(\u003c/span\u003e\u003cspan class=\"n\"\u003ejob\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ejob\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWithCronExpression\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;0 * * * *\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e \u003cspan class=\"c1\"\u003e// Hourly\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eoptions\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddJob\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eHealthCheckJob\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;(\u003c/span\u003e\u003cspan class=\"n\"\u003ejob\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ejob\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWithCronExpression\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;0 6 * * *\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e  \u003cspan class=\"c1\"\u003e// Daily at 6 AM\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eapp\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eBuild\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eapp\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUseNCronJobAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eapp\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRun\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eJobs implement \u003ccode\u003eIJob\u003c/code\u003e:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eCacheCleanupJob\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eIJob\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eICacheService\u003c/span\u003e \u003cspan class=\"n\"\u003e_cache\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eCacheCleanupJob\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eICacheService\u003c/span\u003e \u003cspan class=\"n\"\u003ecache\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003e_cache\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ecache\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e \u003cspan class=\"n\"\u003eRunAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eIJobExecutionContext\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eCancellationToken\u003c/span\u003e \u003cspan class=\"n\"\u003etoken\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_cache\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRemoveExpiredEntriesAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003etoken\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eNCronJob resolves dependencies from the DI container and injects them into jobs. The \u003ccode\u003eIJobExecutionContext\u003c/code\u003e provides metadata—execution time, parameters, cancellation tokens—enabling context-aware job logic.\u003c/p\u003e\n\u003cp\u003eInline jobs reduce boilerplate for simple tasks:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eoptions\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddJob\u003c/span\u003e\u003cspan class=\"p\"\u003e((\u003c/span\u003e\u003cspan class=\"n\"\u003eILogger\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eProgram\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003elogger\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003elogger\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLogInformation\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Heartbeat at {Time}\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eDateTime\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUtcNow\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e},\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;*/5 * * * *\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e \u003cspan class=\"c1\"\u003e// Every 5 minutes\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis fluent API mirrors Minimal APIs in ASP.NET Core, where route handlers are inline delegates with parameter injection. The consistency reduces cognitive load for developers familiar with modern .NET conventions.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"cron-expressions-and-scheduling-semantics\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-5-ncronjob/#cron-expressions-and-scheduling-semantics\" title=\"Cron Expressions and Scheduling Semantics\"\u003eCron Expressions and Scheduling Semantics\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eNCronJob uses standard five-field cron syntax:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e* * * * *\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e│ │ │ │ │\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e│ │ │ │ └─── Day of week \u003cspan class=\"o\"\u003e(\u003c/span\u003e0-7, where \u003cspan class=\"m\"\u003e0\u003c/span\u003e and \u003cspan class=\"m\"\u003e7\u003c/span\u003e are Sunday\u003cspan class=\"o\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e│ │ │ └───── Month \u003cspan class=\"o\"\u003e(\u003c/span\u003e1-12\u003cspan class=\"o\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e│ │ └─────── Day of month \u003cspan class=\"o\"\u003e(\u003c/span\u003e1-31\u003cspan class=\"o\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e│ └───────── Hour \u003cspan class=\"o\"\u003e(\u003c/span\u003e0-23\u003cspan class=\"o\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e└─────────── Minute \u003cspan class=\"o\"\u003e(\u003c/span\u003e0-59\u003cspan class=\"o\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eExamples:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003e0 * * * *\u003c/code\u003e: Every hour at minute 0.\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e0 6 * * *\u003c/code\u003e: Daily at 6 AM.\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e0 0 1 * *\u003c/code\u003e: Monthly on the 1st at midnight.\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e*/15 * * * *\u003c/code\u003e: Every 15 minutes.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eCron expressions are parsed at startup. Invalid expressions cause application startup failures, preventing silent misconfigurations. This fail-fast behavior aligns with modern cloud-native principles: catch configuration errors early, not in production.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"timezone-aware-cron-scheduling\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-5-ncronjob/#timezone-aware-cron-scheduling\" title=\"Timezone-Aware Cron Scheduling\"\u003eTimezone-Aware Cron Scheduling\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eNCronJob supports timezone-aware scheduling:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003eTimeZoneConverter\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eoptions\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddJob\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eReportJob\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;(\u003c/span\u003e\u003cspan class=\"n\"\u003ejob\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ejob\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWithCronExpression\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;0 9 * * *\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWithTimeZone\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eTZConvert\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetTimeZoneInfo\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;America/New_York\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis ensures jobs fire at correct local times regardless of server timezone settings—critical for multi-region deployments or applications serving global users.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"job-parameters-and-instant-execution\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-5-ncronjob/#job-parameters-and-instant-execution\" title=\"Job Parameters and Instant Execution\"\u003eJob Parameters and Instant Execution\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eJobs often need parameters—identifiers, configuration values, dynamic inputs. NCronJob passes parameters via the execution context:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eoptions\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddJob\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eDataImportJob\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;(\u003c/span\u003e\u003cspan class=\"n\"\u003ejob\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ejob\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWithCronExpression\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;0 2 * * *\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWithParameter\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;source\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;external-api\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eDataImportJob\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eIJob\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e \u003cspan class=\"n\"\u003eRunAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eIJobExecutionContext\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eCancellationToken\u003c/span\u003e \u003cspan class=\"n\"\u003etoken\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003esource\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eParameter\u003c/span\u003e \u003cspan class=\"k\"\u003eas\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eImportDataAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003esource\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003etoken\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\n\n\n\u003ch3 id=\"triggering-jobs-programmatically\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-5-ncronjob/#triggering-jobs-programmatically\" title=\"Triggering Jobs Programmatically\"\u003eTriggering Jobs Programmatically\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eFor workflows requiring manual job execution, NCronJob provides instant jobs via \u003ccode\u003eIInstantJobRegistry\u003c/code\u003e:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eOrderService\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eIInstantJobRegistry\u003c/span\u003e \u003cspan class=\"n\"\u003e_registry\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eOrderService\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eIInstantJobRegistry\u003c/span\u003e \u003cspan class=\"n\"\u003eregistry\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003e_registry\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eregistry\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e \u003cspan class=\"n\"\u003eCompleteOrderAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eorderId\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// Process order logic...\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_registry\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRunInstantJobAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eSendConfirmationJob\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;(\u003c/span\u003e\u003cspan class=\"n\"\u003eorderId\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eInstant jobs execute immediately on background threads, decoupling them from HTTP request lifetimes. This pattern suits scenarios where scheduled tasks need programmatic triggers—user-initiated reports, dependent workflows, or event-driven processing.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"job-dependencies-and-chaining\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-5-ncronjob/#job-dependencies-and-chaining\" title=\"Job Dependencies and Chaining\"\u003eJob Dependencies and Chaining\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eNCronJob supports job dependencies, enabling workflows where job B executes only after job A succeeds or fails:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eoptions\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddJob\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eImportDataJob\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;(\u003c/span\u003e\u003cspan class=\"n\"\u003ejob\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ejob\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWithCronExpression\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;0 2 * * *\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eExecuteWhen\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003esuccess\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003es\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003es\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRunJob\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eTransformDataJob\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;(),\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003efaulted\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003es\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003es\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRunJob\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eNotifyFailureJob\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;()));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eWhen \u003ccode\u003eImportDataJob\u003c/code\u003e succeeds, \u003ccode\u003eTransformDataJob\u003c/code\u003e executes automatically. If it fails, \u003ccode\u003eNotifyFailureJob\u003c/code\u003e handles the error. This declarative approach simplifies common workflows without custom orchestration logic.\u003c/p\u003e\n\u003cp\u003eJob chaining also supports inline delegates:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eoptions\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddJob\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eProcessFileJob\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;(\u003c/span\u003e\u003cspan class=\"n\"\u003ejob\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ejob\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWithCronExpression\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;0 3 * * *\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eExecuteWhen\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003esuccess\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003es\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003es\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRunJob\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eINotificationService\u003c/span\u003e \u003cspan class=\"n\"\u003enotifier\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003enotifier\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSendAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;File processed successfully\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e})));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis fluency reduces boilerplate for simple dependent tasks, keeping configuration concise.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"startup-jobs-and-application-lifecycle\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-5-ncronjob/#startup-jobs-and-application-lifecycle\" title=\"Startup Jobs and Application Lifecycle\"\u003eStartup Jobs and Application Lifecycle\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eSome tasks must run immediately when the application starts—cache warming, database migrations, configuration validation. NCronJob supports startup jobs:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eoptions\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddJob\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eCacheWarmupJob\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;(\u003c/span\u003e\u003cspan class=\"n\"\u003ejob\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ejob\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRunAtStartup\u003c/span\u003e\u003cspan class=\"p\"\u003e());\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe \u003ccode\u003eUseNCronJobAsync()\u003c/code\u003e method executes startup jobs before the application begins serving requests, ensuring initialization completes synchronously. This prevents race conditions where HTTP requests arrive before background tasks finish preparing the system.\u003c/p\u003e\n\u003cp\u003eStartup jobs block application startup. If they fail, the application doesn\u0026rsquo;t start—matching fail-fast principles. For long-running initialization, consider splitting startup tasks into instant jobs triggered asynchronously after startup.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"stateless-design-and-cloud-native-fit\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-5-ncronjob/#stateless-design-and-cloud-native-fit\" title=\"Stateless Design and Cloud-Native Fit\"\u003eStateless Design and Cloud-Native Fit\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eNCronJob\u0026rsquo;s stateless design aligns with cloud-native architectures. Jobs run in-process without external dependencies, making deployments trivial: package the application, deploy it, and scheduling works. There\u0026rsquo;s no database to provision, no connection strings to manage, no external services to monitor.\u003c/p\u003e\n\u003cp\u003eThis simplicity shines in Kubernetes environments. Deploy multiple replicas of a service, and each runs its own scheduler. For idempotent tasks—cache cleanup, health checks—duplicate execution across replicas is harmless. For tasks requiring exactly-once execution, use external coordination mechanisms like distributed locks (e.g., \u003ccode\u003eDistributedLock\u003c/code\u003e NuGet package) or delegate scheduling to Kubernetes CronJobs.\u003c/p\u003e\n\u003cp\u003eNCronJob\u0026rsquo;s minimal footprint also reduces resource consumption. No database polling, no network I/O for coordination, no persistent storage writes. Jobs execute with negligible overhead, suitable for resource-constrained environments like edge devices or cost-sensitive cloud deployments.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"limitations-and-trade-offs\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-5-ncronjob/#limitations-and-trade-offs\" title=\"Limitations and Trade-offs\"\u003eLimitations and Trade-offs\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eNCronJob\u0026rsquo;s simplicity imposes constraints:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eNo persistence\u003c/strong\u003e: Jobs don\u0026rsquo;t survive application restarts. If a scheduled task should have executed while the application was down, it won\u0026rsquo;t run upon restart. For workflows requiring guaranteed execution, Hangfire or TickerQ provide persistence.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eNo clustering\u003c/strong\u003e: Multiple instances execute jobs independently. Without external coordination, duplicate execution occurs. For tasks that must run exactly once across a cluster, use distributed locks or alternative schedulers.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eNo dashboard\u003c/strong\u003e: Observability relies on application logging and external monitoring tools. Teams needing real-time job visibility should consider Hangfire or TickerQ.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eNo automatic retries\u003c/strong\u003e: Failed jobs don\u0026rsquo;t retry unless explicitly coded. Hangfire\u0026rsquo;s built-in retry policies and Quartz.NET\u0026rsquo;s misfire handling don\u0026rsquo;t exist in NCronJob.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"constraints-as-design-choices\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-5-ncronjob/#constraints-as-design-choices\" title=\"Constraints As Design Choices\"\u003eConstraints As Design Choices\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThese limitations aren\u0026rsquo;t flaws—they\u0026rsquo;re intentional design choices favoring simplicity. For applications where jobs are transient, observability comes from logs, and horizontal scaling doesn\u0026rsquo;t require coordination, NCronJob\u0026rsquo;s constraints are acceptable.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"when-ncronjob-fits\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-5-ncronjob/#when-ncronjob-fits\" title=\"When NCronJob Fits\"\u003eWhen NCronJob Fits\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eNCronJob excels when:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eStateless deployments are required\u003c/strong\u003e: Containerized microservices, serverless functions, or ephemeral environments benefit from zero-dependency scheduling.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eJobs are idempotent or non-critical\u003c/strong\u003e: Tasks like cache warming, health checks, or metrics collection tolerate occasional duplication or missed executions.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eSimplicity trumps features\u003c/strong\u003e: Teams that value minimal configuration and zero operational overhead over dashboards, clustering, or persistence.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eNative .NET integration is prioritized\u003c/strong\u003e: Developers comfortable with \u003ccode\u003eIHostedService\u003c/code\u003e and modern .NET conventions find NCronJob\u0026rsquo;s API familiar and consistent.\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\n\n\n\n\u003ch3 id=\"when-to-pick-a-different-scheduler\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-5-ncronjob/#when-to-pick-a-different-scheduler\" title=\"When To Pick A Different Scheduler\"\u003eWhen To Pick A Different Scheduler\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eNCronJob is less suitable when:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003ePersistence is required\u003c/strong\u003e: User-initiated reports, financial workflows, or critical business processes demand database-backed job storage (see Hangfire or TickerQ).\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eClustering is essential\u003c/strong\u003e: Distributed systems needing coordinated job execution across instances should use Quartz.NET or external coordination.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eObservability and dashboards matter\u003c/strong\u003e: Production systems requiring real-time job visibility benefit from Hangfire or TickerQ\u0026rsquo;s monitoring features.\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\n\n\n\n\u003ch2 id=\"practical-takeaways\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-5-ncronjob/#practical-takeaways\" title=\"Practical Takeaways\"\u003ePractical Takeaways\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eNCronJob occupies the absolute minimalism position in the scheduling spectrum. It delivers cron-based scheduling with zero dependencies, ideal for cloud-native architectures prioritizing statelessness and simplicity.\u003c/p\u003e\n\u003cp\u003eConsider NCronJob if:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eYour application runs in Kubernetes, serverless, or containerized environments.\u003c/li\u003e\n\u003cli\u003eBackground tasks are transient and don\u0026rsquo;t require durability.\u003c/li\u003e\n\u003cli\u003eYou value zero operational overhead and native .NET integration.\u003c/li\u003e\n\u003cli\u003eJobs are idempotent or tolerate occasional duplication.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eAvoid NCronJob if:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eJobs must persist across restarts (see Hangfire or TickerQ).\u003c/li\u003e\n\u003cli\u003eHorizontal scaling requires coordinated execution (see Quartz.NET).\u003c/li\u003e\n\u003cli\u003eYou need built-in dashboards or retry policies (see Hangfire).\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThe next article explores TickerQ, a framework representing the modern generation of .NET job schedulers. It combines Entity Framework Core persistence with source generation for reflection-free execution, real-time dashboards with SignalR, and async-first design for cloud-native performance—bridging the gap between simplicity and enterprise-grade capabilities.\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2025-12-09T17:00:00+01:00","id":"https://daily-devops.net/posts/dotnet-job-scheduling-5-ncronjob/","language":"en","summary":"NCronJob plugs into ASP.NET Core hosting to deliver zero-dependency, cron-based scheduling for microservices and containerized .NET deployments.","tags":["dotnet","csharp","architecture","nuget","softwareengineering"],"title":".NET Job Scheduling — NCronJob and Native Minimalism","url":"https://daily-devops.net/posts/dotnet-job-scheduling-5-ncronjob/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eYou\u0026rsquo;re building an internal dashboard that aggregates metrics from multiple APIs. Every ten minutes, background tasks fetch data, transform it, and cache results. The dashboard serves a small team—ten users maximum—and runs on a single Azure App Service instance. Sure, if the app restarts at 2 AM during a platform update, you lose the scheduled job. But honestly? The next scheduled run happens at 2:10 AM anyway. The ten-minute gap doesn\u0026rsquo;t justify spinning up SQL Server just for job persistence.\u003c/p\u003e\n\u003cp\u003eYou need background scheduling, but not infrastructure overkill when failures are inconsequential and the application restarts cleanly.\u003c/p\u003e\n\u003cp\u003eCoravel targets this scenario: applications where simplicity, developer velocity, and rapid iteration outweigh the need for persistent job storage or distributed coordination. It provides fluent APIs for scheduling, queuing, caching, and even mailing—all without external dependencies. Jobs run in-memory, configuration is minimal, and integration with ASP.NET Core feels native. The trade-off: jobs don\u0026rsquo;t survive application restarts, and scaling horizontally requires external coordination.\u003c/p\u003e\n\u003cp\u003eFor small to medium applications—internal tools, MVPs, low-traffic SaaS products—Coravel reduces time from idea to deployment. For large-scale systems demanding persistence and clustering, the architectural constraints merit consideration.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"architecture-in-memory-simplicity\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-4-coravel/#architecture-in-memory-simplicity\" title=\"Architecture: In-Memory Simplicity\"\u003eArchitecture: In-Memory Simplicity\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eCoravel\u0026rsquo;s architecture centers on in-memory task scheduling. It uses \u003ccode\u003eSystem.Threading.Timer\u003c/code\u003e under the hood, wrapped in a fluent API that hides timer management complexity. Jobs are defined as classes implementing \u003ccode\u003eIInvocable\u003c/code\u003e or as inline lambda expressions. The scheduler maintains a list of scheduled tasks and fires them based on configured intervals or cron expressions.\u003c/p\u003e\n\u003cp\u003eThere\u0026rsquo;s no database, no message queue, no external storage. When you schedule a job, Coravel stores its definition in memory. When the application restarts, scheduled jobs disappear. This design minimizes operational overhead but requires accepting transience: if persistence matters, Coravel isn\u0026rsquo;t the solution.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"what-comes-bundled-in-the-box\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-4-coravel/#what-comes-bundled-in-the-box\" title=\"What Comes Bundled In The Box\"\u003eWhat Comes Bundled In The Box\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eCoravel provides several integrated features beyond scheduling:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eTask scheduling\u003c/strong\u003e: Cron-based or interval-based job execution.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eQueuing\u003c/strong\u003e: Offload work to background queues processed asynchronously.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCaching\u003c/strong\u003e: In-memory caching with expiration and eviction policies.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMailing\u003c/strong\u003e: SMTP-based email sending with Razor template support.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eEvent broadcasting\u003c/strong\u003e: Loosely coupled event-driven architectures.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThese features share a common design philosophy: convention over configuration. Coravel assumes sensible defaults, reduces boilerplate, and optimizes for developer ergonomics. Teams that value rapid prototyping and reduced operational complexity benefit from this approach.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"configuration-and-integration\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-4-coravel/#configuration-and-integration\" title=\"Configuration and Integration\"\u003eConfiguration and Integration\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eIntegrating Coravel into an ASP.NET Core application requires minimal setup. Install the NuGet package:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003edotnet add package Coravel\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eRegister services and configure scheduling in \u003ccode\u003eProgram.cs\u003c/code\u003e:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eServices\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddScheduler\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eServices\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddTransient\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eDataRefreshTask\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eapp\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eBuild\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eapp\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eServices\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUseScheduler\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003escheduler\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003escheduler\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSchedule\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eDataRefreshTask\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;().\u003c/span\u003e\u003cspan class=\"n\"\u003eEveryTenMinutes\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eapp\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRun\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis configuration schedules \u003ccode\u003eDataRefreshTask\u003c/code\u003e to execute every ten minutes. The task implements \u003ccode\u003eIInvocable\u003c/code\u003e:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eDataRefreshTask\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eIInvocable\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eIApiClient\u003c/span\u003e \u003cspan class=\"n\"\u003e_apiClient\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eICacheService\u003c/span\u003e \u003cspan class=\"n\"\u003e_cacheService\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eDataRefreshTask\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eIApiClient\u003c/span\u003e \u003cspan class=\"n\"\u003eapiClient\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eICacheService\u003c/span\u003e \u003cspan class=\"n\"\u003ecacheService\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003e_apiClient\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eapiClient\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003e_cacheService\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ecacheService\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e \u003cspan class=\"n\"\u003eInvoke\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003edata\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_apiClient\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFetchMetricsAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003e_cacheService\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eStore\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;metrics\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eTimeSpan\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFromMinutes\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e10\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eCoravel resolves \u003ccode\u003eDataRefreshTask\u003c/code\u003e from the DI container, injecting dependencies automatically. This feels consistent with ASP.NET Core\u0026rsquo;s conventions—no special registration or service location patterns required.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"scheduling-inline-lambdas\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-4-coravel/#scheduling-inline-lambdas\" title=\"Scheduling Inline Lambdas\"\u003eScheduling Inline Lambdas\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eAlternatively, schedule inline tasks for quick prototyping:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003escheduler\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSchedule\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003escope\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eapp\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eServices\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCreateScope\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003elogger\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003escope\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eServiceProvider\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetRequiredService\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eILogger\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eProgram\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u0026gt;();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003elogger\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLogInformation\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Health check executed at {Time}\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eDateTime\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUtcNow\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e})\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eEveryFiveMinutes\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eInline tasks bypass the need for separate classes, accelerating development when job logic is simple.\u003c/p\u003e\n\u003cp\u003eCoravel\u0026rsquo;s fluent API supports various scheduling patterns:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003escheduler\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSchedule\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eEmailDigestTask\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eDaily\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAt\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e8\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e  \u003cspan class=\"c1\"\u003e// 8:00 AM\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWeekday\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003escheduler\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSchedule\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eReportTask\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCron\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;0 0 1 * *\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e  \u003cspan class=\"c1\"\u003e// Monthly at midnight on the 1st\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe API reads like natural language, reducing cognitive load compared to raw cron syntax.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"queuing-for-asynchronous-work\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-4-coravel/#queuing-for-asynchronous-work\" title=\"Queuing for Asynchronous Work\"\u003eQueuing for Asynchronous Work\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eCoravel\u0026rsquo;s queuing feature offloads time-consuming operations from HTTP request threads. Unlike scheduling, which triggers jobs at specific times, queuing executes jobs as soon as workers are available.\u003c/p\u003e\n\u003cp\u003eConfigure queuing:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eServices\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddQueue\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eEnqueue jobs from controllers or services:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eOrderController\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eControllerBase\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eIQueue\u003c/span\u003e \u003cspan class=\"n\"\u003e_queue\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eOrderController\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eIQueue\u003c/span\u003e \u003cspan class=\"n\"\u003equeue\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003e_queue\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003equeue\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [HttpPost]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eIActionResult\u003c/span\u003e \u003cspan class=\"n\"\u003eProcessOrder\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eOrder\u003c/span\u003e \u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003e_queue\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eQueueInvocableWithPayload\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eProcessOrderTask\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eOrder\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;(\u003c/span\u003e\u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eAccepted\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe task receives the payload:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eProcessOrderTask\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eIInvocable\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eIInvocableWithPayload\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eOrder\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eOrder\u003c/span\u003e \u003cspan class=\"n\"\u003ePayload\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"k\"\u003eget\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"k\"\u003eset\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e \u003cspan class=\"n\"\u003eInvoke\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// Process order asynchronously\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eProcessAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ePayload\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\n\n\n\u003ch3 id=\"the-cost-of-an-in-memory-queue\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-4-coravel/#the-cost-of-an-in-memory-queue\" title=\"The Cost Of An In-Memory Queue\"\u003eThe Cost Of An In-Memory Queue\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eQueued jobs execute on background threads managed by Coravel. The queue is in-memory—jobs don\u0026rsquo;t persist if the application restarts. For critical workflows requiring durability, Hangfire\u0026rsquo;s persistent queues or message brokers like RabbitMQ are necessary.\u003c/p\u003e\n\u003cp\u003eCoravel\u0026rsquo;s queue simplicity suits scenarios where occasional job loss is acceptable—cache warming, non-critical notifications, internal tools. For user-facing workflows like payment processing, persistence is non-negotiable.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"caching-and-mailing-integrated-conveniences\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-4-coravel/#caching-and-mailing-integrated-conveniences\" title=\"Caching and Mailing: Integrated Conveniences\"\u003eCaching and Mailing: Integrated Conveniences\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eCoravel bundles caching and mailing features that reduce dependency on third-party libraries.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eCaching\u003c/strong\u003e wraps \u003ccode\u003eIMemoryCache\u003c/code\u003e with a fluent API:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eServices\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddCache\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Usage\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003e_cache\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRemember\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;user-123\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eFetchUserAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e123\u003c/span\u003e\u003cspan class=\"p\"\u003e),\u003c/span\u003e \u003cspan class=\"n\"\u003eTimeSpan\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFromMinutes\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e5\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe \u003ccode\u003eRemember\u003c/code\u003e method fetches data from cache if available, otherwise invokes the factory function, caches the result, and returns it. This pattern reduces boilerplate compared to manual cache checks.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"sending-mail-with-razor-templates\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-4-coravel/#sending-mail-with-razor-templates\" title=\"Sending Mail With Razor Templates\"\u003eSending Mail With Razor Templates\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003eMailing\u003c/strong\u003e supports SMTP and in-memory drivers:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eServices\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddMailer\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eConfiguration\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// appsettings.json\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"s\"\u003e\u0026#34;Coravel\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"s\"\u003e\u0026#34;Mail\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"s\"\u003e\u0026#34;Driver\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;SMTP\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"s\"\u003e\u0026#34;Host\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;smtp.example.com\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"s\"\u003e\u0026#34;Port\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"m\"\u003e587\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"s\"\u003e\u0026#34;Username\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;user\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"s\"\u003e\u0026#34;Password\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;pass\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eSend emails using Razor templates:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eWelcomeEmail\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eMailable\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eUser\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"n\"\u003eUser\u003c/span\u003e \u003cspan class=\"n\"\u003e_user\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eWelcomeEmail\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eUser\u003c/span\u003e \u003cspan class=\"n\"\u003euser\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003e_user\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003euser\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003eoverride\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eBuild\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eTo\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003e_user\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eEmail\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFrom\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;noreply@example.com\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSubject\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Welcome!\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eView\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;~/Views/Emails/Welcome.cshtml\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003e_user\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Send email\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_mailer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSendAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eWelcomeEmail\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003euser\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eCoravel\u0026rsquo;s mailing abstraction simplifies email workflows common in small applications—welcome emails, password resets, notifications. For high-volume transactional email requiring templates, deliverability tracking, and vendor integrations, specialized services like SendGrid or Mailgun are more appropriate.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"event-broadcasting-for-loose-coupling\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-4-coravel/#event-broadcasting-for-loose-coupling\" title=\"Event Broadcasting for Loose Coupling\"\u003eEvent Broadcasting for Loose Coupling\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eCoravel\u0026rsquo;s event system decouples components via publish-subscribe patterns:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eServices\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddEvents\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Define event\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eOrderPlacedEvent\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eOrderId\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"k\"\u003eget\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"k\"\u003eset\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Define listener\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eSendOrderConfirmationListener\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eIListener\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eOrderPlacedEvent\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e \u003cspan class=\"n\"\u003eHandleAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eOrderPlacedEvent\u003c/span\u003e \u003cspan class=\"n\"\u003e@event\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eSendConfirmationEmailAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003e@event\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eOrderId\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Register listener\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eServices\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddTransient\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eIListener\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eOrderPlacedEvent\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;,\u003c/span\u003e \u003cspan class=\"n\"\u003eSendOrderConfirmationListener\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Broadcast event\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003e_dispatcher\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eBroadcast\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eOrderPlacedEvent\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"n\"\u003eOrderId\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e123\u003c/span\u003e \u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eListeners execute synchronously unless queued via \u003ccode\u003eQueueBroadcast\u003c/code\u003e, which processes them asynchronously. This pattern suits workflows where side effects—logging, notifications, analytics—shouldn\u0026rsquo;t block primary operations.\u003c/p\u003e\n\u003cp\u003eCoravel\u0026rsquo;s event system is in-process. For distributed event-driven architectures spanning multiple services, message brokers like Azure Service Bus or RabbitMQ provide guarantees Coravel doesn\u0026rsquo;t: durability, at-least-once delivery, and cross-service communication.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"developer-experience-and-rapid-prototyping\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-4-coravel/#developer-experience-and-rapid-prototyping\" title=\"Developer Experience and Rapid Prototyping\"\u003eDeveloper Experience and Rapid Prototyping\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eCoravel\u0026rsquo;s primary strength is developer experience. Its fluent APIs, convention-driven design, and zero external dependencies accelerate development cycles. Teams building MVPs, internal tools, or low-traffic applications spend less time configuring infrastructure and more time delivering features.\u003c/p\u003e\n\u003cp\u003eConsider a startup building a SaaS product. The initial version supports a few dozen users and runs on a single server. Background tasks refresh caches, send emails, and clean up temporary files. Coravel handles these needs without requiring database setup, message queue configuration, or understanding distributed systems concepts. As the product scales, the team can migrate to Hangfire or Quartz.NET—but during the critical early phase, Coravel removes friction.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"why-solo-developers-reach-for-coravel\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-4-coravel/#why-solo-developers-reach-for-coravel\" title=\"Why Solo Developers Reach For Coravel\"\u003eWhy Solo Developers Reach For Coravel\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eCoravel also appeals to solo developers or small teams lacking DevOps expertise. Deploying Hangfire requires provisioning SQL Server, managing connection strings, and monitoring database health. Coravel deploys with the application—no external dependencies, no infrastructure configuration.\u003c/p\u003e\n\u003cp\u003eThe trade-offs are clear: jobs don\u0026rsquo;t persist, scaling horizontally requires external coordination, and observability relies on application logging. For systems where these limitations are acceptable, Coravel\u0026rsquo;s simplicity is a competitive advantage.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"when-coravel-fits\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-4-coravel/#when-coravel-fits\" title=\"When Coravel Fits\"\u003eWhen Coravel Fits\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eCoravel excels when:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eSimplicity and velocity are priorities\u003c/strong\u003e: Teams that value rapid iteration over operational robustness benefit from Coravel\u0026rsquo;s zero-configuration approach.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eJob persistence is unnecessary\u003c/strong\u003e: Applications where background tasks are ephemeral—cache refreshes, health checks, non-critical notifications—don\u0026rsquo;t need database-backed durability.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eSingle-instance deployments suffice\u003c/strong\u003e: Applications running on a single server or containerized environment without horizontal scaling requirements fit Coravel\u0026rsquo;s in-memory design.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eIntegrated features reduce dependencies\u003c/strong\u003e: Teams that need scheduling, queuing, caching, and mailing without pulling in multiple libraries appreciate Coravel\u0026rsquo;s bundled approach.\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eCoravel is less suitable when:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003ePersistence is non-negotiable\u003c/strong\u003e: User-initiated reports, financial transactions, or workflows requiring guaranteed execution demand database-backed storage (see Hangfire or TickerQ).\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eHorizontal scaling is planned\u003c/strong\u003e: Running multiple instances without job duplication requires external coordination mechanisms Coravel doesn\u0026rsquo;t provide.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eHigh observability is critical\u003c/strong\u003e: Production systems needing detailed job execution history, failure analysis, and dashboards benefit from Hangfire or Quartz.NET.\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\n\n\n\n\u003ch2 id=\"operational-simplicity-and-limitations\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-4-coravel/#operational-simplicity-and-limitations\" title=\"Operational Simplicity and Limitations\"\u003eOperational Simplicity and Limitations\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eCoravel\u0026rsquo;s operational footprint is minimal. It runs in-process, consumes minimal memory, and requires no external services. Deployments are straightforward: push the application, and scheduling works. There\u0026rsquo;s no database schema to migrate, no message queue to monitor, no clustering configuration to tune.\u003c/p\u003e\n\u003cp\u003eThe limitations stem from this simplicity. Jobs vanish on restart. If your application crashes mid-execution, queued work is lost. Horizontal scaling without external coordination leads to duplicate job execution—multiple instances schedule the same tasks independently.\u003c/p\u003e\n\u003cp\u003eFor applications where these constraints are acceptable, Coravel\u0026rsquo;s operational simplicity is liberating. Teams avoid the overhead of managing persistent storage, monitoring database health, or troubleshooting distributed coordination failures. Background processing becomes invisible infrastructure rather than a system to operate.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"practical-takeaways\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-4-coravel/#practical-takeaways\" title=\"Practical Takeaways\"\u003ePractical Takeaways\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eCoravel occupies the simplicity-first position in the scheduling spectrum. It trades persistence and clustering for developer velocity and zero dependencies.\u003c/p\u003e\n\u003cp\u003eConsider Coravel if:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eYour application runs on a single instance without horizontal scaling plans.\u003c/li\u003e\n\u003cli\u003eBackground jobs are transient and don\u0026rsquo;t require durability.\u003c/li\u003e\n\u003cli\u003eDeveloper velocity and minimal configuration trump advanced features.\u003c/li\u003e\n\u003cli\u003eYou need integrated queuing, caching, or mailing without managing multiple libraries.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eAvoid Coravel if:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eJobs must survive application restarts (see Hangfire or TickerQ).\u003c/li\u003e\n\u003cli\u003eHorizontal scaling requires coordinated job execution (see Quartz.NET).\u003c/li\u003e\n\u003cli\u003eDetailed observability and dashboards are critical (see Hangfire).\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThe next article examines NCronJob, a framework that emphasizes minimalism even further. Where Coravel bundles features like caching and mailing, NCronJob focuses exclusively on scheduling with direct integration into ASP.NET Core\u0026rsquo;s hosting model, appealing to teams seeking the absolute minimum infrastructure overhead.\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2025-12-04T17:00:00+01:00","id":"https://daily-devops.net/posts/dotnet-job-scheduling-4-coravel/","language":"en","summary":"How Coravel delivers lightweight, convention-driven scheduling without external dependencies, accelerating development for small to medium applications.","tags":["dotnet","csharp","architecture","nuget","softwareengineering"],"title":".NET Job Scheduling — Coravel and Fluent Simplicity","url":"https://daily-devops.net/posts/dotnet-job-scheduling-4-coravel/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eYour financial platform processes millions of transactions daily. At midnight, the system must calculate interest for every account, generate regulatory reports, and trigger fraud detection sweeps. These tasks cannot overlap—interest calculation must complete before reporting begins. Some jobs repeat hourly, others run on the last business day of each month, skipping holidays. A single scheduler instance cannot handle the throughput, but multiple instances must coordinate to prevent duplicate execution.\u003c/p\u003e\n\u003cp\u003eThis is Quartz.NET\u0026rsquo;s domain: enterprise-grade job scheduling where complexity, throughput, and reliability intersect. Quartz.NET targets systems with demanding scheduling semantics—job calendars that respect business rules, priority-based execution, clustering across datacenters, and integration with external monitoring infrastructure.\u003c/p\u003e\n\u003cp\u003eThe trade-off: operational complexity. Quartz.NET requires careful configuration, understanding of its architectural patterns, and infrastructure to support distributed coordination. For systems where scheduling is a first-class concern—ETL pipelines, financial batch processing, multi-tenant SaaS platforms—this investment pays dividends. For applications where background jobs are secondary concerns, the complexity may outweigh the benefits.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"architecture-jobs-triggers-and-the-scheduler\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-3-quartznet/#architecture-jobs-triggers-and-the-scheduler\" title=\"Architecture: Jobs, Triggers, and the Scheduler\"\u003eArchitecture: Jobs, Triggers, and the Scheduler\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eQuartz.NET\u0026rsquo;s architecture decomposes scheduling into three core abstractions: jobs, triggers, and the scheduler.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eJobs\u003c/strong\u003e define what to execute. They implement \u003ccode\u003eIJob\u003c/code\u003e, a single-method interface:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eInterestCalculationJob\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eIJob\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e \u003cspan class=\"n\"\u003eExecute\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eIJobExecutionContext\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eaccountService\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eJobDetail\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eJobDataMap\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGet\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;accountService\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eaccountService\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCalculateInterestAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eJobs are stateless. The scheduler instantiates them on demand, injects dependencies via job data maps or DI containers, and discards them after execution. This statelessness enables clustering: any scheduler instance can execute any job without requiring sticky sessions or shared state.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"trigger-types-and-their-trade-offs\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-3-quartznet/#trigger-types-and-their-trade-offs\" title=\"Trigger Types And Their Trade-offs\"\u003eTrigger Types And Their Trade-offs\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003eTriggers\u003c/strong\u003e define when jobs execute. Quartz.NET supports several trigger types:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eSimple triggers\u003c/strong\u003e: Execute once after a delay or repeat at fixed intervals.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCron triggers\u003c/strong\u003e: Use cron expressions for complex schedules like \u0026ldquo;every Monday at 9 AM\u0026rdquo; or \u0026ldquo;the last Friday of each month.\u0026rdquo;\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCalendar interval triggers\u003c/strong\u003e: Repeat at intervals respecting business calendars—every month, every quarter, skipping holidays.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eDaily time interval triggers\u003c/strong\u003e: Run between specific hours on selected days—useful for jobs that should only execute during business hours.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eTriggers can include misfire policies—rules for handling missed executions when the scheduler is offline or overloaded. For example, a trigger might specify \u0026ldquo;execute immediately upon recovery\u0026rdquo; or \u0026ldquo;skip missed executions and wait for the next scheduled time.\u0026rdquo;\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"how-the-scheduler-claims-triggers\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-3-quartznet/#how-the-scheduler-claims-triggers\" title=\"How The Scheduler Claims Triggers\"\u003eHow The Scheduler Claims Triggers\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003eThe scheduler\u003c/strong\u003e coordinates jobs and triggers. It stores definitions in persistent storage (SQL Server, PostgreSQL, Oracle, or in-memory), polls for triggers whose fire times have arrived, claims them atomically to prevent duplicate execution, and dispatches jobs to worker threads.\u003c/p\u003e\n\u003cp\u003eQuartz.NET\u0026rsquo;s scheduler supports clustering: multiple instances share the same database, coordinating via database locks. When a trigger fires, one instance claims it using optimistic locking (\u003ccode\u003eUPDATE ... WHERE locked_by IS NULL\u003c/code\u003e). If the instance crashes mid-execution, another instance detects the orphaned job and recovers it based on the trigger\u0026rsquo;s misfire policy.\u003c/p\u003e\n\u003cp\u003eThis design scales horizontally. Add more scheduler instances to increase throughput. Each instance competes for jobs via database coordination, distributing workload automatically without manual partitioning or configuration.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"configuration-and-integration-with-aspnet-core\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-3-quartznet/#configuration-and-integration-with-aspnet-core\" title=\"Configuration and Integration with ASP.NET Core\"\u003eConfiguration and Integration with ASP.NET Core\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eIntegrating Quartz.NET into an ASP.NET Core application involves configuring storage, defining jobs and triggers, and starting the scheduler.\u003c/p\u003e\n\u003cp\u003eFirst, install the NuGet packages:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003edotnet add package Quartz\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003edotnet add package Quartz.Extensions.Hosting\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003edotnet add package Quartz.Serialization.Json\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003edotnet add package Quartz.Plugins\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eSecond, configure the scheduler in \u003ccode\u003eProgram.cs\u003c/code\u003e:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eServices\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddQuartz\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eq\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eq\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUseMicrosoftDependencyInjectionJobFactory\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eq\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUsePersistentStore\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003es\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003es\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUsePostgres\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Host=localhost;Database=quartz;\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003es\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUseJsonSerializer\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003ejobKey\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eJobKey\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;InterestCalculation\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eq\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddJob\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eInterestCalculationJob\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;(\u003c/span\u003e\u003cspan class=\"n\"\u003eopts\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eopts\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWithIdentity\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ejobKey\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eq\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddTrigger\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eopts\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eopts\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eForJob\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ejobKey\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWithIdentity\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;InterestCalculation-trigger\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWithCronSchedule\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;0 0 0 * * ?\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e \u003cspan class=\"c1\"\u003e// Daily at midnight\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eServices\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddQuartzHostedService\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eq\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eq\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWaitForJobsToComplete\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"kc\"\u003etrue\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis configuration uses PostgreSQL for storage, schedules a job to run daily at midnight, and ensures the scheduler waits for jobs to complete during application shutdown.\u003c/p\u003e\n\u003cp\u003eQuartz.NET\u0026rsquo;s hosted service integration leverages ASP.NET Core\u0026rsquo;s \u003ccode\u003eIHostedService\u003c/code\u003e, starting and stopping the scheduler alongside the application lifecycle. The \u003ccode\u003eWaitForJobsToComplete\u003c/code\u003e option ensures graceful shutdowns: the scheduler finishes executing jobs before the application terminates, preventing interrupted workflows.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"injecting-services-into-jobs\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-3-quartznet/#injecting-services-into-jobs\" title=\"Injecting Services Into Jobs\"\u003eInjecting Services Into Jobs\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eJobs receive dependencies via constructor injection when using \u003ccode\u003eUseMicrosoftDependencyInjectionJobFactory()\u003c/code\u003e. This eliminates the need for manual service resolution:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eReportGenerationJob\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eIJob\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eIReportService\u003c/span\u003e \u003cspan class=\"n\"\u003e_reportService\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eReportGenerationJob\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eIReportService\u003c/span\u003e \u003cspan class=\"n\"\u003ereportService\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003e_reportService\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ereportService\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e \u003cspan class=\"n\"\u003eExecute\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eIJobExecutionContext\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_reportService\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGenerateMonthlyReportAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe scheduler resolves \u003ccode\u003eIReportService\u003c/code\u003e from the DI container and injects it into the job. This integration feels native to ASP.NET Core, reducing boilerplate compared to manual service location patterns.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"advanced-scheduling-calendars-and-misfires\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-3-quartznet/#advanced-scheduling-calendars-and-misfires\" title=\"Advanced Scheduling: Calendars and Misfires\"\u003eAdvanced Scheduling: Calendars and Misfires\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eQuartz.NET\u0026rsquo;s calendar support enables business-aware scheduling. Calendars exclude specific dates—holidays, maintenance windows—from trigger schedules. For example, a job scheduled to run daily except holidays:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eholidays\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eHolidayCalendar\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eholidays\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddExcludedDate\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eDateTime\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e2025\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"m\"\u003e12\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"m\"\u003e25\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e \u003cspan class=\"c1\"\u003e// Christmas\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eholidays\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddExcludedDate\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eDateTime\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e2025\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"m\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"m\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e   \u003cspan class=\"c1\"\u003e// New Year\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003escheduler\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddCalendar\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;US-Holidays\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eholidays\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ereplace\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kc\"\u003etrue\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eupdateTriggers\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kc\"\u003etrue\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003etrigger\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eTriggerBuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCreate\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWithIdentity\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;DailyProcessing\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWithCronSchedule\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;0 9 * * ?\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ex\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ex\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eInTimeZone\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eTimeZoneInfo\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFindSystemTimeZoneById\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Eastern Standard Time\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eModifiedByCalendar\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;US-Holidays\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eBuild\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis trigger fires at 9 AM daily, Eastern Time, but skips days marked in the \u003ccode\u003eUS-Holidays\u003c/code\u003e calendar. Quartz.NET evaluates calendars during trigger computation, deferring execution to the next valid day.\u003c/p\u003e\n\u003cp\u003eCalendar types include:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eHolidayCalendar\u003c/strong\u003e: Exclude specific dates.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCronCalendar\u003c/strong\u003e: Exclude dates matching a cron expression.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eDailyCalendar\u003c/strong\u003e: Exclude time ranges (e.g., \u0026ldquo;skip execution between 2 AM and 6 AM\u0026rdquo;).\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMonthlyCalendar\u003c/strong\u003e: Exclude specific days of the month.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eCombining calendars creates sophisticated rules. A trigger might exclude weekends, holidays, and the first Monday of each month—all declaratively, without custom logic.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"choosing-a-misfire-policy\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-3-quartznet/#choosing-a-misfire-policy\" title=\"Choosing A Misfire Policy\"\u003eChoosing A Misfire Policy\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003eMisfire policies\u003c/strong\u003e handle execution gaps when the scheduler is offline or overloaded. If a job scheduled for 2 AM doesn\u0026rsquo;t execute until 3 AM because the scheduler was down, the misfire policy determines behavior:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eDoNothing\u003c/strong\u003e: Skip the missed execution.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eFireNow\u003c/strong\u003e: Execute immediately upon recovery.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eFireAndProceed\u003c/strong\u003e: Execute missed runs, then continue with the normal schedule.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eFireOnceNow\u003c/strong\u003e: Execute once immediately, then resume the schedule.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eConfigure misfire policies per trigger:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003etrigger\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eTriggerBuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCreate\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWithIdentity\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;DataImport\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWithCronSchedule\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;0 0 2 * * ?\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ex\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ex\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWithMisfireHandlingInstructionFireAndProceed\u003c/span\u003e\u003cspan class=\"p\"\u003e())\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eBuild\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis trigger ensures missed nightly imports execute upon scheduler recovery, preventing data gaps.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"clustering-and-distributed-coordination\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-3-quartznet/#clustering-and-distributed-coordination\" title=\"Clustering and Distributed Coordination\"\u003eClustering and Distributed Coordination\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eQuartz.NET\u0026rsquo;s clustering enables horizontal scaling and high availability. Multiple scheduler instances share a database, coordinating via optimistic locking to prevent duplicate job execution. I\u0026rsquo;ve run three-node Quartz.NET clusters processing 15,000+ jobs daily, and the coordination works—but you need to understand what\u0026rsquo;s happening under the hood.\u003c/p\u003e\n\u003cp\u003eWhen a trigger fires, the scheduler that claims it updates a database row with its instance ID:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-sql\" data-lang=\"sql\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eUPDATE\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eqrtz_triggers\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eSET\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003estate\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;ACQUIRED\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003einstance_name\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;scheduler-01\u0026#39;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eWHERE\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003etrigger_name\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;DataImport\u0026#39;\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003eAND\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003estate\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;WAITING\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eOnly one scheduler succeeds. Others skip the trigger and poll for the next available job. This database-based coordination avoids requiring external coordination services like ZooKeeper or Consul.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"recovering-orphaned-jobs-after-a-crash\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-3-quartznet/#recovering-orphaned-jobs-after-a-crash\" title=\"Recovering Orphaned Jobs After A Crash\"\u003eRecovering Orphaned Jobs After A Crash\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eIf a scheduler crashes mid-execution, orphaned jobs remain in the \u003ccode\u003eACQUIRED\u003c/code\u003e state. A recovery thread detects these jobs (based on a timeout threshold) and resets them to \u003ccode\u003eWAITING\u003c/code\u003e, allowing another scheduler to claim them. The interval and timeout are configurable:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eq\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUsePersistentStore\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003es\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003es\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUsePostgres\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;...\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003es\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUseClusteredMode\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"kc\"\u003etrue\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003es\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePerformSchemaValidation\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"kc\"\u003etrue\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eClustering introduces latency: each scheduler instance polls the database for triggers, typically every few seconds. For high-throughput scenarios, this creates database load proportional to instance count. Tuning polling intervals balances responsiveness and database overhead.\u003c/p\u003e\n\u003cp\u003eQuartz.NET also supports \u003cstrong\u003ejob persistence without clustering\u003c/strong\u003e. Single-instance deployments benefit from persistent storage (jobs survive restarts) without coordination overhead. This mode suits applications where high availability isn\u0026rsquo;t critical but durability matters.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"job-data-maps-and-parameterization\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-3-quartznet/#job-data-maps-and-parameterization\" title=\"Job Data Maps and Parameterization\"\u003eJob Data Maps and Parameterization\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eJobs often require parameters—account IDs, file paths, configuration values. Quartz.NET uses \u003cstrong\u003ejob data maps\u003c/strong\u003e to pass data:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003ejobData\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eJobDataMap\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;accountId\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"m\"\u003e12345\u003c/span\u003e \u003cspan class=\"p\"\u003e},\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;reportType\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;monthly\u0026#34;\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e};\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003ejob\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eJobBuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCreate\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eReportJob\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWithIdentity\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Report-12345\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUsingJobData\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ejobData\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eBuild\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eJobs retrieve parameters from the execution context:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e \u003cspan class=\"n\"\u003eExecute\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eIJobExecutionContext\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eaccountId\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eMergedJobDataMap\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetInt\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;accountId\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003ereportType\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eMergedJobDataMap\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetString\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;reportType\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eGenerateReportAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eaccountId\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ereportType\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eQuartz.NET serializes job data maps to the database using JSON. Complex types—custom classes, collections—are supported, but large payloads impact performance. For heavyweight data, pass identifiers (e.g., database primary keys) and fetch the data within the job.\u003c/p\u003e\n\u003cp\u003eTriggers can also carry data maps, which merge with job data maps during execution. This enables per-trigger customization: a single job definition with multiple triggers, each passing different parameters.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"monitoring-plugins-and-extensibility\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-3-quartznet/#monitoring-plugins-and-extensibility\" title=\"Monitoring, Plugins, and Extensibility\"\u003eMonitoring, Plugins, and Extensibility\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eQuartz.NET provides listeners for observing job lifecycle events:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eJobExecutionListener\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eIJobListener\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eName\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;JobExecutionListener\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e \u003cspan class=\"n\"\u003eJobWasExecuted\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eIJobExecutionContext\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eJobExecutionException\u003c/span\u003e\u003cspan class=\"p\"\u003e?\u003c/span\u003e \u003cspan class=\"n\"\u003eexception\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eCancellationToken\u003c/span\u003e \u003cspan class=\"n\"\u003ecancellationToken\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eduration\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eJobRunTime\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eLog\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eInformation\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Job {JobKey} executed in {Duration}ms\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eJobDetail\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eKey\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eduration\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTotalMilliseconds\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCompletedTask\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Other lifecycle methods...\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eRegister listeners during configuration:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eq\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddJobListener\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eJobExecutionListener\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eListeners integrate with telemetry systems—Application Insights, Prometheus, Datadog—exporting metrics like job execution time, failure rates, and queue depths. This observability is critical for production systems where job health impacts business operations.\u003c/p\u003e\n\u003cp\u003eQuartz.NET includes plugins for common scenarios:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eXMLSchedulingDataProcessorPlugin\u003c/strong\u003e: Load job definitions from XML files, enabling configuration-driven scheduling.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eLoggingTriggerHistoryPlugin\u003c/strong\u003e: Records trigger fire history to logs for audit trails.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eInterruptMonitorPlugin\u003c/strong\u003e: Monitors job interruptions and logs them for debugging.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003ePlugins integrate via configuration:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eq\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddXMLSchedulingDataProcessorPlugin\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eplugin\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eplugin\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFiles\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e\u003cspan class=\"p\"\u003e[]\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;~/quartz_jobs.xml\u0026#34;\u003c/span\u003e \u003cspan class=\"p\"\u003e};\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eplugin\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eScanInterval\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eTimeSpan\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFromSeconds\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e30\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis plugin watches an XML file and dynamically updates job definitions without application restarts—useful for operational teams adjusting schedules without developer intervention.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"when-quartznet-fits\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-3-quartznet/#when-quartznet-fits\" title=\"When Quartz.NET Fits\"\u003eWhen Quartz.NET Fits\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eQuartz.NET excels when:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eComplex scheduling is essential\u003c/strong\u003e: Job calendars, business day logic, misfire policies, and priority-based execution are first-class requirements.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eHigh throughput demands clustering\u003c/strong\u003e: Thousands or tens of thousands of jobs per minute justify distributed coordination and horizontal scaling.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eObservability and auditability matter\u003c/strong\u003e: Enterprises needing compliance, audit trails, and detailed execution history benefit from Quartz.NET\u0026rsquo;s persistence and plugin ecosystem.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eMulti-tenancy or geo-distribution\u003c/strong\u003e: Systems spanning multiple datacenters or customer tenants require flexible storage and isolation, which Quartz.NET\u0026rsquo;s architecture supports.\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eQuartz.NET is less suitable when:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eSimplicity is paramount\u003c/strong\u003e: Teams seeking minimal configuration overhead should consider Hangfire, Coravel, or NCronJob.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eStateless deployments are preferred\u003c/strong\u003e: While Quartz.NET supports in-memory storage, clustering requires a database. Fully stateless architectures might prefer external message brokers or in-memory frameworks.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eThroughput is modest\u003c/strong\u003e: If job volumes are hundreds per minute, not thousands, Quartz.NET\u0026rsquo;s complexity may outweigh its benefits. Hangfire delivers adequate performance with less operational overhead.\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\n\n\n\n\u003ch2 id=\"operational-complexity-and-trade-offs\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-3-quartznet/#operational-complexity-and-trade-offs\" title=\"Operational Complexity and Trade-offs\"\u003eOperational Complexity and Trade-offs\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eQuartz.NET\u0026rsquo;s power comes with operational demands. Teams must provision and maintain database infrastructure, configure clustering correctly, monitor database performance under polling load, and tune misfire policies based on workload characteristics.\u003c/p\u003e\n\u003cp\u003eThe learning curve is steeper than simpler frameworks. Quartz.NET\u0026rsquo;s abstractions—jobs, triggers, calendars, listeners—require understanding before effective use. Misconfigured misfire policies can cause execution storms (hundreds of missed jobs firing simultaneously). Incorrect clustering settings can lead to duplicate execution or job starvation.\u003c/p\u003e\n\u003cp\u003eHowever, for systems where scheduling is critical, this complexity is justified. Quartz.NET\u0026rsquo;s reliability, flexibility, and scalability enable architectures that simpler frameworks cannot support. Financial platforms, healthcare systems, and enterprise ETL pipelines rely on Quartz.NET for workloads where failures have business consequences.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"practical-takeaways\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-3-quartznet/#practical-takeaways\" title=\"Practical Takeaways\"\u003ePractical Takeaways\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eQuartz.NET occupies the enterprise end of the scheduling spectrum. It provides advanced semantics, clustering, and observability at the cost of operational complexity.\u003c/p\u003e\n\u003cp\u003eConsider Quartz.NET if:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eYour system requires complex scheduling—calendars, misfires, priorities.\u003c/li\u003e\n\u003cli\u003eJob volumes justify horizontal scaling across multiple instances.\u003c/li\u003e\n\u003cli\u003eObservability, auditing, and compliance are critical.\u003c/li\u003e\n\u003cli\u003eYou need fine-grained control over execution policies and error handling.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eAvoid Quartz.NET if:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eYour application needs simple, lightweight scheduling (see NCronJob or Coravel).\u003c/li\u003e\n\u003cli\u003ePersistence suffices without clustering (see Hangfire).\u003c/li\u003e\n\u003cli\u003eDeveloper velocity and minimal configuration are priorities over advanced features.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThe next article explores Coravel, a framework that prioritizes simplicity and developer convenience. Where Quartz.NET offers enterprise control, Coravel provides fluent APIs, zero infrastructure requirements, and rapid integration for small to medium applications.\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2025-12-02T17:00:00+01:00","id":"https://daily-devops.net/posts/dotnet-job-scheduling-3-quartznet/","language":"en","summary":"Quartz.NET delivers enterprise scheduling with clustering, advanced triggers, job calendars, and multi-datacenter coordination for high-volume workloads.","tags":["dotnet","csharp","architecture","nuget","softwareengineering"],"title":".NET Job Scheduling — Quartz.NET for Enterprise Scale","url":"https://daily-devops.net/posts/dotnet-job-scheduling-3-quartznet/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eA user uploads a 200 MB video to your platform at 3:14 PM. Transcoding it into multiple formats—1080p, 720p, mobile—takes twelve minutes on average, sometimes longer. Keeping the HTTP request open that long? Unacceptable. But here\u0026rsquo;s the problem: during our Tuesday maintenance window last month, we restarted the app servers, and boom—87 video processing jobs vanished into thin air. Users got \u0026ldquo;upload successful\u0026rdquo; messages, but their videos never appeared. Not ideal when you\u0026rsquo;re charging for the service.\u003c/p\u003e\n\u003cp\u003eYou need persistence: the ability to store job definitions in a database, detach them from the request lifecycle, and guarantee execution even when infrastructure hiccups.\u003c/p\u003e\n\u003cp\u003eHangfire solves this by turning background jobs into first-class database records. When you enqueue a job, Hangfire serializes the method invocation—class name, method signature, parameters—and persists it to SQL Server, PostgreSQL, or Redis. Worker threads poll the storage, claim jobs, execute them, and record outcomes. If a worker crashes mid-execution, another worker picks up the job and retries it based on configurable policies. If the entire application restarts, queued jobs remain intact, waiting for workers to resume processing.\u003c/p\u003e\n\u003cp\u003eThis architecture makes Hangfire particularly suited for web applications where background work must survive deployments, process restarts, or transient failures. The trade-off: you need a database. For teams already running SQL Server or PostgreSQL, this is minimal overhead. For environments preferring stateless components, the infrastructure requirement merits consideration.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"core-architecture-storage-workers-and-coordination\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-2-hangfire/#core-architecture-storage-workers-and-coordination\" title=\"Core Architecture: Storage, Workers, and Coordination\"\u003eCore Architecture: Storage, Workers, and Coordination\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eHangfire\u0026rsquo;s design centers on three components: the storage backend, the job server (workers), and the client API that enqueues jobs.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eStorage\u003c/strong\u003e holds job definitions, execution history, and metadata. Hangfire serializes method calls—including parameter values—as JSON and stores them in tables like \u003ccode\u003eHangFire.Job\u003c/code\u003e, \u003ccode\u003eHangFire.State\u003c/code\u003e, and \u003ccode\u003eHangFire.JobQueue\u003c/code\u003e. When a job is enqueued, a record appears in the database. When a worker processes it, the state transitions from \u003ccode\u003eEnqueued\u003c/code\u003e to \u003ccode\u003eProcessing\u003c/code\u003e to \u003ccode\u003eSucceeded\u003c/code\u003e or \u003ccode\u003eFailed\u003c/code\u003e. This persistence is what differentiates Hangfire from in-memory schedulers: jobs are durable, observable, and recoverable.\u003c/p\u003e\n\u003cp\u003eSupported storage backends include SQL Server (the default), PostgreSQL, MySQL, MongoDB, and Redis. SQL-based backends offer strong consistency and integrate seamlessly with existing relational infrastructure. Redis provides lower latency for high-throughput scenarios where job volumes exceed thousands per minute. Choosing a backend depends on your existing infrastructure and performance requirements—SQL Server for most .NET shops, Redis for systems already using it for caching or session state.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"how-workers-claim-jobs\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-2-hangfire/#how-workers-claim-jobs\" title=\"How Workers Claim Jobs\"\u003eHow Workers Claim Jobs\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003eWorkers\u003c/strong\u003e execute jobs. Each Hangfire server instance starts dedicated background threads—not the ASP.NET Core thread pool—that poll the storage for \u003ccode\u003eEnqueued\u003c/code\u003e jobs. Polling uses database-specific mechanisms: SQL Server leverages \u003ccode\u003eUPDLOCK\u003c/code\u003e and \u003ccode\u003eREADPAST\u003c/code\u003e hints to claim jobs atomically, ensuring only one worker processes each job even when multiple servers run concurrently. Workers fetch jobs, deserialize method calls, invoke them using reflection, and update job states in the database.\u003c/p\u003e\n\u003cp\u003eThe number of worker threads is configurable. A single-instance application might run five workers; a scaled-out deployment with three servers might run fifteen total workers (five per server). More workers increase throughput but consume more database connections and CPU. Tuning depends on job execution time: CPU-bound jobs benefit from fewer workers matching CPU core counts, while I/O-bound jobs can support more workers since threads spend time waiting on external resources.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"enqueuing-from-the-client-api\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-2-hangfire/#enqueuing-from-the-client-api\" title=\"Enqueuing From the Client API\"\u003eEnqueuing From the Client API\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003eClients\u003c/strong\u003e enqueue jobs via a simple API. \u003ccode\u003eBackgroundJob.Enqueue(() =\u0026gt; Console.WriteLine(\u0026quot;Hello\u0026quot;))\u003c/code\u003e serializes the method call and inserts it into the database. The calling thread returns immediately; the work happens asynchronously on a worker thread. This decoupling is essential for web applications: controllers enqueue jobs in milliseconds and respond to users, while workers process jobs in the background without blocking HTTP requests.\u003c/p\u003e\n\u003cp\u003eHangfire also supports delayed jobs (scheduled to run after a time interval), recurring jobs (executed on a cron schedule), and continuations (jobs that run after a parent job succeeds). Each pattern maps to database records with corresponding state transitions, enabling rich workflows without custom orchestration code.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"configuration-and-integration\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-2-hangfire/#configuration-and-integration\" title=\"Configuration and Integration\"\u003eConfiguration and Integration\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eIntegrating Hangfire into an ASP.NET Core application requires three steps: configuring storage, starting the server, and optionally enabling the dashboard.\u003c/p\u003e\n\u003cp\u003eFirst, install the NuGet package. For SQL Server:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003edotnet add package Hangfire.AspNetCore\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003edotnet add package Hangfire.SqlServer\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eSecond, configure storage and start the server in \u003ccode\u003eProgram.cs\u003c/code\u003e:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eServices\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddHangfire\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003econfiguration\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003econfiguration\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSetDataCompatibilityLevel\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eCompatibilityLevel\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eVersion_180\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUseSimpleAssemblyNameTypeSerializer\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUseRecommendedSerializerSettings\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUseSqlServerStorage\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Server=.;Database=HangfireDB;Integrated Security=True;\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eServices\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddHangfireServer\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eapp\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eBuild\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eapp\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUseHangfireDashboard\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eapp\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRun\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis configuration connects to a SQL Server database, starts worker threads, and exposes the dashboard at \u003ccode\u003e/hangfire\u003c/code\u003e. The dashboard provides real-time visibility into job states: succeeded, failed, processing, scheduled, and enqueued. You can manually trigger recurring jobs, delete failed jobs, or re-enqueue them for retry.\u003c/p\u003e\n\u003cp\u003eThird, enqueue jobs from anywhere in your application—controllers, services, background tasks:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eOrderController\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eControllerBase\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [HttpPost]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eIActionResult\u003c/span\u003e \u003cspan class=\"n\"\u003eProcessOrder\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eOrder\u003c/span\u003e \u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eBackgroundJob\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eEnqueue\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eIOrderProcessor\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;(\u003c/span\u003e\u003cspan class=\"n\"\u003ex\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ex\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eProcessAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eId\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eAccepted\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe controller responds immediately with \u003ccode\u003e202 Accepted\u003c/code\u003e. The \u003ccode\u003eProcessAsync\u003c/code\u003e method executes asynchronously on a worker thread. If processing fails—database timeout, external API unavailable—Hangfire automatically retries it up to ten times with exponential backoff (configurable). Failed jobs appear in the dashboard with full stack traces, enabling debugging without log archaeology.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"scheduling-recurring-jobs\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-2-hangfire/#scheduling-recurring-jobs\" title=\"Scheduling Recurring Jobs\"\u003eScheduling Recurring Jobs\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eRecurring jobs use cron expressions:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eRecurringJob\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddOrUpdate\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;nightly-report\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eGenerateReport\u003c/span\u003e\u003cspan class=\"p\"\u003e(),\u003c/span\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eCron\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eDaily\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e2\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e \u003cspan class=\"c1\"\u003e// 2 AM daily\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eHangfire stores the recurring job definition in the database and triggers it based on the cron schedule. If the application is down during the scheduled time, Hangfire executes the job as soon as a server starts. This \u0026ldquo;catch-up\u0026rdquo; behavior prevents missed executions but can cause bursts if the application was offline for extended periods.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"retry-policies-and-error-handling\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-2-hangfire/#retry-policies-and-error-handling\" title=\"Retry Policies and Error Handling\"\u003eRetry Policies and Error Handling\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eTransient failures—network timeouts, temporary database unavailability—shouldn\u0026rsquo;t cause permanent job failures. Hangfire\u0026rsquo;s automatic retry mechanism handles these transparently.\u003c/p\u003e\n\u003cp\u003eBy default, failed jobs retry up to ten times with exponential backoff: immediate retry, then 1 minute, 2 minutes, 4 minutes, and so on. If all retries exhaust, the job transitions to the \u003ccode\u003eFailed\u003c/code\u003e state and appears in the dashboard. Administrators can manually re-enqueue failed jobs or investigate root causes using stack traces recorded in the database.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"writing-custom-retry-filters\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-2-hangfire/#writing-custom-retry-filters\" title=\"Writing Custom Retry Filters\"\u003eWriting Custom Retry Filters\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eCustom retry logic uses filters:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eCustomRetryAttribute\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eJobFilterAttribute\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eIElectStateFilter\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eOnStateElection\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eElectStateContext\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003efailedState\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCandidateState\u003c/span\u003e \u003cspan class=\"k\"\u003eas\u003c/span\u003e \u003cspan class=\"n\"\u003eFailedState\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003efailedState\u003c/span\u003e \u003cspan class=\"p\"\u003e!=\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCandidateState\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eScheduledState\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eTimeSpan\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFromMinutes\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e5\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e[CustomRetry]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eUnreliableTask\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Custom retry: wait 5 minutes, then retry indefinitely\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis filter intercepts state transitions and reschedules failed jobs with custom delays. Use cases include rate-limited APIs (retry after a cooldown), scheduled maintenance windows (skip retries during known outages), or critical workflows requiring infinite retries until manual intervention.\u003c/p\u003e\n\u003cp\u003eHangfire also supports idempotency checks via filters. If a job should only execute once regardless of retries—for example, charging a customer\u0026rsquo;s credit card—wrap the logic in idempotency tokens or database locks to prevent duplicate execution.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"scalability-from-single-instance-to-distributed-workers\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-2-hangfire/#scalability-from-single-instance-to-distributed-workers\" title=\"Scalability: From Single Instance to Distributed Workers\"\u003eScalability: From Single Instance to Distributed Workers\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eHangfire scales vertically and horizontally. Vertical scaling increases worker threads on a single server. Horizontal scaling adds more servers, each running its own Hangfire server instance. Workers across all servers poll the same database, coordinating via atomic database operations to prevent duplicate job processing.\u003c/p\u003e\n\u003cp\u003eWhen you deploy three application instances, each with five worker threads, you effectively have fifteen workers competing for jobs. Hangfire\u0026rsquo;s SQL-based storage uses \u003ccode\u003eUPDLOCK\u003c/code\u003e and \u003ccode\u003eREADPAST\u003c/code\u003e to ensure only one worker claims each job. This coordination happens at the database level—no external message broker or distributed lock manager required.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"when-to-switch-from-sql-to-redis\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-2-hangfire/#when-to-switch-from-sql-to-redis\" title=\"When To Switch From SQL To Redis\"\u003eWhen To Switch From SQL To Redis\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eFor high-throughput scenarios—tens of thousands of jobs per minute—SQL Server\u0026rsquo;s polling overhead becomes noticeable. Each worker queries the database every few seconds, creating connection churn and CPU load. Redis-based storage reduces this overhead by leveraging Redis\u0026rsquo;s pub/sub for instant job notifications instead of polling. Workers sleep until Redis signals a new job, eliminating unnecessary queries.\u003c/p\u003e\n\u003cp\u003eSwitching to Redis:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003edotnet add package Hangfire.Pro.Redis\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eServices\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddHangfire\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003econfiguration\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003econfiguration\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUseRedisStorage\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;localhost:6379\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eRedis also supports job prioritization, faster dashboard queries, and lower database load. The trade-off: Redis is eventually consistent, so job visibility (dashboard updates) may lag slightly compared to SQL Server\u0026rsquo;s strong consistency.\u003c/p\u003e\n\u003cp\u003eAnother scalability concern: long-running jobs. If a job takes an hour to complete, it ties up a worker thread for that duration. Consider splitting long-running jobs into smaller units or processing them on dedicated servers with higher worker counts. Hangfire\u0026rsquo;s queue-based architecture supports this: route long-running jobs to a specific queue processed by dedicated servers.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003eHangfire.States\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eBackgroundJob\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eEnqueue\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eIReportGenerator\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;(\u003c/span\u003e\u003cspan class=\"n\"\u003ex\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ex\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGenerateLargeReport\u003c/span\u003e\u003cspan class=\"p\"\u003e(),\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eEnqueuedState\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;reports\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eConfigure a dedicated server to process only the \u003ccode\u003ereports\u003c/code\u003e queue:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eServices\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddHangfireServer\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eoptions\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eoptions\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eQueues\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e\u003cspan class=\"p\"\u003e[]\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;reports\u0026#34;\u003c/span\u003e \u003cspan class=\"p\"\u003e};\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eoptions\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWorkerCount\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e2\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"c1\"\u003e// Limit to two concurrent reports\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis isolates resource-intensive jobs from standard background work, preventing them from starving other tasks.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"dashboard-and-observability\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-2-hangfire/#dashboard-and-observability\" title=\"Dashboard and Observability\"\u003eDashboard and Observability\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eHangfire\u0026rsquo;s dashboard is one of its most compelling features. It provides real-time visibility into job states without requiring custom telemetry or logging integration.\u003c/p\u003e\n\u003cp\u003eThe dashboard displays:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eEnqueued jobs\u003c/strong\u003e: Waiting for worker threads.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eProcessing jobs\u003c/strong\u003e: Currently executing, with elapsed time and server information.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eScheduled jobs\u003c/strong\u003e: Delayed or recurring jobs awaiting their trigger time.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSucceeded jobs\u003c/strong\u003e: Completed successfully, with execution duration.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eFailed jobs\u003c/strong\u003e: Errors, stack traces, and retry counts.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eRecurring jobs\u003c/strong\u003e: Cron schedules, last execution time, next execution time.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eAdministrators can manually trigger recurring jobs, delete failed jobs, or re-enqueue them for retry—all from the dashboard without writing code or deploying updates. This operational flexibility reduces time spent diagnosing background job issues.\u003c/p\u003e\n\u003cp\u003eSecurity considerations: the dashboard exposes sensitive information—job parameters, stack traces, server names. Protect it using authentication middleware:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eapp\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUseHangfireDashboard\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;/hangfire\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eDashboardOptions\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eAuthorization\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e\u003cspan class=\"p\"\u003e[]\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eMyAuthorizationFilter\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eImplement \u003ccode\u003eIDashboardAuthorizationFilter\u003c/code\u003e to restrict access based on roles, authentication status, or IP address.\u003c/p\u003e\n\u003cp\u003eFor production systems, consider integrating Hangfire with external monitoring tools. Export job metrics—succeeded jobs per minute, average execution time, retry rates—to Prometheus, Application Insights, or Datadog. Hangfire\u0026rsquo;s extensibility via filters and listeners makes this straightforward.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"when-hangfire-fits\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-2-hangfire/#when-hangfire-fits\" title=\"When Hangfire Fits\"\u003eWhen Hangfire Fits\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eHangfire excels in scenarios where:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003ePersistence is non-negotiable\u003c/strong\u003e: Jobs must survive application restarts, deployments, or server reboots. Examples: user-initiated reports, data imports, long-running workflows.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eObservability matters\u003c/strong\u003e: Teams need real-time visibility into job states without building custom dashboards or integrating logging frameworks.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eWeb applications dominate your architecture\u003c/strong\u003e: Hangfire integrates seamlessly with ASP.NET Core, leveraging existing database infrastructure without requiring separate message brokers or coordination services.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eModerate throughput suffices\u003c/strong\u003e: Thousands of jobs per minute work well. If you need hundreds of thousands, consider Redis-based storage or evaluate Quartz.NET for advanced clustering.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eAutomatic retries reduce operational burden\u003c/strong\u003e: Teams that value hands-off error handling benefit from Hangfire\u0026rsquo;s built-in retry policies, eliminating custom retry logic.\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eHangfire is less suitable when:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eStateless deployments are required\u003c/strong\u003e: Kubernetes environments favoring ephemeral pods may prefer in-memory schedulers like NCronJob, though Hangfire\u0026rsquo;s database dependency isn\u0026rsquo;t prohibitive if managed databases are available.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eSub-second latency is critical\u003c/strong\u003e: Hangfire\u0026rsquo;s polling mechanism introduces latency (typically 1-5 seconds). Real-time event-driven systems might prefer message brokers like RabbitMQ or Azure Service Bus.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eComplex scheduling is paramount\u003c/strong\u003e: While Hangfire supports cron expressions, it lacks Quartz.NET\u0026rsquo;s advanced features like job calendars, misfire handling, or priority-based execution.\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\n\n\n\n\u003ch2 id=\"operational-benefits-and-trade-offs\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-2-hangfire/#operational-benefits-and-trade-offs\" title=\"Operational Benefits and Trade-offs\"\u003eOperational Benefits and Trade-offs\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eHangfire\u0026rsquo;s primary operational benefit is reliability. Jobs stored in a database won\u0026rsquo;t vanish due to application crashes or restarts. Administrators gain confidence that critical workflows—nightly data synchronization, scheduled email campaigns, periodic cache refreshes—execute reliably even during infrastructure turbulence.\u003c/p\u003e\n\u003cp\u003eThe dashboard reduces debugging time. Instead of parsing logs to determine whether a job ran, succeeded, or failed, teams view job states in real-time. Failed jobs display stack traces inline, enabling root cause analysis without log aggregation tools.\u003c/p\u003e\n\u003cp\u003eAutomatic retries reduce operational overhead. Transient failures—network blips, temporary service unavailability—self-heal without manual intervention. Teams spend less time monitoring background jobs and more time building features.\u003c/p\u003e\n\u003cp\u003eThe trade-offs: database dependency and polling overhead. Teams must provision and maintain a database, configure connection strings, and monitor database health. In cloud environments, this might mean managed SQL instances (Azure SQL, Amazon RDS) with associated costs. Polling introduces latency and database load—acceptable for most workloads but noticeable in high-throughput or latency-sensitive scenarios.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"practical-takeaways\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-2-hangfire/#practical-takeaways\" title=\"Practical Takeaways\"\u003ePractical Takeaways\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eHangfire occupies the middle ground between simplicity and enterprise-grade features. It provides persistence without requiring clustering, visibility without custom telemetry, and retries without manual logic. For ASP.NET Core applications needing reliable background processing, Hangfire delivers substantial value with moderate operational complexity.\u003c/p\u003e\n\u003cp\u003eConsider Hangfire if:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eYour application uses SQL Server, PostgreSQL, or Redis.\u003c/li\u003e\n\u003cli\u003eJobs must survive restarts and benefit from automatic retries.\u003c/li\u003e\n\u003cli\u003eYou value built-in dashboards over custom monitoring solutions.\u003c/li\u003e\n\u003cli\u003eThroughput requirements are moderate (thousands per minute, not hundreds of thousands).\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eAvoid Hangfire if:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eYou need stateless, zero-dependency deployments (see NCronJob or Coravel).\u003c/li\u003e\n\u003cli\u003eComplex scheduling with calendars and advanced triggers is essential (see Quartz.NET).\u003c/li\u003e\n\u003cli\u003eUltra-low latency or extremely high throughput is required (consider message brokers or TickerQ).\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThe next article explores Quartz.NET, a framework that extends Hangfire\u0026rsquo;s persistence model with enterprise-grade features: clustering, advanced scheduling semantics, and multi-datacenter coordination. Where Hangfire simplifies reliability for web applications, Quartz.NET targets systems with complex scheduling demands and high-scale distributed deployments.\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2025-11-27T17:00:00+01:00","id":"https://daily-devops.net/posts/dotnet-job-scheduling-2-hangfire/","language":"en","summary":"How Hangfire delivers persistent background processing with built-in dashboards, automatic retries, and distributed job execution for web applications.","tags":["dotnet","csharp","architecture","nuget","softwareengineering"],"title":".NET Job Scheduling — Hangfire and Persistent Reliability","url":"https://daily-devops.net/posts/dotnet-job-scheduling-2-hangfire/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eA backend service receives a customer order at 14:37. The order needs fulfillment, but inventory must be validated, payment authorized, and a confirmation email dispatched. Processing these steps synchronously would lock the HTTP request thread for seconds—unacceptable when hundreds of concurrent users expect instant responses. The solution: offload the work to a background scheduler that handles tasks asynchronously, outside the request pipeline, with guaranteed execution and resilience against failures.\u003c/p\u003e\n\u003cp\u003eThis is the domain of job scheduling, and in .NET, the ecosystem offers a spectrum of solutions—from simple in-memory task runners suitable for internal tools, to enterprise-grade orchestration engines that coordinate work across distributed clusters. Choosing the wrong approach can lead to brittle systems where background jobs fail silently, retry logic becomes unmanageable, or scaling requirements force costly rewrites.\u003c/p\u003e\n\u003cp\u003eThis series examines several frameworks that span this spectrum, each occupying a distinct position defined by its architectural trade-offs—persistence versus simplicity, clustering versus overhead, compile-time safety versus runtime flexibility. Understanding where each framework excels and where it imposes constraints allows you to select the scheduler that matches your system\u0026rsquo;s operational profile, not the one with the most GitHub stars.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"why-background-processing-matters\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-1-landscape/#why-background-processing-matters\" title=\"Why Background Processing Matters\"\u003eWhy Background Processing Matters\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eModern cloud-native applications demand asynchronous execution. HTTP requests must complete quickly; operations like file processing, report generation, or third-party API calls cannot block user interactions. Background jobs decouple time-intensive work from request handling, improving responsiveness and system throughput.\u003c/p\u003e\n\u003cp\u003eConsider a SaaS platform that generates monthly invoices. Generating a single PDF might take 500ms; for 10,000 customers, that\u0026rsquo;s over 80 minutes if processed serially. A background scheduler distributes this workload across multiple workers, processes jobs in parallel, and ensures that transient failures—network timeouts, temporary database unavailability—trigger automatic retries rather than silent data loss.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"why-manual-timer-loops-fail\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-1-landscape/#why-manual-timer-loops-fail\" title=\"Why Manual Timer Loops Fail\"\u003eWhy Manual Timer Loops Fail\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eWithout a scheduler, developers resort to manual implementations using \u003ccode\u003eSystem.Threading.Timer\u003c/code\u003e or \u003ccode\u003eTask.Delay\u003c/code\u003e wrapped in endless loops. These approaches lack persistence: if the application restarts, queued work disappears. They lack observability: tracking which jobs ran, which failed, and why becomes guesswork. They lack coordination: running multiple instances simultaneously can cause duplicate execution or race conditions.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"what-a-scheduler-provides\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-1-landscape/#what-a-scheduler-provides\" title=\"What A Scheduler Provides\"\u003eWhat A Scheduler Provides\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eA job scheduler abstracts these concerns. It provides:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003ePersistence\u003c/strong\u003e: Jobs survive application restarts because they\u0026rsquo;re stored in a database or message queue.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eRetry logic\u003c/strong\u003e: Failed jobs automatically re-execute based on configurable policies.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eScheduling semantics\u003c/strong\u003e: Cron expressions, delayed execution, recurring intervals—without manual date arithmetic.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMonitoring\u003c/strong\u003e: Built-in visibility into job states, execution history, and failure patterns.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eScalability\u003c/strong\u003e: Distributing work across multiple server instances with load balancing and failover.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThe value is operational. Teams that rely on schedulers reduce debugging time spent chasing \u0026ldquo;lost\u0026rdquo; background tasks, avoid building custom retry mechanisms, and gain confidence that critical workflows—nightly data imports, periodic cache refreshes, scheduled email campaigns—execute reliably even when infrastructure hiccups.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-evolution-from-timers-to-schedulers\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-1-landscape/#the-evolution-from-timers-to-schedulers\" title=\"The Evolution from Timers to Schedulers\"\u003eThe Evolution from Timers to Schedulers\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eEarly .NET applications used \u003ccode\u003eSystem.Timers.Timer\u003c/code\u003e or Windows Task Scheduler to trigger background work. These tools were adequate for simple scenarios: run a cleanup job every night at 2 AM. But as systems grew more complex, limitations surfaced.\u003c/p\u003e\n\u003cp\u003eTimers live in memory. If the process crashes, the timer state is lost. There\u0026rsquo;s no record of what ran, when it started, or why it failed. Debugging requires log archaeology. Scaling horizontally—running multiple application instances—introduces coordination challenges: multiple timers firing simultaneously can duplicate work or create contention over shared resources.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"limits-of-windows-task-scheduler\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-1-landscape/#limits-of-windows-task-scheduler\" title=\"Limits Of Windows Task Scheduler\"\u003eLimits Of Windows Task Scheduler\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eWindows Task Scheduler operates outside the application, requiring XML configuration files and administrative access to schedule tasks. Integration with application logic is indirect—typically invoking console executables that bootstrap the full application context just to run a single method. Dependency injection, logging frameworks, and application configuration require manual wiring. Updates to scheduled tasks involve modifying server configurations, not deploying code.\u003c/p\u003e\n\u003cp\u003eThese pain points drove the adoption of in-process schedulers that integrate directly with application frameworks like ASP.NET Core. Frameworks like \u003cstrong\u003eIHostedService\u003c/strong\u003e provided a native hook for long-running background operations, but developers still had to implement scheduling logic, persistence, and retry strategies manually.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"from-infrastructure-to-declaring-intent\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-1-landscape/#from-infrastructure-to-declaring-intent\" title=\"From Infrastructure To Declaring Intent\"\u003eFrom Infrastructure To Declaring Intent\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eModern job schedulers abstract this complexity. They provide structured APIs for defining jobs, flexible storage backends for persistence, and runtime engines that handle execution, retries, and coordination automatically. The shift is from managing infrastructure to declaring intent: \u0026ldquo;run this job every Monday at 9 AM\u0026rdquo; becomes a single line of configuration, and the scheduler handles the rest.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"defining-the-spectrum-simplicity-to-scale\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-1-landscape/#defining-the-spectrum-simplicity-to-scale\" title=\"Defining the Spectrum: Simplicity to Scale\"\u003eDefining the Spectrum: Simplicity to Scale\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eJob scheduling frameworks occupy distinct positions on a spectrum defined by two competing priorities: \u003cstrong\u003esimplicity\u003c/strong\u003e and \u003cstrong\u003econtrol\u003c/strong\u003e.\u003c/p\u003e\n\u003cp\u003eOn one end, frameworks prioritize ease of integration. They minimize configuration, require no external dependencies like databases or message queues, and work out-of-the-box for small to medium applications. These are ideal for microservices, internal tools, or systems where background processing is a secondary concern. The trade-off: limited scalability, no clustering support, and jobs confined to a single process.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"the-enterprise-end-of-the-spectrum\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-1-landscape/#the-enterprise-end-of-the-spectrum\" title=\"The Enterprise End Of The Spectrum\"\u003eThe Enterprise End Of The Spectrum\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eOn the other end, frameworks offer enterprise-grade features: persistent job storage with database backends, distributed coordination across server clusters, advanced scheduling with calendars and priority queues, and rich monitoring dashboards. These handle demanding workloads—thousands of jobs per minute, multi-tenant isolation, geographically distributed workers. The trade-off: increased operational complexity, external infrastructure requirements, and steeper learning curves.\u003c/p\u003e\n\u003cp\u003eSelecting a framework requires matching your system\u0026rsquo;s operational profile to these fundamental trade-offs. Do you need jobs that survive application restarts? Does your workload demand horizontal scaling across multiple instances? Are advanced scheduling semantics—business calendars, priority queues, misfire policies—essential, or would simple cron expressions suffice? Understanding these requirements shapes which end of the spectrum fits your architecture.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"architectural-considerations\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-1-landscape/#architectural-considerations\" title=\"Architectural Considerations\"\u003eArchitectural Considerations\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eBeyond individual framework capabilities, several architectural factors influence scheduler selection:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003ePersistence requirements\u003c/strong\u003e: If jobs must survive application restarts—for example, user-initiated reports that take minutes to generate—you need database-backed persistence. Frameworks like Hangfire, Quartz.NET, and TickerQ support this. If jobs are transient—cache warming, health checks—in-memory schedulers like NCronJob or Coravel suffice.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eScalability and distribution\u003c/strong\u003e: Running a single application instance simplifies deployment but limits throughput. Multiple instances require coordination to prevent duplicate job execution. Quartz.NET\u0026rsquo;s clustering uses database locks to ensure only one instance processes each job. Hangfire distributes jobs across workers using queue-based polling. NCronJob and Coravel lack built-in clustering; scaling them requires external coordination mechanisms or accepting potential duplication.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eRetry and error handling\u003c/strong\u003e: Transient failures—network timeouts, temporary database unavailability—should trigger retries, not job failures. Hangfire and TickerQ provide configurable retry policies with exponential backoff. Quartz.NET supports retry through job listeners and exception handling. Coravel and NCronJob leave retry logic to the job implementation, offering flexibility but requiring more manual code.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eMonitoring and observability\u003c/strong\u003e: Production systems need visibility into job execution. Hangfire\u0026rsquo;s dashboard shows queued, processing, succeeded, and failed jobs in real-time. TickerQ provides a SignalR-powered UI with live updates. Quartz.NET supports custom listeners for telemetry integration. Coravel and NCronJob rely on application logging and external monitoring tools.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eIntegration with existing infrastructure\u003c/strong\u003e: If your application already uses SQL Server, Hangfire integrates seamlessly. If you rely on Redis for caching, both Hangfire and Quartz.NET offer Redis storage backends. If you prefer avoiding external dependencies, NCronJob and Coravel fit stateless or containerized deployments better.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eDevelopment ergonomics\u003c/strong\u003e: Some frameworks prioritize fluent APIs and minimal boilerplate (Coravel, NCronJob). Others favor explicit configuration and type safety (TickerQ\u0026rsquo;s source generation, Quartz.NET\u0026rsquo;s builder patterns). Developer experience matters—especially in teams where background processing is one of many concerns, not the primary focus.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"key-decision-factors\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-1-landscape/#key-decision-factors\" title=\"Key Decision Factors\"\u003eKey Decision Factors\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eWhen evaluating job scheduling frameworks, several dimensions drive selection:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003ePersistence\u003c/strong\u003e: In-memory schedulers suit transient workloads—cache warming, health checks—where losing queued jobs during restarts is acceptable. Database-backed schedulers ensure job durability, critical for user-initiated operations like report generation or order fulfillment.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eClustering\u003c/strong\u003e: Single-instance deployments simplify operations but limit throughput and create single points of failure. Distributed coordination enables horizontal scaling but requires infrastructure for coordination—typically database locks or distributed consensus protocols.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eScheduling complexity\u003c/strong\u003e: Simple use cases—\u0026ldquo;run daily at 2 AM\u0026rdquo;—need only cron expressions. Advanced scenarios—\u0026ldquo;last business day of the quarter, excluding holidays\u0026rdquo;—require calendar support, custom triggers, or misfire handling.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eObservability\u003c/strong\u003e: Production systems need visibility into job states. Built-in dashboards provide real-time monitoring without custom instrumentation. Frameworks without dashboards rely on application logging and external observability tools.\u003c/p\u003e\n\u003cp\u003eUnderstanding where your requirements fall on each dimension guides framework selection more effectively than popularity metrics or feature counts.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"moving-forward\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-1-landscape/#moving-forward\" title=\"Moving Forward\"\u003eMoving Forward\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe next articles traverse the spectrum—from simple in-process scheduling to durable, distributed engines—using real scenarios to surface trade-offs in persistence, scalability, and observability. The journey starts with a pragmatic, database-backed option for web apps, then contrasts lighter in-memory approaches and heavier clustered solutions, concluding with a concise comparative guide to map requirements to the right fit.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"practical-takeaways\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-1-landscape/#practical-takeaways\" title=\"Practical Takeaways\"\u003ePractical Takeaways\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eJob scheduling is infrastructure that fades into the background when chosen correctly and becomes a source of friction when mismatched. Before selecting a framework, evaluate:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003ePersistence needs\u003c/strong\u003e: Do jobs need to survive restarts, or are they ephemeral?\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eScale requirements\u003c/strong\u003e: Single instance or distributed cluster?\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eOperational complexity tolerance\u003c/strong\u003e: How much infrastructure are you willing to manage?\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eIntegration constraints\u003c/strong\u003e: What databases, message queues, or frameworks already exist in your stack?\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTeam priorities\u003c/strong\u003e: Simplicity and speed versus control and features?\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eThe next article begins with Hangfire, a framework that balances usability and reliability for web applications. It demonstrates how persistent job storage, automatic retries, and built-in monitoring simplify background processing without requiring clustering or external coordination.\u003c/p\u003e\n\u003cp\u003eChoosing a scheduler is choosing an operational philosophy. Pick wisely, and background jobs become invisible enablers of system capability. Pick poorly, and they become sources of operational overhead and silent failures.\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2025-11-25T17:00:00+01:00","id":"https://daily-devops.net/posts/dotnet-job-scheduling-1-landscape/","language":"en","summary":"Why background processing matters for cloud-native .NET, and how schedulers evolved from manual timers to robust, distributed orchestration engines.","tags":["dotnet","csharp","architecture","nuget","softwareengineering"],"title":".NET Job Scheduling — The Landscape","url":"https://daily-devops.net/posts/dotnet-job-scheduling-1-landscape/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eBackground processing is one of those things that feels trivial until it isn\u0026rsquo;t. A timer here, a \u003ccode\u003eTask.Run\u003c/code\u003e there — then you\u0026rsquo;re debugging why invoices didn\u0026rsquo;t go out on the first of the month, why the retry logic fired seventeen times, or why two app instances processed the same order simultaneously. At that point, you needed a real scheduler yesterday.\u003c/p\u003e\n\u003cp\u003eThis series exists because \u0026ldquo;.NET job scheduling\u0026rdquo; is not a single problem. It\u0026rsquo;s a spectrum of trade-offs between simplicity and control, between zero dependencies and full persistence, between in-memory execution and distributed coordination across clusters. Picking wrong means either over-engineering a microservice with a Quartz.NET cluster or hitting walls the moment a SaaS platform needs durable job storage.\u003c/p\u003e\n\u003cp\u003eSeven articles. Five frameworks. One comparative review that maps requirements to the right fit.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"why-background-processing-gets-complicated\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling/#why-background-processing-gets-complicated\" title=\"Why Background Processing Gets Complicated\"\u003eWhy Background Processing Gets Complicated\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe problems that push teams toward a real scheduler are almost never visible during development. Locally, \u003ccode\u003eTask.Run\u003c/code\u003e works fine. The job runs, the test passes, the feature ships. The production incidents show up six months later, often at the worst possible time.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"the-lost-job-problem\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling/#the-lost-job-problem\" title=\"The Lost Job Problem\"\u003eThe Lost Job Problem\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThe most common failure mode is the lost job. An application restarts — a deployment, a crash, a container being evicted from a node — and any in-flight or queued work disappears with the process. In-memory scheduling has no persistence by definition. You queued fifty email notifications, the pod restarted during the send loop, and now you don\u0026rsquo;t know which ones went out and which ones didn\u0026rsquo;t. There\u0026rsquo;s no queue to inspect, no log of what ran, no way to replay. The work is simply gone.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"duplicate-execution-across-instances\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling/#duplicate-execution-across-instances\" title=\"Duplicate Execution Across Instances\"\u003eDuplicate Execution Across Instances\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThe second failure mode is the duplicate job. Once you scale beyond a single instance — which happens quickly on any cloud-hosted service — every instance running a \u003ccode\u003eHostedService\u003c/code\u003e-based timer will fire independently. If your job sends a payment confirmation, two instances mean two emails. If it charges a credit card, two instances mean two charges. Preventing this requires distributed locking: some mechanism that ensures only one instance picks up and executes a given job at a time. Rolling that yourself is possible, but the edge cases accumulate fast. What happens when the lock holder crashes mid-execution? When the lock TTL expires before the job completes? When two instances acquire the lock within the same millisecond? Frameworks that solve this problem have already worked through those edge cases. Home-grown implementations usually haven\u0026rsquo;t.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"silent-errors-and-missing-retries\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling/#silent-errors-and-missing-retries\" title=\"Silent Errors And Missing Retries\"\u003eSilent Errors And Missing Retries\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThe third failure mode is silent errors. A job throws an exception. The \u003ccode\u003eTask.Run\u003c/code\u003e wrapper swallows it, or logs it once, and moves on. Nobody knows the job failed. Nobody retries it. The downstream system it was supposed to update is now inconsistent, and the inconsistency accumulates until something upstream notices. Real schedulers give you retry policies — exponential backoff, maximum attempt counts, dead-letter queues for jobs that exhaust their retries. They give you visibility into what failed, when it failed, and why. That visibility doesn\u0026rsquo;t exist when your scheduling layer is a \u003ccode\u003eSystem.Threading.Timer\u003c/code\u003e and a try-catch block.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"operational-blindness-at-runtime\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling/#operational-blindness-at-runtime\" title=\"Operational Blindness At Runtime\"\u003eOperational Blindness At Runtime\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThe fourth failure mode is operational blindness. Even when jobs succeed, in-memory scheduling gives you nothing to observe at runtime. You can\u0026rsquo;t see what\u0026rsquo;s queued, what\u0026rsquo;s running, what ran an hour ago. You can\u0026rsquo;t pause a job that\u0026rsquo;s misbehaving without deploying a code change. You can\u0026rsquo;t trigger a one-off execution without building an admin endpoint. The moment background processing becomes important to the business — not just a convenience — this blindness becomes a liability.\u003c/p\u003e\n\u003cp\u003eNone of these problems are hypothetical. They show up on teams that made perfectly reasonable decisions early in a project and then found those decisions didn\u0026rsquo;t scale to their operational requirements. The goal of this series is to make the trade-offs explicit before you hit them in production.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"what-this-series-covers\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling/#what-this-series-covers\" title=\"What This Series Covers\"\u003eWhat This Series Covers\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003ePart 1 — \u003ca href=\"/posts/dotnet-job-scheduling-1-landscape/\"\u003eThe Landscape\u003c/a\u003e\u003c/strong\u003e sets the foundation. Why background processing matters, how the ecosystem evolved from raw timers to modern schedulers, and what architectural dimensions actually drive framework selection: persistence, clustering, observability, retry behavior, and development ergonomics.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003ePart 2 — \u003ca href=\"/posts/dotnet-job-scheduling-2-hangfire/\"\u003eHangfire and Persistent Reliability\u003c/a\u003e\u003c/strong\u003e covers the framework that balances usability and reliability for web applications. Persistent job storage in SQL Server or Redis, automatic retries, a built-in monitoring dashboard, distributed execution across multiple workers — all without requiring clustering infrastructure. The practical choice for ASP.NET Core applications that need durability without complexity.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003ePart 3 — \u003ca href=\"/posts/dotnet-job-scheduling-3-quartznet/\"\u003eQuartz.NET for Enterprise Scale\u003c/a\u003e\u003c/strong\u003e examines the framework that ports Java\u0026rsquo;s Quartz directly to .NET. Enterprise-grade clustering with database-coordinated distributed locking, advanced triggers, job calendars for business-day scheduling, and multi-datacenter coordination. The right tool when workloads push into thousands of jobs per minute or require sophisticated scheduling semantics — and the wrong tool for most other situations.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003ePart 4 — \u003ca href=\"/posts/dotnet-job-scheduling-4-coravel/\"\u003eCoravel and Fluent Simplicity\u003c/a\u003e\u003c/strong\u003e shows the opposite end of the spectrum. No database, no external dependencies, no infrastructure overhead. Coravel integrates directly with \u003ccode\u003eIServiceCollection\u003c/code\u003e, schedules jobs through a readable fluent API, and gets out of the way. The answer for internal tools, small services, or any application where background processing is a secondary concern rather than a core requirement.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003ePart 5 — \u003ca href=\"/posts/dotnet-job-scheduling-5-ncronjob/\"\u003eNCronJob and Native Minimalism\u003c/a\u003e\u003c/strong\u003e covers the ASP.NET Core–native scheduler built around \u003ccode\u003eIHostedService\u003c/code\u003e. Zero dependencies, cron expressions, execution contexts with cancellation support — and nothing else. NCronJob targets containerized microservices where stateless scheduling is sufficient and adding database dependencies would create more problems than it solves.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003ePart 6 — \u003ca href=\"/posts/dotnet-job-scheduling-6-tickerq/\"\u003eTickerQ and Modern Architecture\u003c/a\u003e\u003c/strong\u003e examines the youngest framework in the series. Source generation eliminates reflection-based job registration. EF Core handles persistence. A SignalR-powered real-time dashboard replaces polling-based UIs. TickerQ makes different bets than Hangfire — compile-time safety over convention, async-first execution, and a smaller surface area.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003ePart 7 — \u003ca href=\"/posts/dotnet-job-scheduling-7-comparative-review/\"\u003eChoosing the Right Framework\u003c/a\u003e\u003c/strong\u003e synthesizes the series into decision guidance. Feature matrices across persistence, clustering, dashboards, retry policies, cron support, and scheduling complexity. Suitability ratings across operational dimensions. Decision heuristics grounded in system maturity and infrastructure constraints rather than GitHub star counts.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"who-this-is-for\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling/#who-this-is-for\" title=\"Who This Is For\"\u003eWho This Is For\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eYou\u0026rsquo;re a .NET developer or architect evaluating background processing options — either for a new project or because the current approach is causing operational pain. You want to understand trade-offs rather than just copy configuration snippets.\u003c/p\u003e\n\u003cp\u003eThe series assumes familiarity with ASP.NET Core and dependency injection. Code examples use \u003ccode\u003eIHostedService\u003c/code\u003e, \u003ccode\u003eIServiceCollection\u003c/code\u003e, and Entity Framework where relevant. Infrastructure examples reference SQL Server, Redis, and Azure — but the architectural conclusions apply regardless of cloud provider.\u003c/p\u003e\n\u003cp\u003eIf you\u0026rsquo;re already running Hangfire or Quartz.NET in production and wondering whether you made the right call, the comparative review in Part 7 is the right starting point. If you\u0026rsquo;re starting fresh and trying to understand the landscape before committing to a framework, Part 1 gives you the context to make that decision with open eyes.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-short-answer\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling/#the-short-answer\" title=\"The Short Answer\"\u003eThe Short Answer\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eIf you need one sentence: use \u003cstrong\u003eHangfire\u003c/strong\u003e unless you have a specific reason not to. It handles the 80% case — durable background jobs in web applications — with minimal setup and a built-in dashboard that makes production operation visible.\u003c/p\u003e\n\u003cp\u003eReach for \u003cstrong\u003eQuartz.NET\u003c/strong\u003e when you need clustering across multiple application instances or advanced scheduling semantics like business calendars. Accept the operational complexity as a deliberate trade-off, not a necessary cost.\u003c/p\u003e\n\u003cp\u003eChoose \u003cstrong\u003eCoravel\u003c/strong\u003e or \u003cstrong\u003eNCronJob\u003c/strong\u003e when you specifically don\u0026rsquo;t want persistence — for stateless containers, internal tools, or cache warming where losing queued work on restart is acceptable.\u003c/p\u003e\n\u003cp\u003eConsider \u003cstrong\u003eTickerQ\u003c/strong\u003e if source generation and compile-time safety matter more than ecosystem maturity, or if you want EF Core integration without building it yourself.\u003c/p\u003e\n\u003cp\u003eThe comparative review in Part 7 maps these heuristics to concrete scenarios with more nuance.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"what-this-series-is-not\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling/#what-this-series-is-not\" title=\"What This Series Is Not\"\u003eWhat This Series Is Not\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eIt\u0026rsquo;s worth being explicit about what this series doesn\u0026rsquo;t cover, because the .NET background processing space is broader than in-process schedulers.\u003c/p\u003e\n\u003cp\u003eThis series does not cover \u003cstrong\u003eAzure Functions\u003c/strong\u003e or any other serverless compute model. Functions-based scheduling — cron triggers, timer triggers, queue-triggered functions — solves a related but distinct problem. The infrastructure model is fundamentally different: you\u0026rsquo;re not running a persistent process, you\u0026rsquo;re invoking isolated functions on demand. If your workload fits serverless, that\u0026rsquo;s a legitimate and often cheaper choice. It just isn\u0026rsquo;t the same trade-off space as embedding a scheduler inside a long-running ASP.NET Core application. The operational characteristics are different, the scaling model is different, and the failure modes are different. Treating them as interchangeable leads to bad decisions in both directions.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"why-message-queues-are-not-schedulers\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling/#why-message-queues-are-not-schedulers\" title=\"Why Message Queues Are Not Schedulers\"\u003eWhy Message Queues Are Not Schedulers\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThis series does not cover \u003cstrong\u003eAzure Service Bus\u003c/strong\u003e, \u003cstrong\u003eRabbitMQ\u003c/strong\u003e, or distributed message queues in general. Message queues and job schedulers overlap in some scenarios — both can defer work, both support retry semantics — but they\u0026rsquo;re architecturally different. A message queue is a communication channel between services. A job scheduler is an execution engine within a service. Using Service Bus as a job queue is valid; this series doesn\u0026rsquo;t tell you how to do it. If you\u0026rsquo;re building a system where the producer and consumer are different services, a message queue is likely the right abstraction. If you\u0026rsquo;re building a system where background jobs run inside the same process as the web application, an embedded scheduler is what you want.\u003c/p\u003e\n\u003cp\u003eThis series does not cover \u003cstrong\u003eactor-model frameworks\u003c/strong\u003e like Akka.NET or Orleans. Actor models can schedule and coordinate distributed work, but they represent a significantly different programming model and architectural commitment. The virtual actor model in Orleans gives you scheduling primitives, grain timers, and reminder services that persist across grain deactivations. That\u0026rsquo;s genuinely powerful for certain workloads — but adopting Orleans to get durable job scheduling is a large investment. If you\u0026rsquo;re already committed to an actor model, you have better options than adding a separate scheduler. If you\u0026rsquo;re not, adding a scheduler is almost certainly simpler than adopting an actor model.\u003c/p\u003e\n\u003cp\u003eThis series also does not benchmark raw throughput in any systematic way. You\u0026rsquo;ll find numbers in the individual articles where they\u0026rsquo;re meaningful, but throughput comparisons between in-memory and persistent schedulers are rarely the deciding factor in framework selection. A persistent scheduler writing jobs to SQL Server will always be slower than an in-memory scheduler. That\u0026rsquo;s expected. The question is whether the throughput floor of the persistent option is acceptable for your workload — and for the vast majority of applications that actually need persistence, the answer is yes. Chasing throughput numbers while ignoring operational requirements is how teams end up with fast schedulers they can\u0026rsquo;t operate.\u003c/p\u003e\n\u003cp\u003eWhat this series does focus on is the practical decision of which framework to embed in an ASP.NET Core application when you need background jobs that survive restarts, don\u0026rsquo;t duplicate across instances, fail visibly, and can be operated by someone who wasn\u0026rsquo;t the original developer. That scope is narrow enough to be useful.\u003c/p\u003e\n","date_modified":"2026-05-25T23:41:10+02:00","date_published":"2025-11-25T17:00:00+01:00","id":"https://daily-devops.net/posts/dotnet-job-scheduling/","language":"en","summary":"Seven articles comparing Hangfire, Quartz.NET, Coravel, NCronJob, and TickerQ—match each .NET job scheduler to the workloads it actually fits.","tags":["dotnet","csharp","architecture","nuget","softwareengineering"],"title":".NET Job Scheduling — The Complete Series","url":"https://daily-devops.net/posts/dotnet-job-scheduling/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\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/"}],"language":"en","title":".NET Development and Framework on Daily DevOps \u0026 .NET","version":"https://jsonfeed.org/version/1.1"}