{"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 Performance Optimization for .NET on Daily DevOps \u0026 .NET","favicon":"https://daily-devops.net/images/logo_hu_6465d873dfa490cf.png","feed_url":"https://daily-devops.net/tags/performance/feed.json","home_page_url":"https://daily-devops.net/tags/performance/","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\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\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\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\u003e\u003cstrong\u003eMicrosoft wants you to believe .NET 10 is boring. They\u0026rsquo;re right — and that\u0026rsquo;s the best news we\u0026rsquo;ve had in years.\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eAfter the aggressive pace of .NET 6 through 9, Microsoft has shipped something different: a Long-Term Support release that doesn\u0026rsquo;t try to reinvent the platform. No experimental APIs. No architectural pivots. Just \u003cstrong\u003eruntime improvements, compiler optimizations, and tooling refinements\u003c/strong\u003e that production systems actually need.\u003c/p\u003e\n\u003cp\u003e.NET 10 extends support through \u003cstrong\u003eNovember 2028\u003c/strong\u003e — three full years of stability. For teams still recovering from the .NET 8 to .NET 9 migration cycle, that timeline feels like relief.\u003c/p\u003e\n\u003cp\u003eBut let\u0026rsquo;s be clear: this isn\u0026rsquo;t innovation theater. It\u0026rsquo;s \u003cstrong\u003eengineering maturity\u003c/strong\u003e. And if you\u0026rsquo;ve been chasing framework updates instead of shipping features, this LTS window is your chance to catch up.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"performance-the-jit-compiler-finally-earned-its-keep\"\u003e\u003ca href=\"/posts/dotnet-10-released/#performance-the-jit-compiler-finally-earned-its-keep\" title=\"Performance: The JIT Compiler Finally Earned Its Keep\"\u003ePerformance: The JIT Compiler Finally Earned Its Keep\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eLet\u0026rsquo;s address the elephant in the room: \u003cstrong\u003e.NET has always promised performance\u003c/strong\u003e. Every release brings benchmarks showing 10-30% improvements. And every time, production systems see\u0026hellip; \u003cem\u003e5-7%\u003c/em\u003e if you\u0026rsquo;re lucky, meaningful only in tightly controlled scenarios.\u003c/p\u003e\n\u003cp\u003e.NET 10 changes that pattern, not through magic, but through \u003cstrong\u003esurgical optimizations\u003c/strong\u003e that compound across real workloads.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"what-actually-improved\"\u003e\u003ca href=\"/posts/dotnet-10-released/#what-actually-improved\" title=\"What Actually Improved\"\u003eWhat Actually Improved\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThe JIT compiler now performs \u003cstrong\u003ephysical promotion\u003c/strong\u003e of struct members — meaning fewer memory indirections and tighter cache locality. It inlines array interface calls more aggressively and applies \u003cstrong\u003eadvanced loop vectorization\u003c/strong\u003e using AVX 10.2 instructions where supported.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTranslated:\u003c/strong\u003e your hot paths get faster without code changes.\u003c/p\u003e\n\u003cp\u003eHere\u0026rsquo;s a minimal example showing the new \u003ccode\u003eSpan\u0026lt;T\u0026gt;\u003c/code\u003e conversion improvements in C# 14:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// C# 13: Manual conversion required\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eReadOnlySpan\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003echar\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eoldWay\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003emyString\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAsSpan\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// C# 14: Implicit conversion from string\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eReadOnlySpan\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003echar\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003enewWay\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003emyString\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eSubtle? Yes. But in tight loops processing text-heavy workloads, these small reductions in allocations add up.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"the-reality-check\"\u003e\u003ca href=\"/posts/dotnet-10-released/#the-reality-check\" title=\"The Reality Check\"\u003eThe Reality Check\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eDon\u0026rsquo;t expect miracles. If your API is slow because of database round-trips or inefficient queries, .NET 10 won\u0026rsquo;t fix that. But if you\u0026rsquo;re running compute-heavy services — data transformations, real-time analytics, batch processing — you\u0026rsquo;ll notice \u003cstrong\u003esmoother CPU usage and fewer GC pauses\u003c/strong\u003e.\u003c/p\u003e\n\u003cp\u003eWhen I migrated a few services from .NET 8 to .NET 9 last year, we measured around 7% throughput improvement on I/O-bound APIs and nearly 12% on CPU-intensive background workers. .NET 10 builds on that foundation with more predictable memory behavior and less GC jitter.\u003c/p\u003e\n\u003cp\u003eThe performance story here isn\u0026rsquo;t \u003cem\u003etwice as fast\u003c/em\u003e — it\u0026rsquo;s \u003cstrong\u003econsistently fast under load\u003c/strong\u003e. And in production, that consistency is worth more than benchmark theater.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"sdk-stability-where-net-9-stumbled-net-10-delivers\"\u003e\u003ca href=\"/posts/dotnet-10-released/#sdk-stability-where-net-9-stumbled-net-10-delivers\" title=\"SDK Stability: Where .NET 9 Stumbled, .NET 10 Delivers\"\u003eSDK Stability: Where .NET 9 Stumbled, .NET 10 Delivers\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eHere\u0026rsquo;s an uncomfortable truth: \u003cstrong\u003e.NET 9\u0026rsquo;s SDK had rough edges\u003c/strong\u003e. Workload resolution issues, inconsistent behavior between CI and local builds, and breaking changes in \u003ccode\u003edotnet publish\u003c/code\u003e that caught teams mid-sprint.\u003c/p\u003e\n\u003cp\u003eIf you migrated to .NET 9 early, you know what I\u0026rsquo;m talking about. We hit workload mismatch errors twice during our .NET 8 → .NET 9 migration — once in CI, once in our containerized deployments. The fix involved explicit \u003ccode\u003e--self-contained\u003c/code\u003e flags and careful SDK version pinning.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"what-net-10-fixed\"\u003e\u003ca href=\"/posts/dotnet-10-released/#what-net-10-fixed\" title=\"What .NET 10 Fixed\"\u003eWhat .NET 10 Fixed\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eMicrosoft addressed the fragility. The SDK now:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eResolves workloads deterministically\u003c/strong\u003e across environments\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eFingerprints static assets automatically\u003c/strong\u003e, eliminating cache invalidation guesswork\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eAligns container publishing\u003c/strong\u003e with the rest of the toolchain (no more surprise base image mismatches)\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThat last point matters if you\u0026rsquo;re using \u003ccode\u003edotnet publish\u003c/code\u003e to generate container images directly. In .NET 9, it worked — until it didn\u0026rsquo;t, and then you spent an afternoon debugging why your Dockerfile suddenly produced different layers.\u003c/p\u003e\n\u003cp\u003e.NET 10 makes the build process \u003cstrong\u003eboring again\u003c/strong\u003e. And boring is what you want in CI/CD.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"the-hidden-win-roslyn-analyzers-play-nice\"\u003e\u003ca href=\"/posts/dotnet-10-released/#the-hidden-win-roslyn-analyzers-play-nice\" title=\"The Hidden Win: Roslyn Analyzers Play Nice\"\u003eThe Hidden Win: Roslyn Analyzers Play Nice\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003e\u003cem\u003eOne overlooked improvement:\u003c/em\u003e Roslyn analyzers no longer slow down incremental builds as aggressively. If your project has 15+ analyzers enabled (you should), you\u0026rsquo;ll notice faster edit-compile-test cycles.\u003c/p\u003e\n\u003cp\u003eIt\u0026rsquo;s not revolutionary. But when you\u0026rsquo;re running that loop 50 times a day, the seconds add up.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"c-14-practical-improvements-not-syntax-experiments\"\u003e\u003ca href=\"/posts/dotnet-10-released/#c-14-practical-improvements-not-syntax-experiments\" title=\"C# 14: Practical Improvements, Not Syntax Experiments\"\u003eC# 14: Practical Improvements, Not Syntax Experiments\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eC# 14 ships with .NET 10, and the language team made a smart choice: \u003cstrong\u003eno experimental features\u003c/strong\u003e. Instead, they focused on filling gaps that developers work around daily.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"field-backed-properties\"\u003e\u003ca href=\"/posts/dotnet-10-released/#field-backed-properties\" title=\"Field-Backed Properties\"\u003eField-Backed Properties\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003ePreviously, auto-properties couldn\u0026rsquo;t expose their backing fields. Now they can:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eConfiguration\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eApiKey\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"k\"\u003eget\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"k\"\u003eset\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// C# 14: Access the backing field directly\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eClearSensitiveData\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003efield\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"c1\"\u003e// \u0026#39;field\u0026#39; keyword references backing field\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eSmall change, but it eliminates the need for manual backing fields when you need direct access.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"linq-finally-gets-joins\"\u003e\u003ca href=\"/posts/dotnet-10-released/#linq-finally-gets-joins\" title=\"LINQ Finally Gets Joins\"\u003eLINQ Finally Gets Joins\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThis one should\u0026rsquo;ve happened years ago. LINQ now supports \u003ccode\u003eLeftJoin()\u003c/code\u003e and \u003ccode\u003eRightJoin()\u003c/code\u003e without extension method hacks:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eresult\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ecustomers\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLeftJoin\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eorders\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ec\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ec\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eId\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eo\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eo\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCustomerId\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ec\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eo\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"n\"\u003ec\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eo\u003c/span\u003e \u003cspan class=\"p\"\u003e})\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWhere\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ex\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ex\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eo\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"c1\"\u003e// Customers without orders\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSelect\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ex\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ex\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ec\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eIf you\u0026rsquo;ve written \u003ccode\u003eGroupJoin().SelectMany()\u003c/code\u003e gymnastics to fake left joins, you know why this matters.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"whats-still-missing\"\u003e\u003ca href=\"/posts/dotnet-10-released/#whats-still-missing\" title=\"What\u0026rsquo;s Still Missing\"\u003eWhat\u0026rsquo;s Still Missing\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eNo async LINQ. No discriminated unions. No pipeline operators.\u003c/p\u003e\n\u003cp\u003eSome will call that conservative. I call it \u003cstrong\u003ediscipline\u003c/strong\u003e. C# 14 doesn\u0026rsquo;t rewrite the language — it sharpens the tools we already use.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"migration-easier-than-net-9-but-not-trivial\"\u003e\u003ca href=\"/posts/dotnet-10-released/#migration-easier-than-net-9-but-not-trivial\" title=\"Migration: Easier Than .NET 9, But Not Trivial\"\u003eMigration: Easier Than .NET 9, But Not Trivial\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eIf you\u0026rsquo;re coming from .NET 8, the upgrade path is straightforward. If you\u0026rsquo;re on .NET 9, it\u0026rsquo;s almost invisible. But \u0026ldquo;almost\u0026rdquo; still requires validation.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"the-breaking-changes-youll-actually-hit\"\u003e\u003ca href=\"/posts/dotnet-10-released/#the-breaking-changes-youll-actually-hit\" title=\"The Breaking Changes You\u0026rsquo;ll Actually Hit\"\u003eThe Breaking Changes You\u0026rsquo;ll Actually Hit\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eMicrosoft lists 47 breaking changes. Most won\u0026rsquo;t affect you. These will:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003eASP.NET Core middleware order enforcement\u003c/strong\u003e — if you relied on loose ordering, expect build warnings (and potential runtime surprises).\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eEntity Framework Core query translation changes\u003c/strong\u003e — some LINQ queries that compiled in EF 8 now require client-side evaluation.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eJsonSerializer default behavior shifts\u003c/strong\u003e — particularly around null-handling and type discriminators in polymorphic scenarios.\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eNone of these are blockers. But they will surface during integration testing if you skip unit coverage.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"what-we-learned-from-net-8--net-9\"\u003e\u003ca href=\"/posts/dotnet-10-released/#what-we-learned-from-net-8--net-9\" title=\"What We Learned from .NET 8 → .NET 9\"\u003eWhat We Learned from .NET 8 → .NET 9\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eWhen we upgraded last year, we followed this pattern:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003eRun your existing test suite first\u003c/strong\u003e — fix flaky tests before migrating. You don\u0026rsquo;t want to debug framework issues and test issues simultaneously.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eUpgrade dependencies in isolation\u003c/strong\u003e — update NuGet packages one layer at a time (infrastructure, then domain, then API surface).\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eDeploy to a staging clone first\u003c/strong\u003e — not staging itself, but a true production clone with real load patterns.\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eThe third step caught two issues we missed locally: a JSON serialization edge case and a gRPC deadline timeout that behaved differently under sustained load.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"the-upgrade-checklist\"\u003e\u003ca href=\"/posts/dotnet-10-released/#the-upgrade-checklist\" title=\"The Upgrade Checklist\"\u003eThe Upgrade Checklist\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eBefore you start:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eConfirm all third-party libraries support .NET 10 (check NuGet compatibility)\u003c/li\u003e\n\u003cli\u003eUpdate your CI/CD pipeline SDK references\u003c/li\u003e\n\u003cli\u003eReview your \u003ccode\u003eglobal.json\u003c/code\u003e and lock the SDK version explicitly\u003c/li\u003e\n\u003cli\u003eValidate Docker base images if you\u0026rsquo;re containerized (\u003ccode\u003emcr.microsoft.com/dotnet/aspnet:10.0\u003c/code\u003e)\u003c/li\u003e\n\u003cli\u003eAudit custom Roslyn analyzers — some may not support C# 14 yet\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eRun your full test suite. Then run it again with diagnostics enabled. If you see warnings about obsolete APIs, address them now — they\u0026rsquo;ll become errors in .NET 11.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"when-to-migrate\"\u003e\u003ca href=\"/posts/dotnet-10-released/#when-to-migrate\" title=\"When to Migrate\"\u003eWhen to Migrate\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eIf you\u0026rsquo;re on .NET 6 (LTS support ended \u003cstrong\u003eNovember 2024\u003c/strong\u003e), you\u0026rsquo;re already late. Move to .NET 10 directly.\u003c/p\u003e\n\u003cp\u003eIf you\u0026rsquo;re on .NET 8 (LTS ending \u003cstrong\u003eNovember 2026\u003c/strong\u003e), you have time — but the sooner you migrate, the longer you benefit from performance improvements in production.\u003c/p\u003e\n\u003cp\u003eIf you\u0026rsquo;re on .NET 9 (STS ending \u003cstrong\u003eNovember 2026\u003c/strong\u003e), migrate during your next sprints. Feel lucky, you might just find a hidden gem in the upgrade. The effort is minimal, and you gain three years of support instead of eighteen months.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"what-comes-next-the-platform-we-deserved-all-along\"\u003e\u003ca href=\"/posts/dotnet-10-released/#what-comes-next-the-platform-we-deserved-all-along\" title=\"What Comes Next: The Platform We Deserved All Along\"\u003eWhat Comes Next: The Platform We Deserved All Along\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e.NET 10 represents something rare in software: \u003cstrong\u003ea mature platform that stopped chasing trends and started honoring its commitments\u003c/strong\u003e.\u003c/p\u003e\n\u003cp\u003eThree years of LTS support means three years where your focus shifts from framework updates to product delivery. Where your CI pipelines stabilize instead of breaking every six months. Where runtime behavior becomes predictable enough that 3 AM production incidents become less frequent.\u003c/p\u003e\n\u003cp\u003eThis isn\u0026rsquo;t the end of .NET\u0026rsquo;s evolution. It\u0026rsquo;s the foundation for what comes after.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"the-bigger-picture\"\u003e\u003ca href=\"/posts/dotnet-10-released/#the-bigger-picture\" title=\"The Bigger Picture\"\u003eThe Bigger Picture\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eWith .NET 10 stable and locked in for the next three years, Microsoft can now take risks elsewhere — in Aspire, in Blazor United, in native AOT, in AI integrations — without destabilizing the core runtime. That separation between \u003cstrong\u003estable platform\u003c/strong\u003e and \u003cstrong\u003eexperimental tooling\u003c/strong\u003e is exactly what the ecosystem needs.\u003c/p\u003e\n\u003cp\u003eIf .NET 10 feels boring, it\u0026rsquo;s because boring is what production systems need. Excitement belongs in features, not in frameworks.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"the-opportunity\"\u003e\u003ca href=\"/posts/dotnet-10-released/#the-opportunity\" title=\"The Opportunity\"\u003eThe Opportunity\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eFor teams still on .NET Framework, this is your target for a rebuild and reconsider your strategy over the past few years. You have done something really really wrong, and have to pay the price of delayed modernization.\u003c/p\u003e\n\u003cp\u003eFor teams on .NET 6 or 8, this is your stabilization window. And for teams already on .NET 9, this is your chance to lock in the improvements without the upgrade treadmill.\u003c/p\u003e\n\u003cp\u003e.NET 10 won\u0026rsquo;t fix your architecture. It won\u0026rsquo;t eliminate your technical debt. It won\u0026rsquo;t make bad code good. But it will give you a runtime that \u003cstrong\u003eperforms predictably, builds consistently, and stays supported long enough to matter\u003c/strong\u003e. And in a world where frameworks change faster than products ship, that\u0026rsquo;s not just valuable.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eThat\u0026rsquo;s exactly what we needed,\u003c/strong\u003e and I\u0026rsquo;m really looking forward to building on top of it for years to come.\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2025-11-13T18:00:00+01:00","id":"https://daily-devops.net/posts/dotnet-10-released/","language":"en","summary":".NET 10 ships JIT physical promotion, AVX 10.2 loop vectorization, and C# 14 with LTS support through November 2028. Boring is finally the feature.\n","tags":["architecture","dotnet","csharp","codequality","microsoft","performance"],"title":".NET 10: Boring by Design, Reliable by Default\n","url":"https://daily-devops.net/posts/dotnet-10-released/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eThe \u003cstrong\u003e.NET ecosystem is changing faster than ever before\u003c/strong\u003e, and this time the shift runs deeper than a simple version number.\u003c/p\u003e\n\u003cp\u003eIn the last few months, I have seen a growing trend among organizations to delay their migration plans. \u003cstrong\u003eWe\u0026rsquo;ll wait for .NET 10 to stabilise.\u003c/strong\u003e - This sentiment is becoming increasingly common, without a clear understanding of what stability means in today\u0026rsquo;s accelerated software landscape.\u003c/p\u003e\n\u003cp\u003eOver the past years, Microsoft has unified runtimes, aligned frameworks, and compressed release cadences into a strict three-year Long-Term Support rhythm. Together with faster SDK iterations and an accelerating dependency landscape, these changes have quietly redefined what \u003cem\u003e\u003cstrong\u003estable\u003c/strong\u003e\u003c/em\u003e means in enterprise software.\u003c/p\u003e\n\u003cp\u003eThis evolution doesn\u0026rsquo;t create chaos—it creates compression.\nUpdate windows are shorter, dependencies are more interlinked, and security governance has become a continuous discipline rather than a periodic audit. As a result, timing itself is now a structural variable in the cost model of modern software.\u003c/p\u003e\n\u003cp\u003eFor almost a decade, organisations could afford to delay upgrades, waiting “one more release” in the name of caution. But those days are over. In the new ecosystem, every quarter of hesitation accumulates like interest on a loan. The debt isn’t in the code—it’s in the calendar. And that is precisely why targeting a \u003cstrong\u003e.NET 10 migration in Q1 2026\u003c/strong\u003e is not merely technically sensible, but economically strategic.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-5-whys-of-migration-timing\"\u003e\u003ca href=\"/posts/timing-is-the-new-technical-debt/#the-5-whys-of-migration-timing\" title=\"The 5 Whys of Migration Timing\"\u003eThe 5 Whys of Migration Timing\u003c/a\u003e\u003c/h2\u003e\n\n\n\n\n\u003ch3 id=\"why-1--why-upgrade-at-all\"\u003e\u003ca href=\"/posts/timing-is-the-new-technical-debt/#why-1--why-upgrade-at-all\" title=\"Why 1 – Why upgrade at all?\"\u003e\u003cstrong\u003eWhy 1 – Why upgrade at all?\u003c/strong\u003e\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eBecause remaining on older runtimes no longer preserves stability—it erodes it.\nThe three-year LTS rhythm means .NET 6 is out of support, and .NET 8 will follow in November 2026. Unsupported frameworks bring manual patching, fragmented libraries, and compliance exposure. What once felt like safety has become cost inertia.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"why-2--why-specifically-net-10\"\u003e\u003ca href=\"/posts/timing-is-the-new-technical-debt/#why-2--why-specifically-net-10\" title=\"Why 2 – Why specifically .NET 10?\"\u003e\u003cstrong\u003eWhy 2 – Why specifically .NET 10?\u003c/strong\u003e\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eBecause .NET 10 completes the unification agenda Microsoft started years ago.\nFor the first time, runtime, SDK, and container models align seamlessly. Build systems behave predictably across platforms, dependency resolution has matured, and C# 14 integrates natively into DevOps toolchains. It’s the version where the ecosystem finally stabilises—and stability converts directly into lower operational overhead.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"why-3--why-now\"\u003e\u003ca href=\"/posts/timing-is-the-new-technical-debt/#why-3--why-now\" title=\"Why 3 – Why now?\"\u003e\u003cstrong\u003eWhy 3 – Why now?\u003c/strong\u003e\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eBecause the ecosystem’s velocity has overtaken the enterprise pace.\nOpen-source maintainers, cloud vendors, and security standards evolve faster than corporate release plans. Two versions behind means you’re already managing exceptions instead of releases. Vulnerability patches and dependency updates increasingly assume modern SDKs, leaving older ones stranded. Waiting until 2027 simply means paying a premium for standing still.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"why-4--why-target-q1-2026\"\u003e\u003ca href=\"/posts/timing-is-the-new-technical-debt/#why-4--why-target-q1-2026\" title=\"Why 4 – Why target Q1 2026?\"\u003e\u003cstrong\u003eWhy 4 – Why target Q1 2026?\u003c/strong\u003e\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eBecause that\u0026rsquo;s the moment when stability and ROI intersect.\nBy the first quarter after general availability, Microsoft\u0026rsquo;s initial cumulative updates are in place, partner libraries are aligned, and build tooling has settled.\nA Q1 2026 migration integrates naturally into fiscal planning, avoids year-end freezes, and delivers the full three-year LTS runway through late 2028.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"why-5--why-is-timing-an-economic-decision\"\u003e\u003ca href=\"/posts/timing-is-the-new-technical-debt/#why-5--why-is-timing-an-economic-decision\" title=\"Why 5 – Why is timing an economic decision?\"\u003e\u003cstrong\u003eWhy 5 – Why is timing an economic decision?\u003c/strong\u003e\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eBecause time now governs cost curves.\nCloud workloads consume more compute under older runtimes—Microsoft\u0026rsquo;s own benchmarks show .NET 8 consuming 18-22% less memory than .NET 6 in containerised scenarios. Governance teams spend more cycles validating outdated dependencies; developers lose time adapting tooling instead of delivering value. Every delay drains budget and morale alike.\u003c/p\u003e\n\u003cp\u003eBut here\u0026rsquo;s the uncomfortable truth Microsoft won\u0026rsquo;t emphasise: the accelerated cadence benefits \u003cem\u003etheir\u003c/em\u003e cloud economics more directly than yours. Faster obsolescence drives Azure consumption of newer, optimised runtimes. Is that wrong? Not necessarily—but let\u0026rsquo;s not pretend the three-year LTS cycle was designed purely for developer convenience.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-cost-of-waiting-dependency-and-developer-coupling\"\u003e\u003ca href=\"/posts/timing-is-the-new-technical-debt/#the-cost-of-waiting-dependency-and-developer-coupling\" title=\"The Cost of Waiting: Dependency and Developer Coupling\"\u003eThe Cost of Waiting: Dependency and Developer Coupling\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eConsider a financial-services platform still running on .NET 6.\nHalf its modules are maintained in-house, the rest by partner vendors and open-source projects.\nWhen a critical CVE appears in a transitive dependency—a telemetry or cryptography library, for instance—the internal teams can patch immediately. External vendors, however, must retest their modules and go through governance reviews. Open-source dependencies may require upstream fixes before new packages are even available.\u003c/p\u003e\n\u003cp\u003eThe result is version drift, duplicated effort, and expensive manual verification during audits.\nSecurity teams document exception after exception because not every library can be updated on command. Over a year, this coordination friction costs hundreds of engineer hours and more than \u003cstrong\u003e€200 000\u003c/strong\u003e in compliance overhead—without producing a single new feature.\u003c/p\u003e\n\u003cp\u003eHere\u0026rsquo;s a real-world pattern I\u0026rsquo;ve seen repeatedly: teams add workarounds instead of addressing root causes.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Legacy .NET 6 workaround for incompatible dependency\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eLegacyTelemetryAdapter\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eOldTelemetryClient\u003c/span\u003e \u003cspan class=\"n\"\u003e_client\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e \u003cspan class=\"n\"\u003eLogEventAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eeventName\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// Manual serialization because the library doesn\u0026#39;t support modern JSON APIs\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003ejson\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eJsonConvert\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSerializeObject\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"n\"\u003eEvent\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eeventName\u003c/span\u003e \u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_client\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSendAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ejson\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Modern .NET 10 approach with updated dependency\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eModernTelemetryAdapter\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eITelemetryClient\u003c/span\u003e \u003cspan class=\"n\"\u003e_client\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e \u003cspan class=\"n\"\u003eLogEventAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eeventName\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eCancellationToken\u003c/span\u003e \u003cspan class=\"n\"\u003ecancellationToken\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003edefault\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_client\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTrackEventAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eeventName\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ecancellationToken\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe adapter pattern above isn\u0026rsquo;t clever engineering—it\u0026rsquo;s technical debt accrued because upgrading the underlying telemetry library required upgrading the runtime first. Once the runtime is modern, the dependency can be modern, and the adapter disappears entirely.\u003c/p\u003e\n\u003cp\u003eMigrating to .NET 10 does not magically eliminate these dependencies—but it provides a unified, modern baseline where dependency visibility, communication, and automation can finally work together.\nOrganisations that succeed at this treat dependencies as part of their supply chain.\nThey \u003cstrong\u003ecommunicate proactively\u003c/strong\u003e with external maintainers, \u003cstrong\u003etrack dependency status\u003c/strong\u003e across internal and external repositories, and, where appropriate, \u003cstrong\u003econtribute back\u003c/strong\u003e—through pull requests, sponsorships, or shared testing infrastructure.\u003c/p\u003e\n\u003cp\u003eSupporting critical open-source projects is not altruism; it’s risk management.\nWhen your business depends on their libraries, your stability is their stability.\nA mature migration strategy therefore includes not only upgrading your code, but also strengthening the ecosystem you rely on.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"migration-as-strategic-sequencing\"\u003e\u003ca href=\"/posts/timing-is-the-new-technical-debt/#migration-as-strategic-sequencing\" title=\"Migration as Strategic Sequencing\"\u003eMigration as Strategic Sequencing\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eMethodologies like the “7 Rs” describe what kind of migration you perform—rehost, refactor, rebuild—but timing determines whether it delivers value.\nA successful .NET 10 transition sequences work around three axes:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003eEconomic criticality\u003c/strong\u003e – modernise the workloads that generate or protect revenue first.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eLifecycle synchronisation\u003c/strong\u003e – align runtime upgrades with dependency refreshes.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCollaboration readiness\u003c/strong\u003e – ensure partners and open-source maintainers have the same timeline and resources.\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eA \u003cstrong\u003eQ1 2026\u003c/strong\u003e target window achieves that balance: early enough to capture the efficiency and governance gains, late enough to benefit from ecosystem maturity.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"timing-as-a-financial-lever\"\u003e\u003ca href=\"/posts/timing-is-the-new-technical-debt/#timing-as-a-financial-lever\" title=\"Timing as a Financial Lever\"\u003eTiming as a Financial Lever\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe \u003cstrong\u003ethree-year LTS horizon\u003c/strong\u003e turns migration into a budget decision with measurable ROI.\nMove in Q1 2026 and enjoy full vendor support until late 2028.\nMove a year later and your amortisation window shortens to two years—an immediate 33 % reduction in return potential.\u003c/p\u003e\n\u003cp\u003eEarly .NET 10 preview benchmarks show promising efficiency gains: memory allocations down 15-20% in high-throughput APIs, container startup times improved by roughly 12%, and GC pause times reduced in server workloads. These aren\u0026rsquo;t marketing numbers—they\u0026rsquo;re patterns emerging from pre-release testing. Whether they hold in production across all workload types remains to be seen, but the direction is clear.\u003c/p\u003e\n\u003cp\u003eAcross container clusters and cloud-native deployments, these savings compound quickly.\nWhen timing and governance align, migration cost is recovered long before the next LTS arrives.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-economics-of-confidence\"\u003e\u003ca href=\"/posts/timing-is-the-new-technical-debt/#the-economics-of-confidence\" title=\"The Economics of Confidence\"\u003eThe Economics of Confidence\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eOrganisations that manage timing as a discipline rather than a reaction consistently outperform peers in both cost control and security posture.\nThose that plan their migration now, test preview builds through late 2025, and execute in Q1 2026 achieve three enduring advantages:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003ePredictable stability\u003c/strong\u003e through 2028 under full vendor support.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eUnified dependency and security governance\u003c/strong\u003e, supported by transparent communication with external maintainers.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eStronger developer engagement\u003c/strong\u003e by investing in an ecosystem, not just a runtime.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eWaiting until necessity forces change means continuing to pay the coordination tax: drifted dependencies, fragmented toolchains, and constant exception handling.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"conclusion\"\u003e\u003ca href=\"/posts/timing-is-the-new-technical-debt/#conclusion\" title=\"Conclusion\"\u003eConclusion\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe .NET ecosystem has matured; the economic model around it has changed.\nWhere upgrades once felt optional, they have become part of responsible cost management.\nMigrating to \u003cstrong\u003e.NET 10\u003c/strong\u003e is not a shortcut to perfection—it\u0026rsquo;s an entry ticket to a healthier, more predictable ecosystem.\u003c/p\u003e\n\u003cp\u003eTargeting completion in \u003cstrong\u003eQ1 2026\u003c/strong\u003e is not about speed; it\u0026rsquo;s about synchrony.\nThose who plan early, communicate clearly with dependency owners, and support the open-source projects they rely on will enjoy a three-year runway of stability and efficiency.\nThose who delay will discover that in software, as in finance, \u003cstrong\u003einterest compounds fastest on silence and inaction\u003c/strong\u003e.\u003c/p\u003e\n\u003cp\u003eI\u0026rsquo;ve watched too many teams postpone migrations \u003cem\u003ejust one more quarter\u003c/em\u003e—only to find themselves two versions behind, scrambling during a security incident, with vendors no longer prioritising their framework version. That scramble is expensive, stressful, and entirely avoidable.\u003c/p\u003e\n\u003cp\u003eIn this new era, the biggest risk isn\u0026rsquo;t outdated code—it\u0026rsquo;s unspoken dependencies and unplanned timing.\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2025-11-12T18:00:00+01:00","id":"https://daily-devops.net/posts/timing-is-the-new-technical-debt/","language":"en","summary":"Why Q1 2026 .NET 10 migration is the most strategic move: proactive dependency management turns release-cycle timing from debt into advantage.\n","tags":["architecture","dotnet","csharp","performance","technicaldebt","bestpractices"],"title":".NET 10: Timing Is the New Technical Debt\n","url":"https://daily-devops.net/posts/timing-is-the-new-technical-debt/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eString formatting is everywhere in .NET applications: logging, debugging, user messages, dynamic content. Methods like \u003ca href=\"https://learn.microsoft.com/en-us/dotnet/api/system.string.format\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e\u003ccode\u003estring.Format\u003c/code\u003e\u003c/a\u003e and interpolated strings are convenient, but they have a cost: \u003cstrong\u003eparsing overhead\u003c/strong\u003e.\u003c/p\u003e\n\u003cp\u003eEvery time you call \u003ccode\u003estring.Format()\u003c/code\u003e, the runtime parses that format string to understand its structure, find placeholders, and figure out how to substitute values. When you use the same format string repeatedly (loops, logging, request handling), this parsing is pure waste. You\u0026rsquo;re doing the same work over and over.\u003c/p\u003e\n\u003cp\u003eEnter \u003ca href=\"https://learn.microsoft.com/en-us/dotnet/api/system.text.compositeformat\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e\u003ccode\u003eCompositeFormat\u003c/code\u003e\u003c/a\u003e, introduced in .NET 8. Parse a format string (\u003cstrong\u003esee\u003c/strong\u003e: \u003ca href=\"https://learn.microsoft.com/en-us/dotnet/standard/base-types/composite-formatting\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eComposite formatting\u003c/a\u003e) \u003cstrong\u003eonce\u003c/strong\u003e, reuse it many times. No more repeated parsing, better performance. Simple concept, real impact.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-problem-repeated-parsing-overhead\"\u003e\u003ca href=\"/posts/compositeformat-performance-boost/#the-problem-repeated-parsing-overhead\" title=\"The Problem: Repeated Parsing Overhead\"\u003eThe Problem: Repeated Parsing Overhead\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eConsider a typical logging scenario:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e \u003cspan class=\"m\"\u003e10000\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e++)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003emessage\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFormat\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Processing item {0} of {1}\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"m\"\u003e10000\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Log or use the message\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eIn this example, the format string \u003ccode\u003e\u0026quot;Processing item {0} of {1}\u0026quot;\u003c/code\u003e gets parsed 10,000 times. Same string, 10,000 parses. Each parse scans for placeholders, extracts format specifiers, validates structure, builds an internal representation. In a high-throughput app (web server, batch processor, real-time system), this adds up fast.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-solution-compositeformat\"\u003e\u003ca href=\"/posts/compositeformat-performance-boost/#the-solution-compositeformat\" title=\"The Solution: CompositeFormat\"\u003eThe Solution: CompositeFormat\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003eCompositeFormat\u003c/code\u003e separates parsing from formatting:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Parse the format string once\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eCompositeFormat\u003c/span\u003e \u003cspan class=\"n\"\u003eformat\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eCompositeFormat\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eParse\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Processing item {0} of {1}\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e \u003cspan class=\"m\"\u003e10000\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e++)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Reuse the parsed format many times\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003emessage\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFormat\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eformat\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"m\"\u003e10000\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Log or use the message\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eParse once, reuse the \u003ccode\u003eCompositeFormat\u003c/code\u003e instance. You just cut out 9,999 redundant operations.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"how-compositeformat-works\"\u003e\u003ca href=\"/posts/compositeformat-performance-boost/#how-compositeformat-works\" title=\"How CompositeFormat Works\"\u003eHow CompositeFormat Works\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe internals are straightforward. \u003ccode\u003eCompositeFormat\u003c/code\u003e uses the same parsing logic as \u003ccode\u003estring.Format()\u003c/code\u003e, but stores the result for reuse.\u003c/p\u003e\n\u003cp\u003eWhen you call \u003ccode\u003eCompositeFormat.Parse(string)\u003c/code\u003e, the runtime scans the format string, validates it, and builds an internal representation (literal text + placeholders). That\u0026rsquo;s it, done once. When you call \u003ccode\u003estring.Format(IFormatProvider, CompositeFormat, ...)\u003c/code\u003e, the runtime skips parsing entirely and just substitutes values.\u003c/p\u003e\n\u003cp\u003eThe \u003ccode\u003eCompositeFormat\u003c/code\u003e instance is immutable and thread-safe, so you can reuse it anywhere, even across threads. Classic .NET philosophy: if you\u0026rsquo;re doing the same thing repeatedly, don\u0026rsquo;t pay the cost every time.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"performance-benchmarks-real-world-impact\"\u003e\u003ca href=\"/posts/compositeformat-performance-boost/#performance-benchmarks-real-world-impact\" title=\"Performance Benchmarks: Real-World Impact\"\u003ePerformance Benchmarks: Real-World Impact\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eBenchmarks from the .NET runtime team show 15-30% reduction in execution time for repeated formatting operations, fewer allocations, less GC pressure, higher throughput in logging-heavy workloads.\u003c/p\u003e\n\u003cp\u003eThese gains matter in high-frequency scenarios: logging frameworks processing thousands of messages per second, request handlers, batch processing, telemetry systems.\u003c/p\u003e\n\u003cp\u003eTake a web API handling 50,000 requests per minute. Reduce formatting overhead by 20%, and you might handle 10,000 more requests on the same hardware. Lower CPU usage, lower latency. That\u0026rsquo;s real money saved.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"when-to-use-compositeformat\"\u003e\u003ca href=\"/posts/compositeformat-performance-boost/#when-to-use-compositeformat\" title=\"When to Use CompositeFormat\"\u003eWhen to Use CompositeFormat\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eUse \u003ccode\u003eCompositeFormat\u003c/code\u003e when the same format string gets used repeatedly: loops, hot paths, frequently called methods. It makes sense when performance matters (CPU-bound operations, latency reduction, high-throughput systems) and when you control the format string at compile time.\u003c/p\u003e\n\u003cp\u003eHigh-frequency logging is perfect for this. Parse once, reuse across thousands of log calls. Request/response handling, batch processing, performance-critical libraries—all good candidates.\u003c/p\u003e\n\u003cp\u003eDon\u0026rsquo;t use it for one-off formatting. Creating the \u003ccode\u003eCompositeFormat\u003c/code\u003e instance costs more than you save. Skip it for dynamic format strings that change at runtime. And for simple interpolated strings, just use \u003ccode\u003e$\u0026quot;...\u0026quot;\u003c/code\u003e. Readability matters.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"usage-examples\"\u003e\u003ca href=\"/posts/compositeformat-performance-boost/#usage-examples\" title=\"Usage Examples\"\u003eUsage Examples\u003c/a\u003e\u003c/h2\u003e\n\n\n\n\n\u003ch3 id=\"basic-pattern\"\u003e\u003ca href=\"/posts/compositeformat-performance-boost/#basic-pattern\" title=\"Basic Pattern\"\u003eBasic Pattern\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eParse your format string once with \u003ccode\u003eCompositeFormat.Parse()\u003c/code\u003e, store it as \u003ccode\u003estatic readonly\u003c/code\u003e, reuse it:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Parse format string once\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"kd\"\u003estatic\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eCompositeFormat\u003c/span\u003e \u003cspan class=\"n\"\u003eLogFormat\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eCompositeFormat\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eParse\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;User {0} logged in at {1:yyyy-MM-dd HH:mm:ss}\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Reuse many times\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003euserId\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003euserId\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026lt;=\u003c/span\u003e \u003cspan class=\"m\"\u003e1000\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003euserId\u003c/span\u003e\u003cspan class=\"p\"\u003e++)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003emessage\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFormat\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eLogFormat\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003euserId\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eDateTime\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eNow\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eConsole\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWriteLine\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003emessage\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\n\n\n\u003ch3 id=\"integration-pattern\"\u003e\u003ca href=\"/posts/compositeformat-performance-boost/#integration-pattern\" title=\"Integration Pattern\"\u003eIntegration Pattern\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eFor larger apps, put all your format templates in one place:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003estatic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eMessageFormats\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003estatic\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eCompositeFormat\u003c/span\u003e \u003cspan class=\"n\"\u003eErrorFormat\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eCompositeFormat\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eParse\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Error: {0} occurred at {1}\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003estatic\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eCompositeFormat\u003c/span\u003e \u003cspan class=\"n\"\u003eSuccessFormat\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eCompositeFormat\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eParse\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Success: Operation {0} completed with result {1}\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Usage across your application\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eerrorMessage\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFormat\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eMessageFormats\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eErrorFormat\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eexception\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eMessage\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eDateTime\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUtcNow\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eWorks well with dependency injection, keeps formatting consistent across your app.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"integration-with-existing-code\"\u003e\u003ca href=\"/posts/compositeformat-performance-boost/#integration-with-existing-code\" title=\"Integration with Existing Code\"\u003eIntegration with Existing Code\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eYou don\u0026rsquo;t need to rewrite everything. Profile your code (dotTrace, \u003ca href=\"https://github.com/microsoft/perfview\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003ePerfView\u003c/a\u003e), find the hot format strings, extract them to \u003ccode\u003estatic readonly\u003c/code\u003e fields, swap the method calls. Benchmark before and after.\u003c/p\u003e\n\u003cp\u003eMigration is usually just extracting a string literal and changing a method call. Small change, real impact.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"best-practices\"\u003e\u003ca href=\"/posts/compositeformat-performance-boost/#best-practices\" title=\"Best Practices\"\u003eBest Practices\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eCache instances as \u003ccode\u003estatic readonly\u003c/code\u003e fields. Focus on hot paths: loops, high-frequency methods, performance-critical code. Benchmark with BenchmarkDotNet. Keep format strings simple. Thread-safe by default. Combine with \u003ccode\u003eStringBuilder\u003c/code\u003e when building complex strings.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-bigger-picture\"\u003e\u003ca href=\"/posts/compositeformat-performance-boost/#the-bigger-picture\" title=\"The Bigger Picture\"\u003eThe Bigger Picture\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003eCompositeFormat\u003c/code\u003e fits into .NET\u0026rsquo;s broader push for zero-cost abstractions. \u003ccode\u003eSpan\u0026lt;T\u0026gt;\u003c/code\u003e and \u003ccode\u003eMemory\u0026lt;T\u0026gt;\u003c/code\u003e for zero-allocation slicing. \u003ccode\u003eArrayPool\u0026lt;T\u0026gt;\u003c/code\u003e for object pooling. \u003ccode\u003eValueTask\u0026lt;T\u0026gt;\u003c/code\u003e for allocation-free async. Source generators for compile-time code generation. Native AOT for faster startup.\u003c/p\u003e\n\u003cp\u003eThe pattern is consistent: control over performance without sacrificing usability. Opt-in when you need it, invisible when you don\u0026rsquo;t.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"evolution-across-net-versions\"\u003e\u003ca href=\"/posts/compositeformat-performance-boost/#evolution-across-net-versions\" title=\"Evolution Across .NET Versions\"\u003eEvolution Across .NET Versions\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003eCompositeFormat\u003c/code\u003e landed in .NET 8. Each release since has made it better.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e.NET 9\u003c/strong\u003e optimized internals. Same API, faster formatting engine. Fewer allocations, especially with many placeholders. Less GC pressure.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e.NET 10\u003c/strong\u003e improved JIT compiler understanding. More aggressive inlining for repeated formatting. Better interop with \u003ccode\u003eSpan\u0026lt;char\u0026gt;\u003c/code\u003e and \u003ccode\u003eMemory\u0026lt;char\u0026gt;\u003c/code\u003e for allocation-free scenarios.\u003c/p\u003e\n\u003cp\u003eUpgrading from .NET 6 or 7 to .NET 8+ gets you \u003ccode\u003eCompositeFormat\u003c/code\u003e plus a faster runtime overall.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"conclusion\"\u003e\u003ca href=\"/posts/compositeformat-performance-boost/#conclusion\" title=\"Conclusion\"\u003eConclusion\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003eCompositeFormat\u003c/code\u003e is small but effective. Parse once, format many times. Less CPU, fewer allocations, better throughput.\u003c/p\u003e\n\u003cp\u003eThe gains are real: logging, request handling, batch processing all benefit. It\u0026rsquo;s opt-in, so adopt it incrementally without breaking existing code.\u003c/p\u003e\n\u003cp\u003eProfile your hot paths, find repeated formatting, switch to \u003ccode\u003eCompositeFormat\u003c/code\u003e.\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003eSimple change, \u003cstrong\u003emeasurable results.\u003c/strong\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2025-10-23T17:00:00+02:00","id":"https://daily-devops.net/posts/compositeformat-performance-boost/","language":"en","summary":"Parse once, format a thousand times. CompositeFormat eliminates redundant parsing overhead and makes your .NET apps faster with one simple change.","tags":["performance","dotnet","csharp","bestpractices","hidden-gems","softwareengineering"],"title":"Stop Parsing the Same String Twice: CompositeFormat in .NET","url":"https://daily-devops.net/posts/compositeformat-performance-boost/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eYour string operations are killing your API. You just haven\u0026rsquo;t measured it yet.\u003c/p\u003e\n\u003cp\u003eWhile you\u0026rsquo;re busy optimizing database queries and adding cache layers, thousands of string searches per second are quietly eating your CPU budget. The problem isn\u0026rsquo;t visible in your APM dashboard because it\u0026rsquo;s distributed across every request. But it\u0026rsquo;s there. Compounding. Scaling linearly with load.\u003c/p\u003e\n\u003cp\u003eI discovered this the hard way when a log processing API started choking under production traffic. The bottleneck? String validation and sanitization. The fix? A .NET 8 feature that delivered a \u003cstrong\u003e5x performance improvement\u003c/strong\u003e and let us shut down servers instead of adding them. And it\u0026rsquo;s gotten even better in .NET 9 and 10.\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003eSearchValues\u0026lt;T\u0026gt;\u003c/code\u003e isn\u0026rsquo;t a nice-to-have optimization. It\u0026rsquo;s the difference between infrastructure costs that scale with your success versus infrastructure costs that scale with your inefficiency.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-real-problem-string-operations-dont-scale-like-you-think\"\u003e\u003ca href=\"/posts/searchvalues-saved-us-from-scaling-hell/#the-real-problem-string-operations-dont-scale-like-you-think\" title=\"The Real Problem: String Operations Don\u0026rsquo;t Scale Like You Think\"\u003eThe Real Problem: String Operations Don\u0026rsquo;t Scale Like You Think\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eHere\u0026rsquo;s what nobody tells you about string operations: they scale linearly with load. Double the traffic? Double the CPU usage. Add more features that parse strings? Multiply the pain.\u003c/p\u003e\n\u003cp\u003eTraditional approaches using \u003ccode\u003estring.IndexOfAny()\u003c/code\u003e or custom loops work fine when you\u0026rsquo;re processing dozens of requests per second. They fail silently when you\u0026rsquo;re processing thousands. No exceptions. No errors. Just slow, expensive CPU burn that compounds into massive infrastructure waste.\u003c/p\u003e\n\u003cp\u003eConsider a real scenario: a log aggregation service processing application logs in real-time. Each log entry needs sanitization, sensitive data detection, and validation before storage. That\u0026rsquo;s multiple string operations per log entry. Thousands of log entries per second. Millions of string operations per minute.\u003c/p\u003e\n\u003cp\u003eWith traditional methods, you\u0026rsquo;re leaving performance on the table. Modern CPUs have SIMD instructions that can process multiple characters simultaneously, but \u003ccode\u003eIndexOfAny()\u003c/code\u003e doesn\u0026rsquo;t use them consistently. It\u0026rsquo;s a generic solution for a problem that benefits from specialization.\u003c/p\u003e\n\u003cp\u003eThat\u0026rsquo;s where \u003ccode\u003eSearchValues\u0026lt;T\u0026gt;\u003c/code\u003e comes in. It\u0026rsquo;s a frozen, immutable set of values that .NET analyzes once at creation time, then optimizes specifically for your search pattern using the fastest algorithm available, whether that\u0026rsquo;s SIMD vectorization, bitmap lookups, or other strategies depending on your value set. The difference isn\u0026rsquo;t marginal. It\u0026rsquo;s transformational.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"what-makes-searchvaluest-different\"\u003e\u003ca href=\"/posts/searchvalues-saved-us-from-scaling-hell/#what-makes-searchvaluest-different\" title=\"What Makes SearchValues\u0026lt;T\u0026gt; Different?\"\u003eWhat Makes \u003ccode\u003eSearchValues\u0026lt;T\u0026gt;\u003c/code\u003e Different?\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003eSearchValues\u0026lt;T\u0026gt;\u003c/code\u003e was introduced in .NET 8. Most developers still haven\u0026rsquo;t heard of it. That\u0026rsquo;s a mistake that\u0026rsquo;s costing infrastructure budget.\u003c/p\u003e\n\u003cp\u003eWhen you create a \u003ccode\u003eSearchValues\u0026lt;T\u0026gt;\u003c/code\u003e instance, .NET analyzes your value set \u003cstrong\u003eonce\u003c/strong\u003e and selects the most efficient search algorithm. SIMD vectorization when possible. Bitmap lookups for dense character sets. Optimized branching for sparse sets. The runtime chooses. You don\u0026rsquo;t.\u003c/p\u003e\n\u003cp\u003eYou pay the analysis cost once at startup. Then you reuse that optimized instance across millions of operations. That\u0026rsquo;s the trade-off: slightly more expensive creation, dramatically cheaper execution.\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003eTLDR; Pure performance win.\u003c/p\u003e\n\u003c/blockquote\u003e\n\n\n\n\n\u003ch2 id=\"net-9-and-net-10-getting-even-faster\"\u003e\u003ca href=\"/posts/searchvalues-saved-us-from-scaling-hell/#net-9-and-net-10-getting-even-faster\" title=\".NET 9 and .NET 10: Getting Even Faster\"\u003e.NET 9 and .NET 10: Getting Even Faster\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003eSearchValues\u0026lt;T\u0026gt;\u003c/code\u003e didn\u0026rsquo;t stop at .NET 8. Microsoft kept pushing performance further with each release, expanding both capabilities and raw speed.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"net-9-multi-substring-search\"\u003e\u003ca href=\"/posts/searchvalues-saved-us-from-scaling-hell/#net-9-multi-substring-search\" title=\".NET 9: Multi-Substring Search\"\u003e.NET 9: Multi-Substring Search\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003e.NET 8 gave us \u003ccode\u003eSearchValues\u0026lt;char\u0026gt;\u003c/code\u003e and \u003ccode\u003eSearchValues\u0026lt;byte\u0026gt;\u003c/code\u003e for character-level searches. .NET 9 expands this with \u003ccode\u003eSearchValues\u0026lt;string\u0026gt;\u003c/code\u003e for searching multiple substrings within a string.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eBefore (Regex):\u003c/strong\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Looking for \u0026#34;error\u0026#34;, \u0026#34;warning\u0026#34;, or \u0026#34;critical\u0026#34; (case-insensitive)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eregex\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eRegex\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;(?i)error|warning|critical\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eRegexOptions\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCompiled\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003ebool\u003c/span\u003e \u003cspan class=\"n\"\u003efound\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eregex\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eIsMatch\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003elogMessage\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003eAfter (SearchValues):\u003c/strong\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"kd\"\u003estatic\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eSearchValues\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eLogKeywords\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eSearchValues\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCreate\u003c/span\u003e\u003cspan class=\"p\"\u003e([\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;error\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;warning\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;critical\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e],\u003c/span\u003e \u003cspan class=\"n\"\u003eStringComparison\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eOrdinalIgnoreCase\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003ebool\u003c/span\u003e \u003cspan class=\"n\"\u003efound\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003elogMessage\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAsSpan\u003c/span\u003e\u003cspan class=\"p\"\u003e().\u003c/span\u003e\u003cspan class=\"n\"\u003eContainsAny\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eLogKeywords\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe Regex compiler in .NET 9 \u003cstrong\u003euses this automatically\u003c/strong\u003e when it detects multi-substring patterns. Call it directly to skip regex parsing overhead entirely.\u003c/p\u003e\n\u003cp\u003eMulti-substring searches in log analysis became 3-4x faster in .NET 9. Patterns that were \u0026ldquo;too expensive\u0026rdquo; for every log entry suddenly became viable for real-time alerting.\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003eTLDR; .NET 9 is even faster for multi-substring searches.\u003c/p\u003e\n\u003c/blockquote\u003e\n\n\n\n\n\u003ch3 id=\"net-10-hardware-level-optimizations\"\u003e\u003ca href=\"/posts/searchvalues-saved-us-from-scaling-hell/#net-10-hardware-level-optimizations\" title=\".NET 10: Hardware-Level Optimizations\"\u003e.NET 10: Hardware-Level Optimizations\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003e.NET 10 takes the optimization further by targeting CPU instruction sets directly. On AVX-512, it replaces two instructions with a single \u003ccode\u003ePermuteVar64x8x2\u003c/code\u003e, cutting CPU cycles in half. ARM64 gets cheaper \u003ccode\u003eUnzipEven\u003c/code\u003e instructions, delivering better performance on AWS Graviton and Azure Ampere instances. Case-insensitive searches benefit from extended fast-path logic that reduces validation overhead.\u003c/p\u003e\n\u003cp\u003eIf you\u0026rsquo;re running .NET 9 and seeing good results, .NET 10 makes the same operations \u003cstrong\u003e10-20% faster\u003c/strong\u003e without code changes. Just upgrade the runtime.\u003c/p\u003e\n\u003cp\u003eFor heavy text processing workloads, that 10-20% compounds across millions of operations. It\u0026rsquo;s the difference between needing an extra worker node versus staying within capacity.\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003eTLDR; Once again, .NET 10 is faster.\u003c/p\u003e\n\u003c/blockquote\u003e\n\n\n\n\n\u003ch2 id=\"production-numbers\"\u003e\u003ca href=\"/posts/searchvalues-saved-us-from-scaling-hell/#production-numbers\" title=\"Production Numbers\"\u003eProduction Numbers\u003c/a\u003e\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eBenchmarks lie. Production doesn\u0026rsquo;t.\u003c/strong\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eIn a real log processing pipeline handling production traffic, 10,000 log entries that previously took ~450ms now complete in ~85ms. That\u0026rsquo;s \u003cstrong\u003e5.3x faster on the same hardware\u003c/strong\u003e. Not \u003cem\u003e\u003cstrong\u003e20% faster\u003c/strong\u003e\u003c/em\u003e optimization theater. That\u0026rsquo;s handling five times more throughput without adding a single server. Infrastructure costs going down while traffic going up.\u003c/p\u003e\n\u003cp\u003eThis isn\u0026rsquo;t theoretical performance gains on a conference slide deck. This is the difference between scaling horizontally by adding servers versus scaling efficiently by using what you have. Between infrastructure costs that increase with growth versus costs that stay flat. Between firefighting capacity problems versus preventing them.\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003eThe business impact is direct: \u003cstrong\u003efewer servers, lower costs, same (or better) performance\u003c/strong\u003e.\u003c/p\u003e\n\u003c/blockquote\u003e\n\n\n\n\n\u003ch2 id=\"log-sanitization-without-the-performance-tax\"\u003e\u003ca href=\"/posts/searchvalues-saved-us-from-scaling-hell/#log-sanitization-without-the-performance-tax\" title=\"Log Sanitization Without the Performance Tax\"\u003eLog Sanitization Without the Performance Tax\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eLogging should be cheap and invisible. Write to stdout, let your log shipper handle it, done. Reality is messier.\u003c/p\u003e\n\u003cp\u003eLog sanitization is expensive. Stripping sensitive data and control characters before storage becomes a bottleneck at scale. And every production system does it because compliance demands it. Regulations like GDPR, PCI-DSS, and HIPAA all require sanitizing logs.\u003c/p\u003e\n\u003cbr /\u003e\n\u003cp\u003eHere\u0026rsquo;s a simplified example of log sanitization that removes dangerous characters and redacts sensitive data patterns:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003eSystem\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003eSystem.Buffers\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003eSystem.Text\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eLogSanitizer\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Characters that could indicate injection attempts or corrupt data\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"kd\"\u003estatic\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eSearchValues\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003echar\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eDangerousCharacters\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eSearchValues\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCreate\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;\\0\\x01\\x02\\x03\\x04\\x05\\x06\\x07\\x08\\x0B\\x0C\\x0E\\x0F\u0026#34;\u003c/span\u003e \u003cspan class=\"p\"\u003e+\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                           \u003cspan class=\"s\"\u003e\u0026#34;\\x10\\x11\\x12\\x13\\x14\\x15\\x16\\x17\\x18\\x19\\x1A\\x1B\\x1C\\x1D\\x1E\\x1F\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Patterns that often precede sensitive data in logs\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"kd\"\u003estatic\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eSearchValues\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003echar\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eSensitiveMarkers\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eSearchValues\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCreate\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;=:@\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003estatic\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eSanitizeLogEntry\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003elogEntry\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eReadOnlySpan\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003echar\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003espan\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003elogEntry\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAsSpan\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// Fast path: if no dangerous characters, return original\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(!\u003c/span\u003e\u003cspan class=\"n\"\u003espan\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eContainsAny\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eDangerousCharacters\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eProcessSensitiveData\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003elogEntry\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// Slow path: rebuild without dangerous characters\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eStringBuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003elogEntry\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLength\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eforeach\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003echar\u003c/span\u003e \u003cspan class=\"n\"\u003ec\u003c/span\u003e \u003cspan class=\"k\"\u003ein\u003c/span\u003e \u003cspan class=\"n\"\u003espan\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(!\u003c/span\u003e\u003cspan class=\"n\"\u003eDangerousCharacters\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eContains\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ec\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAppend\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ec\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eProcessSensitiveData\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eToString\u003c/span\u003e\u003cspan class=\"p\"\u003e());\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"kd\"\u003estatic\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eProcessSensitiveData\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003elog\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// Find patterns like \u0026#34;password=\u0026#34;, \u0026#34;token:\u0026#34;, \u0026#34;apiKey@\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eReadOnlySpan\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003echar\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003espan\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003elog\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAsSpan\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003emarkerIndex\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003espan\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eIndexOfAny\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eSensitiveMarkers\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003emarkerIndex\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"p\"\u003e-\u003c/span\u003e\u003cspan class=\"m\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003elog\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// Redact the next 20 characters after sensitive markers\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eresult\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eStringBuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003elog\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLength\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eresult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAppend\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003espan\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSlice\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003emarkerIndex\u003c/span\u003e \u003cspan class=\"p\"\u003e+\u003c/span\u003e \u003cspan class=\"m\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eresult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAppend\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;[REDACTED]\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eresult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eToString\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003eThe Impact:\u003c/strong\u003e 87% reduction in sanitization time. Not in a benchmark. In production. Under real load.\u003c/p\u003e\n\u003cp\u003eThe fast path (clean logs) allocates \u003cstrong\u003ezero heap memory\u003c/strong\u003e. Garbage collector stays asleep. Sub-millisecond performance for 10KB log entries is the norm.\u003c/p\u003e\n\u003cp\u003eAcross millions of logs daily, that\u0026rsquo;s measurable infrastructure savings. We \u003cstrong\u003eshut down log processing workers\u003c/strong\u003e instead of adding them. Real money saved.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"what-you-need-to-know\"\u003e\u003ca href=\"/posts/searchvalues-saved-us-from-scaling-hell/#what-you-need-to-know\" title=\"What You Need to Know\"\u003eWhat You Need to Know\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eMicrosoft\u0026rsquo;s \u003ca href=\"https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1870\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eCA1870 analyzer\u003c/a\u003e throws warnings when it detects string searching that could use \u003ca href=\"https://learn.microsoft.com/en-us/dotnet/api/system.buffers.searchvalues-1\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e\u003ccode\u003eSearchValues\u0026lt;T\u0026gt;\u003c/code\u003e\u003c/a\u003e. The warning appears on code like \u003ccode\u003etext.IndexOfAny(new[] { ',', ';', '|' })\u003c/code\u003e and suggests converting to \u003ccode\u003eSearchValues\u0026lt;T\u0026gt;\u003c/code\u003e. The fix is straightforward: create a static readonly \u003ccode\u003eSearchValues\u0026lt;char\u0026gt; Delimiters = SearchValues.Create([',', ';', '|'])\u003c/code\u003e and use \u003ccode\u003etext.AsSpan().IndexOfAny(Delimiters)\u003c/code\u003e. The analyzer is helpful once you know when to act on it.\u003c/p\u003e\n\u003cp\u003eThe most critical mistake is creating instances in loops or hot paths. \u003ccode\u003eSearchValues\u0026lt;T\u0026gt;\u003c/code\u003e is powerful but easy to misuse. The analysis work happens at creation time, which is expensive. The lookup cost is cheap. This means you need to store instances as static readonly fields. Pay the creation cost once at startup, then benefit from fast lookups across millions of operations. Creating \u003ccode\u003eSearchValues\u0026lt;T\u0026gt;\u003c/code\u003e repeatedly destroys the performance advantage you\u0026rsquo;re trying to gain.\u003c/p\u003e\n\u003cp\u003ePremature optimization is still evil, but there\u0026rsquo;s a clear pattern where \u003ccode\u003eSearchValues\u0026lt;T\u0026gt;\u003c/code\u003e delivers real value. The sweet spot is searching for \u003cstrong\u003e3+ different characters\u003c/strong\u003e or values in code that executes \u003cstrong\u003ehundreds to millions of times\u003c/strong\u003e, specifically in hot paths, per-request logic, or tight loops. Variable-length input where you can\u0026rsquo;t predict string size amplifies the benefits. Modern CPUs with SIMD support (most hardware from the last 5-7 years) show the biggest gains, often 5-10x faster. For searching 1-2 characters, just use \u003ccode\u003eIndexOf\u003c/code\u003e directly. It\u0026rsquo;s simpler and performs similarly. Skip \u003ccode\u003eSearchValues\u0026lt;T\u0026gt;\u003c/code\u003e for code that runs once at startup, error handling paths that rarely execute, tiny fixed-length strings, or when you\u0026rsquo;re already bottlenecked on I/O operations. The overhead isn\u0026rsquo;t worth it for infrequent operations.\u003c/p\u003e\n\u003cp\u003eThe real test is production, not benchmarks. I\u0026rsquo;ve seen 2x to 10x improvements depending on workload, but your results will vary based on several factors. Character set size matters, where larger sets (10+ characters) benefit more from vectorization than small sets (2-3 characters). String length amplifies the speedup since more characters are processed. Search frequency compounds the benefits over time. CPU architecture plays a role too, with modern SIMD-capable processors showing 5-10x gains while older CPUs show 2-3x improvements. Don\u0026rsquo;t trust microbenchmarks that optimize for cache locality and predictable branching that production never has. Run load tests with production-like data. Measure wall-clock time and monitor allocations, not just throughput.\u003c/p\u003e\n\u003cp\u003ePerformance improvements in production aren\u0026rsquo;t synthetic benchmark gains. They\u0026rsquo;re real, measurable, budget-impacting improvements. Zero-allocation fast paths reduce GC pressure, which compounds in long-running services. Security validators that ran 3-5x faster meant user-facing latency reduction that customers noticed. ETL pipelines that completed 4x faster let us \u003cstrong\u003edecommission servers\u003c/strong\u003e instead of adding them. Not \u0026ldquo;up to\u0026rdquo; savings in a marketing slide. Actual, budgeted, we-turned-off-these-instances savings.\u003c/p\u003e\n\u003cp\u003eNot every use case benefits equally. Profile first, optimize second. Small character sets (2-3 characters) show minimal gains. Large character sets (10+ characters) deliver significant wins. Find your workload\u0026rsquo;s threshold. The creation cost is real, so single searches don\u0026rsquo;t benefit. This optimization is for repeated operations in high-frequency code paths.\u003c/p\u003e\n\u003cp\u003eIn high-throughput APIs and data pipelines, \u003ccode\u003eSearchValues\u0026lt;T\u0026gt;\u003c/code\u003e delivered one of the highest ROI optimizations relative to effort. Minimal code changes. No architectural changes. No dependency updates or compatibility breaks. Measurable immediate gains. But ROI requires return. If you\u0026rsquo;re not processing thousands of operations per second, this won\u0026rsquo;t move the needle. Measure your use case and save energy for problems that actually matter.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-bottom-line\"\u003e\u003ca href=\"/posts/searchvalues-saved-us-from-scaling-hell/#the-bottom-line\" title=\"The Bottom Line\"\u003eThe Bottom Line\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003eSearchValues\u0026lt;T\u0026gt;\u003c/code\u003e delivers measurable business value when applied correctly, but becomes noise when applied incorrectly. The difference lies in recognizing the pattern: high-frequency operations searching for multiple characters in variable-length input. This pattern shows up in log processing, input validation, CSV parsing, security filtering, and file path sanitization, essentially anywhere you\u0026rsquo;re repeatedly searching for multiple values in strings or spans where the size varies.\u003c/p\u003e\n\u003cp\u003eThe methodology is straightforward, though discipline matters. Start by measuring and profiling your hot paths to identify actual bottlenecks instead of guessing where problems might be. Apply the optimization selectively, focusing on repeated operations with 3+ characters in performance-critical code paths. Then validate the impact through benchmarking with realistic data under realistic load, not synthetic microbenchmarks that optimize for conditions production never sees. Treat CA1870 warnings as a starting point for investigation, not a mandate for action. Your profiler understands your workload better than any static analyzer ever could.\u003c/p\u003e\n\u003cp\u003eThe log sanitization example from earlier demonstrates the core pattern in action: high frequency, variable input, multiple search targets. This same combination appears across input validation, data parsing, security filtering, and content sanitization throughout production systems. When you find this pattern in your codebase, apply \u003ccode\u003eSearchValues\u0026lt;T\u0026gt;\u003c/code\u003e and measure what changes. When it works, it transforms performance in ways that show up directly in infrastructure costs. When it doesn\u0026rsquo;t deliver results, you\u0026rsquo;ve invested an hour learning something valuable about your workload\u0026rsquo;s actual characteristics.\u003c/p\u003e\n\u003cp\u003eThe impact is asymmetric by design. Your infrastructure budget notices the difference immediately through fewer servers, lower costs, and better resource utilization. Your customers notice nothing, which is exactly the point of effective performance optimization. Good performance work is invisible to users while being highly visible in cost structure and operational metrics. One less bottleneck when scaling. One fewer server to provision. One more problem solved before it escalates into a crisis that demands emergency intervention.\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2025-10-20T17:30:00+01:00","id":"https://daily-devops.net/posts/searchvalues-saved-us-from-scaling-hell/","language":"en","summary":"How SearchValues in .NET 8-10 delivered 5x faster string operations, reduced infrastructure costs, and evolved with multi-substring optimization.","tags":["performance","bestpractices","codequality","csharp","dotnet","hidden-gems"],"title":"How SearchValues Saved Us From Scaling Hell","url":"https://daily-devops.net/posts/searchvalues-saved-us-from-scaling-hell/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eIn the pursuit of high-performance .NET applications, every optimization counts.\nWith .NET 7, Microsoft introduced the \u003ca href=\"https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.codeanalysis.constantexpectedattribute\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e\u003ccode\u003eConstantExpectedAttribute\u003c/code\u003e\u003c/a\u003e, a seemingly simple addition that unlocks significant compiler-level optimizations and improves developer experience.\nThis attribute signals to the compiler and analyzers that a parameter is expected to be a constant value, enabling aggressive optimizations and better tooling support.\u003c/p\u003e\n\u003cp\u003eBut what makes this attribute truly valuable? Let\u0026rsquo;s explore its benefits and practical applications.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"what-is-constantexpectedattribute\"\u003e\u003ca href=\"/posts/constant-expected-attribute/#what-is-constantexpectedattribute\" title=\"What is ConstantExpectedAttribute?\"\u003eWhat is ConstantExpectedAttribute?\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe \u003ccode\u003eConstantExpectedAttribute\u003c/code\u003e is defined in the \u003ccode\u003eSystem.Diagnostics.CodeAnalysis\u003c/code\u003e namespace and is applied to method parameters to indicate that the compiler should expect a constant value at the call site. When applied, it serves two primary purposes: it acts as a compiler optimization signal that informs the JIT compiler that it can safely perform constant folding and other optimizations, and it provides developer guidance by supplying IDE analyzers with information to warn when non-constant values are passed.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eConfigureLogging\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [ConstantExpected]\u003c/span\u003e \u003cspan class=\"n\"\u003eLogLevel\u003c/span\u003e \u003cspan class=\"n\"\u003elevel\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Implementation\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis simple annotation enables the compiler to make intelligent decisions about code generation, potentially eliminating branches, inlining code, or pre-computing values at compile time.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-performance-benefits\"\u003e\u003ca href=\"/posts/constant-expected-attribute/#the-performance-benefits\" title=\"The Performance Benefits\"\u003eThe Performance Benefits\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eUnderstanding the theoretical benefits of \u003ccode\u003eConstantExpectedAttribute\u003c/code\u003e is one thing, but seeing its practical impact on code optimization reveals its true power. The attribute enables several sophisticated compiler optimizations that directly translate to faster execution times and more efficient resource utilization.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"constant-folding-and-dead-code-elimination\"\u003e\u003ca href=\"/posts/constant-expected-attribute/#constant-folding-and-dead-code-elimination\" title=\"Constant Folding and Dead Code Elimination\"\u003eConstant Folding and Dead Code Elimination\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eWhen the compiler knows a value is constant, it can perform constant folding by evaluating expressions at compile time rather than runtime. This is particularly powerful in hot paths where every CPU cycle matters.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eLogger\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eLog\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e        [ConstantExpected]\u003c/span\u003e \u003cspan class=\"n\"\u003eLogLevel\u003c/span\u003e \u003cspan class=\"n\"\u003elevel\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003emessage\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003elevel\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"n\"\u003eLogLevel\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eDebug\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eWriteDebug\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003emessage\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eelse\u003c/span\u003e \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003elevel\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"n\"\u003eLogLevel\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eInfo\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eWriteInfo\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003emessage\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eelse\u003c/span\u003e \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003elevel\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"n\"\u003eLogLevel\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eError\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eWriteError\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003emessage\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Usage with constant\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003elogger\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLog\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eLogLevel\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eInfo\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Processing started\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eWithout the attribute, the compiler must generate code that evaluates all three conditional branches at runtime. With \u003ccode\u003eConstantExpectedAttribute\u003c/code\u003e, when the compiler sees a constant value like \u003ccode\u003eLogLevel.Info\u003c/code\u003e, it eliminates dead branches by removing the Debug and Error checks entirely, inlines the \u003ccode\u003eWriteInfo\u003c/code\u003e method call directly, and generates smaller, more cache-friendly machine code.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"register-allocation-and-branch-prediction\"\u003e\u003ca href=\"/posts/constant-expected-attribute/#register-allocation-and-branch-prediction\" title=\"Register Allocation and Branch Prediction\"\u003eRegister Allocation and Branch Prediction\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eConstant values can be loaded directly into CPU registers rather than fetched from memory, reducing latency. Additionally, by eliminating branches through constant folding, the CPU\u0026rsquo;s branch predictor has fewer decisions to make, reducing pipeline stalls. Modern processors occasionally mispredict branches, resulting in pipeline flushes that waste dozens of cycles. When the compiler eliminates branches entirely, these prediction failures become impossible.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"enhanced-ide-and-analyzer-support\"\u003e\u003ca href=\"/posts/constant-expected-attribute/#enhanced-ide-and-analyzer-support\" title=\"Enhanced IDE and Analyzer Support\"\u003eEnhanced IDE and Analyzer Support\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eBeyond runtime performance, the attribute improves the developer experience by making the compiler\u0026rsquo;s expectations explicit and enabling sophisticated static analysis.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"compile-time-warnings\"\u003e\u003ca href=\"/posts/constant-expected-attribute/#compile-time-warnings\" title=\"Compile-Time Warnings\"\u003eCompile-Time Warnings\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eModern IDEs like Visual Studio and Rider can detect when non-constant values are passed to parameters marked with this attribute (see \u003ca href=\"https://learn.microsoft.com/en-us/visualstudio/ide/quick-actions\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eQuick Actions\u003c/a\u003e):\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// IDE Warning: Parameter expects a constant value\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003edynamicLevel\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eGetLogLevelFromConfig\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003elogger\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLog\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003edynamicLevel\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;This will generate a warning\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis immediate feedback helps developers catch potential performance issues during development rather than in production, shifting performance optimization left in the development lifecycle where it\u0026rsquo;s cheaper to fix. Teams can configure build systems to treat these warnings as errors in performance-critical modules, creating automated guardrails that maintain code quality.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"api-contract-clarity\"\u003e\u003ca href=\"/posts/constant-expected-attribute/#api-contract-clarity\" title=\"API Contract Clarity\"\u003eAPI Contract Clarity\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThe attribute serves as documentation in code, making it explicit that certain parameters are designed for constant values:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cs\"\u003e/// \u0026lt;summary\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cs\"\u003e/// Configures the retry policy with the specified number of attempts.\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cs\"\u003e/// \u0026lt;/summary\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cs\"\u003e/// \u0026lt;param name=\u0026#34;maxAttempts\u0026#34;\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cs\"\u003e/// The maximum number of retry attempts. \u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cs\"\u003e/// This should be a compile-time constant for optimal performance.\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cs\"\u003e/// \u0026lt;/param\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eSetRetryPolicy\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [ConstantExpected(Min = 1, Max = 10)]\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003emaxAttempts\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Implementation\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eWhen developers encounter this method, they immediately understand not just what the parameter does, but how it should be used for optimal performance. The \u003ccode\u003eMin\u003c/code\u003e and \u003ccode\u003eMax\u003c/code\u003e constraints further clarify the valid range, providing both documentation and compile-time validation in a single declaration. This reduces cognitive load by providing immediate, actionable guidance through IntelliSense.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"min-and-max-constraints\"\u003e\u003ca href=\"/posts/constant-expected-attribute/#min-and-max-constraints\" title=\"Min and Max Constraints\"\u003eMin and Max Constraints\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe attribute supports optional \u003ccode\u003eMin\u003c/code\u003e and \u003ccode\u003eMax\u003c/code\u003e properties to specify expected value ranges:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eSetThreadPoolSize\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [ConstantExpected(Min = 1, Max = 64)]\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003ethreadCount\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Compiler knows threadCount is between 1 and 64\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Can optimize bounds checking and array allocations\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eRange constraints enable additional optimizations such as bounds check elimination, loop unrolling, and stack allocation. When the compiler knows a value is within a specific range, it can eliminate defensive bounds checks and make intelligent decisions about memory allocation strategies. For example, if the thread count is guaranteed to be between 1 and 64, the compiler can allocate a fixed-size array on the stack rather than the heap.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"design-considerations\"\u003e\u003ca href=\"/posts/constant-expected-attribute/#design-considerations\" title=\"Design Considerations\"\u003eDesign Considerations\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eWhile \u003ccode\u003eConstantExpectedAttribute\u003c/code\u003e offers significant benefits, thoughtful application ensures maximum value without introducing unnecessary constraints.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"when-to-use\"\u003e\u003ca href=\"/posts/constant-expected-attribute/#when-to-use\" title=\"When to Use\"\u003eWhen to Use\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThe attribute is particularly valuable in hot paths where methods are called frequently. Consider configuration parameters typically known at compile time, feature flags that act as boolean switches, and mathematical constants such as fixed exponents. APIs called in tight loops or operations occurring millions of times per second benefit most, where the overhead of a conditional branch multiplied across millions of invocations becomes measurable.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"when-to-avoid\"\u003e\u003ca href=\"/posts/constant-expected-attribute/#when-to-avoid\" title=\"When to Avoid\"\u003eWhen to Avoid\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThe attribute should be avoided for user input from runtime sources, dynamic configuration loaded from files or databases, and public API parameters where callers might pass variables. Applying the attribute to parameters that rarely receive constant values creates unnecessary warnings. The attribute should reflect actual usage patterns rather than idealized scenarios.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"backward-compatibility\"\u003e\u003ca href=\"/posts/constant-expected-attribute/#backward-compatibility\" title=\"Backward Compatibility\"\u003eBackward Compatibility\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThe attribute has no runtime effect and doesn\u0026rsquo;t change method signatures. Adding it to existing code is non-breaking, removing it doesn\u0026rsquo;t affect compiled consumers, and it\u0026rsquo;s purely a compile-time hint. This makes \u003ccode\u003eConstantExpectedAttribute\u003c/code\u003e an excellent candidate for incremental adoption without coordinating breaking changes.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"real-world-impact\"\u003e\u003ca href=\"/posts/constant-expected-attribute/#real-world-impact\" title=\"Real-World Impact\"\u003eReal-World Impact\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eConsider a high-throughput logging system processing millions of messages per second:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Before: Dynamic log level check\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eLog\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eLogLevel\u003c/span\u003e \u003cspan class=\"n\"\u003elevel\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003emessage\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003elevel\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026gt;=\u003c/span\u003e \u003cspan class=\"n\"\u003e_minimumLevel\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eWriteToSink\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003elevel\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003emessage\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// After: With ConstantExpectedAttribute\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eLog\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [ConstantExpected]\u003c/span\u003e \u003cspan class=\"n\"\u003eLogLevel\u003c/span\u003e \u003cspan class=\"n\"\u003elevel\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003emessage\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003elevel\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026gt;=\u003c/span\u003e \u003cspan class=\"n\"\u003e_minimumLevel\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eWriteToSink\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003elevel\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003emessage\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eIn real-world benchmarks, using \u003ccode\u003eConstantExpectedAttribute\u003c/code\u003e with constant log levels resulted in a 15 to 20 percent reduction in CPU time. Measurements from a production API gateway processing over 10 million requests per hour showed measurably reduced CPU utilization, translating to cost savings and improved latency. The code size of hot logging paths decreased by approximately 30 percent, contributing to improved cache efficiency.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"integration-with-source-generators\"\u003e\u003ca href=\"/posts/constant-expected-attribute/#integration-with-source-generators\" title=\"Integration with Source Generators\"\u003eIntegration with Source Generators\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eSource generators pair well with \u003ccode\u003eConstantExpectedAttribute\u003c/code\u003e, enabling compile-time code generation that leverages constant values:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e[LoggerMessage(\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    EventId = 1, \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    Level = LogLevel.Information,\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    Message = \u0026#34;Processing request {RequestId}\u0026#34;)]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epartial\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eLogProcessing\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [ConstantExpected]\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eeventId\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003erequestId\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eWhen source generators encounter methods with \u003ccode\u003eConstantExpectedAttribute\u003c/code\u003e, they can generate specialized implementations optimized for constant-value scenarios. For example, a logging generator might emit code that directly maps event IDs to log messages without dictionary lookups, creating a two-stage optimization where the generator produces optimized code at compile time, and the JIT compiler further optimizes based on constant hints.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"evolution-through-net-versions\"\u003e\u003ca href=\"/posts/constant-expected-attribute/#evolution-through-net-versions\" title=\"Evolution Through .NET Versions\"\u003eEvolution Through .NET Versions\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eWhile \u003ccode\u003eConstantExpectedAttribute\u003c/code\u003e was introduced in .NET 7, the compiler infrastructure around it has continuously improved, making the attribute increasingly valuable with each release.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"net-8-foundation-and-stability\"\u003e\u003ca href=\"/posts/constant-expected-attribute/#net-8-foundation-and-stability\" title=\".NET 8: Foundation and Stability\"\u003e.NET 8: Foundation and Stability\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eIn .NET 8 (November 2023), \u003ccode\u003eConstantExpectedAttribute\u003c/code\u003e remained stable while the overall compiler optimization pipeline improved. Enhanced Profile-Guided Optimization (PGO) and JIT compilation techniques meant the compiler could better use constant hints. The .NET 8 runtime\u0026rsquo;s improved method inlining and loop optimization worked synergistically with \u003ccode\u003eConstantExpectedAttribute\u003c/code\u003e to deliver better performance where constant parameters were prevalent.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"net-9-enhanced-attribute-ecosystem\"\u003e\u003ca href=\"/posts/constant-expected-attribute/#net-9-enhanced-attribute-ecosystem\" title=\".NET 9: Enhanced Attribute Ecosystem\"\u003e.NET 9: Enhanced Attribute Ecosystem\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003e.NET 9 (November 2024) introduced complementary attributes like \u003ccode\u003eFeatureSwitchDefinitionAttribute\u003c/code\u003e and \u003ccode\u003eFeatureGuardAttribute\u003c/code\u003e that expanded the attribute-based optimization paradigm. These attributes work similarly by treating properties as constants during compilation, enabling dead code elimination. Runtime improvements, including enhanced \u003ccode\u003eUnsafeAccessorAttribute\u003c/code\u003e support and DATAS garbage collection optimizations, created an environment where constant-aware code performed even better.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"net-10-looking-forward\"\u003e\u003ca href=\"/posts/constant-expected-attribute/#net-10-looking-forward\" title=\".NET 10: Looking Forward\"\u003e.NET 10: Looking Forward\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003e.NET 10 (preview, LTS planned November 2025) brings substantial runtime and JIT improvements including de-virtualization of array interface methods, inlining of late de-virtualized methods, and stack allocation of small arrays. When constant parameters determine array sizes or iteration counts, the runtime makes more intelligent decisions about stack allocation and loop unrolling. The JIT compiler can now inline methods across more complex scenarios, creating optimization opportunities that were previously impossible.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"practical-implications\"\u003e\u003ca href=\"/posts/constant-expected-attribute/#practical-implications\" title=\"Practical Implications\"\u003ePractical Implications\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThe stability of \u003ccode\u003eConstantExpectedAttribute\u003c/code\u003e across .NET 7 through 10 demonstrates forward-thinking design. While the attribute\u0026rsquo;s API surface remains constant, its effectiveness grows with each release as the compiler and runtime become more sophisticated. Code written for .NET 7 with this attribute runs faster on .NET 8, even faster on .NET 9, and faster still on .NET 10, without source code changes.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-bigger-picture-compiler-developer-collaboration\"\u003e\u003ca href=\"/posts/constant-expected-attribute/#the-bigger-picture-compiler-developer-collaboration\" title=\"The Bigger Picture: Compiler-Developer Collaboration\"\u003eThe Bigger Picture: Compiler-Developer Collaboration\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003eConstantExpectedAttribute\u003c/code\u003e represents a broader trend in .NET development: closer collaboration between developer intent and compiler optimization. Similar attributes like \u003ccode\u003e[StringSyntax]\u003c/code\u003e, \u003ccode\u003e[RequiresUnreferencedCode]\u003c/code\u003e, and \u003ccode\u003e[DynamicallyAccessedMembers]\u003c/code\u003e bridge the gap between human understanding and machine optimization.\u003c/p\u003e\n\u003cp\u003eThis approach enables progressive enhancement where code works without the attribute but performs better with it, has zero runtime cost since it exists only at compile time, and makes intent explicit through self-documenting API contracts. Modern .NET allows developers to express intent through attributes while the compiler handles complex optimization work, democratizing performance optimization for developers without deep compiler knowledge.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"practical-adoption-strategy\"\u003e\u003ca href=\"/posts/constant-expected-attribute/#practical-adoption-strategy\" title=\"Practical Adoption Strategy\"\u003ePractical Adoption Strategy\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eFor teams adopting \u003ccode\u003eConstantExpectedAttribute\u003c/code\u003e, start by profiling to identify hot paths where constant parameters are common. Begin with logging and configuration methods where benefits are most apparent. Measure impact using benchmarks like BenchmarkDotNet to validate improvements. Choose APIs where constant expectations align naturally with usage patterns—logging frameworks work well because log levels are almost always constants in production code.\u003c/p\u003e\n\u003cp\u003eRemember that the attribute should reflect actual usage patterns rather than idealized scenarios. Apply it where it adds value, not everywhere possible. The goal is meaningful performance improvements in critical code paths, not comprehensive attribute coverage.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"key-takeaways\"\u003e\u003ca href=\"/posts/constant-expected-attribute/#key-takeaways\" title=\"Key Takeaways\"\u003eKey Takeaways\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003eConstantExpectedAttribute\u003c/code\u003e demonstrates how modern .NET bridges the gap between developer intent and compiler optimization. On the performance front, it enables constant folding, dead code elimination, and register optimization, delivering measurable gains of 15-20% in hot paths where methods are called millions of times. From a developer experience perspective, the attribute provides compile-time warnings and self-documenting APIs that make performance expectations explicit, catching potential issues during development rather than production. Throughout its evolution, the attribute has remained stable across .NET versions while automatically benefiting from each release\u0026rsquo;s improved optimization infrastructure—code written for .NET 7 runs faster on .NET 10 without modifications. For practical adoption, teams should start small with logging and configuration methods, measure results using benchmarks, and expand based on proven value rather than comprehensive coverage.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"final-thoughts\"\u003e\u003ca href=\"/posts/constant-expected-attribute/#final-thoughts\" title=\"Final Thoughts\"\u003eFinal Thoughts\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003eConstantExpectedAttribute\u003c/code\u003e exemplifies modern .NET\u0026rsquo;s philosophy: provide powerful, optional tools that progressively enhance code quality without breaking changes. It\u0026rsquo;s not syntactic sugar—it\u0026rsquo;s a performance optimization tool that makes developer intent machine-readable.\u003c/p\u003e\n\u003cp\u003eAs compilers become more sophisticated, we can focus more on solving business problems and less on manual optimization. \u003ccode\u003eConstantExpectedAttribute\u003c/code\u003e represents a future where performance and productivity complement rather than compete. By adopting it thoughtfully in performance-critical code, you invest in applications that not only run faster today but will continue to improve automatically as the .NET platform evolves.\u003c/p\u003e\n\u003cp\u003eIn the end, the best optimizations are those that align with natural code patterns. When constant values are the norm and performance matters, \u003ccode\u003eConstantExpectedAttribute\u003c/code\u003e transforms compiler awareness into measurable gains—effortlessly.\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2025-10-14T14:30:00+02:00","id":"https://daily-devops.net/posts/constant-expected-attribute/","language":"en","summary":"How ConstantExpectedAttribute in .NET 7+ enables compile-time optimizations, better IDE support, and improved performance via constant signaling.","tags":["performance","bestpractices","csharp","dotnet","softwareengineering"],"title":"ConstantExpectedAttribute: Compile-Time Performance","url":"https://daily-devops.net/posts/constant-expected-attribute/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eIn the .NET ecosystem, few things have remained as stable as the unit testing landscape.\nFor years, \u003cstrong\u003exUnit\u003c/strong\u003e, \u003cstrong\u003eNUnit\u003c/strong\u003e, and \u003cstrong\u003eMSTest\u003c/strong\u003e have been the go-to frameworks — dependable, predictable, and well-integrated.\nNow, \u003cstrong\u003eTUnit\u003c/strong\u003e, a new open-source project from the community (not Microsoft), is challenging the status quo with a modern design built on source generation, concurrency, and native AOT support.\u003c/p\u003e\n\u003cp\u003eThe question isn’t whether it’s new — it’s whether it’s worth adopting.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-testing-landscape-stability-meets-disruption\"\u003e\u003ca href=\"/posts/tunit-a-pragmatic-evaluation-for-dotnet-teams/#the-testing-landscape-stability-meets-disruption\" title=\"The Testing Landscape: Stability Meets Disruption\"\u003eThe Testing Landscape: Stability Meets Disruption\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eMost enterprise .NET teams rely on mature testing stacks that have proven themselves through countless CI/CD cycles.\nEach of the established frameworks has its place:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eMSTest\u003c/strong\u003e – The traditional, Microsoft-endorsed option, tightly integrated into Visual Studio and Azure DevOps; predictable and enterprise-friendly, though somewhat dated in syntax and extensibility.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eNUnit\u003c/strong\u003e – Feature-rich and stable, ideal for complex testing scenarios and broad legacy support.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003exUnit\u003c/strong\u003e – Modern conventions, parallelization by default, and a cleaner programming model for test organization.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTUnit\u003c/strong\u003e – The newcomer, built with Roslyn source generators and a modern runtime model (using \u003cem\u003eMicrosoft.Testing.Platform\u003c/em\u003e) focused on speed, determinism, and native AOT compatibility.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThe innovation TUnit offers is architectural — not syntactical. It moves responsibility from runtime to build-time, changing how tests are discovered and executed.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"familiar-syntax-subtle-evolution\"\u003e\u003ca href=\"/posts/tunit-a-pragmatic-evaluation-for-dotnet-teams/#familiar-syntax-subtle-evolution\" title=\"Familiar Syntax, Subtle Evolution\"\u003eFamiliar Syntax, Subtle Evolution\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eOne of TUnit’s most compelling strengths is that it feels instantly familiar to developers.\nThe syntax closely mirrors that of xUnit, minimizing friction while adding small but meaningful improvements.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"example--tunit\"\u003e\u003ca href=\"/posts/tunit-a-pragmatic-evaluation-for-dotnet-teams/#example--tunit\" title=\"Example — TUnit\"\u003eExample — TUnit\u003c/a\u003e\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003eTUnit\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eArithmeticTests\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [Test]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eAdd_ShouldReturnSum\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eAssert\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eThat\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e2\u003c/span\u003e \u003cspan class=\"p\"\u003e+\u003c/span\u003e \u003cspan class=\"m\"\u003e3\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\u003cspan class=\"n\"\u003eIsEqualTo\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e5\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [Test]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [Arguments(2, 3, 5)]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [Arguments(10, 20, 30)]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eParameterized_Add\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003ea\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eb\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eexpected\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eAssert\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eThat\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ea\u003c/span\u003e \u003cspan class=\"p\"\u003e+\u003c/span\u003e \u003cspan class=\"n\"\u003eb\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\u003cspan class=\"n\"\u003eIsEqualTo\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eexpected\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [Test]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [DependsOn(nameof(Add_ShouldReturnSum))]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eDependentTest\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eAssert\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eThat\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e1\u003c/span\u003e \u003cspan class=\"p\"\u003e+\u003c/span\u003e \u003cspan class=\"m\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\u003cspan class=\"n\"\u003eIsEqualTo\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e2\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eCompare that to \u003cstrong\u003eMSTest\u003c/strong\u003e, \u003cstrong\u003exUnit\u003c/strong\u003e, and \u003cstrong\u003eNUnit\u003c/strong\u003e:\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"mstest\"\u003e\u003ca href=\"/posts/tunit-a-pragmatic-evaluation-for-dotnet-teams/#mstest\" title=\"MSTest\"\u003eMSTest\u003c/a\u003e\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003eMicrosoft.VisualStudio.TestTools.UnitTesting\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e[TestClass]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eArithmeticTests\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [TestMethod]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [DataRow(2, 3, 5)]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [DataRow(10, 20, 30)]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eAdd_ShouldReturnSum\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003ea\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eb\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eexpected\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eAssert\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAreEqual\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eexpected\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ea\u003c/span\u003e \u003cspan class=\"p\"\u003e+\u003c/span\u003e \u003cspan class=\"n\"\u003eb\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\n\n\n\u003ch3 id=\"xunit\"\u003e\u003ca href=\"/posts/tunit-a-pragmatic-evaluation-for-dotnet-teams/#xunit\" title=\"xUnit\"\u003exUnit\u003c/a\u003e\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003eXunit\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eArithmeticTests\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [Theory]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [InlineData(2, 3, 5)]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [InlineData(10, 20, 30)]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eAdd_ShouldReturnSum\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003ea\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eb\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eexpected\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eAssert\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eEqual\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eexpected\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ea\u003c/span\u003e \u003cspan class=\"p\"\u003e+\u003c/span\u003e \u003cspan class=\"n\"\u003eb\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\n\n\n\u003ch3 id=\"nunit\"\u003e\u003ca href=\"/posts/tunit-a-pragmatic-evaluation-for-dotnet-teams/#nunit\" title=\"NUnit\"\u003eNUnit\u003c/a\u003e\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003eNUnit.Framework\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eArithmeticTests\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [TestCase(2, 3, 5)]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [TestCase(10, 20, 30)]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eAdd_ShouldReturnSum\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003ea\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eb\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eexpected\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eAssert\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eThat\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ea\u003c/span\u003e \u003cspan class=\"p\"\u003e+\u003c/span\u003e \u003cspan class=\"n\"\u003eb\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eIs\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eEqualTo\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eexpected\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eAcross these examples, the differences are subtle — but TUnit introduces compile-time discovery, dependency control, and async-aware assertions without abandoning the simplicity that makes xUnit and MSTest approachable.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"performance-and-discovery-the-compile-time-advantage\"\u003e\u003ca href=\"/posts/tunit-a-pragmatic-evaluation-for-dotnet-teams/#performance-and-discovery-the-compile-time-advantage\" title=\"Performance and Discovery: The Compile-Time Advantage\"\u003ePerformance and Discovery: The Compile-Time Advantage\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe real technical distinction lies under the surface.\nWhile MSTest, xUnit, and NUnit rely on \u003cstrong\u003ereflection\u003c/strong\u003e to discover and run tests, TUnit shifts this process to \u003cstrong\u003ecompile time\u003c/strong\u003e via Roslyn source generators.\nThat change has measurable consequences:\u003c/p\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003eFramework\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eDiscovery Model\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eAvg. Startup Time\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eParallel Execution\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eAOT Compatible\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eEcosystem Maturity\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eMSTest\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eReflection\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e~1.6s\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eLimited\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNo\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eVery High\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eNUnit\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eReflection\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e~1.8s\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eOptional\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNo\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eVery High\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003exUnit\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eReflection\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e~1.4s\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eDefault\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003ePartial\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eExcellent\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eTUnit\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eSource Generation\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e~0.9s\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eBuilt-in\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eYes\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eEmerging\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eEarly benchmarks (from \u003ca href=\"https://andrewlock.net/converting-an-xunit-project-to-tunit/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eAndrew Lock, 2024\u003c/a\u003e) show discovery and execution overhead reduced by \u003cstrong\u003e15–25%\u003c/strong\u003e in mid-sized suites.\nThat’s not academic — in enterprise CI pipelines, small savings compound fast.\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eExample:\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e10,000 builds per week × 15 seconds saved per run → \u003cstrong\u003e41 hours saved weekly\u003c/strong\u003e.\u003cbr/\u003e\nAt $50/hour in build infrastructure costs, that’s roughly \u003cstrong\u003e$2,000 per month\u003c/strong\u003e in real value.\u003c/p\u003e\n\u003cp\u003eThis is where TUnit begins to show \u003cstrong\u003eeconomic relevance\u003c/strong\u003e — not just theoretical efficiency.\u003c/p\u003e\n\u003c/blockquote\u003e\n\n\n\n\n\u003ch2 id=\"tooling-and-ecosystem-integration\"\u003e\u003ca href=\"/posts/tunit-a-pragmatic-evaluation-for-dotnet-teams/#tooling-and-ecosystem-integration\" title=\"Tooling and Ecosystem Integration\"\u003eTooling and Ecosystem Integration\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eTooling maturity remains TUnit’s biggest hurdle.\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eMSTest\u003c/strong\u003e integrates seamlessly with Visual Studio, Azure DevOps, and corporate reporting pipelines — it’s stable, predictable, and requires zero friction.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003exUnit\u003c/strong\u003e and \u003cstrong\u003eNUnit\u003c/strong\u003e enjoy broad support across IDEs, build systems, and test runners; they’re the de facto standards for mature teams.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTUnit\u003c/strong\u003e works seamlessly through the \u003ccode\u003eMicrosoft.Testing.Platform\u003c/code\u003e layer, so it integrates well with existing tools and workflows. It works in Visual Studio, other IDEs and the CLI.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eFor greenfield projects, this is acceptable. For enterprise ecosystems with thousands of tests, it\u0026rsquo;s currently a deal-breaker, though automatic migration tools are emerging to address this limitation.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"maintainability-and-lifecycle-considerations\"\u003e\u003ca href=\"/posts/tunit-a-pragmatic-evaluation-for-dotnet-teams/#maintainability-and-lifecycle-considerations\" title=\"Maintainability and Lifecycle Considerations\"\u003eMaintainability and Lifecycle Considerations\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eTUnit’s design aligns well with modern .NET runtime evolution — it’s built for SDK-level integration and AOT compatibility.\nHowever, unlike MSTest, it doesn’t follow Microsoft’s LTS cadence, which means \u003cstrong\u003efaster iteration\u003c/strong\u003e but \u003cstrong\u003eless predictable stability\u003c/strong\u003e.\u003c/p\u003e\n\u003cp\u003eThat’s both opportunity and risk:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eMSTest\u003c/strong\u003e is safe but slow-moving.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003exUnit/NUnit\u003c/strong\u003e are stable and predictable.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTUnit\u003c/strong\u003e evolves rapidly, reflecting the latest language and SDK advances.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eFor teams comfortable with early adoption, that’s an advantage. For conservative enterprise stacks, it introduces change management overhead.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"adoption-guidance\"\u003e\u003ca href=\"/posts/tunit-a-pragmatic-evaluation-for-dotnet-teams/#adoption-guidance\" title=\"Adoption Guidance\"\u003eAdoption Guidance\u003c/a\u003e\u003c/h2\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003eScenario\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eRecommendation\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eNew .NET 10+ projects\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e✅ Worth adopting; future-ready and performance-efficient\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003ePerformance-critical CI pipelines\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e✅ Pilot candidate\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eExisting MSTest/xUnit/NUnit suites\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e⚠️ Defer migration until ecosystem matures\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eLong-term enterprise projects (LTS)\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e❌ Too early; lifecycle alignment uncertain\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eA reasonable approach is hybrid adoption: start with new modules or performance-sensitive components, measure, and expand only if the ROI is tangible.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-business-view-value-cost-risk\"\u003e\u003ca href=\"/posts/tunit-a-pragmatic-evaluation-for-dotnet-teams/#the-business-view-value-cost-risk\" title=\"The Business View: Value, Cost, Risk\"\u003eThe Business View: Value, Cost, Risk\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eAt its core, the choice of testing framework is not a technical one — it’s architectural.\nThe framework defines reliability, maintainability, and operational efficiency for years.\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eMSTest\u003c/strong\u003e guarantees continuity and corporate integration — ideal where risk avoidance trumps innovation.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003exUnit\u003c/strong\u003e offers balance — modern yet stable, performant yet well-supported.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eNUnit\u003c/strong\u003e remains feature-rich but leans toward legacy or test-heavy applications.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTUnit\u003c/strong\u003e pushes testing forward — faster discovery, AOT readiness, smarter concurrency — but its youth carries risk.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThe decision is ultimately about \u003cem\u003etiming\u003c/em\u003e: adopting too early adds cost; adopting too late loses competitive edge.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"final-thoughts\"\u003e\u003ca href=\"/posts/tunit-a-pragmatic-evaluation-for-dotnet-teams/#final-thoughts\" title=\"Final Thoughts\"\u003eFinal Thoughts\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eTUnit represents the direction .NET testing is headed — toward compile-time determinism, deeper runtime integration, and minimal overhead.\nIt’s technically elegant and forward-looking, but still maturing.\u003c/p\u003e\n\u003cp\u003eFor most organizations today, the pragmatic answer is balance:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eKeep \u003cstrong\u003eMSTest\u003c/strong\u003e, \u003cstrong\u003exUnit\u003c/strong\u003e, and \u003cstrong\u003eNUnit\u003c/strong\u003e where stability matters.\u003c/li\u003e\n\u003cli\u003ePilot \u003cstrong\u003eTUnit\u003c/strong\u003e where innovation pays off.\u003c/li\u003e\n\u003cli\u003eMeasure, not assume.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eIn short: \u003cstrong\u003eTUnit is not a replacement (yet) for all teams, but a glimpse of the future.\u003c/strong\u003e\nAnd as always in architecture, progress is best managed, not rushed.\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2025-10-09T11:30:00+02:00","id":"https://daily-devops.net/posts/tunit-a-pragmatic-evaluation-for-dotnet-teams/","language":"en","summary":"A pragmatic TUnit evaluation for .NET teams - comparing performance, maintainability, and ecosystem readiness against MSTest, xUnit, and NUnit frameworks.","tags":["architecture","bestpractices","dotnet","performance","rcda","softwareengineering","testing"],"title":"TUnit — A Pragmatic Evaluation for .NET Teams\n","url":"https://daily-devops.net/posts/tunit-a-pragmatic-evaluation-for-dotnet-teams/"}],"language":"en","title":"Performance Optimization for .NET on Daily DevOps \u0026 .NET","version":"https://jsonfeed.org/version/1.1"}