<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0"><channel><title><![CDATA[Andrew Lock | .NET Escapades]]></title><description><![CDATA[Hi, my name is Andrew, or ‘Sock’ to most people. This blog is where I share my experiences as I journey into ASP.NET Core.]]></description><link>https://andrewlock.net/</link><generator>andrewlock-blog-engine</generator><lastBuildDate>Tue, 21 Apr 2026 10:15:16 GMT</lastBuildDate><atom:link href="https://andrewlock.net/rss/" rel="self" type="application/rss+xml" /><ttl>60</ttl><item><title><![CDATA[Removing byte[] allocations in .NET Framework using ReadOnlySpan<T>]]></title><description><![CDATA[In this post I describe how to remove static byte[] allocations, even on .NET Framework, by using Span<T>, look at the associated IL, and discuss the risks]]></description><link>https://andrewlock.net/removingbyte-array-allocations-in-dotnet-framework-using-readonlyspan-t/</link><guid isPermaLink="true">https://andrewlock.net/removingbyte-array-allocations-in-dotnet-framework-using-readonlyspan-t/</guid><pubDate>Tue, 21 Apr 2026 10:00:00 GMT</pubDate><dc:creator><![CDATA[Andrew Lock]]></dc:creator><media:content url="https://andrewlock.net/content/images/2026/readonlyspan_banner.png" medium="image" /><content:encoded><![CDATA[<img src="https://andrewlock.net/content/images/2026/readonlyspan_banner.png" /><p>In this post I describe a simple way to remove some <code>byte[]</code> allocations, no matter which version of .NET you're targeting, including .NET Framework. This will likely already be familiar to you if you write performance sensitive code with modern .NET, but I recently realised that this can be applied to older runtimes as well, like .NET Framework.</p> <p>This post looks at the changes to your C# code to reduce the allocations, how the compiler implements the change behind the scenes, and some of the caveats and sharp edges to watch out for.</p> <h2 id="spant-and-readonlyspant-are-a-performance-mainstay-for-net" class="heading-with-anchor"><a href="https://andrewlock.net/removingbyte-array-allocations-in-dotnet-framework-using-readonlyspan-t/#spant-and-readonlyspant-are-a-performance-mainstay-for-net" class="relative text-zinc-800 dark:text-white no-underline hover:underline"><code>Span&lt;T&gt;</code> and <code>ReadOnlySpan&lt;T&gt;</code> are a performance mainstay for .NET</a></h2> <p><code>ReadOnlySpan&lt;T&gt;</code> and <code>Span&lt;T&gt;</code> were introduced into .NET a long time ago now, but they have had a significant impact on the code you can (and arguably <em>should</em>) write, particularly when it comes to performance sensitive code. These provide a "window" or "view" over existing data, without creating copies of that data.</p> <p>The classic example is when you're manipulating <code>string</code> objects; instead of using <code>SubString()</code>, and creating additional copies of segments of the string, you can use <code>AsSpan()</code> to create <code>ReadOnlySpan&lt;char&gt;()</code> segments that can be manipulated almost <em>as though</em> they are separate <code>string</code> instances, but without all the copying.</p> <p>This is probably the most common use of <code>Span&lt;T&gt;</code> in application code, but fundamentally the use of <code>Span&lt;T&gt;</code> to provide a view over any piece of memory means it's useful in many other situations. The fact that the backing of a <code>Span&lt;T&gt;</code> can be almost anything, means you can keep the same "public" API which potentially "swapping out" the backend.</p> <p>Another common example of this is if you have some parsing (or similar) code and you need a buffer to store the temporary results. Prior to <code>Span&lt;T&gt;</code>, you would almost certainly have allocated a normal array on the heap for this, but with <code>Span&lt;T&gt;</code>, "stack allocating" using <code>stackalloc</code> becomes just as easy, and reduces pressure on the garbage collector:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp">
<span class="token class-name">Span<span class="token punctuation">&lt;</span><span class="token keyword">byte</span><span class="token punctuation">&gt;</span></span> buffer <span class="token operator">=</span> requiredSize <span class="token operator">&lt;=</span> <span class="token number">256</span>                  <span class="token comment">// If the required buffer size is small </span>
                        <span class="token punctuation">?</span> <span class="token keyword">stackalloc</span> <span class="token keyword">byte</span><span class="token punctuation">[</span>requiredSize<span class="token punctuation">]</span>  <span class="token comment">// enough, then allocate on the stack.</span>
                        <span class="token punctuation">:</span> <span class="token keyword">new</span> <span class="token constructor-invocation class-name"><span class="token keyword">byte</span></span><span class="token punctuation">[</span>requiredSize<span class="token punctuation">]</span><span class="token punctuation">;</span>        <span class="token comment">// Fallback to a normal heap allocation</span>
</code></pre></div> <p>Virtually all new .NET runtime APIs are added with <code>Span&lt;T&gt;</code> or <code>ReadOnlySpan&lt;T&gt;</code> support, and you can even use them in old runtimes like <code>.NET Framework</code> via <a href="https://www.nuget.org/packages/System.Memory">the System.Memory NuGet package</a> (though you don't get all the same perf benefits that you do with .NET Core).</p> <p>The ability to easily and safely (without needing directly falling back to <code>unsafe</code> and pointers) work with blocks of memory regardless of where they're from has really made <code>Span&lt;T&gt;</code> vital for any code that cares about performance. But this ability to provide an "arbitrary" view over memory also provides a way for the compiler to perform additional optimizations, as we'll see in the next section.</p> <h2 id="removing-byte-allocations-with-readonlyspanbyte" class="heading-with-anchor"><a href="https://andrewlock.net/removingbyte-array-allocations-in-dotnet-framework-using-readonlyspan-t/#removing-byte-allocations-with-readonlyspanbyte" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Removing <code>byte[]</code> allocations with <code>ReadOnlySpan&lt;byte&gt;</code></a></h2> <p>The ability for the compiler to provide a view over arbitrary memory is what drives the optimization I'm going to talk about for the rest of this post.</p> <p>Let's imagine you have some <code>byte[]</code> that you need for <em>something</em>. Some kind of processing requires it. You know the data it needs to contain upfront, so you store the array in a <code>static readonly</code> field, so that the data is only created once:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token keyword">class</span> <span class="token class-name">MyStaticData</span>
<span class="token punctuation">{</span>
    <span class="token keyword">private</span> <span class="token keyword">static</span> <span class="token keyword">readonly</span> <span class="token class-name"><span class="token keyword">byte</span><span class="token punctuation">[</span><span class="token punctuation">]</span></span> ByteField <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token constructor-invocation class-name"><span class="token keyword">byte</span><span class="token punctuation">[</span><span class="token punctuation">]</span></span> <span class="token punctuation">{</span> <span class="token number">1</span><span class="token punctuation">,</span> <span class="token number">2</span><span class="token punctuation">,</span> <span class="token number">3</span><span class="token punctuation">,</span> <span class="token number">4</span> <span class="token punctuation">}</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>This works absolutely fine, but it means when you first access that data, the runtime needs to create an instance of the array, fill it with the data, and store it in the field. After that, accessing the field is cheap, but the initial creation adds a small delay to the first use of that type.</p> <p>However, starting with C# 8.0, and as long as that you only need a readonly view of the data, you can use a slightly different pattern, by exposing a <code>ReadOnlySpan&lt;byte&gt;</code> <em>property</em> instead of a field:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token keyword">class</span> <span class="token class-name">MyStaticData</span>
<span class="token punctuation">{</span>
    <span class="token comment">// Before</span>
    <span class="token keyword">private</span> <span class="token keyword">static</span> <span class="token keyword">readonly</span> <span class="token class-name"><span class="token keyword">byte</span><span class="token punctuation">[</span><span class="token punctuation">]</span></span> ByteField <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token constructor-invocation class-name"><span class="token keyword">byte</span><span class="token punctuation">[</span><span class="token punctuation">]</span></span> <span class="token punctuation">{</span> <span class="token number">1</span><span class="token punctuation">,</span> <span class="token number">2</span><span class="token punctuation">,</span> <span class="token number">3</span><span class="token punctuation">,</span> <span class="token number">4</span> <span class="token punctuation">}</span><span class="token punctuation">;</span>

    <span class="token comment">// After</span>
    <span class="token keyword">private</span> <span class="token keyword">static</span> <span class="token return-type class-name">ReadOnlySpan<span class="token punctuation">&lt;</span><span class="token keyword">byte</span><span class="token punctuation">&gt;</span></span> ReadOnlySpanProp <span class="token operator">=&gt;</span> <span class="token keyword">new</span> <span class="token constructor-invocation class-name"><span class="token keyword">byte</span><span class="token punctuation">[</span><span class="token punctuation">]</span></span> <span class="token punctuation">{</span> <span class="token number">1</span><span class="token punctuation">,</span> <span class="token number">2</span><span class="token punctuation">,</span> <span class="token number">3</span><span class="token punctuation">,</span> <span class="token number">4</span> <span class="token punctuation">}</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>Now, <em>normally</em>, that's the sort of code that should be setting off performance alarm bells. It <em>looks</em> like it will be creating a <em>new</em> <code>byte[]</code> every time you access the property😱 But that's <em>not</em> what's happening.</p> <blockquote> <p>We'll take a detailed look at the generated IL code shortly, for now we'll just talk at a high level.</p> </blockquote> <p>When the compiler sees the pattern above, it does the following:</p> <ul><li>Embed the <code>byte[]</code> data into the final assembly's metadata</li> <li>When <code>ReadOnlySpanProp</code> is invoked, instead of creating a <code>byte[]</code>, create a <code>ReadOnlySpan&lt;byte&gt;</code> that points directly to the data in the assembly</li></ul> <p>So the returned <code>ReadOnlySpan&lt;byte&gt;</code> isn't pointing to data that exists on the heap or even on the stack; it's pointing to data that's embedded <em>directly</em> in the assembly. That means there's no allocation at all, which removes that startup overhead and means there's no pressure at all on the garbage collector 🎉</p> <p>It's worth noting as well that this is a <em>compiler</em> feature, which means that as long as a <code>System.ReadOnlySpan&lt;T&gt;</code> type is available, you can use it. So as long as you add the System.Memory NuGet package to your .NET Framework app, you too can benefit from this zero-allocation technique!</p> <p>Also, this doesn't <em>just</em> apply to converting <code>static readonly byte[]</code> fields to <code>static ReadOnlySpan&lt;byte&gt;</code> properties; it also applies to <em>local</em> variables too. Which means things like the following, which <em>look</em> like they allocate an array, actually don't:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token return-type class-name"><span class="token keyword">void</span></span> <span class="token function">TestData</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token punctuation">{</span>
    <span class="token comment">// This looks like it allocates, but it doesn't</span>
    <span class="token class-name">ReadOnlySpan<span class="token punctuation">&lt;</span><span class="token keyword">byte</span><span class="token punctuation">&gt;</span></span> arr <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token constructor-invocation class-name"><span class="token keyword">byte</span><span class="token punctuation">[</span><span class="token punctuation">]</span></span> <span class="token punctuation">{</span> <span class="token number">0</span><span class="token punctuation">,</span> <span class="token number">1</span><span class="token punctuation">,</span> <span class="token number">2</span> <span class="token punctuation">}</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>Another minor thing to point out is that this also works with <a href="https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-11.0/utf8-string-literals">UTF-8 Strings Literals</a>, which are logically represented as a <code>byte[]</code> by the type system. So this is also zero allocation:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token keyword">class</span> <span class="token class-name">MyStaticData</span>
<span class="token punctuation">{</span>
    <span class="token keyword">private</span> <span class="token keyword">static</span> <span class="token return-type class-name">ReadOnlySpan<span class="token punctuation">&lt;</span><span class="token keyword">byte</span><span class="token punctuation">&gt;</span></span> ReadOnlySpanUtf8 <span class="token operator">=&gt;</span> <span class="token string">"Hello world"</span>u8<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>That's all great, but when I first used the <code>byte[]</code> approach, I was a <em>little</em> concerned. After all, it <em>looked</em> like it would be allocating and terribly inefficient, so I wanted to be sure. And what better way than checking the IL code the compiler generates.</p> <h2 id="what-s-happening-behind-the-scenes-" class="heading-with-anchor"><a href="https://andrewlock.net/removingbyte-array-allocations-in-dotnet-framework-using-readonlyspan-t/#what-s-happening-behind-the-scenes-" class="relative text-zinc-800 dark:text-white no-underline hover:underline">What's happening behind the scenes?</a></h2> <p>There are multiple ways to check the generated code that the compiler generates. If you just want to check a "snippet" of code, then <a href="https://sharplab.io/#v2:EYLgtghglgdgNAExAagD4AEBMBGAsAKAPQGYACdbANnM1IFkBPAZQBcIWoBjAEXYgIDeBUiNIB6MaQDyAaWGiADgCcoAN3YBTclVJKNEBAHsYAGwalgDFhoDaAXVIAhKxoBiUDSYSkAvKRgaAO4WLvakAqSYcKTE0QAs0QCspAC+ANzyIspqmtrUltZhAKosAGYAHAAKSoYKGkos5n4ARAASniaGpIGGSl7NAK7lAHQAKoYAgkpKEAwAFACUGYT4oqTZ6tZ5pABK+ghSpswKEDAAPAUaAHy7+4dmTCcw1bW+NwHBl2ER2NFRMfFUssCCkgA=">sharplab.io is a quick and easy option</a>. Alternatively, there's <a href="https://github.com/icsharpcode/ilspy">ILSpy</a>, or the JetBrains tools like <a href="https://www.jetbrains.com/decompiler/">dotPeek</a> and <a href="https://www.jetbrains.com/rider/">Rider</a>, and I'm sure Visual Studio has plugins for it.</p> <p>To comfort myself, I first created a new .NET project using <code>dotnet new classlib</code>, and then I tweaked it to use .NET Framework. To be clear, the techniques shown so far work on all target frameworks, but I wanted to specifically test with .NET Framework, to prove that it's not just "new" frameworks this works with. I tweaked the project file as shown below:</p> <div class="pre-code-wrapper"><pre class="language-xml"><code class="language-xml"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>Project</span> <span class="token attr-name">Sdk</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>Microsoft.NET.Sdk<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PropertyGroup</span><span class="token punctuation">&gt;</span></span>
    <span class="token comment">&lt;!-- Change the target framework to .NET Framework👇 --&gt;</span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>TargetFramework</span><span class="token punctuation">&gt;</span></span>net48<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>TargetFramework</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>ImplicitUsings</span><span class="token punctuation">&gt;</span></span>enable<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>ImplicitUsings</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>Nullable</span><span class="token punctuation">&gt;</span></span>enable<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>Nullable</span><span class="token punctuation">&gt;</span></span>
    <span class="token comment">&lt;!-- Use the latest C# version (C# 14 with .NET 10 SDK)👇 --&gt;</span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>LangVersion</span><span class="token punctuation">&gt;</span></span>latest<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>LangVersion</span><span class="token punctuation">&gt;</span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>PropertyGroup</span><span class="token punctuation">&gt;</span></span>
  
  <span class="token comment">&lt;!-- Reference System.Memory so we can use ReadOnlySpan&lt;T&gt;👇 --&gt;</span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>ItemGroup</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PackageReference</span> <span class="token attr-name">Include</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>System.Memory<span class="token punctuation">"</span></span> <span class="token attr-name">Version</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>4.6.3<span class="token punctuation">"</span></span> <span class="token punctuation">/&gt;</span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>ItemGroup</span><span class="token punctuation">&gt;</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>Project</span><span class="token punctuation">&gt;</span></span>
</code></pre></div> <p>I then created the very simple class below, compiled, and used Rider to view the generated IL:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token keyword">class</span> <span class="token class-name">MyStaticData</span>
<span class="token punctuation">{</span>
    <span class="token keyword">private</span> <span class="token keyword">static</span> <span class="token return-type class-name">ReadOnlySpan<span class="token punctuation">&lt;</span><span class="token keyword">byte</span><span class="token punctuation">&gt;</span></span> ReadOnlySpanProp <span class="token operator">=&gt;</span> <span class="token keyword">new</span> <span class="token constructor-invocation class-name"><span class="token keyword">byte</span><span class="token punctuation">[</span><span class="token punctuation">]</span></span> <span class="token punctuation">{</span> <span class="token number">1</span><span class="token punctuation">,</span> <span class="token number">2</span><span class="token punctuation">,</span> <span class="token number">3</span><span class="token punctuation">,</span> <span class="token number">4</span> <span class="token punctuation">}</span><span class="token punctuation">;</span>
    <span class="token keyword">private</span> <span class="token keyword">static</span> <span class="token return-type class-name">ReadOnlySpan<span class="token punctuation">&lt;</span><span class="token keyword">byte</span><span class="token punctuation">&gt;</span></span> ReadOnlySpanUtf8 <span class="token operator">=&gt;</span> <span class="token string">"Hello world"</span>u8<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>I've commented the IL below to describe what it's doing, but the <em>important</em> thing is that we don't see any calls to <code>newarr</code>, <code>InitializeArray()</code>, or <code>ToArray()</code>, or other problematic calls. Instead, we see IL code that loads an address which points to data embedded in the PE image (i.e. the assembly), loads the <em>length</em> of the data (4 bytes), and then passes the pointer and length to the <code>new ReadOnlySpan&lt;T&gt;()</code> constructor and returns it. No copying, no new arrays, just a wrapper around bytes that are already loaded into memory 🎉</p> <div class="pre-code-wrapper"><pre class="language-msil"><code class="language-msil"><span class="token directive class-name">.class</span> <span class="token keyword">public</span> <span class="token keyword">abstract</span> <span class="token keyword">sealed</span> <span class="token keyword">auto</span> <span class="token keyword">ansi</span> <span class="token keyword">beforefieldinit</span>
  MyStaticData
    <span class="token keyword">extends</span> <span class="token variable">[mscorlib]</span>System.Object
<span class="token punctuation">{</span>

  <span class="token directive class-name">.field</span> <span class="token keyword">private</span> <span class="token keyword">static</span> <span class="token keyword">initonly</span> <span class="token keyword">unsigned</span> <span class="token keyword">int8</span> One

  <span class="token directive class-name">.method</span> <span class="token keyword">private</span> <span class="token keyword">hidebysig</span> <span class="token keyword">static</span> <span class="token keyword">specialname</span> <span class="token keyword">valuetype</span> <span class="token variable">[System.Memory]</span>System.ReadOnlySpan`<span class="token number">1</span>&lt;<span class="token keyword">unsigned</span> <span class="token keyword">int8</span>&gt;
    get_ReadOnlySpanProp<span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token keyword">cil</span> <span class="token keyword">managed</span>
  <span class="token punctuation">{</span>
    <span class="token directive class-name">.maxstack</span> <span class="token number">8</span>

    <span class="token comment">// 👇 Push the address of the static field that contains the array data as a blob onto the stack</span>
    <span class="token punctuation">IL_0000</span><span class="token punctuation">:</span> <span class="token function">ldsflda</span>      <span class="token keyword">int32</span> <span class="token string">'&lt;PrivateImplementationDetails&gt;'</span><span class="token punctuation">:</span><span class="token punctuation">:</span><span class="token string">'9F64A747E1B97F131FABB6B447296C9B6F0201E79FB3C5356E6C77E89B6A806A'</span>
    <span class="token comment">// 👇 Push the value '4' onto the stack</span>
    <span class="token punctuation">IL_0005</span><span class="token punctuation">:</span> <span class="token function">ldc.i4.4</span>
    <span class="token comment">// 👇 Create a new ReadOnlySpan&lt;byte&gt;</span>
    <span class="token punctuation">IL_0006</span><span class="token punctuation">:</span> <span class="token function">newobj</span>       <span class="token keyword">instance</span> <span class="token keyword">void</span> <span class="token keyword">valuetype</span> <span class="token variable">[System.Memory]</span>System.ReadOnlySpan`<span class="token number">1</span>&lt;<span class="token keyword">unsigned</span> <span class="token keyword">int8</span>&gt;<span class="token punctuation">:</span><span class="token punctuation">:</span>.ctor<span class="token punctuation">(</span><span class="token keyword">void</span>*<span class="token punctuation">,</span> <span class="token keyword">int32</span><span class="token punctuation">)</span>
    <span class="token punctuation">IL_000b</span><span class="token punctuation">:</span> <span class="token function">ret</span> <span class="token comment">// Return the span</span>

  <span class="token punctuation">}</span> <span class="token comment">// end of method MyStaticData::get_ReadOnlySpanProp</span>

  <span class="token directive class-name">.method</span> <span class="token keyword">private</span> <span class="token keyword">hidebysig</span> <span class="token keyword">static</span> <span class="token keyword">specialname</span> <span class="token keyword">valuetype</span> <span class="token variable">[System.Memory]</span>System.ReadOnlySpan`<span class="token number">1</span>&lt;<span class="token keyword">unsigned</span> <span class="token keyword">int8</span>&gt;
    get_ReadOnlySpanUtf8<span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token keyword">cil</span> <span class="token keyword">managed</span>
  <span class="token punctuation">{</span>
    <span class="token directive class-name">.maxstack</span> <span class="token number">8</span>

    <span class="token comment">// 👇 Push the address of the static field that contains the UTF-8 data as a blob onto the stack</span>
    <span class="token punctuation">IL_0000</span><span class="token punctuation">:</span> <span class="token function">ldsflda</span>      <span class="token keyword">valuetype</span> <span class="token string">'&lt;PrivateImplementationDetails&gt;'</span>/<span class="token string">'__StaticArrayInitTypeSize=12'</span> <span class="token string">'&lt;PrivateImplementationDetails&gt;'</span><span class="token punctuation">:</span><span class="token punctuation">:</span><span class="token string">'27518BA9683011F6B396072C05F6656D04F5FBC3787CF92490EC606E5092E326'</span>
    <span class="token comment">// 👇 Push the value '11' onto the stack (the length of "Hello world" in UTF-8)</span>
    <span class="token punctuation">IL_0005</span><span class="token punctuation">:</span> <span class="token function">ldc.i4.s</span>     <span class="token number">11</span> <span class="token comment">// 0x0b</span>
    <span class="token comment">// 👇 Create a new ReadOnlySpan&lt;byte&gt;</span>
    <span class="token punctuation">IL_0007</span><span class="token punctuation">:</span> <span class="token function">newobj</span>       <span class="token keyword">instance</span> <span class="token keyword">void</span> <span class="token keyword">valuetype</span> <span class="token variable">[System.Memory]</span>System.ReadOnlySpan`<span class="token number">1</span>&lt;<span class="token keyword">unsigned</span> <span class="token keyword">int8</span>&gt;<span class="token punctuation">:</span><span class="token punctuation">:</span>.ctor<span class="token punctuation">(</span><span class="token keyword">void</span>*<span class="token punctuation">,</span> <span class="token keyword">int32</span><span class="token punctuation">)</span>
    <span class="token punctuation">IL_000c</span><span class="token punctuation">:</span> <span class="token function">ret</span> <span class="token comment">// Return the span</span>

  <span class="token punctuation">}</span> <span class="token comment">// end of method MyStaticData::get_ReadOnlySpanUtf8</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>Great, we can see that it's clearly working as we expected <em>and</em> this is .NET Framework, so it is just a compiler feature and has no runtime requirements, so we can use it everywhere.</p> <p>But we need to be careful… I showed that it works for <code>byte[]</code>, but it doesn't work for <em>everything</em>…</p> <h2 id="be-careful-things-can-go-wrong" class="heading-with-anchor"><a href="https://andrewlock.net/removingbyte-array-allocations-in-dotnet-framework-using-readonlyspan-t/#be-careful-things-can-go-wrong" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Be careful, things can go wrong…</a></h2> <p>If you've read this far, you might be thinking "great, I'll use this for all my static array data", but I'm going to stop you there. Here-be dragons. The pattern above is <em>only</em> safe to use:</p> <ul><li>If you have a <code>byte[]</code>, <code>sbyte[]</code>, or <code>bool[]</code>.</li> <li>If <em>all</em> the values in the array are constants</li> <li>If the array is immutable (i.e. you return a <code>ReadOnlySpan&lt;T&gt;</code> not a <code>Span&lt;T&gt;</code>).</li></ul> <p>Breaking any of these rules may be disastrous for performance, so we'll examine each in turn.</p> <h3 id="only-byte-sbyte-and-bool-are-allowed" class="heading-with-anchor"><a href="https://andrewlock.net/removingbyte-array-allocations-in-dotnet-framework-using-readonlyspan-t/#only-byte-sbyte-and-bool-are-allowed" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Only <code>byte</code>, <code>sbyte</code>, and <code>bool</code> are allowed</a></h3> <p>The compiler optimizations shown so far can <em>only</em> be applied to <code>byte</code>-sized primitives, i.e. <code>byte</code>, <code>sbyte</code>, and <code>bool</code>. That's because the constant data would be stored in a little endian format, and needs to be translated to the <em>runtime</em> endian format, e.g. if the application is run on hardware which utilizes big endian numbers.</p> <p>That means, that if you do the following (using <code>int</code> instead of <code>byte</code>), then the code <em>compiles</em> just fine, but unfortunately it doesn't generate the "zero allocation" code that you might expect:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token keyword">class</span> <span class="token class-name">MyStaticData</span>
<span class="token punctuation">{</span>
    <span class="token comment">// ⚠️ Using `int` instead of `byte` _does_ cause an array </span>
    <span class="token comment">// to be allocated (on .NET Framework and &lt; .NET 7)</span>
    <span class="token keyword">private</span> <span class="token keyword">static</span> <span class="token return-type class-name">ReadOnlySpan<span class="token punctuation">&lt;</span><span class="token keyword">int</span><span class="token punctuation">&gt;</span></span> ReadOnlySpanPropInt <span class="token operator">=&gt;</span> <span class="token keyword">new</span> <span class="token constructor-invocation class-name"><span class="token keyword">int</span><span class="token punctuation">[</span><span class="token punctuation">]</span></span> <span class="token punctuation">{</span> <span class="token number">1</span><span class="token punctuation">,</span> <span class="token number">2</span><span class="token punctuation">,</span> <span class="token number">3</span><span class="token punctuation">,</span> <span class="token number">4</span> <span class="token punctuation">}</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>If we check the generated IL for a .NET Framework app with the above, we can see the problematic <code>newarr</code> and <code>InitializeArray</code> calls. The compiler actually does some work to avoid the <em>really</em> problematic pattern which would create an array every time, by creating the array <em>once</em>, caching it in a static field, and then using that cached data for subsequent calls, but it still has a startup cost, and does more work than the optimized <code>byte[]</code> approach:</p> <div class="pre-code-wrapper"><pre class="language-msil"><code class="language-msil"><span class="token directive class-name">.method</span> <span class="token keyword">private</span> <span class="token keyword">hidebysig</span> <span class="token keyword">static</span> <span class="token keyword">specialname</span> <span class="token keyword">valuetype</span> <span class="token variable">[System.Memory]</span>System.ReadOnlySpan`<span class="token number">1</span>&lt;<span class="token keyword">int32</span>&gt;
  get_ReadOnlySpanPropInt<span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token keyword">cil</span> <span class="token keyword">managed</span>
<span class="token punctuation">{</span>
  <span class="token directive class-name">.maxstack</span> <span class="token number">8</span>

  <span class="token comment">// 👇Try to load the cached int[] data from the static 'cache' field</span>
  <span class="token punctuation">IL_0000</span><span class="token punctuation">:</span> <span class="token function">ldsfld</span>       <span class="token keyword">int32</span><span class="token punctuation">[</span><span class="token punctuation">]</span> <span class="token string">'&lt;PrivateImplementationDetails&gt;'</span><span class="token punctuation">:</span><span class="token punctuation">:</span>CF97ADEEDB59E05BFD73A2B4C2A8885708C4F4F70C84C64B27120E72AB733B72_A6
  <span class="token punctuation">IL_0005</span><span class="token punctuation">:</span> <span class="token function">dup</span>                                  <span class="token comment">// Duplicate the variable</span>
  <span class="token punctuation">IL_0006</span><span class="token punctuation">:</span> <span class="token function">brtrue.s</span>     <span class="token punctuation">IL_0020</span>                 <span class="token comment">// If the data isn't null, we have it cached, so jump to the end</span>
  <span class="token punctuation">IL_0008</span><span class="token punctuation">:</span> <span class="token function">pop</span>                                  <span class="token comment">// The value was null, remove the duplicate</span>
  <span class="token punctuation">IL_0009</span><span class="token punctuation">:</span> <span class="token function">ldc.i4.4</span>                             <span class="token comment">// Load the length of the data (4)</span>
  <span class="token punctuation">IL_000a</span><span class="token punctuation">:</span> <span class="token function">newarr</span>       <span class="token variable">[mscorlib]</span>System.Int32  <span class="token comment">// Allocate a new array on the heap</span>
  <span class="token punctuation">IL_000f</span><span class="token punctuation">:</span> <span class="token function">dup</span>                                  <span class="token comment">// Keep a copy of the array variable</span>
  <span class="token comment">// 👇 Load the address of the int[] data embedded in the assembly</span>
  <span class="token punctuation">IL_0010</span><span class="token punctuation">:</span> <span class="token function">ldtoken</span>      field <span class="token keyword">valuetype</span> <span class="token string">'&lt;PrivateImplementationDetails&gt;'</span>/<span class="token string">'__StaticArrayInitTypeSize=16'</span> <span class="token string">'&lt;PrivateImplementationDetails&gt;'</span><span class="token punctuation">:</span><span class="token punctuation">:</span>CF97ADEEDB59E05BFD73A2B4C2A8885708C4F4F70C84C64B27120E72AB733B72
  <span class="token comment">// 👇 Initialize the new array with the int[] data</span>
  <span class="token punctuation">IL_0015</span><span class="token punctuation">:</span> <span class="token function">call</span>         <span class="token keyword">void</span> <span class="token variable">[mscorlib]</span>System.Runtime.CompilerServices.RuntimeHelpers<span class="token punctuation">:</span><span class="token punctuation">:</span>InitializeArray<span class="token punctuation">(</span><span class="token keyword">class</span> <span class="token variable">[mscorlib]</span>System.Array<span class="token punctuation">,</span> <span class="token keyword">valuetype</span> <span class="token variable">[mscorlib]</span>System.RuntimeFieldHandle<span class="token punctuation">)</span>
  <span class="token punctuation">IL_001a</span><span class="token punctuation">:</span> <span class="token function">dup</span>                                  <span class="token comment">// Duplicate the variable</span>
  <span class="token comment">// 👇 Store the now-populated array into the static 'cache' field</span>
  <span class="token punctuation">IL_001b</span><span class="token punctuation">:</span> <span class="token function">stsfld</span>       <span class="token keyword">int32</span><span class="token punctuation">[</span><span class="token punctuation">]</span> <span class="token string">'&lt;PrivateImplementationDetails&gt;'</span><span class="token punctuation">:</span><span class="token punctuation">:</span>CF97ADEEDB59E05BFD73A2B4C2A8885708C4F4F70C84C64B27120E72AB733B72_A6
  <span class="token comment">// 👇 Create the `ReadOnlySpan&lt;int&gt;` wrapping the array</span>
  <span class="token punctuation">IL_0020</span><span class="token punctuation">:</span> <span class="token function">newobj</span>       <span class="token keyword">instance</span> <span class="token keyword">void</span> <span class="token keyword">valuetype</span> <span class="token variable">[System.Memory]</span>System.ReadOnlySpan`<span class="token number">1</span>&lt;<span class="token keyword">int32</span>&gt;<span class="token punctuation">:</span><span class="token punctuation">:</span>.ctor<span class="token punctuation">(</span>!<span class="token number">0</span>/*<span class="token keyword">int32</span>*/<span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">)</span>
  <span class="token punctuation">IL_0025</span><span class="token punctuation">:</span> <span class="token function">ret</span>

<span class="token punctuation">}</span> <span class="token comment">// end of method MyStaticData::get_ReadOnlySpanPropInt</span>
</code></pre></div> <p>So the "good" news is that this isn't <em>much</em> different to just using a <code>static readonly int[]</code>, but it's still not ideal, and definitely <em>isn't</em> the zero-allocation version that you get with <code>byte[]</code>.</p> <p>Additionally, if you're on .NET 7+, <a href="https://github.com/dotnet/runtime/issues/60948">a new API was added</a> which actually <em>does</em> support this pattern. So if we change the target framework (to .NET 10 in this case), and recompile, then the IL is back to the zero allocation version, thanks to the call <a href="https://github.com/dotnet/runtime/blob/b70c35ed8a2e7ae0d91de76f4f5d26c2e7d2c6cd/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/RuntimeHelpers.cs#L154">to <code>RuntimeHelpers::CreateSpan</code></a>, which handles fixing-up any endianness issues:</p> <div class="pre-code-wrapper"><pre class="language-msil"><code class="language-msil"><span class="token directive class-name">.method</span> <span class="token keyword">private</span> <span class="token keyword">hidebysig</span> <span class="token keyword">static</span> <span class="token keyword">specialname</span> <span class="token keyword">valuetype</span> <span class="token variable">[System.Runtime]</span>System.ReadOnlySpan`<span class="token number">1</span>&lt;<span class="token keyword">int32</span>&gt;
    get_ReadOnlySpanPropInt<span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token keyword">cil</span> <span class="token keyword">managed</span>
  <span class="token punctuation">{</span>
    <span class="token directive class-name">.maxstack</span> <span class="token number">8</span>

    <span class="token comment">// 👇 Load the address of the data</span>
    <span class="token punctuation">IL_0000</span><span class="token punctuation">:</span> <span class="token function">ldtoken</span>      field <span class="token keyword">valuetype</span> <span class="token string">'&lt;PrivateImplementationDetails&gt;'</span>/<span class="token string">'__StaticArrayInitTypeSize=16_Align=4'</span> <span class="token string">'&lt;PrivateImplementationDetails&gt;'</span><span class="token punctuation">:</span><span class="token punctuation">:</span>CF97ADEEDB59E05BFD73A2B4C2A8885708C4F4F70C84C64B27120E72AB733B724
    <span class="token comment">// 👇 Call RuntimeHelpers::CreateSpan and return</span>
    <span class="token punctuation">IL_0005</span><span class="token punctuation">:</span> <span class="token function">call</span>         <span class="token keyword">valuetype</span> <span class="token variable">[System.Runtime]</span>System.ReadOnlySpan`<span class="token number">1</span>&lt;!!<span class="token number">0</span>/*<span class="token keyword">int32</span>*/&gt; <span class="token variable">[System.Runtime]</span>System.Runtime.CompilerServices.RuntimeHelpers<span class="token punctuation">:</span><span class="token punctuation">:</span>CreateSpan&lt;<span class="token keyword">int32</span>&gt;<span class="token punctuation">(</span><span class="token keyword">valuetype</span> <span class="token variable">[System.Runtime]</span>System.RuntimeFieldHandle<span class="token punctuation">)</span>
    <span class="token punctuation">IL_000a</span><span class="token punctuation">:</span> <span class="token function">ret</span>

  <span class="token punctuation">}</span> <span class="token comment">// end of method MyStaticData::get_ReadOnlySpanPropInt</span>
</code></pre></div> <p>So in summary, your mileage will vary here, and you don't really gain anything unless you're on .NET 7+. If you need to target older frameworks, then you're potentially better off just sticking to a good old <code>static readonly int[]</code> field instead.</p> <h3 id="all-values-must-be-constants" class="heading-with-anchor"><a href="https://andrewlock.net/removingbyte-array-allocations-in-dotnet-framework-using-readonlyspan-t/#all-values-must-be-constants" class="relative text-zinc-800 dark:text-white no-underline hover:underline">All values must be constants</a></h3> <p>The next issue is that the whole approach shown in this post <em>only</em> works if all the values in the collection are <em>constants</em>. For example, the following example which uses a <code>static readonly</code> value inside the array <em>compiles</em> just fine:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token keyword">class</span> <span class="token class-name">MyStaticData</span>
<span class="token punctuation">{</span>
    <span class="token keyword">private</span> <span class="token keyword">static</span> <span class="token keyword">readonly</span> <span class="token class-name"><span class="token keyword">byte</span></span> One <span class="token operator">=</span> <span class="token number">1</span><span class="token punctuation">;</span>
    <span class="token keyword">private</span> <span class="token keyword">static</span> <span class="token return-type class-name">ReadOnlySpan<span class="token punctuation">&lt;</span><span class="token keyword">byte</span><span class="token punctuation">&gt;</span></span> ReadOnlySpanPropNonConstant <span class="token operator">=&gt;</span> <span class="token keyword">new</span> <span class="token constructor-invocation class-name"><span class="token keyword">byte</span><span class="token punctuation">[</span><span class="token punctuation">]</span></span> <span class="token punctuation">{</span> One<span class="token punctuation">,</span> <span class="token number">2</span><span class="token punctuation">,</span> <span class="token number">3</span><span class="token punctuation">,</span> <span class="token number">4</span> <span class="token punctuation">}</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>but <em>even</em> on .NET 7+, this <em>won't</em> do the zero-allocation approach that you might be expecting. Instead, you get some really nasty "allocate a new array every time" behaviour 😱:</p> <div class="pre-code-wrapper"><pre class="language-msil"><code class="language-msil"><span class="token directive class-name">.method</span> <span class="token keyword">private</span> <span class="token keyword">hidebysig</span> <span class="token keyword">static</span> <span class="token keyword">specialname</span> <span class="token keyword">valuetype</span> <span class="token variable">[System.Runtime]</span>System.ReadOnlySpan`<span class="token number">1</span>&lt;<span class="token keyword">unsigned</span> <span class="token keyword">int8</span>&gt;
  get_ReadOnlySpanPropNonConstant<span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token keyword">cil</span> <span class="token keyword">managed</span>
<span class="token punctuation">{</span>
  <span class="token directive class-name">.maxstack</span> <span class="token number">8</span>

  <span class="token punctuation">IL_0000</span><span class="token punctuation">:</span> <span class="token function">ldc.i4.4</span>                                  <span class="token comment">// Laod the length of the array</span>
  <span class="token punctuation">IL_0001</span><span class="token punctuation">:</span> <span class="token function">newarr</span>       <span class="token variable">[System.Runtime]</span>System.Byte  <span class="token comment">// Create a new array</span>
  <span class="token punctuation">IL_0006</span><span class="token punctuation">:</span> <span class="token function">dup</span>                                       <span class="token comment">// Duplicate the variable reference</span>
  <span class="token comment">// 👇 Get a reference to the data, and initialize the array</span>
  <span class="token punctuation">IL_0007</span><span class="token punctuation">:</span> <span class="token function">ldtoken</span>      field <span class="token keyword">int32</span> <span class="token string">'&lt;PrivateImplementationDetails&gt;'</span><span class="token punctuation">:</span><span class="token punctuation">:</span><span class="token string">'1E6175315920374CAA0A86B45D862DEE3DDAA28257652189FC1DFBE07479436A'</span>
  <span class="token punctuation">IL_000c</span><span class="token punctuation">:</span> <span class="token function">call</span>         <span class="token keyword">void</span> <span class="token variable">[System.Runtime]</span>System.Runtime.CompilerServices.RuntimeHelpers<span class="token punctuation">:</span><span class="token punctuation">:</span>InitializeArray<span class="token punctuation">(</span><span class="token keyword">class</span> <span class="token variable">[System.Runtime]</span>System.Array<span class="token punctuation">,</span> <span class="token keyword">valuetype</span> <span class="token variable">[System.Runtime]</span>System.RuntimeFieldHandle<span class="token punctuation">)</span>
  <span class="token punctuation">IL_0011</span><span class="token punctuation">:</span> <span class="token function">dup</span>
  <span class="token punctuation">IL_0012</span><span class="token punctuation">:</span> <span class="token function">ldc.i4.0</span>                                      <span class="token comment">// Load the index to change '0'</span>
  <span class="token punctuation">IL_0013</span><span class="token punctuation">:</span> <span class="token function">ldsfld</span>       <span class="token keyword">unsigned</span> <span class="token keyword">int8</span> MyStaticData<span class="token punctuation">:</span><span class="token punctuation">:</span>One  <span class="token comment">// Load the static field `One`</span>
  <span class="token punctuation">IL_0018</span><span class="token punctuation">:</span> <span class="token function">stelem.i1</span>                                     <span class="token comment">// Set array[0] = One</span>
  <span class="token comment">// 👇 return the ReadOnlySpan&lt;byte&gt; around the new array</span>
  <span class="token punctuation">IL_0019</span><span class="token punctuation">:</span> <span class="token function">call</span>         <span class="token keyword">valuetype</span> <span class="token variable">[System.Runtime]</span>System.ReadOnlySpan`<span class="token number">1</span>&lt;!<span class="token number">0</span>/*<span class="token keyword">unsigned</span> <span class="token keyword">int8</span>*/&gt; <span class="token keyword">valuetype</span> <span class="token variable">[System.Runtime]</span>System.ReadOnlySpan`<span class="token number">1</span>&lt;<span class="token keyword">unsigned</span> <span class="token keyword">int8</span>&gt;<span class="token punctuation">:</span><span class="token punctuation">:</span>op_Implicit<span class="token punctuation">(</span>!<span class="token number">0</span>/*<span class="token keyword">unsigned</span> <span class="token keyword">int8</span>*/<span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">)</span>
  <span class="token punctuation">IL_001e</span><span class="token punctuation">:</span> <span class="token function">ret</span>
<span class="token punctuation">}</span> <span class="token comment">// end of method MyStaticData::get_ReadOnlySpanPropNonConstant</span>
</code></pre></div> <p>That's…bad 😬 And it does it on <em>every</em> property access. Definitely watch out for that one, on <em>all</em> target frameworks.</p> <h3 id="only-use-readonlyspant-not-spant" class="heading-with-anchor"><a href="https://andrewlock.net/removingbyte-array-allocations-in-dotnet-framework-using-readonlyspan-t/#only-use-readonlyspant-not-spant" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Only use <code>ReadOnlySpan&lt;T&gt;</code>, not <code>Span&lt;T&gt;</code></a></h3> <p>You have a similar "dangerous" scenario if you use <code>Span&lt;T&gt;</code> instead of <code>ReadOnlySpan&lt;T&gt;</code>:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token keyword">class</span> <span class="token class-name">MyStaticData</span>
<span class="token punctuation">{</span>
    <span class="token keyword">private</span> <span class="token keyword">static</span> <span class="token return-type class-name">Span<span class="token punctuation">&lt;</span><span class="token keyword">byte</span><span class="token punctuation">&gt;</span></span> SpanProp <span class="token operator">=&gt;</span> <span class="token keyword">new</span> <span class="token constructor-invocation class-name"><span class="token keyword">byte</span><span class="token punctuation">[</span><span class="token punctuation">]</span></span> <span class="token punctuation">{</span> <span class="token number">1</span><span class="token punctuation">,</span> <span class="token number">2</span><span class="token punctuation">,</span> <span class="token number">3</span><span class="token punctuation">,</span> <span class="token number">4</span> <span class="token punctuation">}</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>In this case, because you're returning <em>mutable</em> data (<code>Span&lt;T&gt;</code> instead of <code>ReadOnlySpan&lt;T&gt;</code>), the compiler can't use any of its fancy tricks, because the data needs to be mutable. All it can do is create a new array, initialize it with the correct initial values, and then hand it back wrapped in a mutable <code>Span&lt;T&gt;</code>:</p> <div class="pre-code-wrapper"><pre class="language-msil"><code class="language-msil"><span class="token directive class-name">.method</span> <span class="token keyword">private</span> <span class="token keyword">hidebysig</span> <span class="token keyword">static</span> <span class="token keyword">specialname</span> <span class="token keyword">valuetype</span> <span class="token variable">[System.Runtime]</span>System.Span`<span class="token number">1</span>&lt;<span class="token keyword">unsigned</span> <span class="token keyword">int8</span>&gt;
  get_SpanProp<span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token keyword">cil</span> <span class="token keyword">managed</span>
<span class="token punctuation">{</span>
  <span class="token directive class-name">.maxstack</span> <span class="token number">8</span>

  <span class="token comment">// [32 43 - 32 68]</span>
  <span class="token punctuation">IL_0000</span><span class="token punctuation">:</span> <span class="token function">ldc.i4.4</span>
  <span class="token punctuation">IL_0001</span><span class="token punctuation">:</span> <span class="token function">newarr</span>       <span class="token variable">[System.Runtime]</span>System.Byte
  <span class="token punctuation">IL_0006</span><span class="token punctuation">:</span> <span class="token function">dup</span>
  <span class="token punctuation">IL_0007</span><span class="token punctuation">:</span> <span class="token function">ldtoken</span>      field <span class="token keyword">int32</span> <span class="token string">'&lt;PrivateImplementationDetails&gt;'</span><span class="token punctuation">:</span><span class="token punctuation">:</span><span class="token string">'9F64A747E1B97F131FABB6B447296C9B6F0201E79FB3C5356E6C77E89B6A806A'</span>
  <span class="token punctuation">IL_000c</span><span class="token punctuation">:</span> <span class="token function">call</span>         <span class="token keyword">void</span> <span class="token variable">[System.Runtime]</span>System.Runtime.CompilerServices.RuntimeHelpers<span class="token punctuation">:</span><span class="token punctuation">:</span>InitializeArray<span class="token punctuation">(</span><span class="token keyword">class</span> <span class="token variable">[System.Runtime]</span>System.Array<span class="token punctuation">,</span> <span class="token keyword">valuetype</span> <span class="token variable">[System.Runtime]</span>System.RuntimeFieldHandle<span class="token punctuation">)</span>
  <span class="token punctuation">IL_0011</span><span class="token punctuation">:</span> <span class="token function">call</span>         <span class="token keyword">valuetype</span> <span class="token variable">[System.Runtime]</span>System.Span`<span class="token number">1</span>&lt;!<span class="token number">0</span>/*<span class="token keyword">unsigned</span> <span class="token keyword">int8</span>*/&gt; <span class="token keyword">valuetype</span> <span class="token variable">[System.Runtime]</span>System.Span`<span class="token number">1</span>&lt;<span class="token keyword">unsigned</span> <span class="token keyword">int8</span>&gt;<span class="token punctuation">:</span><span class="token punctuation">:</span>op_Implicit<span class="token punctuation">(</span>!<span class="token number">0</span>/*<span class="token keyword">unsigned</span> <span class="token keyword">int8</span>*/<span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">)</span>
  <span class="token punctuation">IL_0016</span><span class="token punctuation">:</span> <span class="token function">ret</span>

<span class="token punctuation">}</span> <span class="token comment">// end of method MyStaticData::get_SpanProp</span>
</code></pre></div> <p>The failure path here is understandable, because there's really no way to do a safe zero-allocation approach when the data needs to be mutable. The big problem is that it's not <em>obvious</em> that it's a super-allocatey property instead of a zero-allocation version. If you accidentally fat-finger and write <code>Span&lt;T&gt;</code> instead of <code>ReadOnlySpan&lt;T&gt;</code>, or, you know, Claude does, then it's <em>really</em> not obvious from simply reviewing the code…</p> <p>The only good news is that if you use modern features, namely collection expressions, you <em>might</em> catch the issue!</p> <h3 id="reducing-the-risk-of-errors-with-collection-expressions" class="heading-with-anchor"><a href="https://andrewlock.net/removingbyte-array-allocations-in-dotnet-framework-using-readonlyspan-t/#reducing-the-risk-of-errors-with-collection-expressions" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Reducing the risk of errors with collection expressions</a></h3> <p>So how do collection expressions help here? Well, those last two points, where one of the values isn't a constant, or where the variable is <code>Span&lt;T&gt;</code> instead of <code>ReadOnlySpan&lt;T&gt;</code> simply won't compile if you use the <code>static</code> property pattern with collection expressions:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token keyword">class</span> <span class="token class-name">MyStaticData</span>
<span class="token punctuation">{</span>
    <span class="token comment">// Doesn't compile (That's good!)</span>
    <span class="token keyword">private</span> <span class="token keyword">static</span> <span class="token return-type class-name">ReadOnlySpan<span class="token punctuation">&lt;</span><span class="token keyword">byte</span><span class="token punctuation">&gt;</span></span> ReadOnlySpanPruopNonConstantCollectionExpression <span class="token operator">=&gt;</span> <span class="token punctuation">[</span>One<span class="token punctuation">,</span> <span class="token number">2</span><span class="token punctuation">,</span> <span class="token number">3</span><span class="token punctuation">,</span> <span class="token number">4</span><span class="token punctuation">]</span><span class="token punctuation">;</span>

    <span class="token comment">// Doesn't compile (That's good!)</span>
    <span class="token keyword">private</span> <span class="token keyword">static</span> <span class="token return-type class-name">Span<span class="token punctuation">&lt;</span><span class="token keyword">byte</span><span class="token punctuation">&gt;</span></span> SpanPropCollectionExpression <span class="token operator">=&gt;</span> <span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">,</span> <span class="token number">2</span><span class="token punctuation">,</span> <span class="token number">3</span><span class="token punctuation">,</span> <span class="token number">4</span><span class="token punctuation">]</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>Attempting to compile this gives <code>CS9203</code> errors:</p> <div class="pre-code-wrapper"><pre class="language-bash"><code class="language-bash">Error CS9203 <span class="token builtin class-name">:</span> A collection expression of <span class="token builtin class-name">type</span> <span class="token string">'ReadOnlySpan&lt;byte&gt;'</span> cannot be used <span class="token keyword">in</span> this context because it may be exposed outside of the current scope.
Error CS9203 <span class="token builtin class-name">:</span> A collection expression of <span class="token builtin class-name">type</span> <span class="token string">'Span&lt;byte&gt;'</span> cannot be used <span class="token keyword">in</span> this context because it may be exposed outside of the current scope.
</code></pre></div> <p><img src="https://andrewlock.net/content/images/2026/readonlyspan_collectionexpressions.png" alt="The above errors in Rider"></p> <p>This gives you something of a safety-net. As long as you always use collection expressions for this scenario, you're blocked from making the most egregious errors. The case where you are using <code>int</code> <em>is</em> allowed, but as already flagged, that's not <em>as</em> bad, because it's actually supported on .NET 7+, and you <em>still</em> only create a single instance of the array and cache it in &lt;.NET 7.</p> <p>Unfortunately, collection expressions <em>only</em> save you in the <code>static</code> property case. If you are creating local variables, then collection expressions <em>don't</em> save you on .NET Framework (or on any .NET versions &lt;.NET 8)</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token keyword">class</span> <span class="token class-name">MyStaticData</span>
<span class="token punctuation">{</span>
    <span class="token keyword">private</span> <span class="token keyword">static</span> <span class="token keyword">readonly</span> <span class="token class-name"><span class="token keyword">byte</span></span> One <span class="token operator">=</span> <span class="token number">1</span><span class="token punctuation">;</span>

    <span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token return-type class-name"><span class="token keyword">void</span></span> <span class="token function">TestData</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        <span class="token comment">// Oh no, these all allocate on .NET Framework!</span>
        <span class="token class-name">ReadOnlySpan<span class="token punctuation">&lt;</span><span class="token keyword">int</span><span class="token punctuation">&gt;</span></span> intArray <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">,</span> <span class="token number">2</span><span class="token punctuation">,</span> <span class="token number">3</span><span class="token punctuation">,</span> <span class="token number">4</span><span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token comment">// .NET 7+ doesn't allocate for this one</span>

        <span class="token class-name">ReadOnlySpan<span class="token punctuation">&lt;</span><span class="token keyword">byte</span><span class="token punctuation">&gt;</span></span> nonConstantArray <span class="token operator">=</span> <span class="token punctuation">[</span>One<span class="token punctuation">,</span> <span class="token number">2</span><span class="token punctuation">,</span> <span class="token number">3</span><span class="token punctuation">,</span> <span class="token number">4</span><span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token comment">// But you need .NET 8+ to avoid</span>
        <span class="token class-name">Span<span class="token punctuation">&lt;</span><span class="token keyword">byte</span><span class="token punctuation">&gt;</span></span> spanArray <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">,</span> <span class="token number">2</span><span class="token punctuation">,</span> <span class="token number">3</span><span class="token punctuation">,</span> <span class="token number">4</span><span class="token punctuation">]</span><span class="token punctuation">;</span>                  <span class="token comment">// allocations for these two!</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>If we take a look at the IL generated for .NET Framework for this method, we can see that the <code>int[]</code> case uses the "create a static array and cache it" approach, while the non-constant and <code>Span&lt;T&gt;</code> cases create a new array every time, the same as happens with a <code>static</code> property:</p> <div class="pre-code-wrapper"><pre class="language-msil"><code class="language-msil">    <span class="token directive class-name">.method</span> <span class="token keyword">public</span> <span class="token keyword">hidebysig</span> <span class="token keyword">static</span> <span class="token keyword">void</span>
    TestData<span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token keyword">cil</span> <span class="token keyword">managed</span>
  <span class="token punctuation">{</span>
    <span class="token directive class-name">.maxstack</span> <span class="token number">5</span>
    <span class="token directive class-name">.locals</span> init <span class="token punctuation">(</span>
      <span class="token variable">[0]</span> <span class="token keyword">valuetype</span> <span class="token variable">[System.Memory]</span>System.ReadOnlySpan`<span class="token number">1</span>&lt;<span class="token keyword">int32</span>&gt; intArray<span class="token punctuation">,</span>
      <span class="token variable">[1]</span> <span class="token keyword">valuetype</span> <span class="token variable">[System.Memory]</span>System.ReadOnlySpan`<span class="token number">1</span>&lt;<span class="token keyword">unsigned</span> <span class="token keyword">int8</span>&gt; nonConstantArray<span class="token punctuation">,</span>
      <span class="token variable">[2]</span> <span class="token keyword">valuetype</span> <span class="token variable">[System.Memory]</span>System.Span`<span class="token number">1</span>&lt;<span class="token keyword">unsigned</span> <span class="token keyword">int8</span>&gt; spanArray
    <span class="token punctuation">)</span>

    <span class="token comment">// [10 5 - 10 6]</span>
    <span class="token punctuation">IL_0000</span><span class="token punctuation">:</span> <span class="token function">nop</span>

    <span class="token comment">// Load or initialize the static int[] field data</span>
    <span class="token punctuation">IL_0001</span><span class="token punctuation">:</span> <span class="token function">ldsfld</span>       <span class="token keyword">int32</span><span class="token punctuation">[</span><span class="token punctuation">]</span> <span class="token string">'&lt;PrivateImplementationDetails&gt;'</span><span class="token punctuation">:</span><span class="token punctuation">:</span>CF97ADEEDB59E05BFD73A2B4C2A8885708C4F4F70C84C64B27120E72AB733B72_A6
    <span class="token punctuation">IL_0006</span><span class="token punctuation">:</span> <span class="token function">dup</span>
    <span class="token punctuation">IL_0007</span><span class="token punctuation">:</span> <span class="token function">brtrue.s</span>     <span class="token punctuation">IL_0021</span>
    <span class="token punctuation">IL_0009</span><span class="token punctuation">:</span> <span class="token function">pop</span>
    <span class="token punctuation">IL_000a</span><span class="token punctuation">:</span> <span class="token function">ldc.i4.4</span>
    <span class="token punctuation">IL_000b</span><span class="token punctuation">:</span> <span class="token function">newarr</span>       <span class="token variable">[mscorlib]</span>System.Int32
    <span class="token punctuation">IL_0010</span><span class="token punctuation">:</span> <span class="token function">dup</span>
    <span class="token punctuation">IL_0011</span><span class="token punctuation">:</span> <span class="token function">ldtoken</span>      field <span class="token keyword">valuetype</span> <span class="token string">'&lt;PrivateImplementationDetails&gt;'</span>/<span class="token string">'__StaticArrayInitTypeSize=16'</span> <span class="token string">'&lt;PrivateImplementationDetails&gt;'</span><span class="token punctuation">:</span><span class="token punctuation">:</span>CF97ADEEDB59E05BFD73A2B4C2A8885708C4F4F70C84C64B27120E72AB733B72
    <span class="token punctuation">IL_0016</span><span class="token punctuation">:</span> <span class="token function">call</span>         <span class="token keyword">void</span> <span class="token variable">[mscorlib]</span>System.Runtime.CompilerServices.RuntimeHelpers<span class="token punctuation">:</span><span class="token punctuation">:</span>InitializeArray<span class="token punctuation">(</span><span class="token keyword">class</span> <span class="token variable">[mscorlib]</span>System.Array<span class="token punctuation">,</span> <span class="token keyword">valuetype</span> <span class="token variable">[mscorlib]</span>System.RuntimeFieldHandle<span class="token punctuation">)</span>
    <span class="token punctuation">IL_001b</span><span class="token punctuation">:</span> <span class="token function">dup</span>
    <span class="token punctuation">IL_001c</span><span class="token punctuation">:</span> <span class="token function">stsfld</span>       <span class="token keyword">int32</span><span class="token punctuation">[</span><span class="token punctuation">]</span> <span class="token string">'&lt;PrivateImplementationDetails&gt;'</span><span class="token punctuation">:</span><span class="token punctuation">:</span>CF97ADEEDB59E05BFD73A2B4C2A8885708C4F4F70C84C64B27120E72AB733B72_A6
    <span class="token punctuation">IL_0021</span><span class="token punctuation">:</span> <span class="token function">newobj</span>       <span class="token keyword">instance</span> <span class="token keyword">void</span> <span class="token keyword">valuetype</span> <span class="token variable">[System.Memory]</span>System.ReadOnlySpan`<span class="token number">1</span>&lt;<span class="token keyword">int32</span>&gt;<span class="token punctuation">:</span><span class="token punctuation">:</span>.ctor<span class="token punctuation">(</span>!<span class="token number">0</span>/*<span class="token keyword">int32</span>*/<span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">)</span>
    <span class="token punctuation">IL_0026</span><span class="token punctuation">:</span> <span class="token function">stloc.0</span>      <span class="token comment">// intArray</span>

    <span class="token comment">// For the non-constant array, a new array is created each time</span>
    <span class="token punctuation">IL_0027</span><span class="token punctuation">:</span> <span class="token function">ldloca.s</span>     nonConstantArray
    <span class="token punctuation">IL_0029</span><span class="token punctuation">:</span> <span class="token function">ldc.i4.4</span>
    <span class="token punctuation">IL_002a</span><span class="token punctuation">:</span> <span class="token function">newarr</span>       <span class="token variable">[mscorlib]</span>System.Byte
    <span class="token punctuation">IL_002f</span><span class="token punctuation">:</span> <span class="token function">dup</span>
    <span class="token punctuation">IL_0030</span><span class="token punctuation">:</span> <span class="token function">ldtoken</span>      field <span class="token keyword">int32</span> <span class="token string">'&lt;PrivateImplementationDetails&gt;'</span><span class="token punctuation">:</span><span class="token punctuation">:</span><span class="token string">'1E6175315920374CAA0A86B45D862DEE3DDAA28257652189FC1DFBE07479436A'</span>
    <span class="token punctuation">IL_0035</span><span class="token punctuation">:</span> <span class="token function">call</span>         <span class="token keyword">void</span> <span class="token variable">[mscorlib]</span>System.Runtime.CompilerServices.RuntimeHelpers<span class="token punctuation">:</span><span class="token punctuation">:</span>InitializeArray<span class="token punctuation">(</span><span class="token keyword">class</span> <span class="token variable">[mscorlib]</span>System.Array<span class="token punctuation">,</span> <span class="token keyword">valuetype</span> <span class="token variable">[mscorlib]</span>System.RuntimeFieldHandle<span class="token punctuation">)</span>
    <span class="token punctuation">IL_003a</span><span class="token punctuation">:</span> <span class="token function">dup</span>
    <span class="token punctuation">IL_003b</span><span class="token punctuation">:</span> <span class="token function">ldc.i4.0</span>
    <span class="token punctuation">IL_003c</span><span class="token punctuation">:</span> <span class="token function">ldsfld</span>       <span class="token keyword">unsigned</span> <span class="token keyword">int8</span> MyStaticData<span class="token punctuation">:</span><span class="token punctuation">:</span>One
    <span class="token punctuation">IL_0041</span><span class="token punctuation">:</span> <span class="token function">stelem.i1</span>
    <span class="token punctuation">IL_0042</span><span class="token punctuation">:</span> <span class="token function">call</span>         <span class="token keyword">instance</span> <span class="token keyword">void</span> <span class="token keyword">valuetype</span> <span class="token variable">[System.Memory]</span>System.ReadOnlySpan`<span class="token number">1</span>&lt;<span class="token keyword">unsigned</span> <span class="token keyword">int8</span>&gt;<span class="token punctuation">:</span><span class="token punctuation">:</span>.ctor<span class="token punctuation">(</span>!<span class="token number">0</span>/*<span class="token keyword">unsigned</span> <span class="token keyword">int8</span>*/<span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">)</span>

    <span class="token comment">// For the `Spam&lt;byte&gt;` array, a new array is created each time</span>
    <span class="token punctuation">IL_0047</span><span class="token punctuation">:</span> <span class="token function">ldloca.s</span>     spanArray
    <span class="token punctuation">IL_0049</span><span class="token punctuation">:</span> <span class="token function">ldc.i4.4</span>
    <span class="token punctuation">IL_004a</span><span class="token punctuation">:</span> <span class="token function">newarr</span>       <span class="token variable">[mscorlib]</span>System.Byte
    <span class="token punctuation">IL_004f</span><span class="token punctuation">:</span> <span class="token function">dup</span>
    <span class="token punctuation">IL_0050</span><span class="token punctuation">:</span> <span class="token function">ldtoken</span>      field <span class="token keyword">int32</span> <span class="token string">'&lt;PrivateImplementationDetails&gt;'</span><span class="token punctuation">:</span><span class="token punctuation">:</span><span class="token string">'9F64A747E1B97F131FABB6B447296C9B6F0201E79FB3C5356E6C77E89B6A806A'</span>
    <span class="token punctuation">IL_0055</span><span class="token punctuation">:</span> <span class="token function">call</span>         <span class="token keyword">void</span> <span class="token variable">[mscorlib]</span>System.Runtime.CompilerServices.RuntimeHelpers<span class="token punctuation">:</span><span class="token punctuation">:</span>InitializeArray<span class="token punctuation">(</span><span class="token keyword">class</span> <span class="token variable">[mscorlib]</span>System.Array<span class="token punctuation">,</span> <span class="token keyword">valuetype</span> <span class="token variable">[mscorlib]</span>System.RuntimeFieldHandle<span class="token punctuation">)</span>
    <span class="token punctuation">IL_005a</span><span class="token punctuation">:</span> <span class="token function">call</span>         <span class="token keyword">instance</span> <span class="token keyword">void</span> <span class="token keyword">valuetype</span> <span class="token variable">[System.Memory]</span>System.Span`<span class="token number">1</span>&lt;<span class="token keyword">unsigned</span> <span class="token keyword">int8</span>&gt;<span class="token punctuation">:</span><span class="token punctuation">:</span>.ctor<span class="token punctuation">(</span>!<span class="token number">0</span>/*<span class="token keyword">unsigned</span> <span class="token keyword">int8</span>*/<span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">)</span>

    <span class="token comment">// [15 5 - 15 6]</span>
    <span class="token punctuation">IL_005f</span><span class="token punctuation">:</span> <span class="token function">ret</span>

  <span class="token punctuation">}</span> <span class="token comment">// end of method MyStaticData::TestData</span>
</code></pre></div> <p>So unfortunately, collection expressions don't save you here. Of course, you likely can (and should) be using <code>stackalloc</code> here for these small arrays, so this isn't <em>necessarily</em> a big deal. But you do need to <em>know</em> how to do this.</p> <p>So what should we make of all this?</p> <h2 id="conclusion" class="heading-with-anchor"><a href="https://andrewlock.net/removingbyte-array-allocations-in-dotnet-framework-using-readonlyspan-t/#conclusion" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Conclusion</a></h2> <p>The good news is that if you use the right patterns, using <code>static ReadOnlySpan&lt;byte&gt;</code> properties to replace existing <code>static readonly byte[]</code> fields that contain read-only data <em>can</em> give a zero-allocation and essentially zero-startup cost improvement, even on .NET Framework.</p> <p>However, if the field that you're "converting" is not <code>byte[]</code>, <code>bool[]</code> or <code>sbyte[]</code>, then you should think carefully about whether to convert it. <code>int[]</code> and other types <em>are</em> supported for similar optimizations on .NET 7+, but this requires runtime support, so if you're also targeting .NET Framework, .NET Standard, or .NET 6 and below, then I would seriously consider whether it's worth making the change.</p> <blockquote> <p>You <a href="https://github.com/dotnet/runtime/issues/60948">likely <em>will</em> see perf benefits on .NET 7+</a>, but as far as I can tell, you're talking about a ~15% speed improvement for the initial creation of the array. But if you're calling <code>RuntimeHelpers.CreateSpan()</code> with every access, versus just loading a field, does that actually improve steady state performance? I don't know, I haven't checked, I'm just wondering😄</p> </blockquote> <p>Where you <em>really</em> need to be careful is to <em>only</em> use constant values in your arrays (no <code>static readonly</code> values, please) and only use <code>ReadOnlySpan&lt;T&gt;</code>, not <code>Span&lt;T&gt;</code>. Luckily, you'll catch these automatically in your <code>static</code> properties if you're using collection expressions, as they simply won't compile. Which just another reason you should use collection expressions everywhere you can!😃</p> <p>Replacing <code>static byte[]</code> fields with <code>static ReadOnlySpan&lt;byte&gt;</code> is probably the most common scenario you'll find, but you <em>can</em> also apply this to local variables. However, I suspect that scenario is going to be less common, simply because that's so <em>clearly</em> very allocating, it means that presumably you "don't care about performance" here, in which case there's no <em>point</em> making the <code>ReadOnlySpan&lt;byte&gt;</code> change.</p> <blockquote> <p>There's another reason for not touching local definitions, which is that the collection expression "solution" described above <em>doesn't</em> cause compilation failures with local variables, so there isn't the same easy guardrails there.</p> </blockquote> <p>If you're anything like me, then the fact that there are so many edge cases where you fall off a performance cliff is somewhat surprising. Generally the .NET team try quite hard to avoid these cliffs, or at least add analyzers to help steer you in the right direction. There seems to be little here to stop you doing the "wrong" thing.</p> <p>Looking through the various issues and discussions, that's something that's come up multiple times, but it seems like the difficulty is generally "the problematic code patterns are actually valid <em>sometimes</em>". There's also the "well, you should be using <code>stackalloc</code> <em>anyway</em>" argument, as well as "collection expressions partially protect you":</p> <ul><li><a href="https://github.com/dotnet/runtime/issues/69577">[Analyzer]: Unexpected allocation when converting arrays to spans</a></li> <li><a href="https://github.com/dotnet/csharplang/issues/5295">[Proposal]: ReadOnlySpan initialization from static data</a></li> <li><a href="https://github.com/dotnet/csharplang/discussions/955">compile-time readonly arrays, const T[]</a></li> <li><a href="https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-10-09.md#readonlyspan-initialization-from-static-data">"ReadOnlySpan initialization from static data" language design notes</a></li> <li><a href="https://github.com/dotnet/runtime/issues/60948">[API Proposal]: ReadOnlySpan<t> CreateSpan<t>(RuntimeFieldHandle)</t></t></a></li></ul> <p>So all-in-all, this approach seems to be "use at your own risk". I still think it might be nice to have <em>optional</em> analyzers at least to try to protect you (and maybe someone's already written those). Nevertheless, the ability to reduce initialization costs to 0 if you have a bunch of static data is definitely a win; just make sure you only use it in a safe way!</p> ]]></content:encoded><category><![CDATA[C#;Performance]]></category></item><item><title><![CDATA[Running AI agents with customized templates using docker sandbox]]></title><description><![CDATA[In this post I describe how to create custom templates for Docker Sandboxes, so that your sandboxes start with additional tools immediately available]]></description><link>https://andrewlock.net/running-ai-agents-with-customized-templates-in-docker-sandbox/</link><guid isPermaLink="true">https://andrewlock.net/running-ai-agents-with-customized-templates-in-docker-sandbox/</guid><pubDate>Tue, 14 Apr 2026 10:00:00 GMT</pubDate><dc:creator><![CDATA[Andrew Lock]]></dc:creator><media:content url="https://andrewlock.net/content/images/2026/sbx_templates_banner.png" medium="image" /><content:encoded><![CDATA[<img src="https://andrewlock.net/content/images/2026/sbx_templates_banner.png" /><p>This post follows on directly from <a href="https://andrewlock.net/running-ai-agents-safely-in-a-microvm-using-docker-sandbox/">my previous post</a>, in which I describe how to run AI agents safely using the docker sandbox tool, <code>sbx</code>. In this post I describe how to create custom templates, so that your sandboxes start with additional tools. I show both how to add tools to the default template, and how to start with a different docker image and layer-on the docker sandbox tooling later.</p> <blockquote> <p>An initial caveat to this post: I've been somewhat struggling to get my personal projects working in docker sandboxes, with .NET just hanging indefinitely during builds. This seems to be specific to my projects, as a hello world build is fine, but a word of caution: your mileage may vary.</p> </blockquote> <h2 id="running-agents-safely-in-a-docker-sandbox" class="heading-with-anchor"><a href="https://andrewlock.net/running-ai-agents-with-customized-templates-in-docker-sandbox/#running-agents-safely-in-a-docker-sandbox" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Running agents safely in a docker sandbox</a></h2> <p>As I described in <a href="https://andrewlock.net/running-ai-agents-safely-in-a-microvm-using-docker-sandbox/">my previous post</a>, working with AI agents in their default mode can mean an infuriating number of tool calls, that interrupt your flow, and generally slow you down:</p> <p><img src="https://andrewlock.net/content/images/2026/claude_code_tool.png" alt="The Claude Code permissions call, asking &quot;Do you want to create test.txt?&quot;"></p> <p>However, ignoring these tool calls using the <a href="https://code.claude.com/docs/en/permission-modes#switch-permission-modes">"bypass permissions" mode</a> (AKA YOLO/dangerous mode) can be, well, <em>dangerous</em>. There's plenty of examples of AI agents going rogue; do you want to risk it? <a href="https://docs.docker.com/ai/sandboxes/">Docker Sandboxes</a> provide one solution.</p> <p>Docker sandboxes run in microVMs, which are isolated from the host machine. The only folder the sandbox can access is the working directory you give access to, and all network traffic goes through a network proxy, which can either block traffic, or it can inject credentials such that the coding agent never sees them directly.</p> <p>I've only used docker sandboxes for a short while, but I've found they work relatively well for my purposes. However, one limitation is that <a href="https://github.com/datadog/dd-trace-dotnet">some of the projects</a> I'm working on have a bunch of requirements for tooling, which always needs to be installed in the sandbox. Doing that every time is a bit of a pain. Luckily, there's a solution: custom templates.</p> <h2 id="creating-a-custom-claude-code-template" class="heading-with-anchor"><a href="https://andrewlock.net/running-ai-agents-with-customized-templates-in-docker-sandbox/#creating-a-custom-claude-code-template" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Creating a custom Claude Code template</a></h2> <p><a href="https://docs.docker.com/ai/sandboxes/agents/custom-environments/">The Docker sandbox documentation</a> describes how to create a custom template, based on one of the default templates. I'm going to use the Claud Code examples in this post, but there's different templates for each of <a href="https://docs.docker.com/ai/sandboxes/agents/custom-environments/">the supported agents</a>. For each supported image there are also 2 variants: one that includes a Docker Engine, and one that doesn't. e.g.</p> <ul><li><code>claude-code</code>—includes a variety of dev tools.</li> <li><code>claude-code-docker</code>—includes the same as above, but also has Docker Engine.</li></ul> <blockquote> <p>There's also a <code>claude-code-minimal</code> template which is similar to <code>claude-code</code>, but includes fewer tools, so you don't have npm, python, or golang, for example.</p> </blockquote> <p>To create a custom template, you need to have Docker Desktop installed as you're basically building an OCI image (<a href="https://github.com/opencontainers/image-spec"><em>effectively</em> a docker image, kinda, sorta</a>). That's despite the fact that docker sandboxes <em>don't</em> run as docker containers, but rather as microVMs.</p> <p>The following example, <a href="https://docs.docker.com/ai/sandboxes/agents/custom-environments/#build-a-custom-template">based on the documentation</a> shows how to start from the default template, how to install package manager dependencies, and how to install other tools, using <code>dotnet</code> as an example:</p> <div class="pre-code-wrapper"><pre class="language-dockerfile"><code class="language-dockerfile"><span class="token instruction"><span class="token keyword">FROM</span> docker/sandbox-templates:claude-code-docker</span>

<span class="token comment"># Switch to root to run package manager installs (.NET dependencies)</span>
<span class="token instruction"><span class="token keyword">USER</span> root</span>
<span class="token instruction"><span class="token keyword">RUN</span> apt-get update <span class="token operator">\</span>
    &amp;&amp; apt-get install -y --no-install-recommends <span class="token operator">\</span>
    ca-certificates <span class="token operator">\</span>
    libc6 <span class="token operator">\</span>
    libgcc-s1 <span class="token operator">\</span>
    libgssapi-krb5-2 <span class="token operator">\</span>
    libicu76 <span class="token operator">\</span>
    libssl3t64 <span class="token operator">\</span>
    libstdc++6 <span class="token operator">\</span>
    tzdata <span class="token operator">\</span>
    zlib1g</span>

<span class="token comment"># Most tools should be installed at user-level, using the agent user</span>
<span class="token instruction"><span class="token keyword">USER</span> agent</span>
<span class="token instruction"><span class="token keyword">RUN</span> curl -sSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin --channel 10.0 --no-path</span>
<span class="token instruction"><span class="token keyword">ENV</span> DOTNET_ROOT=/home/agent/.dotnet <span class="token operator">\</span>
    PATH=<span class="token variable">$PATH</span>:/home/agent/.dotnet:/home/agent/.dotnet/tools</span>
</code></pre></div> <p>This shows several important things:</p> <ul><li>The base <code>docker/sandbox-templates</code> images are based on Ubuntu, so use <code>apt-get</code> for managing packages.</li> <li>The base images include two users, <code>root</code> and <code>agent</code>. <ul><li>System-level package installations must be made using the <code>root</code> user.</li> <li>Tools that install into the home directory must be installed using the <code>agent</code> user.</li></ul> </li></ul> <p>You can build the package using familiar <code>docker build</code> commands, but you <em>must</em> push it straight to an OCI registry (<a href="https://hub.docker.com/">Docker Hub</a> works!). You can't just build it locally as the docker sandbox doesn't share the image store with your local Docker host.</p> <div class="pre-code-wrapper"><pre class="language-bash"><code class="language-bash"><span class="token function">docker</span> build <span class="token parameter variable">-t</span> my-org/my-template:v1 <span class="token parameter variable">--push</span> <span class="token builtin class-name">.</span>
</code></pre></div> <p>Once you've pushed the image to an OCI registry you can use it locally in a sandbox by using the <code>--template</code> or <code>-t</code> argument when calling <code>sbx run</code>:</p> <div class="pre-code-wrapper"><pre class="language-bash"><code class="language-bash">sbx run <span class="token parameter variable">-t</span> docker.io/my-org/my-template:v1 claude
</code></pre></div> <p>This will pull (and cache) the template you specify, and you'll have the extra tools immediately available in your sandbox. Note that you must include the <code>docker.io</code> (Docker Hub) or other prefix when specifying the template (which differs from when you're running "normal" docker commands).</p> <blockquote> <p>I've created <a href="https://hub.docker.com/r/andrewlock/sandbox/tags">some sandboxes for .NET</a>, similar to the above, and pushed them to dockerhub. You can see <a href="https://github.com/andrewlock/dotnet-docker-images/blob/main/rhel.Dockerfile">the definition of the images</a> here. Feel free to use them if you wish!</p> </blockquote> <p>Basing your custom templates on the standard default templates works well when you just want to make some extra tools available to your sandbox, but what if you fundamentally want to use a different base image? That's a bit trickier…</p> <h2 id="what-if-you-need-to-change-the-base-image-" class="heading-with-anchor"><a href="https://andrewlock.net/running-ai-agents-with-customized-templates-in-docker-sandbox/#what-if-you-need-to-change-the-base-image-" class="relative text-zinc-800 dark:text-white no-underline hover:underline">What if you need to change the base image?</a></h2> <p>The "supported" approach to these custom templates is shown in the previous section: you start with the <code>docker/sandbox-templates</code> and then install the extra tools on that base image. Currently, those images are based on ubuntu 25.10, which is a nice current base image. But what if you <em>need</em> to use an older image for running tests. This is the case for the <a href="https://github.com/DataDog/dd-trace-dotnet">Datadog .NET SDK</a> where we build using old distro versions to ensure we can support customers running with early glibc versions.</p> <p>This proves a little tricky, as it's not officially supported. On the one hand, to emulate the work the base images do, <em>mostly</em> there's just a few crucial configurations you need to add, such as setting <code>NO_PROXY</code>, creating an <code>agent</code> user, and installing the <code>claude</code> CLI. However, the <code>docker/sandbox-templates</code> images contain a lot more than that. Unfortunately, the contents of these images aren't readily available on GitHub, for example.</p> <p>Luckily, you <em>can</em> see the contents of each layer <a href="https://hub.docker.com/layers/docker/sandbox-templates/claude-code-minimal/images/sha256-39c0713656fb1f8531df04fbd6cf8d5a64e6002c5331e47ded1d7d3250ff2230">on Docker Hub</a>. It's a little bit messed up due to how buildkit renders it, but it <em>is</em> understandable. Based on each of those layers, I was able to effectively reverse-engineer the layering the <code>docker/sandbox-templates:claude-code-docker</code> image <em>on top</em> of a different base image.</p> <blockquote> <p>This is all very hacky, could change at any time, and comes with no guarantees that it works for you 😅</p> </blockquote> <p>The following shows a dockerfile that aims to perform all the steps the default <code>docker/sandbox-templates</code> to, but based on an arbitrary base image. There's quite a lot in here, but in summary:</p> <ul><li>It configures various environment variables.</li> <li>Installs various basic tools (curl, certificates) and sets up various keyrings.</li> <li>Configures the <code>agent</code> user.</li> <li>Sets up a <code>CLAUDE_ENV_FILE</code> temporary session file.</li> <li>Installs a variety of tools (npm, golang, python, make etc).</li> <li>Installs Claude Code.</li></ul> <p>All in all, it looks a bit like this:</p> <div class="pre-code-wrapper"><pre class="language-dockerfile"><code class="language-dockerfile"><span class="token instruction"><span class="token keyword">FROM</span> dd-trace-dotnet/debian-tester <span class="token keyword">AS</span> base</span>

<span class="token comment"># Grab stuff from the original sandbox</span>
<span class="token instruction"><span class="token keyword">ENV</span> NPM_CONFIG_PREFIX=/usr/local/share/npm-global</span>
<span class="token instruction"><span class="token keyword">ENV</span> PATH=/home/agent/.local/bin:/usr/local/share/npm-global/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin</span>
<span class="token instruction"><span class="token keyword">ENV</span> NO_PROXY=localhost,127.0.0.1,::1,172.17.0.0/16</span>
<span class="token instruction"><span class="token keyword">ENV</span> no_proxy=localhost,127.0.0.1,::1,172.17.0.0/16</span>

<span class="token instruction"><span class="token keyword">WORKDIR</span> /home/agent/workspace</span>
<span class="token instruction"><span class="token keyword">RUN</span> apt-get update <span class="token operator">\</span>
    &amp;&amp; apt-get install -yy --no-install-recommends <span class="token operator">\</span>
    ca-certificates <span class="token operator">\</span>
    curl <span class="token operator">\</span>
    gnupg <span class="token operator">\</span>
    &amp;&amp; install -m 0755 -d /etc/apt/keyrings <span class="token operator">\</span>
    &amp;&amp; curl -fsSL https://download.docker.com/linux/debian/gpg | <span class="token operator">\</span>
    gpg --dearmor -o /etc/apt/keyrings/docker.gpg <span class="token operator">\</span>
    &amp;&amp; chmod a+r /etc/apt/keyrings/docker.gpg <span class="token operator">\</span>
    &amp;&amp; echo <span class="token operator">\</span>
    <span class="token string">"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian \
    $(. /etc/os-release &amp;&amp; echo "</span><span class="token variable">${UBUNTU_CODENAME:-$VERSION_CODENAME}</span><span class="token string">") stable"</span> | <span class="token operator">\</span>
    tee /etc/apt/sources.list.d/docker.list &gt; /dev/null</span>

<span class="token comment"># Remove base image user</span>
<span class="token comment"># Create non-root user</span>
<span class="token comment"># Configure sudoers</span>
<span class="token comment"># Create sandbox config</span>
<span class="token comment"># Set up npm global package folder under /usr/local/share</span>
<span class="token instruction"><span class="token keyword">RUN</span> userdel ubuntu || true <span class="token operator">\</span>
    &amp;&amp; useradd --create-home --uid 1000 --shell /bin/bash agent <span class="token operator">\</span>
    &amp;&amp; groupadd -f docker <span class="token operator">\</span>
    &amp;&amp; usermod -aG sudo agent <span class="token operator">\</span>
    &amp;&amp; usermod -aG docker agent <span class="token operator">\</span>
    &amp;&amp; mkdir /etc/sudoers.d <span class="token operator">\</span>
    &amp;&amp; chmod 0755 /etc/sudoers.d <span class="token operator">\</span>
    &amp;&amp; echo <span class="token string">"agent ALL=(ALL) NOPASSWD:ALL"</span> &gt; /etc/sudoers.d/agent <span class="token operator">\</span>
    &amp;&amp; echo <span class="token string">"Defaults:%sudo env_keep += \"http_proxy https_proxy no_proxy HTTP_PROXY HTTPS_PROXY NO_PROXY SSL_CERT_FILE NODE_EXTRA_CA_CERTS REQUESTS_CA_BUNDLE JAVA_TOOL_OPTIONS\""</span> &gt; /etc/sudoers.d/proxyconfig <span class="token operator">\</span>
    &amp;&amp; mkdir -p /home/agent/.docker/sandbox/locks <span class="token operator">\</span>
    &amp;&amp; chown -R agent:agent /home/agent <span class="token operator">\</span>
    &amp;&amp; mkdir -p /usr/local/share/npm-global <span class="token operator">\</span>
    &amp;&amp; chown -R agent:agent /usr/local/share/npm-global</span>

<span class="token instruction"><span class="token keyword">RUN</span> touch /etc/sandbox-persistent.sh &amp;&amp; chmod 644 /etc/sandbox-persistent.sh &amp;&amp; chown agent:agent /etc/sandbox-persistent.sh</span>
<span class="token instruction"><span class="token keyword">ENV</span> BASH_ENV=/etc/sandbox-persistent.sh</span>

<span class="token comment"># Source the sandbox persistent environment file</span>
<span class="token comment"># Export BASH_ENV so non-interactive child shells also source the persistent env</span>
<span class="token instruction"><span class="token keyword">RUN</span> echo <span class="token string">'if [ -f /etc/sandbox-persistent.sh ]; then . /etc/sandbox-persistent.sh; fi; export BASH_ENV=/etc/sandbox-persistent.sh'</span> <span class="token operator">\</span>
    | tee /etc/profile.d/sandbox-persistent.sh /tmp/sandbox-bashrc-prepend /home/agent/.bashrc &gt; /dev/null <span class="token operator">\</span>
    &amp;&amp; chmod 644 /etc/profile.d/sandbox-persistent.sh <span class="token operator">\</span>
    &amp;&amp; cat /tmp/sandbox-bashrc-prepend /etc/bash.bashrc &gt; /tmp/new-bashrc <span class="token operator">\</span>
    &amp;&amp; mv /tmp/new-bashrc /etc/bash.bashrc <span class="token operator">\</span>
    &amp;&amp; chmod 644 /etc/bash.bashrc <span class="token operator">\</span>
    &amp;&amp; rm /tmp/sandbox-bashrc-prepend</span>
    &amp;&amp; chmod 644 /home/agent/.bashrc \
    &amp;&amp; chown agent:agent /home/agent/.bashrc

<span class="token instruction"><span class="token keyword">USER</span> root</span>

<span class="token comment"># Setup Github keys</span>
<span class="token instruction"><span class="token keyword">RUN</span> curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg <span class="token operator">\</span>
    | tee /etc/apt/keyrings/githubcli-archive-keyring.gpg &gt; /dev/null <span class="token operator">\</span>
    &amp;&amp; chmod a+r /etc/apt/keyrings/githubcli-archive-keyring.gpg <span class="token operator">\</span>
    &amp;&amp; echo <span class="token string">"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main"</span> <span class="token operator">\</span>
    | tee /etc/apt/sources.list.d/github-cli.list &gt; /dev/null</span>

<span class="token comment"># Install all the tools available in the claude-code-docker image</span>
<span class="token instruction"><span class="token keyword">RUN</span> apt-get update <span class="token operator">\</span>
    &amp;&amp; apt-get install -yy --no-install-recommends <span class="token operator">\</span>
    dnsutils <span class="token operator">\</span>
    docker-buildx-plugin <span class="token operator">\</span>
    docker-ce-cli <span class="token operator">\</span>
    docker-compose-plugin <span class="token operator">\</span>
    git <span class="token operator">\</span>
    jq <span class="token operator">\</span>
    less <span class="token operator">\</span>
    lsof <span class="token operator">\</span>
    make <span class="token operator">\</span>
    procps <span class="token operator">\</span>
    psmisc <span class="token operator">\</span>
    ripgrep <span class="token operator">\</span>
    rsync <span class="token operator">\</span>
    socat <span class="token operator">\</span>
    sudo <span class="token operator">\</span>
    unzip <span class="token operator">\</span>
    gh <span class="token operator">\</span>
    bc <span class="token operator">\</span>
    default-jdk-headless <span class="token operator">\</span>
    golang <span class="token operator">\</span>
    man-db <span class="token operator">\</span>
    nodejs <span class="token operator">\</span>
    npm <span class="token operator">\</span>
    python3 <span class="token operator">\</span>
    python3-pip <span class="token operator">\</span>
    containerd.io docker-ce <span class="token operator">\</span>
    &amp;&amp; apt-get clean <span class="token operator">\</span>
    &amp;&amp; rm -rf /var/lib/apt/lists/*</span>

<span class="token instruction"><span class="token keyword">LABEL</span> com.docker.sandboxes.start-docker=true</span>

<span class="token instruction"><span class="token keyword">USER</span> agent</span>

<span class="token instruction"><span class="token keyword">FROM</span> base <span class="token keyword">AS</span> claude</span>

<span class="token comment"># Install Claude Code</span>
<span class="token instruction"><span class="token keyword">RUN</span> curl -fsSL https://claude.ai/install.sh | bash</span>

<span class="token instruction"><span class="token keyword">ENV</span> CLAUDE_ENV_FILE=/etc/sandbox-persistent.sh</span>
<span class="token instruction"><span class="token keyword">CMD</span> [<span class="token string">"claude"</span>, <span class="token string">"--dangerously-skip-permissions"</span>]</span>
</code></pre></div> <p>If you don't want <em>all</em> the extra tools like npm, python and golang, you can instead base it on the <code>claude-code-minimal</code> image instead. In that case, the final tool install step looks a bit like this instead:</p> <div class="pre-code-wrapper"><pre class="language-dockerfile"><code class="language-dockerfile"><span class="token instruction"><span class="token keyword">RUN</span> apt-get update <span class="token operator">\</span>
    &amp;&amp; apt-get install -yy --no-install-recommends <span class="token operator">\</span>
        bubblewrap <span class="token operator">\</span>
        dnsutils <span class="token operator">\</span>
        docker-buildx-plugin <span class="token operator">\</span>
        docker-ce-cli <span class="token operator">\</span>
        docker-compose-plugin <span class="token operator">\</span>
        git <span class="token operator">\</span>
        gh <span class="token operator">\</span>
        jq <span class="token operator">\</span>
        less <span class="token operator">\</span>
        lsof <span class="token operator">\</span>
        make <span class="token operator">\</span>
        procps <span class="token operator">\</span>
        psmisc <span class="token operator">\</span>
        ripgrep <span class="token operator">\</span>
        rsync <span class="token operator">\</span>
        socat <span class="token operator">\</span>
        sudo <span class="token operator">\</span>
        unzip <span class="token operator">\</span>
    &amp;&amp; apt-get clean <span class="token operator">\</span>
    &amp;&amp; rm -rf /var/lib/apt/lists/*</span>
</code></pre></div> <p>Or, you know, install a mix of those tools. That's the advantage of this approach at least, you can install more of fewer tools, whatever you want! Whichever approach you like, you can again build and push the image to an OCI registry:</p> <div class="pre-code-wrapper"><pre class="language-bash"><code class="language-bash"><span class="token function">docker</span> build <span class="token parameter variable">--tag</span> dd-trace-dotnet/sandbox <span class="token parameter variable">--push</span> <span class="token builtin class-name">.</span>
</code></pre></div> <p>You can then use the image in your <code>sbx</code> sandbox, just as before, but this time you'll be running in a base image that has all of your prerequisites installed.</p> <h2 id="updating-the-version-of-claude-code-only" class="heading-with-anchor"><a href="https://andrewlock.net/running-ai-agents-with-customized-templates-in-docker-sandbox/#updating-the-version-of-claude-code-only" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Updating the version of Claude Code only</a></h2> <p>You might notice in the above Dockerfile that I put the Claude Code image in its own section of the multi-stage build:</p> <div class="pre-code-wrapper"><pre class="language-dockerfile"><code class="language-dockerfile"><span class="token instruction"><span class="token keyword">FROM</span> base <span class="token keyword">AS</span> claude</span>

<span class="token comment"># Install Claude Code</span>
<span class="token instruction"><span class="token keyword">RUN</span> curl -fsSL https://claude.ai/install.sh | bash</span>

<span class="token instruction"><span class="token keyword">ENV</span> CLAUDE_ENV_FILE=/etc/sandbox-persistent.sh</span>
<span class="token instruction"><span class="token keyword">CMD</span> [<span class="token string">"claude"</span>, <span class="token string">"--dangerously-skip-permissions"</span>]</span>
</code></pre></div> <p>That's not necessary, but I did it for a subtle reason. Claude Code updates a <em>lot</em>, but I didn't really want to update the <em>entire</em> image repeatedly for performance reasons. By moving the Claude Code install to its own final stage, I could rebuild <em>just</em> that stage, without having to rebuild the <em>entire</em> image, by using <code>--no-cache-filter</code>:</p> <div class="pre-code-wrapper"><pre class="language-bash"><code class="language-bash"><span class="token function">docker</span> build <span class="token parameter variable">--tag</span> dd-trace-dotnet/sandbox <span class="token parameter variable">--push</span> --no-cache-filter claude <span class="token builtin class-name">.</span>
</code></pre></div> <p>It's just a minor thing, but it means updating to the latest Claude Code version is a much quicker process.</p> <p>I still need to test this image out properly, but I tried it out with a previous version and it was working pretty well for me. I'd be interested to know if anyone else has tried something similar, or if you have a better solution (short of just yolo/dangerous direct on the host!).</p> <h2 id="summary" class="heading-with-anchor"><a href="https://andrewlock.net/running-ai-agents-with-customized-templates-in-docker-sandbox/#summary" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Summary</a></h2> <p>In this post I described how to create custom templates for Docker Sandboxes. First I showed the official approach, which layers tools on top of yhe default sandbox templates in <code>docker/sandbox-templates</code>. This is the easiest approach, and works well if the specific base image doesn't matter too much to you. Then I showed how I reverse-engineered the sandbox templates to allow completely swapping out the base image. This was necessary for a project I was working on, where I specifically wanted to run agents in the same base image we use to build the project. this approach isn't supported, and I'm not 100% it's quite right, but it seems to do the job well enough!</p> ]]></content:encoded><category><![CDATA[Getting Started;AI;Docker;Installation]]></category></item><item><title><![CDATA[Running AI agents safely in a microVM using docker sandbox]]></title><description><![CDATA[In this post I show how to run AI coding agents safely while still using YOLO/dangerous mode using docker sandboxes and the sbx tool]]></description><link>https://andrewlock.net/running-ai-agents-safely-in-a-microvm-using-docker-sandbox/</link><guid isPermaLink="true">https://andrewlock.net/running-ai-agents-safely-in-a-microvm-using-docker-sandbox/</guid><pubDate>Tue, 07 Apr 2026 10:00:00 GMT</pubDate><dc:creator><![CDATA[Andrew Lock]]></dc:creator><media:content url="https://andrewlock.net/content/images/2026/sbx_banner.png" medium="image" /><content:encoded><![CDATA[<img src="https://andrewlock.net/content/images/2026/sbx_banner.png" /><p>In this post I describe one way to run coding agents locally <em>safely</em> while still using "YOLO" or "dangerous" mode, by using <a href="https://docs.docker.com/ai/sandboxes/">Docker Sandboxes</a>.</p> <h2 id="powerful-agents-but-they-need-a-lot-of-hand-holding" class="heading-with-anchor"><a href="https://andrewlock.net/running-ai-agents-safely-in-a-microvm-using-docker-sandbox/#powerful-agents-but-they-need-a-lot-of-hand-holding" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Powerful agents, but they need a lot of hand holding</a></h2> <p>It's pretty safe to assume that if you're reading this, you're <em>probably</em> using some sort of coding agent these days, whether that's Claude Code, Codex, Copilot, or something else. I have a whole bunch of ethical, environmental, and sustainability concerns about the technology, but the fact is that in 2026, they've got <em>so much better</em> than they were even 6 months ago.</p> <blockquote> <p>I am <em>massively</em> conflicted about the role of AI in software engineering (let alone other areas of life), but I'm not going to address that in this post. For better or worse, it feels like working with coding agents is practically becoming a job requirement, so learning how to do it safely seems important..</p> </blockquote> <p>I've been <em>very</em> impressed with how effective Claude Code can be at adding new features, maintenance, and problem solving, but there's one thing that's infuriating… the dreaded tool-call permissions.</p> <p><img src="https://andrewlock.net/content/images/2026/claude_code_tool.png" alt="The claude code permissions call, asking &quot;Do you want to create test.txt?&quot;"></p> <p>The real problem is that Claude asks this for <em>endless</em> things. Want to use <code>grep</code>? Confirm permission. Want to use <code>sed</code>? Confirm permission. Want to use <code>cd</code> <a href="https://github.com/anthropics/claude-code/issues/30524">because Claude Code doesn't understand Windows</a>? Confirm permission.</p> <p>This is an absolute <em>killer</em> for productivity. Using the tools like this becomes exhausting, constantly switching between terminal windows to find the agent that's managed to run into a wall again 🙄</p> <blockquote> <p>In this post I'm focusing on Claude Code as that's what I have the most experience with, but everything pretty much works the same for other agents as I understand it too.</p> </blockquote> <h2 id="live-dangerously-if-you-dare" class="heading-with-anchor"><a href="https://andrewlock.net/running-ai-agents-safely-in-a-microvm-using-docker-sandbox/#live-dangerously-if-you-dare" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Live dangerously, if you dare</a></h2> <p>Of course, there <em>is</em> a solution, but it's not for the feint-hearted. Claude Code has the flag <code>--allow-dangerously-skip-permissions</code>, <a href="https://code.claude.com/docs/en/permission-modes#switch-permission-modes">which adds a "bypass permissions" mode</a> to the standard "plan" and "accept edit" modes.</p> <blockquote> <p>This flag means "bypass permissions" mode is available, but it doesn't <em>start</em> in that mode. If you want to start in that mode, you can use <code>--permission-mode bypassPermissions</code> or <code>--dangerously-skip-permissions</code> instead.</p> </blockquote> <p>If you start claude code using:</p> <div class="pre-code-wrapper"><pre class="language-bash"><code class="language-bash">claude --allow-dangerously-skip-permissions
</code></pre></div> <p>Then you'll get a warning:</p> <p><img src="https://andrewlock.net/content/images/2026/claude_code_bypass.png" alt="In Bypass Permissions mode, Claude Code will not ask for your approval before running potentially dangerous commands. This mode should only be used in a sandboxed container/VM that has restricted internet access and can easily be restored if damaged."></p> <p>and you can select bypass permissions on demand by cycling through modes with <kbd>Shift</kbd>+<kbd>Tab</kbd></p> <p><img src="https://andrewlock.net/content/images/2026/claude_code_bypass_2.png" alt="bypass-permisisons mode in Claude Code"></p> <p>The trouble is, then you won't get <em>any</em> permission tool requests. If Claude decides to run something stupid that deletes your User folder then sorry, that's you hosed. It's right there in the warning…this is dangerous😅</p> <p>And yet…</p> <p>The experience in bypass permissions mode is just <em>so</em> much better when you want the agent to just go and <em>do</em> something (or even if you just want it to create a plan). It doesn't bother you about every little thing, it just <em>does the task</em>. That's a hard experience to give up, but there <em>are</em> options that can get you pretty close to this, <em>safely</em>.</p> <h2 id="live-safely-in-a-sandbox" class="heading-with-anchor"><a href="https://andrewlock.net/running-ai-agents-safely-in-a-microvm-using-docker-sandbox/#live-safely-in-a-sandbox" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Live safely, in a sandbox</a></h2> <p>Docker recently released <a href="https://docs.docker.com/ai/sandboxes/">Docker Sandboxes</a>. It might surprise you that this actually <em>isn't</em> built on containers, but rather on isolated microVM sandboxes. This has some <a href="https://docs.docker.com/ai/sandboxes/security/">security advantages</a>:</p> <ul><li>Unlike containers, which share the host kernel, each sandbox has its own kernel</li> <li>The microVM runs a separate docker engine inside so you can build and run containers without having to mount your host docker socket</li> <li>The network in the microVM is isolated from the host. A network proxy runs on the host side, intercepting traffic, blocking access to the host's localhost, and automatically injecting authentication headers (so that the sandbox doesn't have access to them).</li></ul> <figure> <picture> <img src="https://andrewlock.net/content/images/2026/sbx-security.png"> </picture><figcaption>Sandbox security model showing the hypervisor boundary between the sandbox VM and the host system. The workspace directory is shared read-write. The agent process, Docker engine, packages, and VM filesystem are inside the VM. Host filesystem, processes, Docker engine, and network are outside the VM and not accessible. A proxy enforces allow/deny policies and injects credentials into outbound requests. From <a href="https://docs.docker.com/ai/sandboxes/security/">https://docs.docker.com/ai/sandboxes/security/</a></figcaption></figure> <p>With all this isolation, the idea is that you can just let your agents run amok, without having to babysit them. Sounds ideal, right?</p> <blockquote> <p>Docker sandboxes are experimental. Until recently, it was shipping with Docker Desktop and you ran commands like <code>docker sandbox run</code>. However, they recently switched to shipping a dedicated <code>sbx</code> tool that doesn't require docker desktop.</p> </blockquote> <p>For the rest of this post I'll discuss the basics of getting started with sandboxes, and my brief experience with them.</p> <h2 id="getting-started-with-sbx-sandbox" class="heading-with-anchor"><a href="https://andrewlock.net/running-ai-agents-safely-in-a-microvm-using-docker-sandbox/#getting-started-with-sbx-sandbox" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Getting started with <code>sbx</code> sandbox</a></h2> <p>Due to the experimental nature of the <code>sbx</code> tool and Docker Sandboxes in general, this post is going to be relatively light on details and will focus on the basics, as I expect it will go out of date rapidly. Instead, I recommend you check <a href="https://docs.docker.com/ai/sandboxes/usage/">the docs</a> for more advanced usages.</p> <h3 id="installing-the-sbx-tool" class="heading-with-anchor"><a href="https://andrewlock.net/running-ai-agents-safely-in-a-microvm-using-docker-sandbox/#installing-the-sbx-tool" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Installing the <code>sbx</code> tool</a></h3> <p>To get started, you'll first need to install the <code>sbx</code> tool. Only macOS (arm64) and Windows (x86_64, Windows 11) are currently supported, and I'm going to provide the instructions for Windows seeing as that's what I'm using.</p> <p>First, you'll need to make sure you have the <code>HypervisorPlatform</code> feature enabled. This is different (but related to) the full Hyper-V feature. It's used by WSL2, so it's very likely you <em>already</em> have this enabled anyway, but just in case, run the following in an administrator PowerShell prompt:</p> <div class="pre-code-wrapper"><pre class="language-powershell"><code class="language-powershell"><span class="token function">Enable-WindowsOptionalFeature</span> <span class="token operator">-</span>Online <span class="token operator">-</span>FeatureName HypervisorPlatform <span class="token operator">-</span>All
</code></pre></div> <p>Next, install the <code>sbx</code> tool using <a href="https://learn.microsoft.com/en-us/windows/package-manager/winget/">WinGet</a>, or by downloading the MSI from <a href="https://github.com/docker/sbx-releases/releases">the Github releases page</a>:</p> <div class="pre-code-wrapper"><pre class="language-powershell"><code class="language-powershell">winget install <span class="token operator">-</span>h Docker<span class="token punctuation">.</span>sbx
</code></pre></div> <p>You'll need to open a new terminal window to make sure the tool is available.</p> <h3 id="signing-in-and-configuring-the-defaults" class="heading-with-anchor"><a href="https://andrewlock.net/running-ai-agents-safely-in-a-microvm-using-docker-sandbox/#signing-in-and-configuring-the-defaults" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Signing in and configuring the defaults</a></h3> <p>Once you've installed (and opened a new terminal window), you have to sign-in to use docker sandboxes, so start by running <code>sbx login</code> (or just <code>sbx</code>, it'll do the same thing):</p> <div class="pre-code-wrapper"><pre class="language-powershell"><code class="language-powershell">❯ sbx
You are not authenticated to Docker<span class="token punctuation">.</span> Starting the sign-in flow<span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span>

Your one-time device confirmation code is: FXDG-FKTF
Open this URL to sign in: https:<span class="token operator">/</span><span class="token operator">/</span>login<span class="token punctuation">.</span>docker<span class="token punctuation">.</span>com/activate?user_code=FXDG-FKTF

By logging in<span class="token punctuation">,</span> you agree to our Subscription Service Agreement<span class="token punctuation">.</span> <span class="token keyword">For</span> more details<span class="token punctuation">,</span> see https:<span class="token operator">/</span><span class="token operator">/</span>www<span class="token punctuation">.</span>docker<span class="token punctuation">.</span>com/legal/docker-subscription-service-agreement/

Waiting <span class="token keyword">for</span> authentication<span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span>
</code></pre></div> <p>This pops up a login window, where you first confirm, and then login to docker:</p> <p><img src="https://andrewlock.net/content/images/2026/sbx-login.png" alt="The docker login"></p> <p>You're then provided with a choice of how to configure your network:</p> <div class="pre-code-wrapper"><pre class="language-powershell"><code class="language-powershell">Signed in as andrewlock<span class="token punctuation">.</span>
Daemon started <span class="token punctuation">(</span>PID: 52268<span class="token punctuation">,</span> socket: \\<span class="token punctuation">.</span>\pipe\docker_kaname_sandboxd<span class="token punctuation">)</span>
Logs: C:\Users\sock\AppData\Local\DockerSandboxes\sandboxes\state\sandboxd\daemon<span class="token punctuation">.</span>log

<span class="token function">Select</span> a default network policy <span class="token keyword">for</span> your sandboxes:

     1<span class="token punctuation">.</span> Open         — All network traffic allowed<span class="token punctuation">,</span> no restrictions<span class="token punctuation">.</span>
     2<span class="token punctuation">.</span> Balanced     — Default deny<span class="token punctuation">,</span> with common dev sites allowed<span class="token punctuation">.</span>
     3<span class="token punctuation">.</span> Locked Down  — All network traffic blocked unless you allow it<span class="token punctuation">.</span>

  Use ↑<span class="token operator">/</span>↓ or 1–3 to navigate<span class="token punctuation">,</span> Enter to confirm<span class="token punctuation">,</span> Esc to cancel<span class="token punctuation">.</span>
</code></pre></div> <p>The descriptions here are relatively self-evident, and it depends on how locked down you want your sandbox to be. All communication from the sandbox goes through a proxy, so it's really this <em>proxy</em> you're configuring.</p> <p>The network policies are a new feature since I started working with the sandboxes, so I haven't experimented with these myself yet. I tried out Balanced, and used <code>sbx policy ls</code> to describe the policies, and it configured the following as allowed domains; all other network requests will be blocked:</p> <table><thead><tr><th style="text-align:left">Name</th><th style="text-align:left">Resources</th></tr></thead><tbody><tr><td style="text-align:left">default ai services</td><td style="text-align:left"><code>**.chatgpt.com:443</code>, <code>**.oaistatic.com:443</code>, <code>**.oaiusercontent.com:443</code>, <code>**.openai.com:443</code>, <code>api.anthropic.com:443</code>, <code>api.perplexity.ai:443</code>, <code>cdn.openaimerge.com:443</code>, <code>chatgpt.com:443</code>, <code>gemini.google.com:443</code>, <code>generativelanguage.googleapis.com:443</code>, <code>models.dev:443</code>, <code>nanoclaw.dev:443</code>, <code>platform.claude.com:443</code>, <code>play.googleapis.com:443</code>, <code>statsig.anthropic.com:443</code></td></tr><tr><td style="text-align:left">default package managers</td><td style="text-align:left"><code>**.bun.sh:443</code>, <code>**.gradle.org:443</code>, <code>**.packagist.org:443</code>, <code>**.yarnpkg.com:443</code>, <code>apache.org:443</code>, <code>astral.sh:443</code>, <code>bootstrap.pypa.io:443</code>, <code>bun.sh:443</code>, <code>cocoapods.org:443</code>, <code>cpan.org:443</code>, <code>crates.io:443</code>, <code>dot.net:443</code>, <code>dotnet.microsoft.com:443</code>, <code>eclipse.org:443</code>, <code>files.pythonhosted.org:443</code>, <code>golang.org:443</code>, <code>goproxy.io:443</code>, <code>gradle.org:443</code>, <code>haskell.org:443</code>, <code>hex.pm:443</code>, <code>index.crates.io:443</code>, <code>java.com:443</code>, <code>java.net:443</code>, <code>maven.org:443</code>, <code>metacpan.org:443</code>, <code>nodejs.org:443</code>, <code>nodesource.com:443</code>, <code>npm.duckdb.org:443</code>, <code>npmjs.com:443</code>, <code>npmjs.org:443</code>, <code>nuget.org:443</code>, <code>packagist.com:443</code>, <code>packagist.org:443</code>, <code>pkg.go.dev:443</code>, <code>proxy.golang.org:443</code>, <code>pub.dev:443</code>, <code>pypa.io:443</code>, <code>pypi.org:443</code>, <code>pypi.python.org:443</code>, <code>pythonhosted.org:443</code>, <code>registry.npmjs.org:443</code>, <code>repo.maven.apache.org:443</code>, <code>ruby-lang.org:443</code>, <code>rubygems.org:443</code>, <code>rubyonrails.org:443</code>, <code>rustup.rs:443</code>, <code>rvm.io:443</code>, <code>sh.rustup.rs:443</code>, <code>spring.io:443</code>, <code>static.crates.io:443</code>, <code>static.rust-lang.org:443</code>, <code>sum.golang.org:443</code>, <code>swift.org:443</code>, <code>tuf-repo-cdn.sigstore.dev:443</code>, <code>yarnpkg.com:443</code>, <code>ziglang.org:443</code></td></tr><tr><td style="text-align:left">default code and containers</td><td style="text-align:left"><code>**.business.githubcopilot.com:443</code>, <code>**.docker.com:443</code>, <code>**.docker.io:443</code>, <code>**.gcr.io:443</code>, <code>**.github.com:443</code>, <code>**.githubusercontent.com:443</code>, <code>**.gitlab.com:443</code>, <code>**.production.cloudflare.docker.com:443</code>, <code>bitbucket.org:443</code>, <code>dhi.io:443</code>, <code>docker-images-prod.6aa30f8b08e16409b46e0173d6de2f56.r2.cloudflarestorage.com:443</code>, <code>docker.com:443</code>, <code>docker.io:443</code>, <code>gcr.io:443</code>, <code>ghcr.io:443</code>, <code>github.com:443</code>, <code>gitlab.com:443</code>, <code>k8s.io:443</code>, <code>launchpad.net:443</code>, <code>mcr.microsoft.com:443</code>, <code>ppa.launchpad.net:443</code>, <code>production.cloudflare.docker.com:443</code>, <code>public.ecr.aws:443</code>, <code>quay.io:443</code>, <code>registry.k8s.io:443</code>, <code>sourceforge.net:443</code></td></tr><tr><td style="text-align:left">default cloud infrastructure</td><td style="text-align:left"><code>**.amazonaws.com:443</code>, <code>**.googleapis.com:443</code>, <code>**.googleusercontent.com:443</code>, <code>**.gstatic.com:443</code>, <code>**.gvt1.com:443</code>, <code>**.public.blob.vercel-storage.com:443</code>, <code>**.visualstudio.com:443</code>, <code>apis.google.com:443</code>, <code>app.daytona.io:443</code>, <code>azure.com:443</code>, <code>binaries.prisma.sh:443</code>, <code>challenges.cloudflare.com:443</code>, <code>clerk.com:443</code>, <code>csp.withgoogle.com:443</code>, <code>dev.azure.com:443</code>, <code>dl.google.com:443</code>, <code>fastly.com:443</code>, <code>figma.com:443</code>, <code>hashicorp.com:443</code>, <code>jsdelivr.net:443</code>, <code>json-schema.org:443</code>, <code>json.schemastore.org:443</code>, <code>login.microsoftonline.com:443</code>, <code>mise-versions.jdx.dev:443</code>, <code>mise.run:443</code>, <code>packages.microsoft.com:443</code>, <code>play.google.com:443</code>, <code>playwright.azureedge.net:443</code>, <code>supabase.com:443</code>, <code>unpkg.com:443</code>, <code>vercel.com:443</code>, <code>visualstudio.com:443</code>, <code>www.google.com:443</code></td></tr><tr><td style="text-align:left">default os packages</td><td style="text-align:left"><code>**.debian.org:443</code>, <code>alpinelinux.org:443</code>, <code>apt.llvm.org:443</code>, <code>archive.ubuntu.com:443</code>, <code>archlinux.org:443</code>, <code>centos.org:443</code>, <code>debian.org:443</code>, <code>dl-cdn.alpinelinux.org:443</code>, <code>fedoraproject.org:443</code>, <code>packagecloud.io:443</code>, <code>ports.ubuntu.com:443</code>, <code>ports.ubuntu.com:80</code>, <code>security.ubuntu.com:443</code>, <code>ubuntu.com:443</code></td></tr></tbody></table> <p>As you can see, this includes pretty much everything you might need for building applications, but it's notably missing things like documentation sites, so if your agent needs to go out to Microsoft learn (for example), it's going to be stuck. I think that could be a big gap in the balanced mode, so I switched to "open" mode instead by running <code>sbx policy reset</code> and choosing again.</p> <h3 id="creating-a-sandbox" class="heading-with-anchor"><a href="https://andrewlock.net/running-ai-agents-safely-in-a-microvm-using-docker-sandbox/#creating-a-sandbox" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Creating a sandbox</a></h3> <p>Once you've chosen your network policy, you can create your first sandbox. Navigate to your project folder, and run <code>sbx run claude</code>:</p> <div class="pre-code-wrapper"><pre class="language-powershell"><code class="language-powershell">cd <span class="token punctuation">.</span>\NetEscapades<span class="token punctuation">.</span>EnumGenerators
sbx run claude
</code></pre></div> <p>This downloads a docker image for the selected agent, and creates a sandbox named after the current working directory. Once downloaded, <code>sbx</code> uses the image to spin up a microVM and runs your agent of choice in YOLO/dangerously skip permissions mode:</p> <div class="pre-code-wrapper"><pre class="language-powershell"><code class="language-powershell">Creating new sandbox <span class="token string">'claude-NetEscapades.EnumGenerators'</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span>
aeacf85cf4c8: Download complete
4f33085e2ac1: Download complete
6b4ac13f7bd1: Download complete
Digest: sha256:aeacf85cf4c8e40f5d1a3709ed7f2a7f466f78787e56780ec321f0db6bc1a53a
Status: Downloaded newer image <span class="token keyword">for</span> docker/sandbox-templates:claude-code
✓ Created sandbox <span class="token string">'claude-NetEscapades.EnumGenerators'</span>
  Workspace: D:\repos\oss\NetEscapades<span class="token punctuation">.</span>EnumGenerators <span class="token punctuation">(</span>direct <span class="token function">mount</span><span class="token punctuation">)</span>
  Agent: claude

To connect to this sandbox<span class="token punctuation">,</span> run:
  sbx run claude-NetEscapades<span class="token punctuation">.</span>EnumGenerators

Starting claude agent in sandbox <span class="token string">'claude-NetEscapades.EnumGenerators'</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span>
Workspace: D:\repos\oss\NetEscapades<span class="token punctuation">.</span>EnumGenerators
 ▐▛███▜▌   Claude Code v2<span class="token punctuation">.</span>1<span class="token punctuation">.</span>90
▝▜█████▛▘  Sonnet 4<span class="token punctuation">.</span>6 · API Usage Billing
  ▘▘ ▝▝    <span class="token operator">/</span>d/repos/oss/NetEscapades<span class="token punctuation">.</span>EnumGenerators

  ↑ Opus now defaults to 1M context · 5x more room<span class="token punctuation">,</span> same pricing

───────────────────────────────────────────────────────────────────────────────────────────
❯ 
───────────────────────────────────────────────────────────────────────────────────────────
  ⏵⏵ bypass permissions on <span class="token punctuation">(</span>shift+tab to cycle<span class="token punctuation">)</span>
</code></pre></div> <p>And you're off to the races! You can hack away and know that the sandbox <em>only</em> has access to your working directory, so yes, it could delete your git repo (and there are ways to avoid that too), but that's basically the extent of the damage it can do.</p> <blockquote> <p>Docker sandboxes <a href="https://docs.docker.com/ai/sandboxes/agents/">currently have support</a> for Claude Code, Codex, Copilot, Gemini, Kiro, OpenCode, and Docker Agent.</p> </blockquote> <p>You can now set Claude to work, and it runs in <code>--dangerously-skip-permissions</code> mode, without needing to prompt you everytime it needs to use a tool. So at this point, you probably need to review and push/reject those changes. So it's worth thinking about how git works with <code>sbx</code>.</p> <h3 id="committing-changes-to-a-git-repository" class="heading-with-anchor"><a href="https://andrewlock.net/running-ai-agents-safely-in-a-microvm-using-docker-sandbox/#committing-changes-to-a-git-repository" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Committing changes to a git repository</a></h3> <p>There's basically 2 ways you can use <code>sbx</code> sandboxes:</p> <ul><li>Direct mode</li> <li>Branch mode</li></ul> <p>In <em>direct mode</em>, the agent just edits files in your working directory, and commits directly to the git repository in that directory. This is the easiest to use and understand, but be aware that it has access to the <em>whole</em> git history, so technically the agent <em>could</em> end up breaking your git repo. I've never seen it, but it's important to be aware it <em>could</em> happen😅</p> <p>In <em>branch mode</em>, the <code>sbx</code> sandbox <a href="https://andrewlock.net/working-on-two-git-branches-at-once-with-git-worktree/">creates a git worktree</a> in a <code>.sbx/</code> sub-folder in your root directory, and starts the agent in that sub-folder. The agent still has <em>access</em> to the root directory, but it means you can continue to work in the "main" working directory, or you could start additional agents working in <em>other</em> worktrees.</p> <p>To start a sandbox in branch mode, pass the <code>--branch</code> flag. For example:</p> <div class="pre-code-wrapper"><pre class="language-bash"><code class="language-bash"><span class="token comment"># agent creates a worktree at .sbx/&lt;sandbox-name&gt;-worktrees/my-feature</span>
sbx run claude <span class="token parameter variable">--branch</span> my-feature

<span class="token comment"># agent generates its own name for the branch + worktree</span>
sbx run claude <span class="token parameter variable">--branch</span> auto
</code></pre></div> <p>Now, it's important to note that this creates the folder <em>inside</em> your git working directory:</p> <div class="pre-code-wrapper"><pre class="language-bash"><code class="language-bash">├── .sbx/
│   └── claude-NetEscapades.EnumGenerators-worktrees/
│       └── my-feature/
│           ├── build/
│           ├── docs/
│           ├── samples/
│           ├── src/
│           │   ├── NetEscapades.EnumGenerators/
│           │   ├── NetEscapades.EnumGenerators.Attributes/
│           │   ├── NetEscapades.EnumGenerators.Generators/
│           │   ├── NetEscapades.EnumGenerators.Interceptors/
│           │   ├── NetEscapades.EnumGenerators.Interceptors.Attributes/
│           │   └── NetEscapades.EnumGenerators.RuntimeDependencies/
│           ├── tests/
│           │   ├── NetEscapades.EnumGenerators.Tests/
│           │   ├── NetEscapades.EnumGenerators.IntegrationTests/
│           │   ├── NetEscapades.EnumGenerators.Interceptors.IntegrationTests/
│           │   ├── NetEscapades.EnumGenerators.Benchmarks/
│           └── NetEscapades.EnumGenerators.sln
├── build/
├── docs/
├── samples/
├── src/
│   ├── NetEscapades.EnumGenerators/
│   ├── NetEscapades.EnumGenerators.Attributes/
│   ├── NetEscapades.EnumGenerators.Generators/
│   ├── NetEscapades.EnumGenerators.Interceptors/
│   ├── NetEscapades.EnumGenerators.Interceptors.Attributes/
│   └── NetEscapades.EnumGenerators.RuntimeDependencies/
├── tests/
│   ├── NetEscapades.EnumGenerators.Tests/
│   ├── NetEscapades.EnumGenerators.IntegrationTests/
│   ├── NetEscapades.EnumGenerators.Interceptors.IntegrationTests/
│   ├── NetEscapades.EnumGenerators.Benchmarks/
└── NetEscapades.EnumGenerators.sln
</code></pre></div> <p>That's a bit of a pain in general, because that <em>whole</em> working directory shows up in the git diff:</p> <div class="pre-code-wrapper"><pre class="language-bash"><code class="language-bash">❯ <span class="token function">git</span> status
On branch my-feature
Untracked files:
  <span class="token punctuation">(</span>use <span class="token string">"git add &lt;file&gt;..."</span> to include <span class="token keyword">in</span> what will be committed<span class="token punctuation">)</span>
        .sbx/

nothing added to commit but untracked files present <span class="token punctuation">(</span>use <span class="token string">"git add"</span> to track<span class="token punctuation">)</span>
</code></pre></div> <p>That means you need to add this directory to your project's <em>.gitignore</em> file. <em>Or</em>, a neater way, is to add the folder to the gitignore <em>globally</em> on your machine. The following PowerShell script reads <a href="https://git-scm.com/docs/gitignore">the <code>core.excludesFile</code> setting</a> (if it's set) and either adds the <code>.sbx/</code> folder to this file, or adds it to the default location at <em>$HOME/.config/git/ignore</em>.</p> <div class="pre-code-wrapper"><pre class="language-powershell"><code class="language-powershell"><span class="token comment"># Get the path to the default ignore file</span>
<span class="token variable">$path</span> = git config <span class="token operator">--</span>global core<span class="token punctuation">.</span>excludesFile
<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">-not</span> <span class="token variable">$path</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token variable">$path</span> = <span class="token string">"<span class="token variable">$HOME</span>/.config/git/ignore"</span> <span class="token punctuation">}</span>

<span class="token comment"># Create the parent directory</span>
<span class="token function">New-Item</span> <span class="token operator">-</span>ItemType Directory <span class="token operator">-</span>Force <span class="token operator">-</span>Path <span class="token punctuation">(</span><span class="token function">Split-Path</span> <span class="token variable">$path</span><span class="token punctuation">)</span> <span class="token punctuation">|</span> <span class="token function">Out-Null</span>

<span class="token comment"># Add .sbx/ to the file</span>
<span class="token function">Add-Content</span> <span class="token operator">-</span>Path <span class="token variable">$path</span> <span class="token operator">-</span>Value <span class="token string">".sbx/"</span>
</code></pre></div> <p>This seems to work pretty well, but again, be aware that the agent <em>could</em> still screw up your git directory, because it fundamentally has access to it. So make sure you have a backup (e.g. you've pushed to a remote repository), just in case. Or alternatively, work on an entirely separate clone of the repo.</p> <blockquote> <p>⚠️ One git workflow that <em>won't</em> work is creating a worktree yourself, and then running a sandbox directly in this folder. In this scenario, the agent doesn't have access to the "parent" git repository, so it won't be able to commit any changes, which is a great way to confuse both it and you 😅.</p> </blockquote> <p>I mentioned earlier that Docker Sandboxes don't just run docker containers, they run in microVMs. However, that also means you can't get an overview of your sandboxes using docker or docker desktop. So how do you know what's going on with your sandboxes?</p> <h3 id="getting-an-overview-with-a-tui-dashboard" class="heading-with-anchor"><a href="https://andrewlock.net/running-ai-agents-safely-in-a-microvm-using-docker-sandbox/#getting-an-overview-with-a-tui-dashboard" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Getting an overview with a TUI dashboard</a></h3> <p><code>sbx</code> ships with several commands for viewing and managing sandboxes:</p> <div class="pre-code-wrapper"><pre class="language-bash"><code class="language-bash">❯ sbx <span class="token parameter variable">--help</span>
Docker Sandboxes creates isolated sandbox environments <span class="token keyword">for</span> AI agents, powered by Docker.

Run without a <span class="token builtin class-name">command</span> to launch interactive mode, or pass a <span class="token builtin class-name">command</span> <span class="token keyword">for</span> CLI usage.

Usage:
  sbx.exe
  sbx.exe <span class="token punctuation">[</span>command<span class="token punctuation">]</span>

Available Commands:
  completion  Generate the autocompletion script <span class="token keyword">for</span> the specified shell
  create      Create a sandbox <span class="token keyword">for</span> an agent
  <span class="token builtin class-name">exec</span>        Execute a <span class="token builtin class-name">command</span> inside a sandbox
  <span class="token builtin class-name">help</span>        Help about any <span class="token builtin class-name">command</span>
  login       Sign <span class="token keyword">in</span> to Docker
  <span class="token builtin class-name">logout</span>      Sign out of Docker
  <span class="token function">ls</span>          List sandboxes
  policy      Manage sandbox policies
  ports       Manage sandbox port publishing
  reset       Reset all sandboxes and clean up state
  <span class="token function">rm</span>          Remove one or <span class="token function">more</span> sandboxes
  run         Run an agent <span class="token keyword">in</span> a sandbox
  save        Save a snapshot of the sandbox as a template
  secret      Manage stored secrets
  stop        Stop one or <span class="token function">more</span> sandboxes without removing them
  version     Show Docker Sandboxes version information
</code></pre></div> <p>but there's also a neat "dashboard" view, which you can start by running <code>sbx</code> without any arguments (once you login for the first time):</p> <p><img src="https://andrewlock.net/content/images/2026/sbx_dashboard.png" alt="The sbx dashboard shows the networking requests, memory usage, uptime, and all your sandboxes"></p> <p>This dashboard shows each of your running sandboxes, the resources they're using, the network requests they're making, and the global network rules. It's a neat little TUI you can use to get an overview of your sandboxes!</p> <p>With that, you should have most of the basics ready for working safely with agents in a sandbox! In the next post I'll look at how you can run custom templates in your sandbox instead of the default template, but before we leave, it's worth highlighting some of the limitations.</p> <h2 id="so-what-s-the-catch-" class="heading-with-anchor"><a href="https://andrewlock.net/running-ai-agents-safely-in-a-microvm-using-docker-sandbox/#so-what-s-the-catch-" class="relative text-zinc-800 dark:text-white no-underline hover:underline">So what's the catch?</a></h2> <p>Before the release of <code>sbx</code>, there was a <a href="https://docs.docker.com/ai/sandboxes/docker-desktop/">Docker Desktop based version of Docker Sandbox</a> that worked pretty much the same way as <code>sbx</code> in many ways. But it had a <em>massive</em> limitation; it was limited to using a maximum of 4GB of memory, and was not configurable. For <a href="https://github.com/DataDog/dd-trace-dotnet">large projects</a>, this proved to be a big issue, making it virtually unusable for me. Luckily, that's not the case with <code>sbx</code>, which has a <code>--memory</code> option to control this, and defaults to 50% of host memory.</p> <p>One thing I <em>haven't</em> figured out yet is how to get commit signing work. I use 1Password to sign my commits, which runs an <code>ssh-agent.exe</code> for commit signing. But I haven't worked out how to share that into the sandbox. As a workaround, I've settled for letting the sandbox create unsigned commits. Then, once it's all finished, on the host side I do a simple rebase, which then signs all the commits. It's a bit annoying, but not the end of the world. If you know of a workaround, I'd love to hear about it!</p> <p>Another tricky point is the network policies. It looks like a nice way to limit the blast radius of a rogue agent, but I feel like I'd always be running into limitations, trying to curate the policies. Seems like a useful "organisation policy" level control, but frankly I'm probably just going to run it in open mode. The sandbox ensures the agent can't mess up my system, and as it doesn't have access to any of my keys or private data, I'm not too worried about what sites it tries to access.</p> <p>The final issue is performance, which has, unfortunately, been the deal breaker for me in many cases. Even for simple projects, I've found that the performance hit from running in a sandbox can be crippling. I only recently ran into this issue (I swear it wasn't so much of an issue a couple of weeks ago), so I'm <em>hoping</em> it's something that will be addressed soon😬</p> <h2 id="summary" class="heading-with-anchor"><a href="https://andrewlock.net/running-ai-agents-safely-in-a-microvm-using-docker-sandbox/#summary" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Summary</a></h2> <p>In this post I described how to use the docker sandbox tool <code>sbx</code> to run AI coding agents in a sandbox. Using a sandbox means you can run the tools in <code>yolo</code> or <code>--dangerously-skip-permissions</code> mode, so you don't have to babysit it constantly. I've found this greatly increases velocity, and running in a sandbox removes the sense of uneasiness that I get whenever I choose to live dangerously on my machine! This post describes how to set up the <code>sbx</code> tool, discusses the network policy architecture, and how to commit to git. In the next post I'll describe how to create custom templates, which can be useful if you have specific tools you need installed in the sandbox for the agent to work with.</p> ]]></content:encoded><category><![CDATA[Getting Started;AI;Docker;Installation]]></category></item><item><title><![CDATA[Configuring contextual options with Microsoft.Extensions.Options.Contextual]]></title><description><![CDATA[In this post I take a brief look at the Microsoft.Extensions.Options.Contextual package to understand what it's for, how to use it, and whether to use it or not]]></description><link>https://andrewlock.net/configuring-contextual-options-with-microsoft-extensions-options-contextual/</link><guid isPermaLink="true">https://andrewlock.net/configuring-contextual-options-with-microsoft-extensions-options-contextual/</guid><pubDate>Wed, 01 Apr 2026 10:00:00 GMT</pubDate><dc:creator><![CDATA[Andrew Lock]]></dc:creator><media:content url="https://andrewlock.net/content/images/2026/contextual_options_banner.png" medium="image" /><content:encoded><![CDATA[<img src="https://andrewlock.net/content/images/2026/contextual_options_banner.png" /><p>In this post I take a brief look at the <em>Microsoft.Extensions.Options.Contextual</em> package that I came across the other day. I try to understand the purpose of the package, look at how to install and configure it, and finally consider whether it's something people should consider using themselves.</p> <h2 id="what-is-microsoft-extensions-options-contextual-" class="heading-with-anchor"><a href="https://andrewlock.net/configuring-contextual-options-with-microsoft-extensions-options-contextual/#what-is-microsoft-extensions-options-contextual-" class="relative text-zinc-800 dark:text-white no-underline hover:underline">What is Microsoft.Extensions.Options.Contextual?</a></h2> <p>I was browsing around in <a href="https://github.com/dotnet">the dotnet repositories</a> for something the other day, when <a href="https://github.com/dotnet/extensions/tree/main/src/Libraries/Microsoft.Extensions.Options.Contextual">I spotted this library: Microsoft.Extensions.Options.Contextual</a>. I was somewhat intrigued considering I hadn't heard this library mentioned before, or had seen it used. What's more, a little probing seemed to suggest it's not used by any <em>other</em> first party .NET libraries either. So what is it for, and how does it work?</p> <p><a href="https://github.com/dotnet/extensions/tree/4e8e9a258918dacf17a5dd033f9a01a23fd6692d/src/Libraries/Microsoft.Extensions.Options.Contextual">According to the library itself</a>, the library provides:</p> <blockquote> <p>APIs for dynamically configuring options based on a given context</p> </blockquote> <p>That's pretty vague 😅 What are "options" and what is "context" here? It's likely easiest to understand with an example, even if it's a simple one. The example below is based on <a href="https://github.com/dotnet/extensions/tree/main/src/Libraries/Microsoft.Extensions.Options.Contextual">the documentation in the package</a>, using the classic "weather forecast" scenario.</p> <h2 id="configuring-options-based-on-another-type" class="heading-with-anchor"><a href="https://andrewlock.net/configuring-contextual-options-with-microsoft-extensions-options-contextual/#configuring-options-based-on-another-type" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Configuring options based on another type</a></h2> <p>First of all, let's imagine we have some configuration options:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">internal</span> <span class="token keyword">class</span> <span class="token class-name">WeatherForecastOptions</span>
<span class="token punctuation">{</span>
    <span class="token keyword">public</span> <span class="token return-type class-name"><span class="token keyword">string</span></span> TemperatureScale <span class="token punctuation">{</span> <span class="token keyword">get</span><span class="token punctuation">;</span> <span class="token keyword">set</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token string">"Celsius"</span><span class="token punctuation">;</span> <span class="token comment">// Celsius or Fahrenheit</span>
    <span class="token keyword">public</span> <span class="token return-type class-name"><span class="token keyword">int</span></span> ForecastDays <span class="token punctuation">{</span> <span class="token keyword">get</span><span class="token punctuation">;</span> <span class="token keyword">set</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>This is pretty standard stuff—which unit to use for temperature display, and how many days to include in the forecast.</p> <p>The interesting thing is <em>how</em> these options are configured. If you're doing global configuration for your app then you might have something like this (using <code>Configure()</code> and/or <code>AddOptions()</code>):</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token class-name"><span class="token keyword">var</span></span> builder <span class="token operator">=</span> WebApplication<span class="token punctuation">.</span><span class="token function">CreateBuilder</span><span class="token punctuation">(</span>args<span class="token punctuation">)</span><span class="token punctuation">;</span>

builder<span class="token punctuation">.</span>Services
    <span class="token punctuation">.</span><span class="token generic-method"><span class="token function">AddOptions</span><span class="token generic class-name"><span class="token punctuation">&lt;</span>WeatherForecastOptions<span class="token punctuation">&gt;</span></span></span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token comment">// Register the options</span>
    <span class="token punctuation">.</span><span class="token function">Configure</span><span class="token punctuation">(</span>x <span class="token operator">=&gt;</span> x<span class="token punctuation">.</span>ForecastDays <span class="token operator">=</span> <span class="token number">7</span><span class="token punctuation">)</span> <span class="token comment">// Configure in Code</span>
    <span class="token punctuation">.</span><span class="token function">BindConfiguration</span><span class="token punctuation">(</span>builder<span class="token punctuation">.</span>Configuration<span class="token punctuation">[</span><span class="token string">"Weather"</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// Bind to configuration</span>
</code></pre></div> <p>That's all well and good, but you're fundamentally "globally" configuring the <code>WeatherForecastOptions</code> instances (whether you're using singleton, transient, or scoped instances).</p> <blockquote> <p>I'm not going to get into the general <code>IOptions&lt;&gt;</code> abstraction debate here. Many people rail against it, and I sympathize. That said, if you don't like <em>those</em> abstractions, I don't know that you'll like the ones we're introducing shortly 😉.</p> </blockquote> <p>What if, instead, you want to configure an options object arbitrarily based on some context object? For example, imagine you want to change the <code>TemperatureScale</code> based on the country associated with the current user. This "context" might be encapsulated in some <code>Appcontext</code> object:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">internal</span> <span class="token keyword">class</span> <span class="token class-name">AppContext</span>
<span class="token punctuation">{</span>
    <span class="token keyword">public</span> <span class="token return-type class-name">Guid</span> UserId <span class="token punctuation">{</span> <span class="token keyword">get</span><span class="token punctuation">;</span> <span class="token keyword">set</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
    <span class="token keyword">public</span> <span class="token return-type class-name"><span class="token keyword">string</span><span class="token punctuation">?</span></span> Country <span class="token punctuation">{</span> <span class="token keyword">get</span><span class="token punctuation">;</span> <span class="token keyword">set</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div> <blockquote> <p>Note that this is a different use case to <a href="https://andrewlock.net/configuring-named-options-using-iconfigurenamedoptions-and-configureall/"><em>named options</em></a> where you have a distinct named set of "global" options. For <em>contextual</em> options we explicitly configure the app based on the provided context.</p> </blockquote> <p>For example, at the call site, contextual options would look something like this:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token comment">// Get an IContextualOptions&lt;TOptions, TContext&gt; from DI</span>
<span class="token class-name">IContextualOptions<span class="token punctuation">&lt;</span>WeatherForecastOptions<span class="token punctuation">,</span> AppContext<span class="token punctuation">&gt;</span></span> _contextualOptions<span class="token punctuation">;</span>

<span class="token class-name">AppContext</span> context<span class="token punctuation">;</span> <span class="token comment">// Get an instance of the context object from somewhere</span>

<span class="token comment">// Get an instance of WeatherForecastOptions using the AppContext instance</span>
<span class="token class-name">WeatherForecastOptions</span> options <span class="token operator">=</span> <span class="token keyword">await</span> _contextualOptions<span class="token punctuation">.</span><span class="token function">GetAsync</span><span class="token punctuation">(</span>context<span class="token punctuation">,</span> <span class="token named-parameter punctuation">cancellationToken</span><span class="token punctuation">:</span> <span class="token keyword">default</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre></div> <p>The <code>IContextualOptions&lt;&gt;</code> instance is available in DI when you set up contextual options, and the <code>AppContext</code> is whatever you want to use to control <em>how</em> your options object is created. You then pass one to the other, and voila, you have a configured contextual options object, with properties set based on <code>AppContext</code>.</p> <p>Of course, there's a lot more to it behind the scenes, so we'll look at how to get started with this in the next section.</p> <h2 id="adding-the-microsoft-extensions-options-contextual-package" class="heading-with-anchor"><a href="https://andrewlock.net/configuring-contextual-options-with-microsoft-extensions-options-contextual/#adding-the-microsoft-extensions-options-contextual-package" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Adding the <em>Microsoft.Extensions.Options.Contextual</em> package</a></h2> <p>To get started with contextual option, you need to add the <em>Microsoft.Extensions.Options.Contextual</em> package to your project. Interestingly, it appears that this package has never made it out of preview:</p> <p><img src="https://andrewlock.net/content/images/2026/contextual_options.png" alt="The available Microsoft.Extensions.Options.Contextual package versions"></p> <p>This means you must use the <code>--prerelease</code> flag when adding the package to your project with the .NET CLI:</p> <div class="pre-code-wrapper"><pre class="language-bash"><code class="language-bash">dotnet <span class="token function">add</span> package Microsoft.Extensions.Options.Contextual <span class="token parameter variable">--prerelease</span>
</code></pre></div> <p>This adds the latest version to your project file:</p> <div class="pre-code-wrapper"><pre class="language-xml"><code class="language-xml"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>Project</span> <span class="token attr-name">Sdk</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>Microsoft.NET.Sdk.Web<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>

  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PropertyGroup</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>TargetFramework</span><span class="token punctuation">&gt;</span></span>net10.0<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>TargetFramework</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>Nullable</span><span class="token punctuation">&gt;</span></span>enable<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>Nullable</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>ImplicitUsings</span><span class="token punctuation">&gt;</span></span>enable<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>ImplicitUsings</span><span class="token punctuation">&gt;</span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>PropertyGroup</span><span class="token punctuation">&gt;</span></span>

  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>ItemGroup</span><span class="token punctuation">&gt;</span></span>
    <span class="token comment">&lt;!-- Add this 👇 --&gt;</span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PackageReference</span> <span class="token attr-name">Include</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>Microsoft.Extensions.Options.Contextual<span class="token punctuation">"</span></span> <span class="token attr-name">Version</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>10.4.0-preview.1.26160.2<span class="token punctuation">"</span></span> <span class="token punctuation">/&gt;</span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>ItemGroup</span><span class="token punctuation">&gt;</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>Project</span><span class="token punctuation">&gt;</span></span>
</code></pre></div> <p>The latest version of the package supports .NET Framework and .NET 8+.</p> <p>Once you have the package installed, you can start configuring your options and context.</p> <h2 id="configuring-the-contextual-options" class="heading-with-anchor"><a href="https://andrewlock.net/configuring-contextual-options-with-microsoft-extensions-options-contextual/#configuring-the-contextual-options" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Configuring the contextual options</a></h2> <p>Once we have the package installed, we need to configure all the moving parts:</p> <ul><li>Add <code>[OptionsContext]</code> to your context object, to drive a source generator</li> <li>Define an <code>IOptionsContextReceiver</code> object for extracting value from the context</li> <li>Define how to take the extracted context values and apply them to your options object</li></ul> <p>It likely isn't clear why you need all these components at this points, but just go with it for now. It'll become clearer how they all fit together later.</p> <h3 id="adding-the-contextualoptionsgenerator" class="heading-with-anchor"><a href="https://andrewlock.net/configuring-contextual-options-with-microsoft-extensions-options-contextual/#adding-the-contextualoptionsgenerator" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Adding the <code>ContextualOptionsGenerator</code></a></h3> <p>The first step is to mark your context object <code>partial</code> and add the <code>[OptionsContext]</code> attribute:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">OptionsContext</span></span><span class="token punctuation">]</span> <span class="token comment">// 👈 Add attribute, and make the type partial</span>
<span class="token keyword">internal</span> <span class="token keyword">partial</span> <span class="token keyword">class</span> <span class="token class-name">AppContext</span>
<span class="token punctuation">{</span>
    <span class="token keyword">public</span> <span class="token return-type class-name">Guid</span> UserId <span class="token punctuation">{</span> <span class="token keyword">get</span><span class="token punctuation">;</span> <span class="token keyword">set</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
    <span class="token keyword">public</span> <span class="token return-type class-name"><span class="token keyword">string</span><span class="token punctuation">?</span></span> Country <span class="token punctuation">{</span> <span class="token keyword">get</span><span class="token punctuation">;</span> <span class="token keyword">set</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div> <blockquote> <p>I've used a mutable class for <code>AppContext</code> in the example above, but you could also make it a <code>struct</code>, and (ideally) make it <code>readonly</code> too.</p> </blockquote> <p>The <code>[OptionsContext]</code> object drives a source generator, the <code>ContextualOptionsGenerator</code>, which generates a separate partial class, which looks a little like this:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token comment">// &lt;auto-generated/&gt;</span>
<span class="token keyword">using</span> <span class="token namespace">Microsoft<span class="token punctuation">.</span>Extensions<span class="token punctuation">.</span>Options<span class="token punctuation">.</span>Contextual</span><span class="token punctuation">;</span>

<span class="token keyword">partial</span> <span class="token keyword">class</span> <span class="token class-name">MyAppContext</span> <span class="token punctuation">:</span> <span class="token type-list"><span class="token class-name">IOptionsContext</span></span>
<span class="token punctuation">{</span>
    <span class="token return-type class-name"><span class="token keyword">void</span></span> IOptionsContext<span class="token punctuation">.</span><span class="token generic-method"><span class="token function">PopulateReceiver</span><span class="token generic class-name"><span class="token punctuation">&lt;</span>T<span class="token punctuation">&gt;</span></span></span><span class="token punctuation">(</span><span class="token class-name">T</span> receiver<span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        receiver<span class="token punctuation">.</span><span class="token function">Receive</span><span class="token punctuation">(</span><span class="token keyword">nameof</span><span class="token punctuation">(</span>Country<span class="token punctuation">)</span><span class="token punctuation">,</span> Country<span class="token punctuation">)</span><span class="token punctuation">;</span>
        receiver<span class="token punctuation">.</span><span class="token function">Receive</span><span class="token punctuation">(</span><span class="token keyword">nameof</span><span class="token punctuation">(</span>UserId<span class="token punctuation">)</span><span class="token punctuation">,</span> UserId<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>This makes your context type implement <code>IOptionsContext</code>:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">public</span> <span class="token keyword">interface</span> <span class="token class-name">IOptionsContext</span>
<span class="token punctuation">{</span>
    <span class="token return-type class-name"><span class="token keyword">void</span></span> <span class="token generic-method"><span class="token function">PopulateReceiver</span><span class="token generic class-name"><span class="token punctuation">&lt;</span>T<span class="token punctuation">&gt;</span></span></span><span class="token punctuation">(</span><span class="token class-name">T</span> receiver<span class="token punctuation">)</span>
        <span class="token keyword">where</span> <span class="token class-name">T</span> <span class="token punctuation">:</span> <span class="token type-list"><span class="token class-name">IOptionsContextReceiver</span></span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>which interacts with:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">public</span> <span class="token keyword">interface</span> <span class="token class-name">IOptionsContextReceiver</span>
<span class="token punctuation">{</span>
    <span class="token return-type class-name"><span class="token keyword">void</span></span> <span class="token generic-method"><span class="token function">Receive</span><span class="token generic class-name"><span class="token punctuation">&lt;</span>T<span class="token punctuation">&gt;</span></span></span><span class="token punctuation">(</span><span class="token class-name"><span class="token keyword">string</span></span> key<span class="token punctuation">,</span> <span class="token class-name">T</span> <span class="token keyword">value</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>O…K… so what has that got us? 😅 Effectively the source generator provides a way to extract properties from a type by their name, and pass them to a "receiver" type. So now we'll implement that receiver.</p> <h3 id="implementing-an-ioptionscontextreceiver" class="heading-with-anchor"><a href="https://andrewlock.net/configuring-contextual-options-with-microsoft-extensions-options-contextual/#implementing-an-ioptionscontextreceiver" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Implementing an <code>IOptionsContextReceiver</code></a></h3> <p>Implementing an <code>IOptionsContextReceiver</code> for your target is pretty simple; just create a type that implements the interface, and which can receive any type of <code>T</code> value. as you saw above, this receiver will be invoked with each of the properties of the "context" object, so the idea is to simply extract the keys that you care about in your context.</p> <p>In the following example, we only care about the <code>"Country"</code> key, so that's the value we extract. I've done a little further manipulation of the value in this example, converting the <code>"Country"</code> into an assumed default temperature unit:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token comment">// Implement IOptionsContextReceiver to receive the context values</span>
<span class="token keyword">internal</span> <span class="token keyword">class</span> <span class="token class-name">CountryTemperatureReceiver</span> <span class="token punctuation">:</span> <span class="token type-list"><span class="token class-name">IOptionsContextReceiver</span></span>
<span class="token punctuation">{</span>
    <span class="token comment">// This property exposes the "extracted" values to others</span>
    <span class="token keyword">public</span> <span class="token return-type class-name"><span class="token keyword">string</span><span class="token punctuation">?</span></span> DefaultUnit <span class="token punctuation">{</span> <span class="token keyword">get</span><span class="token punctuation">;</span> <span class="token keyword">private</span> <span class="token keyword">set</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>

    <span class="token comment">// The receive implementation could receive a T of any type</span>
    <span class="token keyword">public</span> <span class="token return-type class-name"><span class="token keyword">void</span></span> <span class="token generic-method"><span class="token function">Receive</span><span class="token generic class-name"><span class="token punctuation">&lt;</span>T<span class="token punctuation">&gt;</span></span></span><span class="token punctuation">(</span><span class="token class-name"><span class="token keyword">string</span></span> key<span class="token punctuation">,</span> <span class="token class-name">T</span> <span class="token keyword">value</span><span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        <span class="token comment">// When you receive the key you're looking for...</span>
        <span class="token keyword">if</span> <span class="token punctuation">(</span>key <span class="token operator">==</span> <span class="token string">"Country"</span><span class="token punctuation">)</span>
        <span class="token punctuation">{</span>
            <span class="token comment">// ... extract the value and store it</span>
            DefaultUnit <span class="token operator">=</span> <span class="token keyword">value</span> <span class="token keyword">is</span> <span class="token string">"USA"</span> <span class="token punctuation">?</span> <span class="token string">"Fahrenheit"</span> <span class="token punctuation">:</span> <span class="token string">"Celsius"</span><span class="token punctuation">;</span>
        <span class="token punctuation">}</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>Ok, we're almost there, we have:</p> <ul><li>Our "context" object</li> <li>A source generated <code>IOptionsContext</code> which populates a "receiver" based on the context object</li> <li>Our "receiver" object</li> <li>Our "target" options that we want to configure</li></ul> <p>Now it's time to put it all together</p> <h3 id="configuring-the-weatherforecastoptions" class="heading-with-anchor"><a href="https://andrewlock.net/configuring-contextual-options-with-microsoft-extensions-options-contextual/#configuring-the-weatherforecastoptions" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Configuring the <code>WeatherForecastOptions</code></a></h3> <p>We've almost got all the basic pieces, all that remains is to set up the configuration of our options object, <code>WeatherForecastOptions</code>. We can do this partially using "normal" options configuration, by providing lambdas or binding to configuration, and partially using our new "contextual" configuration options which serves as a contextual "loader":</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token comment">// Create a standard .NET app builder (in this case a web app)</span>
<span class="token class-name"><span class="token keyword">var</span></span> builder <span class="token operator">=</span> WebApplication<span class="token punctuation">.</span><span class="token function">CreateBuilder</span><span class="token punctuation">(</span>args<span class="token punctuation">)</span><span class="token punctuation">;</span>

builder<span class="token punctuation">.</span>Services
    <span class="token punctuation">.</span><span class="token generic-method"><span class="token function">Configure</span><span class="token generic class-name"><span class="token punctuation">&lt;</span>WeatherForecastOptions<span class="token punctuation">&gt;</span></span></span><span class="token punctuation">(</span>x <span class="token operator">=&gt;</span> x<span class="token punctuation">.</span>ForecastDays <span class="token operator">=</span> <span class="token number">7</span><span class="token punctuation">)</span> <span class="token comment">// Configure in Code</span>
    <span class="token punctuation">.</span><span class="token function">Configure</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token class-name">IOptionsContext</span> ctx<span class="token punctuation">,</span> <span class="token class-name">WeatherForecastOptions</span> opts<span class="token punctuation">)</span> <span class="token operator">=&gt;</span> <span class="token comment">// Configure contextual options</span>
    <span class="token punctuation">{</span>
        <span class="token comment">// 1. Create an instance of the receiver </span>
        <span class="token class-name"><span class="token keyword">var</span></span> receiver <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token constructor-invocation class-name">CountryTemperatureReceiver</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

        <span class="token comment">// 2. Populate the receiver based on the context</span>
        ctx<span class="token punctuation">.</span><span class="token function">PopulateReceiver</span><span class="token punctuation">(</span>receiver<span class="token punctuation">)</span><span class="token punctuation">;</span>

        <span class="token comment">// 3. Update the options based on the receiver's values</span>
        <span class="token keyword">if</span> <span class="token punctuation">(</span>receiver<span class="token punctuation">.</span>DefaultUnit <span class="token keyword">is</span> <span class="token keyword">not</span> <span class="token keyword">null</span><span class="token punctuation">)</span>
        <span class="token punctuation">{</span>
            opts<span class="token punctuation">.</span>TemperatureScale <span class="token operator">=</span> receiver<span class="token punctuation">.</span>DefaultUnit<span class="token punctuation">;</span>
        <span class="token punctuation">}</span>
    <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre></div> <p>With the code above we've now configured an <code>WeatherForecastOptions</code> object, based on some unknown <code>IOptionsContext</code>, via the <code>CountryTemperatureReceiver</code>. This code is invoked as needs be, using the provided <code>IOptionsContext</code> instance, by the <code>IContextualOptions</code> implementation.</p> <h3 id="retrieving-a-contextual-weatherforecastoptions-object" class="heading-with-anchor"><a href="https://andrewlock.net/configuring-contextual-options-with-microsoft-extensions-options-contextual/#retrieving-a-contextual-weatherforecastoptions-object" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Retrieving a contextual <code>WeatherForecastOptions</code> object</a></h3> <p>Finally, we have all the pieces in place. All that remains is to use the <code>IContextualOptions</code> object, as we saw earlier:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token comment">// Get an IContextualOptions&lt;TOptions, TContext&gt; from DI</span>
<span class="token class-name">IContextualOptions<span class="token punctuation">&lt;</span>WeatherForecastOptions<span class="token punctuation">,</span> AppContext<span class="token punctuation">&gt;</span></span> _contextualOptions<span class="token punctuation">;</span>

<span class="token class-name">AppContext</span> context<span class="token punctuation">;</span> <span class="token comment">// Get an instance of the context object from somewhere</span>

<span class="token comment">// Get an instance of WeatherForecastOptions using the AppContext instance</span>
<span class="token class-name">WeatherForecastOptions</span> options <span class="token operator">=</span> <span class="token keyword">await</span> _contextualOptions<span class="token punctuation">.</span><span class="token function">GetAsync</span><span class="token punctuation">(</span>context<span class="token punctuation">,</span> <span class="token named-parameter punctuation">cancellationToken</span><span class="token punctuation">:</span> <span class="token keyword">default</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre></div> <p>When invoked, the <code>IContextualOptions&lt;&gt;</code> object loads a "globally" configured <code>IOptions&lt;WeatherForecastOptions&gt;</code> instance, and then calls our "loader" function. This uses the <code>IOptionsContext</code> to pass values to our <code>IOptionsReceiver</code> object, and then we use <em>those</em> values to configure the <code>WeatherForecastOptions</code>. Finally, the configured object is returned, configured both via the standard <code>IOptions</code> system and by contextual options.</p> <h2 id="so-what-s-this-for-again-" class="heading-with-anchor"><a href="https://andrewlock.net/configuring-contextual-options-with-microsoft-extensions-options-contextual/#so-what-s-this-for-again-" class="relative text-zinc-800 dark:text-white no-underline hover:underline">So, what's this for again?</a></h2> <p>So, if you're anything like me, you might reading this and thinking…Why? Why go to all that length?</p> <p>On first blush it looks a <em>little</em> bit like "loose coupling". <em>Sure</em>, you <em>could</em> set the default <code>WeatherForecastOptions</code> based on the properties of the <code>AppContext</code> <em>directly</em>, but by using the receiver, you're now "loosely coupled".</p> <p>Except, you're not, really, are you?</p> <p>If you had code like this, then it would be very clear that you're directly coupled:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token class-name">WeatherForecastOptions</span> opts <span class="token operator">=</span> <span class="token keyword">new</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token class-name">AppContext</span> ctx <span class="token operator">=</span> <span class="token keyword">new</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token comment">// Configure the options directly based on the AppContext</span>
opts<span class="token punctuation">.</span>TemperatureScale <span class="token operator">=</span> ctx<span class="token punctuation">.</span>Country <span class="token keyword">is</span> <span class="token string">"USA"</span> <span class="token punctuation">?</span> <span class="token string">"Fahrenheit"</span> <span class="token punctuation">:</span> <span class="token string">"Celsius"</span><span class="token punctuation">;</span>
</code></pre></div> <p>But to my eyes, introducing the receiver doesn't <em>really</em> reduce that coupling. It just means that you're now coupled indirectly via the magic string <code>"Country"</code>. If you rename the <code>Country</code> property, then suddenly this breaks.</p> <p>All of which makes me wonder, why bother? Is all this infrastructure <em>really</em> necessary to provide another way to configure an <code>IOptions</code> object? What was the thinking here?</p> <h2 id="feature-flags-theoretically" class="heading-with-anchor"><a href="https://andrewlock.net/configuring-contextual-options-with-microsoft-extensions-options-contextual/#feature-flags-theoretically" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Feature flags, theoretically</a></h2> <p>Luckily, after playing around for a while, I <a href="https://github.com/dotnet/extensions/issues/5049">found an API proposal for introducing these contextual options</a>. But the funny thing was, the API proposal was closed as "not planned" with the API "needs work" 😅 So… what happened here?!</p> <p><img src="https://andrewlock.net/content/images/2026/contextual_options_2.png" alt="API Proposal: Introduce Contextual Options #5049"></p> <p>The proposed API and API usage given in the issue are pretty much exactly what is shipped and currently available, and is pretty much the only example around, i.e. the classic <code>WeatherForecast</code> example.</p> <p>What is revealing is one question raised in the API review meeting:</p> <blockquote> <ul><li>How many IOptionsContextReceiver implementations are there in practice? <ul><li>There's an internal one for Azure, but we don't know of an open-source version of a &gt; service that provides contextual options.</li> <li>LaunchDarkly is a commercial service that could provide contextual options.</li></ul> </li></ul> </blockquote> <p>The fact that they mention LaunchDarkly here is telling. It shows that they were clearly thinking about this as a "feature flags" solution.</p> <p>Except, there's <em>already</em> a feature flags solution from Microsoft, that <a href="https://andrewlock.net/introducing-the-microsoft-featuremanagement-library-adding-feature-flags-to-an-asp-net-core-app-part-1/">I wrote about extensively 7 years ago</a> 😅 That's <em>also</em> based on the .NET configuration system. Now, I haven't really looked at that solution in a <em>loong</em> time, but seeing as <a href="https://www.nuget.org/packages/Microsoft.FeatureManagement">Microsoft.FeatureManagement</a> has about 140 million downloads, I think it's safe to say <em>some</em> people think it does the job. So introducing a sideways approach to do something very similar feels a little strange to me.</p> <blockquote> <p>Right now, I can only see one package that uses the <em>Microsoft.Extensions.Options.Contextual</em> and that <em>is</em> a package for doing feature flagging/experimentation: <a href="https://github.com/excos-platform/config-client">https://github.com/excos-platform/config-client</a></p> </blockquote> <p>In Microsoft's defence, <em>Microsoft.Extensions.Options.Contextual</em> is very much marked as experimental, such that you really have to put some effort in if you actually want to use it.</p> <h2 id="definitely-experimental" class="heading-with-anchor"><a href="https://andrewlock.net/configuring-contextual-options-with-microsoft-extensions-options-contextual/#definitely-experimental" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Definitely experimental</a></h2> <p>To start with, there's no stable version of the <em>Microsoft.Extensions.Options.Contextual</em> package. All versions of the package are marked preview, so you'll need to bear that in mind when you install it.</p> <p>Additionally, all the APIs are also decorated with the <code>[Experimental]</code> attribute, which generate the <code>EXTEXP0018</code> compile-time error. This is similar to the <code>[Obsolete]</code> attribute, but it's applied to new APIs that might change, as opposed to old ones that are deprecated. If you want to use any of the APIs you have to explicitly opt in by using <code>#pragma</code> or other methods to disable the warnings:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token preprocessor property">#<span class="token directive keyword">pragma</span> warning disable EXTEXP0018</span>
</code></pre></div> <p>A particularly irritating aspect is the fact that even the source generated code has this error, and seeing as you can't add <code>#pragma</code> to these APIs, you effectively <em>must</em> disable the warning globally (which kind of defeats the purpose of the attribute a bit in my opinion). The easiest way to disable it is to add it to the <code>NoWarn</code> variable in your <em>.csproj</em> file:</p> <div class="pre-code-wrapper"><pre class="language-xml"><code class="language-xml"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PropertyGroup</span><span class="token punctuation">&gt;</span></span>
  <span class="token comment">&lt;!-- Suppresses "For evaluation purposes only and is subject to change or removal in future updates" --&gt;</span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>NoWarn</span><span class="token punctuation">&gt;</span></span>$(NoWarn);EXTEXP0018<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>NoWarn</span><span class="token punctuation">&gt;</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>PropertyGroup</span><span class="token punctuation">&gt;</span></span>
</code></pre></div> <p>So given all that, is there any value to these APIs, and should you use them?</p> <p>In short, I don't think so. If you're completely bought into the <code>IOptions</code> life, and you <em>specifically</em> have a need for something like that, then, maybe, I guess. But with <a href="https://www.nuget.org/packages/Microsoft.Extensions.Options.Contextual/10.4.0-preview.1.26160.2#versions-body-tab">none of the package versions</a> ever reaching 1000 downloads, it doesn't seem like <em>many</em> people consider it worth it.</p> <p>And if what you really want is feature flagging and/or experimentation, then maybe consider taking a look at <a href="https://openfeature.dev/">OpenFeature</a> instead (something that I might do a post on soon)!</p> <h2 id="summary" class="heading-with-anchor"><a href="https://andrewlock.net/configuring-contextual-options-with-microsoft-extensions-options-contextual/#summary" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Summary</a></h2> <p>In this post I took an introductory look at the <em>Microsoft.Extensions.Options.Contextual</em> package. This package builds on the <code>IOptions</code> abstractions common to .NET. I show the basics of how to use the package, focusing on the partial decoupling it provides between a "context" object and your options object. Ultimately, I'm not convinced that this "decoupling" is worth this extra complexity, so if you have any experience with the package yourself, I'd be interested in your experiences in the comments!</p> ]]></content:encoded><category><![CDATA[Configuration;.NET Core]]></category></item><item><title><![CDATA[Splitting the NetEscapades.EnumGenerators packages: the road to a stable release]]></title><description><![CDATA[In this post I describe the recent architectural changes to the NetEscapades.EnumGenerators package, which is now a metapackage, to support more scenarios]]></description><link>https://andrewlock.net/splitting-the-netescapades-enumgenerators-packages-the-road-to-a-stable-release/</link><guid isPermaLink="true">https://andrewlock.net/splitting-the-netescapades-enumgenerators-packages-the-road-to-a-stable-release/</guid><pubDate>Tue, 10 Mar 2026 09:00:00 GMT</pubDate><dc:creator><![CDATA[Andrew Lock]]></dc:creator><media:content url="https://andrewlock.net/content/images/2026/netescapades_banner.png" medium="image" /><content:encoded><![CDATA[<img src="https://andrewlock.net/content/images/2026/netescapades_banner.png" /><p>In this post I describe some of the significant restructuring to my source generator NuGet package <a href="https://github.com/andrewlock/NetEscapades.EnumGenerators">NetEscapades.EnumGenerators</a> which you can use to add fast methods for working with <code>enum</code>s. I start by describing why the package exists and what you can use it for, then I describe what motivated the restructuring, and finally what the changes are and a call to action.</p> <blockquote> <p>The <strong>tl;dr;</strong> is that there are now <em>three</em> different packages, and exactly which one is best for you depends on what you're trying to do. Check <a href="https://andrewlock.net/splitting-the-netescapades-enumgenerators-packages-the-road-to-a-stable-release/#choosing-the-correct-packages-for-your-scenario">the section below</a> or <a href="https://github.com/andrewlock/NetEscapades.EnumGenerators?tab=readme-ov-file#package-referencing-options">the project's README</a> for details!</p> </blockquote> <p>As an aside, I <em>really</em> want to give this package a stable 1.0.0 release shortly, as I think we've solved most of the corner cases that were bugging me. Which means if the new package structure <em>doesn't</em> work for you, now is the time to <a href="https://github.com/andrewlock/NetEscapades.EnumGenerators/issues">raise an issue</a>, before it gets set in stone!</p> <h2 id="why-should-you-use-an-enum-source-generator-" class="heading-with-anchor"><a href="https://andrewlock.net/splitting-the-netescapades-enumgenerators-packages-the-road-to-a-stable-release/#why-should-you-use-an-enum-source-generator-" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Why should you use an enum source generator?</a></h2> <p><a href="https://github.com/andrewlock/NetEscapades.EnumGenerators">NetEscapades.EnumGenerators</a> was one of the first source generators I created using <a href="https://andrewlock.net/exploring-dotnet-6-part-9-source-generator-updates-incremental-generators/">the incremental generator support introduced in .NET 6</a>. I chose to create this package to work around an annoying characteristic of working with enums: some operations are surprisingly slow.</p> <blockquote> <p>Note that while this has <em>historically</em> been true, this fact won't necessarily remain true forever. In fact, .NET 8+ provided a bunch of improvements to enum handling in the runtime.</p> </blockquote> <p>As an example, let's say you have the following enum:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">public</span> <span class="token keyword">enum</span> <span class="token class-name">Colour</span>
<span class="token punctuation">{</span>
    Red <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">,</span>
    Blue <span class="token operator">=</span> <span class="token number">1</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>At some point, you want to print the name of a <code>Color</code> variable, so you create this helper method:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">public</span> <span class="token return-type class-name"><span class="token keyword">void</span></span> <span class="token function">PrintColour</span><span class="token punctuation">(</span><span class="token class-name">Colour</span> colour<span class="token punctuation">)</span>
<span class="token punctuation">{</span>
    Console<span class="token punctuation">.</span><span class="token function">WriteLine</span><span class="token punctuation">(</span><span class="token string">"You chose "</span><span class="token operator">+</span> colour<span class="token punctuation">.</span><span class="token function">ToString</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// You chose Red</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>While this <em>looks</em> like it should be fast, it's really not. <em>NetEscapades.EnumGenerators</em> works by automatically generating an implementation that <em>is</em> fast. It generates a <code>ToStringFast()</code> method that looks something like this:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token keyword">class</span> <span class="token class-name">ColourExtensions</span>
<span class="token punctuation">{</span>
    <span class="token keyword">public</span> <span class="token return-type class-name"><span class="token keyword">string</span></span> <span class="token function">ToStringFast</span><span class="token punctuation">(</span><span class="token keyword">this</span> <span class="token class-name">Colour</span> colour<span class="token punctuation">)</span>
        <span class="token operator">=&gt;</span> colour <span class="token keyword">switch</span>
        <span class="token punctuation">{</span>
            Colour<span class="token punctuation">.</span>Red <span class="token operator">=&gt;</span> <span class="token keyword">nameof</span><span class="token punctuation">(</span>Colour<span class="token punctuation">.</span>Red<span class="token punctuation">)</span><span class="token punctuation">,</span>
            Colour<span class="token punctuation">.</span>Blue <span class="token operator">=&gt;</span> <span class="token keyword">nameof</span><span class="token punctuation">(</span>Colour<span class="token punctuation">.</span>Blue<span class="token punctuation">)</span><span class="token punctuation">,</span>
            _ <span class="token operator">=&gt;</span> colour<span class="token punctuation">.</span><span class="token function">ToString</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
        <span class="token punctuation">}</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>This simple switch statement checks for each of the known values of <code>Colour</code> and uses <code>nameof</code> to return the textual representation of the <code>enum</code>. If it's an unknown value, then it falls back to the built-in <code>ToString()</code> implementation to ensure correct handling of unknown values (for example this is valid C#: <code>PrintColour((Colour)123)</code>).</p> <p>If we compare these two implementations using <a href="https://benchmarkdotnet.org/">BenchmarkDotNet</a> for a known colour, you can see how much faster <code>ToStringFast()</code> implementation is:</p> <table><thead><tr><th>Method</th><th>FX</th><th style="text-align:right">Mean</th><th style="text-align:right">Error</th><th style="text-align:right">StdDev</th><th style="text-align:right">Ratio</th><th style="text-align:right">Gen 0</th><th style="text-align:right">Allocated</th></tr></thead><tbody><tr><td>ToString</td><td><code>net48</code></td><td style="text-align:right">578.276 ns</td><td style="text-align:right">3.3109 ns</td><td style="text-align:right">3.0970 ns</td><td style="text-align:right">1.000</td><td style="text-align:right">0.0458</td><td style="text-align:right">96 B</td></tr><tr><td>ToStringFast</td><td><code>net48</code></td><td style="text-align:right">3.091 ns</td><td style="text-align:right">0.0567 ns</td><td style="text-align:right">0.0443 ns</td><td style="text-align:right">0.005</td><td style="text-align:right">-</td><td style="text-align:right">-</td></tr><tr><td>ToString</td><td><code>net6.0</code></td><td style="text-align:right">17.985 ns</td><td style="text-align:right">0.1230 ns</td><td style="text-align:right">0.1151 ns</td><td style="text-align:right">1.000</td><td style="text-align:right">0.0115</td><td style="text-align:right">24 B</td></tr><tr><td>ToStringFast</td><td><code>net6.0</code></td><td style="text-align:right">0.121 ns</td><td style="text-align:right">0.0225 ns</td><td style="text-align:right">0.0199 ns</td><td style="text-align:right">0.007</td><td style="text-align:right">-</td><td style="text-align:right">-</td></tr><tr><td>ToString</td><td><code>net10.0</code></td><td style="text-align:right">6.4389 ns</td><td style="text-align:right">0.1038 ns</td><td style="text-align:right">0.0971 ns</td><td style="text-align:right">1.000</td><td style="text-align:right">0.0038</td><td style="text-align:right">24 B</td></tr><tr><td>ToStringFast</td><td><code>net10.0</code></td><td style="text-align:right">0.0050 ns</td><td style="text-align:right">0.0202 ns</td><td style="text-align:right">0.0189 ns</td><td style="text-align:right">0.001</td><td style="text-align:right">-</td><td style="text-align:right">-</td></tr></tbody></table> <p>Even though recent versions of .NET are way faster, the overall pattern hasn't changed: .NET is <em>way</em> faster than .NET Framework, and the <code>ToStringFast()</code> implementation is way faster than the built-in <code>ToString()</code>. Obviously your mileage may vary and the results will depend on the specific <code>enum</code> you're using, but in general, using the source generator should give you a free performance boost.</p> <p>That's the basics of the package, for more details see <a href="https://github.com/andrewlock/NetEscapades.EnumGenerators/">the project's README</a>. In the next section I describe how adding some new features managed to break users, and what we did in response.</p> <h2 id="adding-new-features-by-adding-to-the-marker-attribute-dll" class="heading-with-anchor"><a href="https://andrewlock.net/splitting-the-netescapades-enumgenerators-packages-the-road-to-a-stable-release/#adding-new-features-by-adding-to-the-marker-attribute-dll" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Adding new features by adding to the marker attribute dll</a></h2> <p>In version <code>1.0.0-beta19</code> of <em>NetEscapades.EnumGenerators</em> <a href="https://andrewlock.net/updates-to-netescapaades-enumgenerators-new-apis-and-system-memory-support/">I introduced a bunch of new features </a>that had been <a href="https://github.com/andrewlock/NetEscapades.EnumGenerators/issues/80">long standing</a> <a href="https://github.com/andrewlock/NetEscapades.EnumGenerators/pull/177">requests</a>:</p> <ul><li>Support for disabling number parsing.</li> <li>Support for automatically calling <code>ToLowerInvariant()</code> or <code>ToUpperInvariant()</code> on the serialized enum.</li></ul> <p>There wasn't really a <em>technical</em> reason I took so long to add these features. The problem was that I didn't want to add dozens of different overloads of <code>Enum.Parse()</code> or <code>ToString()</code> to accommodate all the different possible options. Similarly, I didn't want to add these all as different (IMO, ugly) additional extension methods.</p> <p>I solved this issue in what I <em>thought</em> was a neat way. When you referenced the <em>NetEscapades.EnumGenerators</em> package, your library references the <em>NetEscapades.EnumGenerators.Attributes.dll</em> file that is shipped in the package:</p> <p><img src="https://andrewlock.net/content/images/2026/netescapades_attributes_dll.png" alt="Inside the NetEscapades.EnumGenerators 1.0.0-beta19 package"></p> <p>This is just <em>one</em> way to add "marker" attributes to a target application, and <a href="https://andrewlock.net/exploring-dotnet-10-preview-features-4-solving-the-source-generator-marker-attribute-problem-in-dotnet-10/">until recently</a> it was the most reliable way. My epiphany was to realise that I could put <em>other</em> types in this dll too; including types that are part of the target app's "public" API.</p> <p>So I added <code>EnumParseOptions</code> and <code>SerializationOptions</code> types to <em>NetEscapades.EnumGenerators.Attributes.dll</em>, and used these types to add "general, extensible" overloads for the generated <code>Parse</code> and <code>ToString()</code> methods, something like this:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token keyword">partial</span> <span class="token keyword">class</span> <span class="token class-name">ColourExtensions</span>
<span class="token punctuation">{</span>
    <span class="token comment">// EnumParseOptions controls case sensitivity, number parsing, matching metadata attributes </span>
    <span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token return-type class-name">Colour</span> <span class="token function">Parse</span><span class="token punctuation">(</span><span class="token class-name"><span class="token keyword">string</span><span class="token punctuation">?</span></span> name<span class="token punctuation">,</span> <span class="token keyword">in</span> <span class="token class-name">EnumParseOptions</span> options<span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token comment">// SerializationOptions controls case transforms and whether to use metadata attributes</span>
    <span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token return-type class-name"><span class="token keyword">string</span></span> <span class="token function">ToString</span><span class="token punctuation">(</span><span class="token class-name">Color</span> valu<span class="token punctuation">,</span> <span class="token keyword">in</span> <span class="token class-name">SerializationOptions</span> options<span class="token punctuation">)</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>This all seemed very neat. If we want to add more features, we can just extend the options objects, no need for new methods or anything. However, I inadvertently managed to break a bunch of users 🤦‍♂️</p> <h2 id="when-new-features-break-users" class="heading-with-anchor"><a href="https://andrewlock.net/splitting-the-netescapades-enumgenerators-packages-the-road-to-a-stable-release/#when-new-features-break-users" class="relative text-zinc-800 dark:text-white no-underline hover:underline">When new features break users…</a></h2> <p>Shortly after publishing the new version of the package, I <a href="https://github.com/andrewlock/NetEscapades.EnumGenerators/issues/231">received reports</a> of users seeing the following error:</p> <div class="pre-code-wrapper"><pre><code>Error CS0012: The type 'EnumParseOptions' is defined in an assembly that is not referenced. You must add a reference to assembly 'NetEscapades.EnumGenerators.Attributes, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'. (184, 11)
</code></pre></div> <p>The issue seemed pretty clear: the <em>NetEscapades.EnumGenerators.Attributes.dll</em> that contains the new options objects used in the API wasn't being referenced in the final application. As I said in my <a href="https://github.com/andrewlock/NetEscapades.EnumGenerators/issues/231#issuecomment-3716473001">initial response</a>:</p> <blockquote> <p>I'm guessing that you're excluding assets where you reference the package, e.g. something like this?</p> <div class="pre-code-wrapper"><pre class="language-xml"><code class="language-xml"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PackageReference</span> <span class="token attr-name">Include</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>NetEscapades.EnumGenerators<span class="token punctuation">"</span></span> <span class="token attr-name">Version</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>1.0.0-beta19<span class="token punctuation">"</span></span> <span class="token punctuation">&gt;</span></span> PrivateAssets="all" /&gt;
</code></pre></div> <p>or like this:</p> <div class="pre-code-wrapper"><pre class="language-xml"><code class="language-xml"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PackageReference</span> <span class="token attr-name">Include</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>NetEscapades.EnumGenerators<span class="token punctuation">"</span></span> <span class="token attr-name">Version</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>1.0.0-beta19<span class="token punctuation">"</span></span> <span class="token attr-name">ExcludeAssets</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>all<span class="token punctuation">"</span></span> <span class="token punctuation">/&gt;</span></span>
</code></pre></div> <p>you can't do that, you need just a normal reference:</p> <div class="pre-code-wrapper"><pre class="language-xml"><code class="language-xml"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PackageReference</span> <span class="token attr-name">Include</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>NetEscapades.EnumGenerators<span class="token punctuation">"</span></span> <span class="token attr-name">Version</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>1.0.0-beta19<span class="token punctuation">"</span></span> <span class="token punctuation">/&gt;</span></span>
</code></pre></div> </blockquote> <p>But what I totally overlooked is that many people <em>intentionally</em> add references to source generators using this pattern:</p> <blockquote> <p>For all our code generators, we list them in the Directory.Build.props at the solution level as such:</p> <div class="pre-code-wrapper"><pre class="language-xml"><code class="language-xml"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PackageReference</span> <span class="token attr-name">Include</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>NetEscapades.EnumGenerators<span class="token punctuation">"</span></span>
                  <span class="token attr-name">ExcludeAssets</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>runtime<span class="token punctuation">"</span></span>
                  <span class="token attr-name">PrivateAssets</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>all<span class="token punctuation">"</span></span>
                  <span class="token attr-name">TreatAsUsed</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>true<span class="token punctuation">"</span></span><span class="token punctuation">/&gt;</span></span>
</code></pre></div> <p>We do this so that every project has access to code generation, yet doesn't pass on the generator assemblies themselves as transitive dependencies. Our expectation is such that when we distribute our packages, we don't impose upon our consumers any unnecessary dependencies through transitive references (i.e. available in everything, pass it onto nothing).</p> </blockquote> <p>Unfortunately for me, that makes <em>total</em> sense 😅</p> <p>For some reason, I thought that source generators (and analyzers) <em>didn't</em> flow transitively to downstream projects. But that's not true, they <em>do</em> flow transitively. To make that clearer, imagine you have two projects, <code>MyProject.Lib</code> and <code>MyProject.Web</code>. <code>MyProject.Web</code> has a reference to <code>MyProject.Lib</code>, and <code>MyProject.Lib</code> adds a reference to <em>NetEscapades.EnumGenerators</em>. By default, <code>MyProject.Web</code> <strong>also</strong> gets a transitive reference to <em>NetEscapades.EnumGenerators</em>.</p> <div class="pre-code-wrapper"><pre class="language-ini"><code class="language-ini">     <span class="token section"><span class="token punctuation">[</span><span class="token section-name selector">MyProject.Lib</span><span class="token punctuation">]</span></span>          ←              [MyProject.Web]
            ↓                                      (↓)  
<span class="token section"><span class="token punctuation">[</span><span class="token section-name selector">NetEscapades.EnumGenerators</span><span class="token punctuation">]</span></span>           <span class="token section"><span class="token punctuation">[</span><span class="token section-name selector">NetEscapades.EnumGenerators</span><span class="token punctuation">]</span></span>
</code></pre></div> <p>This is how "normal" project/package references work, but for some reason I thought source generators/analyzers were special. But it turns out no.</p> <p>So why does this matter? Well it means that adding <code>PrivateAssets="all"</code> and <code>ExcludeAssets="runtime"</code> actually makes sense in a <em>lot</em> of cases, particularly if you're creating reusable libraries. If you're using a generator to generate code in a single package, and you <em>don't</em> want to force downstream consumers of your package to automatically be opted in to source generator, then these attributes can help, though they serve slightly different reasons:</p> <ul><li><code>PrivateAssets="all"</code> ensures that any projects referencing this project <em>don't</em> get the dependency as a transitive dependency. In this example above, if <code>MyProject.Lib</code> set <code>PrivateAssets="all"</code>, then <code>MyProject.Web</code> <em>wouldn't</em> get a transitive reference to <em>NetEscapades.EnumGenerators</em>.</li> <li><code>ExcludeAssets="runtime"</code> ensures that any runtime assets included in the package are <em>not</em> copied to the build output. In our case, that means the <em>NetEscapades.EnumGenerators.Attributes.dll</em> would <em>not</em> be copied to the build output.</li></ul> <p>So on that basis, it's clear that adding <code>ExcludeAssets="runtime"</code> was causing the missing reference error at run time. Previously the attribute only contained enum attributes, and those were <a href="https://github.com/andrewlock/NetEscapades.EnumGenerators?tab=readme-ov-file#preserving-usages-of-the-enumextensions-attribute">elided by default <em>anyway</em></a>, so the "runtime dependencies" were actually just a benign spandrel that could be excluded without issue. But when I added the <code>EnumParseOptions</code> and <code>SerializationOptions</code>, suddenly there was a hard dependency and things went boom 💥</p> <h2 id="the-solution-more-packages" class="heading-with-anchor"><a href="https://andrewlock.net/splitting-the-netescapades-enumgenerators-packages-the-road-to-a-stable-release/#the-solution-more-packages" class="relative text-zinc-800 dark:text-white no-underline hover:underline">The solution: more packages</a></h2> <p>Now, the "easy" fix would be to just tell users they're holding it wrong, and to just not use <code>ExcludeAssets="runtime"</code> but there were some <a href="https://github.com/andrewlock/NetEscapades.EnumGenerators/issues/231#issuecomment-3719300265">very reasonable explanations</a> that users had for not wanting to do this.</p> <p>One of the most compelling reasons was that the <em>NetEscapades.EnumGenerators</em> source generator is <em>entirely</em> an "implementation detail". <em>Forcing</em> additional downstream runtime dependencies on consumers just to be "allowed" to use the source generator doesn't feel right. Especially if you're not even <em>using</em> the <code>EnumParseOptions</code> or <code>SerializationOptions</code> overloads!</p> <p>After some various discussion and back and forth, we settled on two key changes:</p> <ul><li>Move the "runtime dependencies" assembly (which currently only contains <code>EnumParseOptions</code> and <code>SerializationOptions</code>) into a separate package, <em>NetEscapades.EnumGenerators.RuntimeDependencies</em>.</li> <li><em>If</em> the <code>EnumParseOptions</code> types are available in the compilation (because you added a reference to the runtime dependencies package) then the source generator uses the <code>EnumParseOptions</code> and <code>SerializationOptions</code> types in its public API. However, if they're <em>not</em> available, it emits <code>enum</code>-specific versions of these types instead, which avoids the need to add the runtime dependencies package. More on this shortly!</li></ul> <p>In addition, to make onboarding easier, the actual source generator was split into a completely separate package, <em>NetEscapades.EnumGenerators.Generator</em>, and _<em>NetEscapades.EnumGenerators</em> becomes a metapackage instead:</p> <div class="pre-code-wrapper"><pre><code>NetEscapades.EnumGenerators
  |____NetEscapades.EnumGenerators.Generators
  |____NetEscapades.EnumGenerators.RuntimeDependencies
</code></pre></div> <p>As discussed, these packages provide the following:</p> <ul><li><code>NetEscapades.EnumGenerators</code> is a meta package for easy install.</li> <li><code>NetEscapades.EnumGenerators.Generators</code> contains the source generator itself.</li> <li><code>NetEscapades.EnumGenerators.RuntimeDependencies</code> contains dependencies that need to be referenced at runtime by the generated code.</li></ul> <p>The default approach is to reference the meta-package in your project. The runtime dependencies and generator packages will then flow transitively to any project that references yours, and the generator will run in those projects by default. But by splitting these packages up, we get more flexibility.</p> <h3 id="avoiding-runtime-dependencies" class="heading-with-anchor"><a href="https://andrewlock.net/splitting-the-netescapades-enumgenerators-packages-the-road-to-a-stable-release/#avoiding-runtime-dependencies" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Avoiding runtime dependencies</a></h3> <p>As already discussed, in some cases you may not want these dependencies to flow to other projects, such as when you're using <em>NetEscapades.EnumGenerators</em> internally in your own library. In this scenario, you can take the following approach:</p> <ul><li>Reference <a href="https://www.nuget.org/packages/NetEscapades.EnumGenerators.Generators"><em>NetEscapades.EnumGenerators.Generators</em></a> directly, and set <code>PrivateAssets=All</code> (and optionally <code>ExcludeAssets="runtime"</code>)</li> <li><em>Optionally</em> reference <a href="https://www.nuget.org/packages/NetEscapades.EnumGenerators.RuntimeDependencies"><em>NetEscapades.EnumGenerators.RuntimeDependencies</em></a> directly.</li></ul> <div class="pre-code-wrapper"><pre class="language-xml"><code class="language-xml"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>Project</span> <span class="token attr-name">Sdk</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>Microsoft.NET.Sdk<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>

  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PropertyGroup</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>OutputType</span><span class="token punctuation">&gt;</span></span>Exe<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>OutputType</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>TargetFramework</span><span class="token punctuation">&gt;</span></span>net8.0<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>TargetFramework</span><span class="token punctuation">&gt;</span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>PropertyGroup</span><span class="token punctuation">&gt;</span></span>

  <span class="token comment">&lt;!-- 👇 Add the generator package with PrivateAssets --&gt;</span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PackageReference</span> <span class="token attr-name">Include</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>NetEscapades.EnumGenerators.Generators<span class="token punctuation">"</span></span> <span class="token attr-name">Version</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>1.0.0-beta21<span class="token punctuation">"</span></span> <span class="token attr-name">PrivateAssets</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>All<span class="token punctuation">"</span></span><span class="token punctuation">/&gt;</span></span>

  <span class="token comment">&lt;!-- Optionally add the runtime dependencies package --&gt;</span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PackageReference</span> <span class="token attr-name">Include</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>NetEscapades.EnumGenerators.RuntimeDependencies<span class="token punctuation">"</span></span> <span class="token attr-name">Version</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>1.0.0-beta21<span class="token punctuation">"</span></span> <span class="token punctuation">/&gt;</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>Project</span><span class="token punctuation">&gt;</span></span>
</code></pre></div> <p>The <a href="https://www.nuget.org/packages/NetEscapades.EnumGenerators.RuntimeDependencies"><em>NetEscapades.EnumGenerators.RuntimeDependencies</em></a> package is a "normal" dependency that contains the <code>EnumParseOptions</code>, <code>SerializationOptions</code>, and <code>SerializationTransform</code> types:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">namespace</span> <span class="token namespace">NetEscapades<span class="token punctuation">.</span>EnumGenerators</span><span class="token punctuation">;</span>

<span class="token comment">/// &lt;summary&gt;</span>
<span class="token comment">/// Defines the options use when parsing enums using members provided by NetEscapades.EnumGenerator.</span>
<span class="token comment">/// &lt;/summary&gt;</span>
<span class="token keyword">public</span> <span class="token keyword">readonly</span> <span class="token keyword">struct</span> <span class="token class-name">EnumParseOptions</span> <span class="token punctuation">{</span> <span class="token punctuation">}</span>

<span class="token comment">/// &lt;summary&gt;</span>
<span class="token comment">/// Options to apply when calling &lt;c&gt;ToStringFast&lt;/c&gt; on an enum.</span>
<span class="token comment">/// &lt;/summary&gt;</span>
<span class="token keyword">public</span> <span class="token keyword">readonly</span> <span class="token keyword">struct</span> <span class="token class-name">SerializationOptions</span>

<span class="token comment">/// &lt;summary&gt;</span>
<span class="token comment">/// Transform to apply when calling &lt;c&gt;ToStringFast&lt;/c&gt;</span>
<span class="token comment">/// &lt;/summary&gt;</span>
<span class="token keyword">public</span> <span class="token keyword">enum</span> <span class="token class-name">SerializationTransform</span>
</code></pre></div> <p>However, if you <em>don't</em> add a reference to the <a href="https://www.nuget.org/packages/NetEscapades.EnumGenerators.RuntimeDependencies"><em>NetEscapades.EnumGenerators.RuntimeDependencies</em></a> package, the source generator creates "nested" versions of the dependencies in each generated extension method instead of using the "global" versions:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">namespace</span> <span class="token namespace">SomeNameSpace</span><span class="token punctuation">;</span>

<span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token keyword">partial</span> <span class="token keyword">class</span> <span class="token class-name">MyEnumExtensions</span>
<span class="token punctuation">{</span>
    <span class="token comment">// ... generated members</span>

    <span class="token comment">// The runtime dependencies are generated as nested types instead</span>
    <span class="token keyword">public</span> <span class="token keyword">readonly</span> <span class="token keyword">struct</span> <span class="token class-name">EnumParseOptions</span> <span class="token punctuation">{</span> <span class="token punctuation">}</span>
    <span class="token keyword">public</span> <span class="token keyword">readonly</span> <span class="token keyword">struct</span> <span class="token class-name">SerializationOptions</span>
    <span class="token keyword">public</span> <span class="token keyword">enum</span> <span class="token class-name">SerializationTransform</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>Generating the runtime dependencies as nested types like this has both upsides and downsides:</p> <ul><li>It avoids placing downstream dependency requirements on consumers of your library.</li> <li>You can still use these additional overloads internally, even without having additional runtime dependencies</li> <li>It makes consuming the APIs that use the runtime dependencies more verbose.</li></ul> <p>To make that last point concrete, if you add a reference to the <em>RuntimeDependencies</em> package, your code might look something like this:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token comment">// parsing</span>
<span class="token class-name"><span class="token keyword">string</span></span> serialized <span class="token operator">=</span> <span class="token string">"Red"</span>
<span class="token class-name">Color</span> <span class="token keyword">value</span> <span class="token operator">=</span> ColourExtensions<span class="token punctuation">.</span><span class="token function">Parse</span><span class="token punctuation">(</span>
    serialized<span class="token punctuation">,</span> <span class="token keyword">new</span> <span class="token constructor-invocation class-name">EnumParseOptions</span><span class="token punctuation">(</span><span class="token named-parameter punctuation">enableNumberParsing</span><span class="token punctuation">:</span> <span class="token boolean">false</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token comment">// serialization</span>
<span class="token class-name"><span class="token keyword">var</span></span> result <span class="token operator">=</span> <span class="token keyword">value</span><span class="token punctuation">.</span><span class="token function">ToStringFast</span><span class="token punctuation">(</span>
    <span class="token keyword">new</span> <span class="token constructor-invocation class-name">SerializationOptions</span><span class="token punctuation">(</span><span class="token named-parameter punctuation">transform</span><span class="token punctuation">:</span> SerializationTransform<span class="token punctuation">.</span>LowerInvariant<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre></div> <p>In contrast, if you omit the runtime dependencies, you have to use the full (nested) type names:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token comment">// parsing</span>
<span class="token class-name"><span class="token keyword">string</span></span> serialized <span class="token operator">=</span> <span class="token string">"Red"</span>
<span class="token class-name">Color</span> <span class="token keyword">value</span> <span class="token operator">=</span> ColourExtensions<span class="token punctuation">.</span><span class="token function">Parse</span><span class="token punctuation">(</span>
    serialized<span class="token punctuation">,</span> <span class="token keyword">new</span> <span class="token constructor-invocation class-name">ColourExtensions<span class="token punctuation">.</span>EnumParseOptions</span><span class="token punctuation">(</span><span class="token named-parameter punctuation">enableNumberParsing</span><span class="token punctuation">:</span> <span class="token boolean">false</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token comment">// serialization</span>
<span class="token class-name"><span class="token keyword">var</span></span> result <span class="token operator">=</span> <span class="token keyword">value</span><span class="token punctuation">.</span><span class="token function">ToStringFast</span><span class="token punctuation">(</span>
    <span class="token keyword">new</span> <span class="token constructor-invocation class-name">ColourExtensions<span class="token punctuation">.</span>SerializationOptions</span><span class="token punctuation">(</span><span class="token named-parameter punctuation">transform</span><span class="token punctuation">:</span> ColourExtensions<span class="token punctuation">.</span>SerializationTransform<span class="token punctuation">.</span>LowerInvariant<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre></div> <p>which, you know, is pretty ugly. But it does avoid those runtime dependencies, and solves the users problems, so it's what is available today!</p> <h3 id="choosing-the-correct-packages-for-your-scenario" class="heading-with-anchor"><a href="https://andrewlock.net/splitting-the-netescapades-enumgenerators-packages-the-road-to-a-stable-release/#choosing-the-correct-packages-for-your-scenario" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Choosing the correct packages for your scenario</a></h3> <p>So where does that leave us?</p> <p>In general, for simplicity, if you're creating an app of some sort I recommend just using a "normal" package reference to <a href="https://www.nuget.org/packages/NetEscapades.EnumGenerators"><em>NetEscapades.EnumGenerators</em></a> (and thereby implicitly using <a href="https://www.nuget.org/packages/NetEscapades.EnumGenerators.RuntimeDependencies"><em>NetEscapades.EnumGenerators.RuntimeDependencies</em></a>). This particularly makes sense when you are the primary consumer of the extension methods, or where you don't mind if consumers end up referencing the generator package:</p> <div class="pre-code-wrapper"><pre class="language-xml"><code class="language-xml"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>Project</span> <span class="token attr-name">Sdk</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>Microsoft.NET.Sdk<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PropertyGroup</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>OutputType</span><span class="token punctuation">&gt;</span></span>Exe<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>OutputType</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>TargetFramework</span><span class="token punctuation">&gt;</span></span>net8.0<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>TargetFramework</span><span class="token punctuation">&gt;</span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>PropertyGroup</span><span class="token punctuation">&gt;</span></span>

  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PackageReference</span> <span class="token attr-name">Include</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>NetEscapades.EnumGenerators<span class="token punctuation">"</span></span> <span class="token attr-name">Version</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>1.0.0-beta21<span class="token punctuation">"</span></span> <span class="token punctuation">/&gt;</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>Project</span><span class="token punctuation">&gt;</span></span>
</code></pre></div> <blockquote> <p>This "default" scenario also gets the best experience in terms of <a href="https://andrewlock.net/recent-updates-to-netescapaades-enumgenerators/#new-analyzers-to-warn-of-incorrect-usage">the optional "usage analyzers"</a>, which flag places that you should consider calling <code>ToStringFast()</code>.</p> </blockquote> <p>In contrast, if you are producing a reusable library and don't want <em>any</em> runtime dependencies to be exposed to consumers, I recommend using <a href="https://www.nuget.org/packages/NetEscapades.EnumGenerators.Generators">NetEscapades.EnumGenerators.Generators</a> and setting <code>PrivateAssets=All</code> and <code>ExcludeAssets="runtime"</code>.</p> <div class="pre-code-wrapper"><pre class="language-xml"><code class="language-xml"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>Project</span> <span class="token attr-name">Sdk</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>Microsoft.NET.Sdk<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PropertyGroup</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>OutputType</span><span class="token punctuation">&gt;</span></span>Exe<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>OutputType</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>TargetFramework</span><span class="token punctuation">&gt;</span></span>net8.0<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>TargetFramework</span><span class="token punctuation">&gt;</span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>PropertyGroup</span><span class="token punctuation">&gt;</span></span>

  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PackageReference</span> <span class="token attr-name">Include</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>NetEscapades.EnumGenerators.Generators<span class="token punctuation">"</span></span> <span class="token attr-name">Version</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>1.0.0-beta21<span class="token punctuation">"</span></span> <span class="token attr-name">PrivateAssets</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>All<span class="token punctuation">"</span></span> <span class="token attr-name">ExcludeAssets</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>runtime<span class="token punctuation">"</span></span> <span class="token punctuation">/&gt;</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>Project</span><span class="token punctuation">&gt;</span></span>
</code></pre></div> <p>The final option is to reference <a href="https://www.nuget.org/packages/NetEscapades.EnumGenerators.Generators">NetEscapades.EnumGenerators.Generators</a> and set <code>PrivateAssets=All</code> and <code>ExcludeAssets="runtime"</code> (to avoid it being referenced transitively), but then <em>also</em> reference <a href="https://www.nuget.org/packages/NetEscapades.EnumGenerators.RuntimeDependencies">NetEscapades.EnumGenerators.RuntimeDependencies</a>, to produce easier-to consume APIs.</p> <div class="pre-code-wrapper"><pre class="language-xml"><code class="language-xml"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>Project</span> <span class="token attr-name">Sdk</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>Microsoft.NET.Sdk<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PropertyGroup</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>OutputType</span><span class="token punctuation">&gt;</span></span>Exe<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>OutputType</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>TargetFramework</span><span class="token punctuation">&gt;</span></span>net8.0<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>TargetFramework</span><span class="token punctuation">&gt;</span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>PropertyGroup</span><span class="token punctuation">&gt;</span></span>

  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PackageReference</span> <span class="token attr-name">Include</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>NetEscapades.EnumGenerators.Generators<span class="token punctuation">"</span></span> <span class="token attr-name">Version</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>1.0.0-beta21<span class="token punctuation">"</span></span> <span class="token attr-name">PrivateAssets</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>All<span class="token punctuation">"</span></span> <span class="token attr-name">ExcludeAssets</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>runtime<span class="token punctuation">"</span></span><span class="token punctuation">/&gt;</span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PackageReference</span> <span class="token attr-name">Include</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>NetEscapades.EnumGenerators.RuntimeDependencies<span class="token punctuation">"</span></span> <span class="token attr-name">Version</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>1.0.0-beta21<span class="token punctuation">"</span></span> <span class="token punctuation">/&gt;</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>Project</span><span class="token punctuation">&gt;</span></span>
</code></pre></div> <blockquote> <p>⚠️ When using the <a href="https://www.nuget.org/packages/NetEscapades.EnumGenerators">NetEscapades.EnumGenerators</a> metapackage, it's important you <em>don't</em> set <code>PrivateAssets=All</code>. If you want to use <code>PrivateAssets=All</code> or <code>ExcludeAssets="runtime"</code> use <a href="https://www.nuget.org/packages/NetEscapades.EnumGenerators.Generators">NetEscapades.EnumGenerators.Generators</a> for this scenario.</p> </blockquote> <p>All of which means the package now supports many different scenarios, though with the associated complexity. All of which leads me to the final part of this post: how close are we to a stable release?</p> <h2 id="open-questions-before-a-stable-release" class="heading-with-anchor"><a href="https://andrewlock.net/splitting-the-netescapades-enumgenerators-packages-the-road-to-a-stable-release/#open-questions-before-a-stable-release" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Open questions before a stable release</a></h2> <p>I've been very hesitant to put out a stable <code>1.0.0</code> release for the package, mostly because I don't really intend there to be a <code>2.0.0</code> unless there's a <em>very</em> good reason. The reason being is that I want consumers of the package to not have to worry about things breaking underneath them.</p> <blockquote> <p>I have a separate rant about how I feel many NuGet library authors have misunderstood the increased speed of the .NET ecosystem in recent years as a license to churn out major versions with endless breaking changes. But that's for a completely different post😅</p> </blockquote> <p>In terms of features, I'm <em>pretty</em> happy with where the generator is today compared to even a few months ago:</p> <ul><li>Optional <em>automatic</em> support for System.Memory if it's available.</li> <li>Optional usage analyzers to encourage <em>using</em> the extension methods where possible.</li> <li>The extensible <code>EnumParseOptions</code> and <code>SerializationOptions</code> discussed in this post.</li> <li>Catering to "internal" usages of the generator (where you don't want downstream consumers to be <em>aware</em> you're using it).</li> <li>Support for enums defined in <em>other</em> libraries (including BCL enums)</li></ul> <p>Nevertheless, there are a few things I'm not entirely happy with. For example:</p> <ul><li>The usage analyzers are IMO pretty cool, should they just be enabled as warnings by default? <ul><li>Presumably, if you're adding this package, you want to preferentially use them, which would suggest yes, enable them.</li> <li>But on the other hand, that would potentially be a big breaking change and annoy a bunch of people that just have a targeted usage</li> <li><em>However</em> they could just disable them in that case.</li> <li>Unfortunately, you need to "preserve attribute usages" if you want the analyzers to work in "downstream" projects <em>and</em> ensure you don't exclude runtime assets, but that's maybe just a limitation we'll have to live with s🤷‍♂️</li></ul> </li> <li>The nested runtime dependency types are pretty ugly <ul><li>Should they just be omitted/made private implementation details entirely?</li> <li>My concern is that while they're a neat solution to a problem, maybe they should just be omitted (or made <code>private</code>, depending on complexity) to reduce confusion. The onboarding story becomes easier in this scenario: "add the <em>NetEscapades.EnumGenerators.RuntimeDependencies</em> package and get these extra features".</li></ul> </li> <li>You can't use "target-typed" <code>new</code> on the <code>EnumParseOptions</code> and <code>SerializationOptions</code> overloads due to ambiguity with existing methods (shown below) <ul><li>This is <em>really</em> annoying, but I think the only way to "solve" it is to remove the overloads that contain the same number of parameters, which is again, pretty disruptive, and those methods are there for a reason (convenience)!</li> <li>The biggest concern is <code>EnumExtensions.Parse(string name, bool ignoreCase)</code>, as that bool is more convenient than using <code>EnumExtensions.Parse(string name, EnumParseOptions options)</code> if you <em>just</em> want to ignore case.</li> <li>But maybe it's worth seriously entertaining making that break sooner rather than later.</li></ul> </li></ul> <p><img src="https://andrewlock.net/content/images/2026/netescapades_attributes_dll_ambiguous.png" alt="Screenshot showing ambiguous invocation error in Rider"></p> <p>Anyway, those are my current thoughts with the library. I need to figure out the answer to these questions before pushing out the 1.0.0, but I'd love for feedback from any users of the package that would be impacted by any of the changes above. Let me know your thoughts here or in <a href="https://github.com/andrewlock/NetEscapades.EnumGenerators/issues/67">the issue on Github</a>. And hopefully, <em>hopefully</em>, we can get a stable version soon 😅</p> <p>In the mean time, do try out <a href="https://www.nuget.org/packages/NetEscapades.EnumGenerators/1.0.0-beta21">the latest version</a>, <code>1.0.0-beta21</code>, read the <a href="https://github.com/andrewlock/NetEscapades.EnumGenerators">package notes</a>, and let me know about any issues or thoughts. There's extra goodness in there I haven't talked about in this post, so give it a try!</p> <h2 id="summary" class="heading-with-anchor"><a href="https://andrewlock.net/splitting-the-netescapades-enumgenerators-packages-the-road-to-a-stable-release/#summary" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Summary</a></h2> <p>In this post I described the recent architectural to the <em>NetEscapades.EnumGenerators</em> package (which is now a metapackage) to support more scenarios. If you're a "standard" user of the package, there's nothing to worry about, you can simply add a reference to <em>NetEscapades.EnumGenerators</em> and everything should work smoothly.</p> <p>However, if you want to control how the package flows transitively to consumers of your project then you may want to reference <em>NetEscapades.EnumGenerators.Generators</em> directly, and optionally add the <em>NetEscapades.EnumGenerators.RuntimeDependencies</em> package so you can use cleaner APIs.</p> <p>I'd love any feedback you have about these recent changes; whether they're too confusing, if you can't work out which path you should take, or if you have any thoughts about the specific points I raised above. Just drop a comment here or in <a href="https://github.com/andrewlock/NetEscapades.EnumGenerators/issues/67">the issue on Github</a>.</p> ]]></content:encoded><category><![CDATA[.NET Core;Roslyn;Source Generators]]></category></item><item><title><![CDATA[Recording metrics in-process using MeterListener: System.Diagnostics.Metrics APIs - Part 4]]></title><description><![CDATA[In this post I show how you can use MeterListener to listen to Instrument measurements, how to trigger Observable measurements, and how to aggregate values.]]></description><link>https://andrewlock.net/recording-metrics-in-process-using-meterlistener/</link><guid isPermaLink="true">https://andrewlock.net/recording-metrics-in-process-using-meterlistener/</guid><pubDate>Tue, 24 Feb 2026 10:00:00 GMT</pubDate><dc:creator><![CDATA[Andrew Lock]]></dc:creator><media:content url="https://andrewlock.net/content/images/2026/instruments_banner.png" medium="image" /><content:encoded><![CDATA[<img src="https://andrewlock.net/content/images/2026/instruments_banner.png" /><nav><p>This is the fourth post in the series: <a href="https://andrewlock.net/series/system-diagnostics-metrics-apis/">System.Diagnostics.Metrics APIs</a>. </p> <ol class="list-none"><li><a href="https://andrewlock.net/creating-and-consuming-metrics-with-system-diagnostics-metrics-apis/">Part 1 - Creating and consuming metrics with System.Diagnostics.Metrics APIs</a></li><li><a href="https://andrewlock.net/creating-strongly-typed-metics-with-a-source-generator/">Part 2 - Exploring the (underwhelming) System.Diagnostics.Metrics source generators</a></li><li><a href="https://andrewlock.net/creating-standard-and-observable-instruments/">Part 3 - Creating standard and "observable" instruments</a></li><li>Part 4 - Recording metrics in-process using MeterListener (this post) </li></ol></nav><p>So far in this series I've described how to <a href="https://andrewlock.net/creating-and-consuming-metrics-with-system-diagnostics-metrics-apis/">create and consume metrics using <code>dotnet-counters</code></a>, how to <a href="https://andrewlock.net/creating-standard-and-observable-instruments/#system-diagnostics-metrics-apis">create each of the different <code>Instrument</code> types</a> exposed by the <em>System.Diagnostics.Metrics</em> APIs, and how to <a href="https://andrewlock.net/creating-strongly-typed-metics-with-a-source-generator/">use a source generator to produce values</a>. In this post, I look at how to <em>consume</em> the stream of values produced by <code>Instrument</code> implementations in-process, using the <code>MeterListener</code> type.</p> <p>I start by describing the scenario of an app that wants to record and process a specific subset of metrics exposed via the <em>System.Diagnostics.Metrics</em> APIs. We'll create a simple app that generates some load, use <code>MeterListener</code> to listen for <code>Instrument</code> measurements, and display the results in a table using <a href="https://spectreconsole.net/">Spectre.Console</a> (because everyone loves <a href="https://spectreconsole.net/">Spectre.Console</a>)!</p> <blockquote> <p>Note that I'm <em>not</em> suggesting you use <code>MeterListener</code> directly in your production apps. In production, you'll likely want to use a solution like OpenTelemetry or Datadog that does all this work for you!</p> </blockquote> <h2 id="creating-the-test-asp-net-core-app" class="heading-with-anchor"><a href="https://andrewlock.net/recording-metrics-in-process-using-meterlistener/#creating-the-test-asp-net-core-app" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Creating the test ASP.NET Core app</a></h2> <p>As described above, for the purposes of this post, I created a simple "hello world" ASP.NET Core app using <code>dotnet new web</code>, and tweaked it so that it will send requests to itself, as long as the app is running:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">using</span> <span class="token namespace">Microsoft<span class="token punctuation">.</span>AspNetCore<span class="token punctuation">.</span>Hosting<span class="token punctuation">.</span>Server</span><span class="token punctuation">;</span>
<span class="token keyword">using</span> <span class="token namespace">Microsoft<span class="token punctuation">.</span>AspNetCore<span class="token punctuation">.</span>Hosting<span class="token punctuation">.</span>Server<span class="token punctuation">.</span>Features</span><span class="token punctuation">;</span>

<span class="token comment">// Very basic hello-world app</span>
<span class="token class-name"><span class="token keyword">var</span></span> builder <span class="token operator">=</span> WebApplication<span class="token punctuation">.</span><span class="token function">CreateBuilder</span><span class="token punctuation">(</span>args<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token class-name"><span class="token keyword">var</span></span> app <span class="token operator">=</span> builder<span class="token punctuation">.</span><span class="token function">Build</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

app<span class="token punctuation">.</span><span class="token function">MapGet</span><span class="token punctuation">(</span><span class="token string">"/"</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=&gt;</span> <span class="token string">"Hello World!"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token class-name"><span class="token keyword">var</span></span> task <span class="token operator">=</span> app<span class="token punctuation">.</span><span class="token function">RunAsync</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token comment">// Grab the address Kestrel's listening on</span>
<span class="token class-name"><span class="token keyword">var</span></span> address <span class="token operator">=</span> app<span class="token punctuation">.</span>Services<span class="token punctuation">.</span><span class="token generic-method"><span class="token function">GetRequiredService</span><span class="token generic class-name"><span class="token punctuation">&lt;</span>IServer<span class="token punctuation">&gt;</span></span></span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">!</span>
        <span class="token punctuation">.</span>Features<span class="token punctuation">.</span><span class="token generic-method"><span class="token function">Get</span><span class="token generic class-name"><span class="token punctuation">&lt;</span>IServerAddressesFeature<span class="token punctuation">&gt;</span></span></span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">!</span>
        <span class="token punctuation">.</span>Addresses<span class="token punctuation">.</span><span class="token function">First</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token keyword">try</span>
<span class="token punctuation">{</span>
    <span class="token comment">// Run 4 loops in parallel, sending HTTP requests continuously</span>
    <span class="token comment">// until the app gets the shut down notification</span>
    <span class="token keyword">await</span> Parallel<span class="token punctuation">.</span><span class="token function">ForAsync</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">,</span> <span class="token number">4</span><span class="token punctuation">,</span> app<span class="token punctuation">.</span>Lifetime<span class="token punctuation">.</span>ApplicationStopping<span class="token punctuation">,</span> <span class="token keyword">async</span> <span class="token punctuation">(</span>i<span class="token punctuation">,</span> ct<span class="token punctuation">)</span> <span class="token operator">=&gt;</span>
    <span class="token punctuation">{</span>
        <span class="token class-name"><span class="token keyword">var</span></span> httpClient <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token constructor-invocation class-name">HttpClient</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
        <span class="token punctuation">{</span>
            BaseAddress <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token constructor-invocation class-name">Uri</span><span class="token punctuation">(</span>address<span class="token punctuation">)</span><span class="token punctuation">,</span>
        <span class="token punctuation">}</span><span class="token punctuation">;</span>

        <span class="token comment">// Just keep hammering requests!</span>
        <span class="token keyword">while</span> <span class="token punctuation">(</span><span class="token operator">!</span>ct<span class="token punctuation">.</span>IsCancellationRequested<span class="token punctuation">)</span>
        <span class="token punctuation">{</span>
            <span class="token class-name"><span class="token keyword">string</span></span> _ <span class="token operator">=</span> <span class="token keyword">await</span> httpClient<span class="token punctuation">.</span><span class="token function">GetStringAsync</span><span class="token punctuation">(</span><span class="token string">"/"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token punctuation">}</span>
    <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">catch</span> <span class="token punctuation">(</span><span class="token class-name">OperationCanceledException</span><span class="token punctuation">)</span>
<span class="token punctuation">{</span>
    <span class="token comment">// expected on shutdown</span>
<span class="token punctuation">}</span>

<span class="token comment">// Wait for the final cleanup</span>
<span class="token keyword">await</span> task<span class="token punctuation">;</span>
</code></pre></div> <p>The code above isn't particularly pretty, but it does the following:</p> <ul><li>Creates a "hello world" minimal API ASP.NET Core app.</li> <li>After the app starts up, it starts 4 parallel jobs</li> <li>Each job has its own <code>HttpClient</code> and continuously makes HTTP requests to the app</li> <li><kbd>ctrl</kbd>+<kbd>c</kbd> in the console stops the requests and shut's down the app.</li></ul> <p>Now that we have this app, we can start grabbing some metrics out of it. We're aiming for something like the following, which shows the majority of metrics from <a href="https://andrewlock.net/creating-standard-and-observable-instruments/#system-diagnostics-metrics-apis/">my previous post</a> in a <a href="https://spectreconsole.net/console/how-to/live-rendering-and-dynamic-updates">live-updating Spectre.Console</a> <a href="https://spectreconsole.net/console/how-to/displaying-tabular-data">table</a>:</p> <div class="pre-code-wrapper"><pre class="language-ini"><code class="language-ini">                                  ASP.NET Core Metrics                                  
┌────────────────────────────────────────────┬─────────────────────────┬─────────────┐
│ Metric                                     │ Type                    │       Value │
├────────────────────────────────────────────┼─────────────────────────┼─────────────┤
│ aspnetcore.routing.match_attempts          │ Counter                 │     250,428 │
│ dotnet.gc.heap.total_allocated             │ ObservableCounter       │ 849,743,376 │
│ http.server.active_requests                │ UpDownCounter           │           4 │
│ dotnet.gc.last_collection.heap.size (gen0) │ ObservableUpDownCounter │   2,497,080 │
│ dotnet.gc.last_collection.heap.size (gen1) │ ObservableUpDownCounter │     774,872 │
│ dotnet.gc.last_collection.heap.size (gen2) │ ObservableUpDownCounter │   1,219,120 │
│ dotnet.gc.last_collection.heap.size (loh)  │ ObservableUpDownCounter │      98,384 │
│ dotnet.gc.last_collection.heap.size (poh)  │ ObservableUpDownCounter │      65,728 │
│ process.cpu.utilization                    │ ObservableGauge         │         36% │
│ http.server.request.duration               │ Histogram               │     0.011ms │
│ http.server.request.duration (count)       │ Histogram               │     250,425 │
└────────────────────────────────────────────┴─────────────────────────┴─────────────┘
</code></pre></div> <p>To record the values from these metrics, we're going to use the <code>MeterListener</code> type.</p> <h2 id="recording-metrics-with-meterlistener" class="heading-with-anchor"><a href="https://andrewlock.net/recording-metrics-in-process-using-meterlistener/#recording-metrics-with-meterlistener" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Recording metrics with <code>MeterListener</code></a></h2> <p>In my previous post I discussed how <code>Instrument</code>s have both a consumer and a producer side. To consume the output of <code>Instrument</code>s inside your app you must subscribe to them using a <code>MeterListener</code>. To manage all this configuration, we'll create a helper type called <code>MetricManager</code>.</p> <h3 id="creating-a-wrapper-metricmanager-for-working-with-metrics" class="heading-with-anchor"><a href="https://andrewlock.net/recording-metrics-in-process-using-meterlistener/#creating-a-wrapper-metricmanager-for-working-with-metrics" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Creating a wrapper <code>MetricManager</code> for working with metrics</a></h3> <p>To encapsulate the collection and aggregation of metrics emitted by the <em>System.Diagnostics.Metrics</em> APIs, I'm going to create a type called <code>MetricManager</code>. This is entirely optional, it's just helpful for my scenario. The public API for this type is shown below, which we'll be fleshing out shortly.</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">MetricManager</span> <span class="token punctuation">:</span> <span class="token type-list"><span class="token class-name">IDisposable</span></span>
<span class="token punctuation">{</span>
    <span class="token keyword">public</span> <span class="token return-type class-name"><span class="token keyword">void</span></span> <span class="token function">Dispose</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">public</span> <span class="token return-type class-name">MetricValues</span> <span class="token function">GetMetrics</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>The <code>MetricManager</code> is responsible for interacting with the <em>System.Diagnostics.Metrics</em> APIs. And when you call <code>GetMetrics()</code>, the manager returns the values for each of the <code>Instruments</code> we listed above:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">public</span> <span class="token keyword">readonly</span> <span class="token keyword">record</span> <span class="token class-name"><span class="token keyword">struct</span></span> <span class="token function">MetricValues</span><span class="token punctuation">(</span>
    <span class="token class-name"><span class="token keyword">long</span></span> TotalMatchAttempts<span class="token punctuation">,</span>
    <span class="token class-name"><span class="token keyword">long</span></span> TotalHeapAllocated<span class="token punctuation">,</span>
    <span class="token class-name"><span class="token keyword">long</span></span> ActiveRequests<span class="token punctuation">,</span>
    <span class="token class-name"><span class="token keyword">long</span></span> HeapSizeGen0<span class="token punctuation">,</span>
    <span class="token class-name"><span class="token keyword">long</span></span> HeapSizeGen1<span class="token punctuation">,</span>
    <span class="token class-name"><span class="token keyword">long</span></span> HeapSizeGen2<span class="token punctuation">,</span>
    <span class="token class-name"><span class="token keyword">long</span></span> HeapSizeLoh<span class="token punctuation">,</span>
    <span class="token class-name"><span class="token keyword">long</span></span> HeapSizePoh<span class="token punctuation">,</span>
    <span class="token class-name"><span class="token keyword">double</span></span> CpuUtilization<span class="token punctuation">,</span>
    <span class="token class-name"><span class="token keyword">double</span></span> AverageDuration<span class="token punctuation">,</span>
    <span class="token class-name"><span class="token keyword">long</span></span> TotalRequests<span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre></div> <p>Just to reiterate, this is not <em>required</em>. It's just how I've chosen in this post to expose the interactions with the <em>System.Diagnostics.Metrics</em> APIs.</p> <blockquote> <p>Note also that I'm creating a very well-defined API here. If you want to have more of a "generalised" listener, that can listen to <em>all</em> metrics, and records all the tags for those metrics, I strongly recommend looking at OpenTelemetry instead!</p> </blockquote> <p>So we have our basic public API, now let's create a <code>MeterListener</code> and hook it up.</p> <h3 id="creating-a-meterlistener-and-configuring-callbacks" class="heading-with-anchor"><a href="https://andrewlock.net/recording-metrics-in-process-using-meterlistener/#creating-a-meterlistener-and-configuring-callbacks" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Creating a <code>MeterListener</code> and configuring callbacks</a></h3> <p>One of the design tenants of the <em>System.Diagnostics.Metrics</em> APIs is that they should be high performance. Commonly for .NET, that mostly means "you don't need additional allocations". That shows up in some of the design of the <code>MeterListener</code> as you'll see shortly.</p> <p>The code below shows how we would extend <code>MetricManager</code> to create a <code>MeterListener</code>, initialize it, and configure callbacks:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">MetricManager</span> <span class="token punctuation">:</span> <span class="token type-list"><span class="token class-name">IDisposable</span></span>
<span class="token punctuation">{</span>
    <span class="token keyword">private</span> <span class="token keyword">readonly</span> <span class="token class-name">MeterListener</span> _listener<span class="token punctuation">;</span>

    <span class="token keyword">public</span> <span class="token function">MetricManager</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        <span class="token comment">// Create a MeterListener, and configure the method to call</span>
        <span class="token comment">// when a new instrument is published in the application</span>
        _listener <span class="token operator">=</span> <span class="token keyword">new</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
        <span class="token punctuation">{</span>
            InstrumentPublished <span class="token operator">=</span> OnInstrumentPublished
        <span class="token punctuation">}</span><span class="token punctuation">;</span>

        <span class="token comment">// Configure the callbacks to invoke when an Instrument emits a value.</span>
        <span class="token comment">// In this case, we know that the .NET runtime instruments we listen to only</span>
        <span class="token comment">// produce long or double values, so that's all we listen for here</span>
        _listener<span class="token punctuation">.</span><span class="token generic-method"><span class="token function">SetMeasurementEventCallback</span><span class="token generic class-name"><span class="token punctuation">&lt;</span><span class="token keyword">long</span><span class="token punctuation">&gt;</span></span></span><span class="token punctuation">(</span>OnMeasurementRecordedLong<span class="token punctuation">)</span><span class="token punctuation">;</span>
        _listener<span class="token punctuation">.</span><span class="token generic-method"><span class="token function">SetMeasurementEventCallback</span><span class="token generic class-name"><span class="token punctuation">&lt;</span><span class="token keyword">double</span><span class="token punctuation">&gt;</span></span></span><span class="token punctuation">(</span>OnMeasurementRecordedDouble<span class="token punctuation">)</span><span class="token punctuation">;</span>

        <span class="token comment">// Start the listener, which invokes OnInstrumentPublished for already-published Instruments</span>
        _listener<span class="token punctuation">.</span><span class="token function">Start</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>

    <span class="token comment">// Call Dispose on the listener to prevent further callbacks being invoked</span>
    <span class="token keyword">public</span> <span class="token return-type class-name"><span class="token keyword">void</span></span> <span class="token function">Dispose</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=&gt;</span> _listener<span class="token punctuation">.</span><span class="token function">Dispose</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token comment">// Callback invoked whenever an instrument is published</span>
    <span class="token keyword">private</span> <span class="token return-type class-name"><span class="token keyword">void</span></span> <span class="token function">OnInstrumentPublished</span><span class="token punctuation">(</span><span class="token class-name">Instrument</span> instrument<span class="token punctuation">,</span> <span class="token class-name">MeterListener</span> listener<span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        <span class="token comment">// ...</span>
    <span class="token punctuation">}</span>

    <span class="token comment">// Callback invoked whenever a `long` measurement is recorded</span>
    <span class="token keyword">private</span> <span class="token keyword">static</span> <span class="token return-type class-name"><span class="token keyword">void</span></span> <span class="token function">OnMeasurementRecordedLong</span><span class="token punctuation">(</span><span class="token class-name">Instrument</span> instrument<span class="token punctuation">,</span> <span class="token class-name"><span class="token keyword">long</span></span> measurement<span class="token punctuation">,</span>
        <span class="token class-name">ReadOnlySpan<span class="token punctuation">&lt;</span>KeyValuePair<span class="token punctuation">&lt;</span><span class="token keyword">string</span><span class="token punctuation">,</span> <span class="token keyword">object</span><span class="token punctuation">?</span><span class="token punctuation">&gt;</span><span class="token punctuation">&gt;</span></span> tags<span class="token punctuation">,</span> <span class="token class-name"><span class="token keyword">object</span><span class="token punctuation">?</span></span> state<span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        <span class="token comment">// ...</span>
    <span class="token punctuation">}</span>

    <span class="token comment">// Callback invoked whenever a `double` measurement is recorded</span>
    <span class="token keyword">private</span> <span class="token keyword">static</span> <span class="token return-type class-name"><span class="token keyword">void</span></span> <span class="token function">OnMeasurementRecordedDouble</span><span class="token punctuation">(</span><span class="token class-name">Instrument</span> instrument<span class="token punctuation">,</span> <span class="token class-name"><span class="token keyword">double</span></span> measurement<span class="token punctuation">,</span>
        <span class="token class-name">ReadOnlySpan<span class="token punctuation">&lt;</span>KeyValuePair<span class="token punctuation">&lt;</span><span class="token keyword">string</span><span class="token punctuation">,</span> <span class="token keyword">object</span><span class="token punctuation">?</span><span class="token punctuation">&gt;</span><span class="token punctuation">&gt;</span></span> tags<span class="token punctuation">,</span> <span class="token class-name"><span class="token keyword">object</span><span class="token punctuation">?</span></span> state<span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        <span class="token comment">// ...</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>I've heavily commented the code above, but I'll highlight some interesting points.</p> <p>Firstly, the <code>OnInstrument</code> callback allows the listener to choose which <code>Meter</code>s and <code>Instrument</code>s it wants to subscribe to. This callback is invoked once for each existing <code>Instrument</code> when you call <code>MeterListener.Start()</code>, and is then subsequently invoked whenever a new <code>Meter</code> or <code>Instrument</code> is subsequently registered.</p> <p>In addition, we have the <code>SetMeasurementEventCallback&lt;T&gt;()</code> method. This is a generic method, because it allows you to register a different callback for each <em>type</em> of <code>Instrument</code> measurement you might receive. Instruments can be created with <code>byte</code>, <code>short</code>, <code>int</code>, <code>long</code>, <code>float</code>, <code>double</code>, and <code>decimal</code> types, so <a href="https://learn.microsoft.com/en-us/dotnet/core/diagnostics/metrics-collection#create-a-custom-collection-tool-using-the-net-meterlistener-api">it's recommended</a> that you register a callback for each of these types.</p> <blockquote> <p>Note that if you use a generic argument that <em>isn't</em> one of these types, you'll get an exception at runtime.</p> </blockquote> <p>This kind of API might seem a little unusual; having to register virtually identical callbacks for each different type feels a bit clumsy. But it's written this way for performance reasons. By having a dedicated callback for each supported <code>T</code>, you can avoid any allocation or overhead that would come from having a "generic" callback that would only work with <code>object</code>.</p> <p>Also note that the callback you register doesn't <em>have</em> to be different methods like I have used above. You <em>could</em> also have a single generic method with a signature like this:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">static</span> <span class="token return-type class-name"><span class="token keyword">void</span></span> <span class="token generic-method"><span class="token function">OnMeasurementRecorded</span><span class="token generic class-name"><span class="token punctuation">&lt;</span>T<span class="token punctuation">&gt;</span></span></span><span class="token punctuation">(</span>
    <span class="token class-name">Instrument</span> instrument<span class="token punctuation">,</span>
    <span class="token class-name">T</span> measurement<span class="token punctuation">,</span>
    <span class="token class-name">ReadOnlySpan<span class="token punctuation">&lt;</span>KeyValuePair<span class="token punctuation">&lt;</span><span class="token keyword">string</span><span class="token punctuation">,</span> <span class="token keyword">object</span><span class="token punctuation">?</span><span class="token punctuation">&gt;</span><span class="token punctuation">&gt;</span></span> tags<span class="token punctuation">,</span>
    <span class="token class-name"><span class="token keyword">object</span><span class="token punctuation">?</span></span> state<span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre></div> <p>However, you would still need to call <code>SetMeasurementEventCallback&lt;T&gt;</code> once for each measurement type you want to handle, for example:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp">_listener<span class="token punctuation">.</span><span class="token generic-method"><span class="token function">SetMeasurementEventCallback</span><span class="token generic class-name"><span class="token punctuation">&lt;</span><span class="token keyword">long</span><span class="token punctuation">&gt;</span></span></span><span class="token punctuation">(</span>OnMeasurementRecorded<span class="token punctuation">)</span><span class="token punctuation">;</span>
_listener<span class="token punctuation">.</span><span class="token generic-method"><span class="token function">SetMeasurementEventCallback</span><span class="token generic class-name"><span class="token punctuation">&lt;</span><span class="token keyword">double</span><span class="token punctuation">&gt;</span></span></span><span class="token punctuation">(</span>OnMeasurementRecorded<span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre></div> <p>We are yet to implement these measurement callbacks, but before we get to that, we'll take a look at the <code>OnInstrumentPublished()</code> callback.</p> <h3 id="selecting-which-instruments-to-listen-to" class="heading-with-anchor"><a href="https://andrewlock.net/recording-metrics-in-process-using-meterlistener/#selecting-which-instruments-to-listen-to" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Selecting which <code>Instrument</code>s to listen to</a></h3> <p>The <code>MeterListener</code> is "connected" to all of the <code>Meter</code>s and <code>Instrument</code>s in the application, but it won't automatically receive measurements from all of them unless you enable each one. For this demo, we only care about a subset of <code>Meter</code>s and <code>Instrument</code>s, so our <code>OnInstrumentPublished()</code> callback uses a switch expression to check for specific values of <code>Instrument.Name</code> and <code>Meter.Name</code>:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">private</span> <span class="token return-type class-name"><span class="token keyword">void</span></span> <span class="token function">OnInstrumentPublished</span><span class="token punctuation">(</span><span class="token class-name">Instrument</span> instrument<span class="token punctuation">,</span> <span class="token class-name">MeterListener</span> listener<span class="token punctuation">)</span>
<span class="token punctuation">{</span>
    <span class="token class-name"><span class="token keyword">string</span></span> meterName <span class="token operator">=</span> instrument<span class="token punctuation">.</span>Meter<span class="token punctuation">.</span>Name<span class="token punctuation">;</span>
    <span class="token class-name"><span class="token keyword">string</span></span> instrumentName <span class="token operator">=</span> instrument<span class="token punctuation">.</span>Name<span class="token punctuation">;</span>

    <span class="token comment">// Is this a Meter and Instrument we care about?</span>
    <span class="token class-name"><span class="token keyword">var</span></span> enable <span class="token operator">=</span> meterName <span class="token keyword">switch</span>
    <span class="token punctuation">{</span>
        <span class="token string">"Microsoft.AspNetCore.Routing"</span> <span class="token operator">=&gt;</span> instrumentName <span class="token operator">==</span> <span class="token string">"aspnetcore.routing.match_attempts"</span><span class="token punctuation">,</span>
        <span class="token string">"System.Runtime"</span>               <span class="token operator">=&gt;</span> instrumentName <span class="token keyword">is</span> <span class="token string">"dotnet.gc.heap.total_allocated"</span>
                                                            <span class="token keyword">or</span> <span class="token string">"dotnet.gc.last_collection.heap.size"</span><span class="token punctuation">,</span>
        <span class="token string">"Microsoft.AspNetCore.Hosting"</span> <span class="token operator">=&gt;</span> instrumentName <span class="token keyword">is</span> <span class="token string">"http.server.active_requests"</span>
                                                            <span class="token keyword">or</span> <span class="token string">"http.server.request.duration"</span><span class="token punctuation">,</span>
        <span class="token string">"Microsoft.Extensions.Diagnostics.ResourceMonitoring"</span> <span class="token operator">=&gt;</span> instrumentName <span class="token operator">==</span> <span class="token string">"process.cpu.utilization"</span><span class="token punctuation">,</span>
        _ <span class="token operator">=&gt;</span> <span class="token boolean">false</span>
    <span class="token punctuation">}</span><span class="token punctuation">;</span>

    <span class="token keyword">if</span> <span class="token punctuation">(</span>enable<span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        <span class="token comment">// If yes, enable measurements, and pass the `MetricManager` as "state"</span>
        listener<span class="token punctuation">.</span><span class="token function">EnableMeasurementEvents</span><span class="token punctuation">(</span>instrument<span class="token punctuation">,</span> <span class="token named-parameter punctuation">state</span><span class="token punctuation">:</span> <span class="token keyword">this</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>To enable measurements, you call <code>MeterListener.EnableMeasurementEvents()</code>, passing in the <code>Instrument</code> to listen to. One interesting point here is that we're also passing the <code>MetricManager</code> as the <code>state</code> variable. This variable is passed in to our <code>OnMeasurementRecorded</code> callbacks and is a way of avoiding closures or expensive lookups in the callback events. You'll see how it's used shortly.</p> <p>Note that if we were creating a generic implementation that listened to <em>all</em> <code>Insturment</code>s emitted by the app, we could implement this method very simply:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">private</span> <span class="token return-type class-name"><span class="token keyword">void</span></span> <span class="token function">OnInstrumentPublished</span><span class="token punctuation">(</span><span class="token class-name">Instrument</span> instrument<span class="token punctuation">,</span> <span class="token class-name">MeterListener</span> listener<span class="token punctuation">)</span>
    <span class="token operator">=&gt;</span> listener<span class="token punctuation">.</span><span class="token function">EnableMeasurementEvents</span><span class="token punctuation">(</span>instrument<span class="token punctuation">,</span> <span class="token named-parameter punctuation">state</span><span class="token punctuation">:</span> <span class="token keyword">this</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre></div> <p>So at this point we've enabled the instruments, we've called <code>MeterListener.Start()</code>, and it's time to start receiving some measurements!</p> <h3 id="triggering-observableinstruments-to-emit-measurements" class="heading-with-anchor"><a href="https://andrewlock.net/recording-metrics-in-process-using-meterlistener/#triggering-observableinstruments-to-emit-measurements" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Triggering <code>ObservableInstrument</code>s to emit measurements</a></h3> <p>Now that we've subscribed to the instruments, the <code>OnMeasurementRecorded</code> callbacks are invoked whenever an <code>Instrument</code> emits a value. For "standard" <code>Instrument</code>s, that happens immediately, whenever a value is recorded: add a value to a <code>Counter&lt;long&gt;</code>, and our <code>OnMeasurementRecorded</code> callback is immediately called. But that's not how it works for observable instruments.</p> <p>In my <a href="https://andrewlock.net/creating-standard-and-observable-instruments/#what-is-an-observable-instrument-">previous post</a>, I described how observable instruments don't emit any values until the consumer <em>asks</em> them to. Well, the consumer here is <code>MeterListener</code>, and it needs to ask all the <code>Instrument</code>s it is interested in to emit values when <code>GetMetrics()</code> is called:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">public</span> <span class="token return-type class-name">MetricValues</span> <span class="token function">GetMetrics</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token punctuation">{</span>
    <span class="token comment">// This triggers the observable metrics to go and read the values and</span>
    <span class="token comment">// then invoke the OnMeasurementRecorded callback to send the values to us</span>
    _listener<span class="token punctuation">.</span><span class="token function">RecordObservableInstruments</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token comment">// ...</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>Calling <code>RecordObservableInstruments()</code> triggers all the observable instruments that we enabled to emit a measurement (by invoking their associated callbacks, such as <a href="https://andrewlock.net/creating-standard-and-observable-instruments/#observablecountert">those described in my previous post</a>). These measurements are then reported via the callbacks registered with the <code>MeterListener</code>.</p> <p>Our <code>MeterListener</code> is now completely configured, so it's time to flesh out the <code>OnMeasurementRecorded</code> callbacks.</p> <h3 id="recording-instrument-measurements" class="heading-with-anchor"><a href="https://andrewlock.net/recording-metrics-in-process-using-meterlistener/#recording-instrument-measurements" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Recording <code>Instrument</code> measurements</a></h3> <p>Whenever a measurement is recorded by an <code>Instrument</code>, the registered callback of the appropriate type is invoked (if you haven't registered an appropriate callback, none will be invoked). Exactly what you should <em>do</em> with that metric depends on how you want to aggregate your data.</p> <p>The following implementation of the <code>OnMeasurementRecordedLong</code> method shows one way to aggregate the data, focusing on displaying long running totals for the duration of the app:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">private</span> <span class="token keyword">static</span> <span class="token return-type class-name"><span class="token keyword">void</span></span> <span class="token function">OnMeasurementRecordedLong</span><span class="token punctuation">(</span><span class="token class-name">Instrument</span> instrument<span class="token punctuation">,</span> <span class="token class-name"><span class="token keyword">long</span></span> measurement<span class="token punctuation">,</span>
    <span class="token class-name">ReadOnlySpan<span class="token punctuation">&lt;</span>KeyValuePair<span class="token punctuation">&lt;</span><span class="token keyword">string</span><span class="token punctuation">,</span> <span class="token keyword">object</span><span class="token punctuation">?</span><span class="token punctuation">&gt;</span><span class="token punctuation">&gt;</span></span> tags<span class="token punctuation">,</span> <span class="token class-name"><span class="token keyword">object</span><span class="token punctuation">?</span></span> state<span class="token punctuation">)</span>
<span class="token punctuation">{</span>
    <span class="token class-name"><span class="token keyword">var</span></span> handler <span class="token operator">=</span> <span class="token punctuation">(</span>MetricManager<span class="token punctuation">)</span>state<span class="token operator">!</span><span class="token punctuation">;</span>
    <span class="token keyword">switch</span> <span class="token punctuation">(</span>instrument<span class="token punctuation">.</span>Name<span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        <span class="token comment">// Counter</span>
        <span class="token keyword">case</span> <span class="token string">"aspnetcore.routing.match_attempts"</span><span class="token punctuation">:</span>
            Interlocked<span class="token punctuation">.</span><span class="token function">Add</span><span class="token punctuation">(</span><span class="token keyword">ref</span> handler<span class="token punctuation">.</span>_matchAttempts<span class="token punctuation">,</span> measurement<span class="token punctuation">)</span><span class="token punctuation">;</span>
            <span class="token keyword">break</span><span class="token punctuation">;</span>

        <span class="token comment">// ObservableCounter</span>
        <span class="token keyword">case</span> <span class="token string">"dotnet.gc.heap.total_allocated"</span><span class="token punctuation">:</span>
            Interlocked<span class="token punctuation">.</span><span class="token function">Exchange</span><span class="token punctuation">(</span><span class="token keyword">ref</span> handler<span class="token punctuation">.</span>_totalHeapAllocated<span class="token punctuation">,</span> measurement<span class="token punctuation">)</span><span class="token punctuation">;</span>
            <span class="token keyword">break</span><span class="token punctuation">;</span>

        <span class="token comment">// UpDownCounter</span>
        <span class="token keyword">case</span> <span class="token string">"http.server.active_requests"</span><span class="token punctuation">:</span>
            Interlocked<span class="token punctuation">.</span><span class="token function">Add</span><span class="token punctuation">(</span><span class="token keyword">ref</span> handler<span class="token punctuation">.</span>_activeRequests<span class="token punctuation">,</span> measurement<span class="token punctuation">)</span><span class="token punctuation">;</span>
            <span class="token keyword">break</span><span class="token punctuation">;</span>

        <span class="token comment">// ObservableUpDownCounter</span>
        <span class="token keyword">case</span> <span class="token string">"dotnet.gc.last_collection.heap.size"</span><span class="token punctuation">:</span>
            <span class="token keyword">foreach</span> <span class="token punctuation">(</span><span class="token class-name"><span class="token keyword">var</span></span> tag <span class="token keyword">in</span> tags<span class="token punctuation">)</span>
            <span class="token punctuation">{</span>
                <span class="token keyword">if</span> <span class="token punctuation">(</span>tag <span class="token keyword">is</span> <span class="token punctuation">{</span> Key<span class="token punctuation">:</span> <span class="token string">"gc.heap.generation"</span><span class="token punctuation">,</span> <span class="token named-parameter punctuation">Value</span><span class="token punctuation">:</span> <span class="token keyword">string</span> gen <span class="token punctuation">}</span><span class="token punctuation">)</span>
                <span class="token punctuation">{</span>
                    <span class="token keyword">switch</span> <span class="token punctuation">(</span>gen<span class="token punctuation">)</span>
                    <span class="token punctuation">{</span>
                        <span class="token keyword">case</span> <span class="token string">"gen0"</span><span class="token punctuation">:</span> Interlocked<span class="token punctuation">.</span><span class="token function">Exchange</span><span class="token punctuation">(</span><span class="token keyword">ref</span> handler<span class="token punctuation">.</span>_heapSizeGen0<span class="token punctuation">,</span> measurement<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">break</span><span class="token punctuation">;</span>
                        <span class="token keyword">case</span> <span class="token string">"gen1"</span><span class="token punctuation">:</span> Interlocked<span class="token punctuation">.</span><span class="token function">Exchange</span><span class="token punctuation">(</span><span class="token keyword">ref</span> handler<span class="token punctuation">.</span>_heapSizeGen1<span class="token punctuation">,</span> measurement<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">break</span><span class="token punctuation">;</span>
                        <span class="token keyword">case</span> <span class="token string">"gen2"</span><span class="token punctuation">:</span> Interlocked<span class="token punctuation">.</span><span class="token function">Exchange</span><span class="token punctuation">(</span><span class="token keyword">ref</span> handler<span class="token punctuation">.</span>_heapSizeGen2<span class="token punctuation">,</span> measurement<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">break</span><span class="token punctuation">;</span>
                        <span class="token keyword">case</span> <span class="token string">"loh"</span><span class="token punctuation">:</span> Interlocked<span class="token punctuation">.</span><span class="token function">Exchange</span><span class="token punctuation">(</span><span class="token keyword">ref</span> handler<span class="token punctuation">.</span>_heapSizeLoh<span class="token punctuation">,</span> measurement<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">break</span><span class="token punctuation">;</span>
                        <span class="token keyword">case</span> <span class="token string">"poh"</span><span class="token punctuation">:</span> Interlocked<span class="token punctuation">.</span><span class="token function">Exchange</span><span class="token punctuation">(</span><span class="token keyword">ref</span> handler<span class="token punctuation">.</span>_heapSizePoh<span class="token punctuation">,</span> measurement<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">break</span><span class="token punctuation">;</span>
                    <span class="token punctuation">}</span>
                <span class="token punctuation">}</span>
            <span class="token punctuation">}</span>

            <span class="token keyword">break</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>The first step is to cast the <code>state</code> object back to the <code>MetricManager</code> instance that we passed in when calling <code>EnableMeasurementEvents()</code>. We then switch based on the instrument name, and handle the measurement value differently depending on the instrument type:</p> <ul><li>For <code>Counter</code> and <code>UpDownCounter</code>, the callback is invoked once for every time a new value is recorded, with the <code>measurement</code> value as the increment. To create a running total of values, you must <em>add</em> the new measurement to the current running total.</li> <li>For <code>ObservableCounter</code> and <code>ObservableUpDownCounter</code>, the callback is only invoked when you call <code>RecordObservableInstruments()</code>. The <code>measurement</code> value in these cases <em>aren't</em> incremental values, but rather they're the "final" current value, so you can use the value "as is" for the current running total.</li></ul> <p>You can see these rules applied in the above method, where the <code>Counter</code> and <code>UpDownCounter</code> metrics are aggregated using <code>Interlocked.Add()</code>, whereas the <code>ObservableCounter</code> and <code>ObservableUpDownCounter</code> metrics are "aggregated" by using <code>Interlocked.Exchange</code>.</p> <p>Another interesting aspect is the handling of tags. The <code>"dotnet.gc.last_collection.heap.size"</code> is an <code>ObservableUpDownCounter</code>, so the values are emitted only when you call <code>RecordObservableInstruments()</code>. In this case, we receive one invocation of the callback <em>per generation</em>, with the <code>gc.heap.generation</code> tag indicating to which generation the current measurement applies.</p> <p>In addition to the <code>OnMeasurementRecordedLong</code> callback, we also have the <code>OnMeasurementRecordedDouble</code> callback, which we use to record the <code>ObservableGauge</code> and <code>Histogram</code> metrics:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">private</span> <span class="token keyword">static</span> <span class="token return-type class-name"><span class="token keyword">void</span></span> <span class="token function">OnMeasurementRecordedDouble</span><span class="token punctuation">(</span><span class="token class-name">Instrument</span> instrument<span class="token punctuation">,</span> <span class="token class-name"><span class="token keyword">double</span></span> measurement<span class="token punctuation">,</span>
    <span class="token class-name">ReadOnlySpan<span class="token punctuation">&lt;</span>KeyValuePair<span class="token punctuation">&lt;</span><span class="token keyword">string</span><span class="token punctuation">,</span> <span class="token keyword">object</span><span class="token punctuation">?</span><span class="token punctuation">&gt;</span><span class="token punctuation">&gt;</span></span> tags<span class="token punctuation">,</span> <span class="token class-name"><span class="token keyword">object</span><span class="token punctuation">?</span></span> state<span class="token punctuation">)</span>
<span class="token punctuation">{</span>
    <span class="token class-name"><span class="token keyword">var</span></span> handler <span class="token operator">=</span> <span class="token punctuation">(</span>MetricManager<span class="token punctuation">)</span>state<span class="token operator">!</span><span class="token punctuation">;</span>
    <span class="token keyword">switch</span> <span class="token punctuation">(</span>instrument<span class="token punctuation">.</span>Name<span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        <span class="token comment">// ObservableGauge</span>
        <span class="token keyword">case</span> <span class="token string">"process.cpu.utilization"</span><span class="token punctuation">:</span>
            Interlocked<span class="token punctuation">.</span><span class="token function">Exchange</span><span class="token punctuation">(</span><span class="token keyword">ref</span> handler<span class="token punctuation">.</span>_cpuUtilization<span class="token punctuation">,</span> measurement<span class="token punctuation">)</span><span class="token punctuation">;</span>
            <span class="token keyword">break</span><span class="token punctuation">;</span>

        <span class="token comment">// Histogram</span>
        <span class="token keyword">case</span> <span class="token string">"http.server.request.duration"</span><span class="token punctuation">:</span>
            Interlocked<span class="token punctuation">.</span><span class="token function">Increment</span><span class="token punctuation">(</span><span class="token keyword">ref</span> handler<span class="token punctuation">.</span>_totalRequestCount<span class="token punctuation">)</span><span class="token punctuation">;</span>
            <span class="token keyword">lock</span> <span class="token punctuation">(</span>handler<span class="token punctuation">.</span>_durationLock<span class="token punctuation">)</span>
            <span class="token punctuation">{</span>
                handler<span class="token punctuation">.</span>_intervalRequests<span class="token operator">++</span><span class="token punctuation">;</span>
                handler<span class="token punctuation">.</span>_totalDuration <span class="token operator">+=</span> measurement<span class="token punctuation">;</span>
            <span class="token punctuation">}</span>

            <span class="token keyword">break</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>The structure for this callback is very similar to the previous one:</p> <ul><li>We cast the <code>state</code> variable to our <code>MetricManager</code> instance that we passed in when registering the callback.</li> <li>For the <code>ObservableGauge</code> (as for all of the observable instruments), we <em>replace</em> our recorded value, using <code>Interlocked.Exchange()</code></li> <li>For the <code>Histogram</code>, there are many different ways we could aggregate the data, especially considering that these measurements contain a lot of high cardinality tags. I chose to calculate just two values from this data: <ul><li>The total number of requests since app start, stored in <code>_totalRequestCount</code>.</li> <li>The average request duration in the current time interval. This requires recording the number of requests (<code>_intervalRequests</code>) during the interval, and the sum of the durations of requests during the interval (<code>_totalDuration</code>). We'll use these values to calculate the average shortly.</li></ul> </li></ul> <p>Some of these measurements may be recorded concurrently with when while we are reading the values, which is why I've used <code>Interlocked</code> where possible, to make updates atomic. Where I couldn't use <code>Interlocked</code>, I used a <code>lock</code> for simplicity, though you should be careful about this in practice; in high performance applications it might be possible to run into lock contention, if many requests are trying to increment these values.</p> <p>Now that all of our <code>Instrument</code>s are recording values, both standard and observable, it's time to report the results.</p> <h3 id="reporting-the-results-from-getmetrics" class="heading-with-anchor"><a href="https://andrewlock.net/recording-metrics-in-process-using-meterlistener/#reporting-the-results-from-getmetrics" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Reporting the results from <code>GetMetrics</code></a></h3> <p>I have already partially shown the <code>GetMetrics()</code> implementation, in so far as it's where we called <code>RecordObservableInstruments()</code>. Other than triggering the observable measurements to be taken, all <code>GetMetrics()</code> does is read the values recorded in the fields, calculate the average duration, and return a <code>MetricValues</code> instance:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">public</span> <span class="token return-type class-name">MetricValues</span> <span class="token function">GetMetrics</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token punctuation">{</span>
    <span class="token comment">// This triggers the observable metrics to go and read the values</span>
    <span class="token comment">// and then call the OnMeasurement callbacks to send the values to us</span>
    _listener<span class="token punctuation">.</span><span class="token function">RecordObservableInstruments</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token comment">// Read all of the values from the fields and return a MetricValues object</span>
    <span class="token keyword">return</span> <span class="token keyword">new</span> <span class="token constructor-invocation class-name">MetricValues</span><span class="token punctuation">(</span>
        <span class="token named-parameter punctuation">TotalMatchAttempts</span><span class="token punctuation">:</span> Interlocked<span class="token punctuation">.</span><span class="token function">Read</span><span class="token punctuation">(</span><span class="token keyword">ref</span> _matchAttempts<span class="token punctuation">)</span><span class="token punctuation">,</span>
        <span class="token named-parameter punctuation">TotalHeapAllocated</span><span class="token punctuation">:</span> Interlocked<span class="token punctuation">.</span><span class="token function">Read</span><span class="token punctuation">(</span><span class="token keyword">ref</span> _totalHeapAllocated<span class="token punctuation">)</span><span class="token punctuation">,</span>
        <span class="token named-parameter punctuation">ActiveRequests</span><span class="token punctuation">:</span> Interlocked<span class="token punctuation">.</span><span class="token function">Read</span><span class="token punctuation">(</span><span class="token keyword">ref</span> _activeRequests<span class="token punctuation">)</span><span class="token punctuation">,</span>
        <span class="token named-parameter punctuation">HeapSizeGen0</span><span class="token punctuation">:</span> Interlocked<span class="token punctuation">.</span><span class="token function">Read</span><span class="token punctuation">(</span><span class="token keyword">ref</span> _heapSizeGen0<span class="token punctuation">)</span><span class="token punctuation">,</span>
        <span class="token named-parameter punctuation">HeapSizeGen1</span><span class="token punctuation">:</span> Interlocked<span class="token punctuation">.</span><span class="token function">Read</span><span class="token punctuation">(</span><span class="token keyword">ref</span> _heapSizeGen1<span class="token punctuation">)</span><span class="token punctuation">,</span>
        <span class="token named-parameter punctuation">HeapSizeGen2</span><span class="token punctuation">:</span> Interlocked<span class="token punctuation">.</span><span class="token function">Read</span><span class="token punctuation">(</span><span class="token keyword">ref</span> _heapSizeGen2<span class="token punctuation">)</span><span class="token punctuation">,</span>
        <span class="token named-parameter punctuation">HeapSizeLoh</span><span class="token punctuation">:</span> Interlocked<span class="token punctuation">.</span><span class="token function">Read</span><span class="token punctuation">(</span><span class="token keyword">ref</span> _heapSizeLoh<span class="token punctuation">)</span><span class="token punctuation">,</span>
        <span class="token named-parameter punctuation">HeapSizePoh</span><span class="token punctuation">:</span> Interlocked<span class="token punctuation">.</span><span class="token function">Read</span><span class="token punctuation">(</span><span class="token keyword">ref</span> _heapSizePoh<span class="token punctuation">)</span><span class="token punctuation">,</span>
        <span class="token named-parameter punctuation">CpuUtilization</span><span class="token punctuation">:</span> Volatile<span class="token punctuation">.</span><span class="token function">Read</span><span class="token punctuation">(</span><span class="token keyword">ref</span> _cpuUtilization<span class="token punctuation">)</span><span class="token punctuation">,</span>
        <span class="token named-parameter punctuation">AverageDuration</span><span class="token punctuation">:</span> <span class="token function">ComputeAndResetAverageDuration</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
        <span class="token named-parameter punctuation">TotalRequests</span><span class="token punctuation">:</span> Interlocked<span class="token punctuation">.</span><span class="token function">Read</span><span class="token punctuation">(</span><span class="token keyword">ref</span> _totalRequestCount<span class="token punctuation">)</span>
    <span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token return-type class-name"><span class="token keyword">double</span></span> <span class="token function">ComputeAndResetAverageDuration</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        <span class="token class-name"><span class="token keyword">long</span></span> count<span class="token punctuation">;</span>
        <span class="token class-name"><span class="token keyword">double</span></span> sum<span class="token punctuation">;</span>
        <span class="token keyword">lock</span> <span class="token punctuation">(</span>_durationLock<span class="token punctuation">)</span>
        <span class="token punctuation">{</span>
            <span class="token comment">// Grab the current values</span>
            count <span class="token operator">=</span> _intervalRequests<span class="token punctuation">;</span>
            sum <span class="token operator">=</span> _totalDuration<span class="token punctuation">;</span>
            <span class="token comment">// Reset the values</span>
            _intervalRequests <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span>
            _totalDuration <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span>
        <span class="token punctuation">}</span>

        <span class="token comment">// Do the calculation</span>
        <span class="token keyword">return</span> count <span class="token operator">&gt;</span> <span class="token number">0</span> <span class="token punctuation">?</span> sum <span class="token operator">/</span> count <span class="token punctuation">:</span> <span class="token number">0</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>And with that, the implementation of <code>MetricManager</code> and its usage of <code>MeterListener</code> is complete. All that remains is to plug the listener into our app.</p> <h2 id="creating-a-service-to-display-the-results" class="heading-with-anchor"><a href="https://andrewlock.net/recording-metrics-in-process-using-meterlistener/#creating-a-service-to-display-the-results" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Creating a service to display the results</a></h2> <p>To view the metrics being collected by <code>MetricManager</code> and its <code>MeterListener</code>, I created a <code>BackgroundService</code> that would render a <a href="https://spectreconsole.net/">Spectre.Console</a> live table to the console, and update it periodically:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">using</span> <span class="token namespace">MyMetrics</span><span class="token punctuation">;</span>
<span class="token keyword">using</span> <span class="token namespace">Spectre<span class="token punctuation">.</span>Console</span><span class="token punctuation">;</span>

<span class="token keyword">internal</span> <span class="token keyword">class</span> <span class="token class-name">MetricDisplayService</span> <span class="token punctuation">:</span> <span class="token type-list"><span class="token class-name">BackgroundService</span></span>
<span class="token punctuation">{</span>
    <span class="token keyword">protected</span> <span class="token keyword">override</span> <span class="token keyword">async</span> <span class="token return-type class-name">Task</span> <span class="token function">ExecuteAsync</span><span class="token punctuation">(</span><span class="token class-name">CancellationToken</span> stoppingToken<span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        <span class="token keyword">using</span> <span class="token class-name"><span class="token keyword">var</span></span> manager <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token constructor-invocation class-name">MetricManager</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        
        <span class="token class-name"><span class="token keyword">var</span></span> table <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token constructor-invocation class-name">Table</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
            <span class="token punctuation">.</span><span class="token function">Title</span><span class="token punctuation">(</span><span class="token string">"[bold]ASP.NET Core Metrics[/]"</span><span class="token punctuation">)</span>
            <span class="token punctuation">.</span><span class="token function">Border</span><span class="token punctuation">(</span>TableBorder<span class="token punctuation">.</span>Rounded<span class="token punctuation">)</span>
            <span class="token punctuation">.</span><span class="token function">AddColumn</span><span class="token punctuation">(</span><span class="token string">"Metric"</span><span class="token punctuation">)</span>
            <span class="token punctuation">.</span><span class="token function">AddColumn</span><span class="token punctuation">(</span><span class="token string">"Type"</span><span class="token punctuation">)</span>
            <span class="token punctuation">.</span><span class="token function">AddColumn</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token constructor-invocation class-name">TableColumn</span><span class="token punctuation">(</span><span class="token string">"Value"</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">RightAligned</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

        table<span class="token punctuation">.</span><span class="token function">AddRow</span><span class="token punctuation">(</span><span class="token string">"aspnetcore.routing.match_attempts"</span><span class="token punctuation">,</span> <span class="token string">"Counter"</span><span class="token punctuation">,</span> <span class="token string">"0"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        table<span class="token punctuation">.</span><span class="token function">AddRow</span><span class="token punctuation">(</span><span class="token string">"dotnet.gc.heap.total_allocated"</span><span class="token punctuation">,</span> <span class="token string">"ObservableCounter"</span><span class="token punctuation">,</span> <span class="token string">"0"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        table<span class="token punctuation">.</span><span class="token function">AddRow</span><span class="token punctuation">(</span><span class="token string">"http.server.active_requests"</span><span class="token punctuation">,</span> <span class="token string">"UpDownCounter"</span><span class="token punctuation">,</span> <span class="token string">"0"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        table<span class="token punctuation">.</span><span class="token function">AddRow</span><span class="token punctuation">(</span><span class="token string">"dotnet.gc.last_collection.heap.size (gen0)"</span><span class="token punctuation">,</span> <span class="token string">"ObservableUpDownCounter"</span><span class="token punctuation">,</span> <span class="token string">"0"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        table<span class="token punctuation">.</span><span class="token function">AddRow</span><span class="token punctuation">(</span><span class="token string">"dotnet.gc.last_collection.heap.size (gen1)"</span><span class="token punctuation">,</span> <span class="token string">"ObservableUpDownCounter"</span><span class="token punctuation">,</span> <span class="token string">"0"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        table<span class="token punctuation">.</span><span class="token function">AddRow</span><span class="token punctuation">(</span><span class="token string">"dotnet.gc.last_collection.heap.size (gen2)"</span><span class="token punctuation">,</span> <span class="token string">"ObservableUpDownCounter"</span><span class="token punctuation">,</span> <span class="token string">"0"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        table<span class="token punctuation">.</span><span class="token function">AddRow</span><span class="token punctuation">(</span><span class="token string">"dotnet.gc.last_collection.heap.size (loh)"</span><span class="token punctuation">,</span> <span class="token string">"ObservableUpDownCounter"</span><span class="token punctuation">,</span> <span class="token string">"0"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        table<span class="token punctuation">.</span><span class="token function">AddRow</span><span class="token punctuation">(</span><span class="token string">"dotnet.gc.last_collection.heap.size (poh)"</span><span class="token punctuation">,</span> <span class="token string">"ObservableUpDownCounter"</span><span class="token punctuation">,</span> <span class="token string">"0"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        table<span class="token punctuation">.</span><span class="token function">AddRow</span><span class="token punctuation">(</span><span class="token string">"process.cpu.utilization"</span><span class="token punctuation">,</span> <span class="token string">"ObservableGauge"</span><span class="token punctuation">,</span> <span class="token string">"0%"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        table<span class="token punctuation">.</span><span class="token function">AddRow</span><span class="token punctuation">(</span><span class="token string">"http.server.request.duration"</span><span class="token punctuation">,</span> <span class="token string">"Histogram"</span><span class="token punctuation">,</span> <span class="token string">"0.000ms"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        table<span class="token punctuation">.</span><span class="token function">AddRow</span><span class="token punctuation">(</span><span class="token string">"http.server.request.duration (count)"</span><span class="token punctuation">,</span> <span class="token string">"Histogram"</span><span class="token punctuation">,</span> <span class="token string">"0"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

        <span class="token keyword">await</span> AnsiConsole<span class="token punctuation">.</span><span class="token function">Live</span><span class="token punctuation">(</span>table<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">StartAsync</span><span class="token punctuation">(</span><span class="token keyword">async</span> ctx <span class="token operator">=&gt;</span>
        <span class="token punctuation">{</span>
            <span class="token comment">// This is the update loop, where we poll the `MetricManager`</span>
            <span class="token keyword">while</span> <span class="token punctuation">(</span><span class="token operator">!</span>stoppingToken<span class="token punctuation">.</span>IsCancellationRequested<span class="token punctuation">)</span>
            <span class="token punctuation">{</span>
                <span class="token keyword">await</span> Task<span class="token punctuation">.</span><span class="token function">Delay</span><span class="token punctuation">(</span>TimeSpan<span class="token punctuation">.</span><span class="token function">FromSeconds</span><span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">,</span> stoppingToken<span class="token punctuation">)</span><span class="token punctuation">;</span>
                <span class="token function">RenderMetricValues</span><span class="token punctuation">(</span>table<span class="token punctuation">,</span> ctx<span class="token punctuation">,</span> manager<span class="token punctuation">.</span><span class="token function">GetMetrics</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
            <span class="token punctuation">}</span>
        <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>

    <span class="token keyword">private</span> <span class="token return-type class-name"><span class="token keyword">void</span></span> <span class="token function">RenderMetricValues</span><span class="token punctuation">(</span><span class="token class-name">Table</span> table<span class="token punctuation">,</span> <span class="token class-name">LiveDisplayContext</span> ctx<span class="token punctuation">,</span> <span class="token keyword">in</span> <span class="token class-name">MetricManager<span class="token punctuation">.</span>MetricValues</span> values<span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        table<span class="token punctuation">.</span><span class="token function">UpdateCell</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">,</span> <span class="token number">2</span><span class="token punctuation">,</span> values<span class="token punctuation">.</span>TotalMatchAttempts<span class="token punctuation">.</span><span class="token function">ToString</span><span class="token punctuation">(</span><span class="token string">"N0"</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        table<span class="token punctuation">.</span><span class="token function">UpdateCell</span><span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">,</span> <span class="token number">2</span><span class="token punctuation">,</span> values<span class="token punctuation">.</span>TotalHeapAllocated<span class="token punctuation">.</span><span class="token function">ToString</span><span class="token punctuation">(</span><span class="token string">"N0"</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        table<span class="token punctuation">.</span><span class="token function">UpdateCell</span><span class="token punctuation">(</span><span class="token number">2</span><span class="token punctuation">,</span> <span class="token number">2</span><span class="token punctuation">,</span> values<span class="token punctuation">.</span>ActiveRequests<span class="token punctuation">.</span><span class="token function">ToString</span><span class="token punctuation">(</span><span class="token string">"N0"</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        table<span class="token punctuation">.</span><span class="token function">UpdateCell</span><span class="token punctuation">(</span><span class="token number">3</span><span class="token punctuation">,</span> <span class="token number">2</span><span class="token punctuation">,</span> values<span class="token punctuation">.</span>HeapSizeGen0<span class="token punctuation">.</span><span class="token function">ToString</span><span class="token punctuation">(</span><span class="token string">"N0"</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        table<span class="token punctuation">.</span><span class="token function">UpdateCell</span><span class="token punctuation">(</span><span class="token number">4</span><span class="token punctuation">,</span> <span class="token number">2</span><span class="token punctuation">,</span> values<span class="token punctuation">.</span>HeapSizeGen1<span class="token punctuation">.</span><span class="token function">ToString</span><span class="token punctuation">(</span><span class="token string">"N0"</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        table<span class="token punctuation">.</span><span class="token function">UpdateCell</span><span class="token punctuation">(</span><span class="token number">5</span><span class="token punctuation">,</span> <span class="token number">2</span><span class="token punctuation">,</span> values<span class="token punctuation">.</span>HeapSizeGen2<span class="token punctuation">.</span><span class="token function">ToString</span><span class="token punctuation">(</span><span class="token string">"N0"</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        table<span class="token punctuation">.</span><span class="token function">UpdateCell</span><span class="token punctuation">(</span><span class="token number">6</span><span class="token punctuation">,</span> <span class="token number">2</span><span class="token punctuation">,</span> values<span class="token punctuation">.</span>HeapSizeLoh<span class="token punctuation">.</span><span class="token function">ToString</span><span class="token punctuation">(</span><span class="token string">"N0"</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        table<span class="token punctuation">.</span><span class="token function">UpdateCell</span><span class="token punctuation">(</span><span class="token number">7</span><span class="token punctuation">,</span> <span class="token number">2</span><span class="token punctuation">,</span> values<span class="token punctuation">.</span>HeapSizePoh<span class="token punctuation">.</span><span class="token function">ToString</span><span class="token punctuation">(</span><span class="token string">"N0"</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        table<span class="token punctuation">.</span><span class="token function">UpdateCell</span><span class="token punctuation">(</span><span class="token number">8</span><span class="token punctuation">,</span> <span class="token number">2</span><span class="token punctuation">,</span> <span class="token interpolation-string"><span class="token string">$"</span><span class="token interpolation"><span class="token punctuation">{</span><span class="token expression language-csharp">values<span class="token punctuation">.</span>CpuUtilization</span><span class="token format-string"><span class="token punctuation">:</span>F0</span><span class="token punctuation">}</span></span><span class="token string">%"</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        table<span class="token punctuation">.</span><span class="token function">UpdateCell</span><span class="token punctuation">(</span><span class="token number">9</span><span class="token punctuation">,</span> <span class="token number">2</span><span class="token punctuation">,</span> <span class="token interpolation-string"><span class="token string">$"</span><span class="token interpolation"><span class="token punctuation">{</span><span class="token expression language-csharp">values<span class="token punctuation">.</span>AverageDuration <span class="token operator">*</span> <span class="token number">1000</span></span><span class="token format-string"><span class="token punctuation">:</span>F3</span><span class="token punctuation">}</span></span><span class="token string">ms"</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        table<span class="token punctuation">.</span><span class="token function">UpdateCell</span><span class="token punctuation">(</span><span class="token number">10</span><span class="token punctuation">,</span> <span class="token number">2</span><span class="token punctuation">,</span> values<span class="token punctuation">.</span>TotalRequests<span class="token punctuation">.</span><span class="token function">ToString</span><span class="token punctuation">(</span><span class="token string">"N0"</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        ctx<span class="token punctuation">.</span><span class="token function">Refresh</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>Most of this code is simply setting up the table, the "important" part in terms of the interaction with the <code>MetricManager</code> all takes place in the <code>AnsiConsole.Live</code> block:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token comment">// As long as the app keeps running...</span>
<span class="token keyword">while</span> <span class="token punctuation">(</span><span class="token operator">!</span>stoppingToken<span class="token punctuation">.</span>IsCancellationRequested<span class="token punctuation">)</span>
<span class="token punctuation">{</span>
    <span class="token comment">// ...wait 1 second...</span>
    <span class="token keyword">await</span> Task<span class="token punctuation">.</span><span class="token function">Delay</span><span class="token punctuation">(</span>TimeSpan<span class="token punctuation">.</span><span class="token function">FromSeconds</span><span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">,</span> stoppingToken<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token comment">// ...and then grab the metrics, and render them</span>
    <span class="token function">RenderMetricValues</span><span class="token punctuation">(</span>table<span class="token punctuation">,</span> ctx<span class="token punctuation">,</span> manager<span class="token punctuation">.</span><span class="token function">GetMetrics</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>All that remains is to plug our background service into our app:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">using</span> <span class="token namespace">Microsoft<span class="token punctuation">.</span>AspNetCore<span class="token punctuation">.</span>Hosting<span class="token punctuation">.</span>Server</span><span class="token punctuation">;</span>
<span class="token keyword">using</span> <span class="token namespace">Microsoft<span class="token punctuation">.</span>AspNetCore<span class="token punctuation">.</span>Hosting<span class="token punctuation">.</span>Server<span class="token punctuation">.</span>Features</span><span class="token punctuation">;</span>

<span class="token class-name"><span class="token keyword">var</span></span> builder <span class="token operator">=</span> WebApplication<span class="token punctuation">.</span><span class="token function">CreateBuilder</span><span class="token punctuation">(</span>args<span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token comment">// Register the MetricDisplayService as an `IHostedService`</span>
builder<span class="token punctuation">.</span>Services<span class="token punctuation">.</span><span class="token generic-method"><span class="token function">AddHostedService</span><span class="token generic class-name"><span class="token punctuation">&lt;</span>MetricDisplayService<span class="token punctuation">&gt;</span></span></span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token comment">// Add the ResourceMonitoring package so that we can retrieve "process.cpu.utilization"</span>
builder<span class="token punctuation">.</span>Services<span class="token punctuation">.</span><span class="token function">AddResourceMonitoring</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token class-name"><span class="token keyword">var</span></span> app <span class="token operator">=</span> builder<span class="token punctuation">.</span><span class="token function">Build</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

app<span class="token punctuation">.</span><span class="token function">MapGet</span><span class="token punctuation">(</span><span class="token string">"/"</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=&gt;</span> <span class="token string">"Hello World!"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

app<span class="token punctuation">.</span><span class="token function">Run</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre></div> <p>and that's it! If we run the app, and generate some load, we'll see our metrics being reported to the console 🎉</p> <div class="pre-code-wrapper"><pre class="language-ini"><code class="language-ini">┌────────────────────────────────────────────┬─────────────────────────┬─────────────┐
│ Metric                                     │ Type                    │       Value │
├────────────────────────────────────────────┼─────────────────────────┼─────────────┤
│ aspnetcore.routing.match_attempts          │ Counter                 │     250,428 │
│ dotnet.gc.heap.total_allocated             │ ObservableCounter       │ 849,743,376 │
│ http.server.active_requests                │ UpDownCounter           │           4 │
│ dotnet.gc.last_collection.heap.size (gen0) │ ObservableUpDownCounter │   2,497,080 │
│ dotnet.gc.last_collection.heap.size (gen1) │ ObservableUpDownCounter │     774,872 │
│ dotnet.gc.last_collection.heap.size (gen2) │ ObservableUpDownCounter │   1,219,120 │
│ dotnet.gc.last_collection.heap.size (loh)  │ ObservableUpDownCounter │      98,384 │
│ dotnet.gc.last_collection.heap.size (poh)  │ ObservableUpDownCounter │      65,728 │
│ process.cpu.utilization                    │ ObservableGauge         │         36% │
│ http.server.request.duration               │ Histogram               │     0.011ms │
│ http.server.request.duration (count)       │ Histogram               │     250,425 │
└────────────────────────────────────────────┴─────────────────────────┴─────────────┘
</code></pre></div> <p>And with that we reach the end. Our app is able to report metrics about itself, and report those in any way it sees fit. In this example we just blindly report them to the console, but you could do anything with them. That said, if you're thinking of doing anything <em>serious</em> with these metrics, you should likely consider using <a href="https://learn.microsoft.com/en-us/dotnet/core/diagnostics/observability-with-otel">the OpenTelemetry libraries</a> instead!</p> <h2 id="summary" class="heading-with-anchor"><a href="https://andrewlock.net/recording-metrics-in-process-using-meterlistener/#summary" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Summary</a></h2> <p>In this post I describe the scenario of an app that wants to record and process a specific subset of metrics exposed via the <em>System.Diagnostics.Metrics</em> APIs. I then show a simple app that generates some load, use <code>MeterListener</code> to listen for <code>Instrument</code> measurements, and display the results in a table using <a href="https://spectreconsole.net/">Spectre.Console</a>. Along the way I show the difference between the standard <code>Instrument</code> and <code>ObservableInstrument</code> measurements, show how to trigger observable measurements to be reported, and discuss performance aspects, such as passing state to the callback functions.</p> ]]></content:encoded><category><![CDATA[Observability;.NET Core]]></category></item><item><title><![CDATA[Creating standard and "observable" instruments: System.Diagnostics.Metrics APIs - Part 3]]></title><description><![CDATA[In this post I discuss the various Instrument<T> types exposed by the System.Diagnostics.Metrics API and show examples from the .NET libraries and ASP.NET Core]]></description><link>https://andrewlock.net/creating-standard-and-observable-instruments/</link><guid isPermaLink="true">https://andrewlock.net/creating-standard-and-observable-instruments/</guid><pubDate>Tue, 17 Feb 2026 10:00:00 GMT</pubDate><dc:creator><![CDATA[Andrew Lock]]></dc:creator><media:content url="https://andrewlock.net/content/images/2026/instruments.png" medium="image" /><content:encoded><![CDATA[<img src="https://andrewlock.net/content/images/2026/instruments.png" /><nav><p>This is the third post in the series: <a href="https://andrewlock.net/series/system-diagnostics-metrics-apis/">System.Diagnostics.Metrics APIs</a>. </p> <ol class="list-none"><li><a href="https://andrewlock.net/creating-and-consuming-metrics-with-system-diagnostics-metrics-apis/">Part 1 - Creating and consuming metrics with System.Diagnostics.Metrics APIs</a></li><li><a href="https://andrewlock.net/creating-strongly-typed-metics-with-a-source-generator/">Part 2 - Exploring the (underwhelming) System.Diagnostics.Metrics source generators</a></li><li>Part 3 - Creating standard and "observable" instruments (this post) </li><li><a href="https://andrewlock.net/recording-metrics-in-process-using-meterlistener/">Part 4 - Recording metrics in-process using MeterListener</a></li></ol></nav><p>In the <a href="https://andrewlock.net/creating-and-consuming-metrics-with-system-diagnostics-metrics-apis/">first post in this series</a> I provided an introduction to the <em>System.Diagnostics.Metrics</em> APIs introduced in .NET 6. I initially introduced the concept of "observable" <code>Instrument</code>s in that post, but didn't go into more details. In this post, we'll understand what being "observable" means, and how these <code>Instrument</code>s differ from non-observable <code>Instrument</code>s.</p> <p>I start the post with a quick refresher on the basics of the <em>System.Diagnostics.Metrics</em> APIs, such as the different types of instruments available. I then show how you can create each of the instrument types and produce values from them.</p> <h2 id="system-diagnostics-metrics-apis" class="heading-with-anchor"><a href="https://andrewlock.net/creating-standard-and-observable-instruments/#system-diagnostics-metrics-apis" class="relative text-zinc-800 dark:text-white no-underline hover:underline">System.Diagnostics.Metrics APIs</a></h2> <p>The <em>System.Diagnostics.Metrics</em> APIs were introduced in .NET 6 but are available in earlier runtimes (including .NET Framework) by using the <a href="https://www.nuget.org/packages/System.Diagnostics.DiagnosticSource/"><em>System.Diagnostics.DiagnosticSource</em></a> NuGet package. There are two primary concepts exposed by these APIs: <code>Instrument</code> and <code>Meter</code>:</p> <ul><li><code>Instrument</code>: An instrument records the values for a single metric of interest. You might have separate <code>Instrument</code>s for "products sold", "invoices created", "invoice total", or "GC heap size".</li> <li><code>Meter</code>: A <code>Meter</code> is a logical grouping of multiple instruments. For example, the <a href="https://learn.microsoft.com/en-us/dotnet/core/diagnostics/built-in-metrics-runtime"><code>System.Runtime</code> <code>Meter</code></a> contains multiple <code>Instrument</code>s about the workings of the runtime, while <a href="https://learn.microsoft.com/en-us/aspnet/core/log-mon/metrics/built-in?view=aspnetcore-10.0#microsoftaspnetcorehosting">the <code>Microsoft.AspNetCore.Hosting</code> <code>Meter</code></a> contains <code>Instrument</code>s about the HTTP requests received by ASP.NET Core.</li></ul> <p>There are also (currently, as of .NET 10) 7 different types of <code>Instrument</code>:</p> <ul><li><code>Counter&lt;T&gt;</code></li> <li><code>ObservableCounter&lt;T&gt;</code></li> <li><code>UpDownCounter&lt;T&gt;</code></li> <li><code>ObservableUpDownCounter&lt;T&gt;</code></li> <li><code>Gauge&lt;T&gt;</code></li> <li><code>ObservableGauge&lt;T&gt;</code></li> <li><code>Histogram&lt;T&gt;</code>.</li></ul> <p>To create a custom metric, you need to choose the type of <code>Instrument</code> to use, and associate it with a <code>Meter</code>. I'll discuss the differences between each of these instruments shortly, but first we'll look at the difference between "observable" instruments, and "normal" instruments.</p> <h2 id="what-is-an-observable-instrument-" class="heading-with-anchor"><a href="https://andrewlock.net/creating-standard-and-observable-instruments/#what-is-an-observable-instrument-" class="relative text-zinc-800 dark:text-white no-underline hover:underline">What is an <code>Observable*</code> instrument?</a></h2> <p>When using the <em>System.Diagnostic.Metrics</em> APIs there's a "producer" side and a "consumer" side. The producer of metrics is the app itself, recording values and details about how it's operating. The consumer could be an in-process consumer, such as <a href="https://learn.microsoft.com/en-us/dotnet/core/diagnostics/observability-with-otel">the OpenTelemetry libraries</a>, or it could be an external process, such as <a href="https://learn.microsoft.com/en-us/dotnet/core/diagnostics/dotnet-counters"><code>dotnet-counters</code></a> or <a href="https://learn.microsoft.com/en-us/dotnet/core/diagnostics/dotnet-monitor"><code>dotnet-monitor</code></a>.</p> <p>The differences between a "normal" instrument and an "observable" instrument stem from who controls when and how a value is emitted:</p> <ul><li>For "normal" instruments, the <em>producer</em> emits values as they occur. For example, when a request is received, ASP.NET Core emits the <code>http.server.active_requests</code> metric, indicating a new request is in-flight.</li> <li>For "observable" instruments, the <em>consumer</em> side <em>asks</em> for the value. For example, the <code>dotnet.gc.pause.time</code> metric returns "The total amount of time paused in GC since the process has started", but only when you <em>ask</em> for it.</li></ul> <p>In general, observable instruments are used when you have an effectively continuous value that you wouldn't make sense for the consumer to actively emit, such as the <code>dotnet.gc.pause.time</code> above, or where emitting all of the intermediate values would be too expensive from a performance point of view.</p> <blockquote> <p>Technically, you <em>could</em> potentially emit this metric every time the GC pauses, but given that these values are more fine-grained than you would likely want <em>anyway</em>, it's much more efficient to allow the consumer to "poll" the values on demand, and therefore it makes the most sense as an observable instrument.</p> </blockquote> <p>Now we understand the difference between observable and normal instruments, let's walk through all the instrumentation types and see how they're used in the .NET base class libraries.</p> <h2 id="understanding-the-different-instrument-types" class="heading-with-anchor"><a href="https://andrewlock.net/creating-standard-and-observable-instruments/#understanding-the-different-instrument-types" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Understanding the different <code>Instrument</code> types</a></h2> <p>So far in this series we've used a simple <code>Counter&lt;T&gt;</code> that records every time a given event occurs. In this post we'll look at each of the possible <code>Instrument</code>s in turn, showing how you create an instrument of that type to produce a given metric. Where possible, I'm showing places within the .NET or ASP.NET Core libraries that use each of these instruments, to give "real world" versions of how these are used.</p> <h3 id="countert" class="heading-with-anchor"><a href="https://andrewlock.net/creating-standard-and-observable-instruments/#countert" class="relative text-zinc-800 dark:text-white no-underline hover:underline"><code>Counter&lt;T&gt;</code></a></h3> <p>The <code>Counter&lt;T&gt;</code> instrument is one of the simplest instruments conceptually. It is used to record how many times a given event occurs.</p> <p>For example, <a href="https://github.com/dotnet/aspnetcore/blob/102119ab7ceb911130fad4a485ec0a4828aa9e53/src/Middleware/Diagnostics/src/DiagnosticsMetrics.cs#L24-L27">the <code>aspnetcore.diagnostics.exceptions</code> metric</a> is a <code>Counter&lt;long&gt;</code> which records the <code>"Number of exceptions caught by exception handling middleware."</code></p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp">_handlerExceptionCounter <span class="token operator">=</span> _meter<span class="token punctuation">.</span><span class="token generic-method"><span class="token function">CreateCounter</span><span class="token generic class-name"><span class="token punctuation">&lt;</span><span class="token keyword">long</span><span class="token punctuation">&gt;</span></span></span><span class="token punctuation">(</span>
    <span class="token string">"aspnetcore.diagnostics.exceptions"</span><span class="token punctuation">,</span>
    <span class="token named-parameter punctuation">unit</span><span class="token punctuation">:</span> <span class="token string">"{exception}"</span><span class="token punctuation">,</span>
    <span class="token named-parameter punctuation">description</span><span class="token punctuation">:</span> <span class="token string">"Number of exceptions caught by exception handling middleware."</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre></div> <p>Every time the <code>ExceptionHandlerMiddleware</code> (or <code>DeveloperExceptionHandlerMiddleware</code>) <a href="https://github.com/dotnet/aspnetcore/blob/102119ab7ceb911130fad4a485ec0a4828aa9e53/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddlewareImpl.cs#L126">catches an exception</a>, it adds <code>1</code> to this counter, first constructing an appropriate set of tags, and then calling <code>Add(1, tags)</code>:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"> <span class="token keyword">private</span> <span class="token return-type class-name"><span class="token keyword">void</span></span> <span class="token function">RequestExceptionCore</span><span class="token punctuation">(</span><span class="token class-name"><span class="token keyword">string</span></span> exceptionName<span class="token punctuation">,</span> <span class="token class-name">ExceptionResult</span> result<span class="token punctuation">,</span> <span class="token class-name"><span class="token keyword">string</span><span class="token punctuation">?</span></span> handler<span class="token punctuation">)</span>
<span class="token punctuation">{</span>
    <span class="token class-name"><span class="token keyword">var</span></span> tags <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token constructor-invocation class-name">TagList</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    tags<span class="token punctuation">.</span><span class="token function">Add</span><span class="token punctuation">(</span><span class="token string">"error.type"</span><span class="token punctuation">,</span> exceptionName<span class="token punctuation">)</span><span class="token punctuation">;</span>
    tags<span class="token punctuation">.</span><span class="token function">Add</span><span class="token punctuation">(</span><span class="token string">"aspnetcore.diagnostics.exception.result"</span><span class="token punctuation">,</span> <span class="token function">GetExceptionResult</span><span class="token punctuation">(</span>result<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">if</span> <span class="token punctuation">(</span>handler <span class="token operator">!=</span> <span class="token keyword">null</span><span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        tags<span class="token punctuation">.</span><span class="token function">Add</span><span class="token punctuation">(</span><span class="token string">"aspnetcore.diagnostics.handler.type"</span><span class="token punctuation">,</span> handler<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
    _handlerExceptionCounter<span class="token punctuation">.</span><span class="token function">Add</span><span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">,</span> tags<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>As this <code>Counter&lt;T&gt;</code> is tracking a number of occurrences, you're always adding positive values, never negative values, though you can increase by more than <code>1</code> at a time if needs be.</p> <h3 id="observablecountert" class="heading-with-anchor"><a href="https://andrewlock.net/creating-standard-and-observable-instruments/#observablecountert" class="relative text-zinc-800 dark:text-white no-underline hover:underline"><code>ObservableCounter&lt;T&gt;</code></a></h3> <p>The <code>ObservableCounter&lt;T&gt;</code> is conceptually similar to a <code>Counter&lt;T&gt;</code>, in that it records monotonically increasing values. Being an "observable" instrument, it only records the values when "observed" (we'll look at how to observe the instruments in your own code in a subsequent post).</p> <p>For example, <a href="https://github.com/dotnet/runtime/blob/v10.0.1/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/RuntimeMetrics.cs#L44-L48">the <code>dotnet.gc.heap.total_allocated</code> metric</a> is an <code>ObservableCounter&lt;long&gt;</code> which records the <code>"The approximate number of bytes allocated on the managed GC heap since the process has started"</code>:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp">s_meter<span class="token punctuation">.</span><span class="token function">CreateObservableCounter</span><span class="token punctuation">(</span>
    <span class="token string">"dotnet.gc.heap.total_allocated"</span><span class="token punctuation">,</span>
    <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=&gt;</span> GC<span class="token punctuation">.</span><span class="token function">GetTotalAllocatedBytes</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
    <span class="token named-parameter punctuation">unit</span><span class="token punctuation">:</span> <span class="token string">"By"</span><span class="token punctuation">,</span>
    <span class="token named-parameter punctuation">description</span><span class="token punctuation">:</span> <span class="token string">"The approximate number of bytes allocated on the managed GC heap since the process has started. The returned value does not include any native allocations."</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre></div> <p>When observed, the lambda included in the definition is called, which invokes <code>GC.GetTotalAllocatedBytes()</code>. Note that this value steadily increases during the lifetime of the app, so it's not returning the difference since <em>last</em> invocation, it's returning the current running total.</p> <h3 id="updowncountert" class="heading-with-anchor"><a href="https://andrewlock.net/creating-standard-and-observable-instruments/#updowncountert" class="relative text-zinc-800 dark:text-white no-underline hover:underline"><code>UpDownCounter&lt;T&gt;</code></a></h3> <p>The <code>UpDownCounter&lt;T&gt;</code> is similar to the <code>Counter&lt;T&gt;</code>, but it supports reporting positive or negative values.</p> <p>For example, <a href="https://github.com/dotnet/aspnetcore/blob/9a93048bd0afd7c2d09bdb5ce47ef7d78827c647/src/Hosting/Hosting/src/Internal/HostingMetrics.cs#L24-L27">the <code>http.server.active_requests</code> metric</a> is an <code>UpDownCounter&lt;T&gt;</code> that records the <code>"Number of active HTTP server requests."</code>:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp">_activeRequestsCounter <span class="token operator">=</span> _meter<span class="token punctuation">.</span><span class="token generic-method"><span class="token function">CreateUpDownCounter</span><span class="token generic class-name"><span class="token punctuation">&lt;</span><span class="token keyword">long</span><span class="token punctuation">&gt;</span></span></span><span class="token punctuation">(</span>
    <span class="token string">"http.server.active_requests"</span><span class="token punctuation">,</span>
    <span class="token named-parameter punctuation">unit</span><span class="token punctuation">:</span> <span class="token string">"{request}"</span><span class="token punctuation">,</span>
    <span class="token named-parameter punctuation">description</span><span class="token punctuation">:</span> <span class="token string">"Number of active HTTP server requests."</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre></div> <p><a href="https://github.com/dotnet/aspnetcore/blob/9a93048bd0afd7c2d09bdb5ce47ef7d78827c647/src/Hosting/Hosting/src/Internal/HostingMetrics.cs#L37">When a request is started</a>, the server calls <code>Add()</code> and increments the value of the counter:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">public</span> <span class="token return-type class-name"><span class="token keyword">void</span></span> <span class="token function">RequestStart</span><span class="token punctuation">(</span><span class="token class-name"><span class="token keyword">string</span></span> scheme<span class="token punctuation">,</span> <span class="token class-name"><span class="token keyword">string</span></span> method<span class="token punctuation">)</span>
<span class="token punctuation">{</span>
    <span class="token comment">// Tags must match request end.</span>
    <span class="token class-name"><span class="token keyword">var</span></span> tags <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token constructor-invocation class-name">TagList</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token function">InitializeRequestTags</span><span class="token punctuation">(</span><span class="token keyword">ref</span> tags<span class="token punctuation">,</span> scheme<span class="token punctuation">,</span> method<span class="token punctuation">)</span><span class="token punctuation">;</span>
    _activeRequestsCounter<span class="token punctuation">.</span><span class="token function">Add</span><span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">,</span> tags<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>

<span class="token keyword">private</span> <span class="token keyword">static</span> <span class="token return-type class-name"><span class="token keyword">void</span></span> <span class="token function">InitializeRequestTags</span><span class="token punctuation">(</span><span class="token keyword">ref</span> <span class="token class-name">TagList</span> tags<span class="token punctuation">,</span> <span class="token class-name"><span class="token keyword">string</span></span> scheme<span class="token punctuation">,</span> <span class="token class-name"><span class="token keyword">string</span></span> method<span class="token punctuation">)</span>
<span class="token punctuation">{</span>
    tags<span class="token punctuation">.</span><span class="token function">Add</span><span class="token punctuation">(</span>HostingTelemetryHelpers<span class="token punctuation">.</span>AttributeUrlScheme<span class="token punctuation">,</span> scheme<span class="token punctuation">)</span><span class="token punctuation">;</span>
    tags<span class="token punctuation">.</span><span class="token function">Add</span><span class="token punctuation">(</span>HostingTelemetryHelpers<span class="token punctuation">.</span>AttributeHttpRequestMethod<span class="token punctuation">,</span> HostingTelemetryHelpers<span class="token punctuation">.</span><span class="token function">GetNormalizedHttpMethod</span><span class="token punctuation">(</span>method<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>Similarly, <a href="https://github.com/dotnet/aspnetcore/blob/9a93048bd0afd7c2d09bdb5ce47ef7d78827c647/src/Hosting/Hosting/src/Internal/HostingMetrics.cs#L45C1-L54C10">when the request ends</a>, the server calls <code>Add()</code> to <em>decrement</em> the value of the counter:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">public</span> <span class="token return-type class-name"><span class="token keyword">void</span></span> <span class="token function">RequestEnd</span><span class="token punctuation">(</span><span class="token class-name"><span class="token keyword">string</span></span> protocol<span class="token punctuation">,</span> <span class="token class-name"><span class="token keyword">string</span></span> scheme<span class="token punctuation">,</span> <span class="token class-name"><span class="token keyword">string</span></span> method<span class="token punctuation">,</span> <span class="token class-name"><span class="token keyword">string</span><span class="token punctuation">?</span></span> route<span class="token punctuation">,</span> <span class="token class-name"><span class="token keyword">int</span></span> statusCode<span class="token punctuation">,</span> <span class="token class-name"><span class="token keyword">bool</span></span> unhandledRequest<span class="token punctuation">,</span> <span class="token class-name">Exception<span class="token punctuation">?</span></span> exception<span class="token punctuation">,</span> <span class="token class-name">List<span class="token punctuation">&lt;</span>KeyValuePair<span class="token punctuation">&lt;</span><span class="token keyword">string</span><span class="token punctuation">,</span> <span class="token keyword">object</span><span class="token punctuation">?</span><span class="token punctuation">&gt;</span><span class="token punctuation">&gt;</span><span class="token punctuation">?</span></span> customTags<span class="token punctuation">,</span> <span class="token class-name"><span class="token keyword">long</span></span> startTimestamp<span class="token punctuation">,</span> <span class="token class-name"><span class="token keyword">long</span></span> currentTimestamp<span class="token punctuation">,</span> <span class="token class-name"><span class="token keyword">bool</span></span> disableHttpRequestDurationMetric<span class="token punctuation">)</span>
<span class="token punctuation">{</span>
    <span class="token class-name"><span class="token keyword">var</span></span> tags <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token constructor-invocation class-name">TagList</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token function">InitializeRequestTags</span><span class="token punctuation">(</span><span class="token keyword">ref</span> tags<span class="token punctuation">,</span> scheme<span class="token punctuation">,</span> method<span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token comment">// Tags must match request start.</span>
    <span class="token keyword">if</span> <span class="token punctuation">(</span>_activeRequestsCounter<span class="token punctuation">.</span>Enabled<span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        _activeRequestsCounter<span class="token punctuation">.</span><span class="token function">Add</span><span class="token punctuation">(</span><span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">,</span> tags<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>

    <span class="token comment">// ...</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>Consequently, the <code>UpDownCounter&lt;T&gt;</code> receives a series of increment/decrement values representing the movement of the metric.</p> <h3 id="observableupdowncountert" class="heading-with-anchor"><a href="https://andrewlock.net/creating-standard-and-observable-instruments/#observableupdowncountert" class="relative text-zinc-800 dark:text-white no-underline hover:underline"><code>ObservableUpDownCounter&lt;T&gt;</code></a></h3> <p>The <code>ObservableUpDownCounter&lt;T&gt;</code> is similar to the <code>UpDownCounter&lt;T&gt;</code> in that it reports increasing or decreasing values of a metric. The difference is that it returns the absolute value of the metric when observed, as opposed to a stream of deltas.</p> <p>For example, the <code>dotnet.gc.last_collection.heap.size</code> metric is an <code>ObservableUpDownCounter&lt;long&gt;</code> that reports <code>"The managed GC heap size (including fragmentation), as observed during the latest garbage collection"</code>:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp">s_meter<span class="token punctuation">.</span><span class="token function">CreateObservableUpDownCounter</span><span class="token punctuation">(</span>
    <span class="token string">"dotnet.gc.last_collection.heap.size"</span><span class="token punctuation">,</span>
    GetHeapSizes<span class="token punctuation">,</span>
    <span class="token named-parameter punctuation">unit</span><span class="token punctuation">:</span> <span class="token string">"By"</span><span class="token punctuation">,</span>
    <span class="token named-parameter punctuation">description</span><span class="token punctuation">:</span> <span class="token string">"The managed GC heap size (including fragmentation), as observed during the latest garbage collection."</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre></div> <p>When observed, the <code>GetHeapSizes()</code> method is invoked and returns a collection of <code>Measurement</code>s, each tagged by the heap generation name:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">private</span> <span class="token keyword">static</span> <span class="token keyword">readonly</span> <span class="token class-name"><span class="token keyword">string</span><span class="token punctuation">[</span><span class="token punctuation">]</span></span> s_genNames <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token string">"gen0"</span><span class="token punctuation">,</span> <span class="token string">"gen1"</span><span class="token punctuation">,</span> <span class="token string">"gen2"</span><span class="token punctuation">,</span> <span class="token string">"loh"</span><span class="token punctuation">,</span> <span class="token string">"poh"</span><span class="token punctuation">]</span><span class="token punctuation">;</span>
<span class="token keyword">private</span> <span class="token keyword">static</span> <span class="token keyword">readonly</span> <span class="token class-name"><span class="token keyword">int</span></span> s_maxGenerations <span class="token operator">=</span> Math<span class="token punctuation">.</span><span class="token function">Min</span><span class="token punctuation">(</span>GC<span class="token punctuation">.</span><span class="token function">GetGCMemoryInfo</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span>GenerationInfo<span class="token punctuation">.</span>Length<span class="token punctuation">,</span> s_genNames<span class="token punctuation">.</span>Length<span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token keyword">private</span> <span class="token keyword">static</span> <span class="token return-type class-name">IEnumerable<span class="token punctuation">&lt;</span>Measurement<span class="token punctuation">&lt;</span><span class="token keyword">long</span><span class="token punctuation">&gt;</span><span class="token punctuation">&gt;</span></span> <span class="token function">GetHeapSizes</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token punctuation">{</span>
    <span class="token class-name">GCMemoryInfo</span> gcInfo <span class="token operator">=</span> GC<span class="token punctuation">.</span><span class="token function">GetGCMemoryInfo</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token class-name"><span class="token keyword">int</span></span> i <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span> i <span class="token operator">&lt;</span> s_maxGenerations<span class="token punctuation">;</span> <span class="token operator">++</span>i<span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        <span class="token keyword">yield</span> <span class="token keyword">return</span> <span class="token keyword">new</span> <span class="token constructor-invocation class-name">Measurement<span class="token punctuation">&lt;</span><span class="token keyword">long</span><span class="token punctuation">&gt;</span></span><span class="token punctuation">(</span>gcInfo<span class="token punctuation">.</span>GenerationInfo<span class="token punctuation">[</span>i<span class="token punctuation">]</span><span class="token punctuation">.</span>SizeAfterBytes<span class="token punctuation">,</span> <span class="token keyword">new</span> <span class="token constructor-invocation class-name">KeyValuePair<span class="token punctuation">&lt;</span><span class="token keyword">string</span><span class="token punctuation">,</span> <span class="token keyword">object</span><span class="token punctuation">?</span><span class="token punctuation">&gt;</span></span><span class="token punctuation">(</span><span class="token string">"gc.heap.generation"</span><span class="token punctuation">,</span> s_genNames<span class="token punctuation">[</span>i<span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>This returns the size of each heap at the last GC collection, the value of which may obviously increase or decrease.</p> <h3 id="gauget" class="heading-with-anchor"><a href="https://andrewlock.net/creating-standard-and-observable-instruments/#gauget" class="relative text-zinc-800 dark:text-white no-underline hover:underline"><code>Gauge&lt;T&gt;</code></a></h3> <p>The <code>Gauge&lt;T&gt;</code> is used to record "non-additive" values whenever they occur. These values can go up and down, and be positive or negative, but the point is that they "overwrite" all previous values.</p> <p>Interestingly, this <code>Instrument</code> type was only added in .NET 9, and I couldn't find a single case of <code>Gauge&lt;T&gt;</code> being used in the .NET runtime, ASP.NET Core, or the .NET extensions packages 😅 So I made one up: for example, consider a gauge that reports the current room temperature when it changes:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token class-name"><span class="token keyword">var</span></span> instrument <span class="token operator">=</span> _meter<span class="token punctuation">.</span><span class="token generic-method"><span class="token function">CreateGauge</span><span class="token generic class-name"><span class="token punctuation">&lt;</span><span class="token keyword">double</span><span class="token punctuation">&gt;</span></span></span><span class="token punctuation">(</span>
    <span class="token named-parameter punctuation">name</span><span class="token punctuation">:</span> <span class="token string">"locations.room.temperature"</span><span class="token punctuation">,</span>
    <span class="token named-parameter punctuation">unit</span><span class="token punctuation">:</span> <span class="token string">"°C"</span><span class="token punctuation">,</span>
    <span class="token named-parameter punctuation">description</span><span class="token punctuation">:</span> <span class="token string">"Current room temperature"</span>
<span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre></div> <p>Then when the temperature of the room changes, you would report the new value:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">public</span> <span class="token return-type class-name"><span class="token keyword">void</span></span> <span class="token function">OnOfficeTemperatureChanged</span><span class="token punctuation">(</span><span class="token class-name"><span class="token keyword">double</span></span> newTemperature<span class="token punctuation">)</span>
<span class="token punctuation">{</span>
    instrument<span class="token punctuation">.</span><span class="token function">Record</span><span class="token punctuation">(</span>newTemperature<span class="token punctuation">,</span> <span class="token keyword">new</span> <span class="token constructor-invocation class-name">KeyValuePair<span class="token punctuation">&lt;</span><span class="token keyword">string</span><span class="token punctuation">,</span> <span class="token keyword">object</span><span class="token punctuation">?</span><span class="token punctuation">&gt;</span></span><span class="token punctuation">(</span><span class="token string">"room"</span><span class="token punctuation">,</span> <span class="token string">"office"</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>The gauge values are record whenever the temperature changes.</p> <h3 id="observablegauget" class="heading-with-anchor"><a href="https://andrewlock.net/creating-standard-and-observable-instruments/#observablegauget" class="relative text-zinc-800 dark:text-white no-underline hover:underline"><code>ObservableGauge&lt;T&gt;</code></a></h3> <p>Conceptually the <code>ObservableGauge&lt;T&gt;</code> is the same as a <code>Gauge&lt;T&gt;</code>, except that it only produces a value when observed. <code>ObservableGauge&lt;T&gt;</code> was added way back in .NET 6, and there are some examples of its use in this case.</p> <p>For example, <a href="https://github.com/dotnet/extensions/blob/9974fbf7a3fede68d7e5f22b9b249aebd819a26d/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsSnapshotProvider.cs#L94">the <code>process.cpu.utilization</code> metric</a> is an <code>ObservableGauge&lt;double&gt;</code> instrument which reports <code>"The CPU consumption of the running application in range [0, 1]"</code>.</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp">_ <span class="token operator">=</span> meter<span class="token punctuation">.</span><span class="token function">CreateObservableGauge</span><span class="token punctuation">(</span>
    <span class="token named-parameter punctuation">name</span><span class="token punctuation">:</span> <span class="token string">"process.cpu.utilization"</span><span class="token punctuation">,</span>
    <span class="token named-parameter punctuation">observeValue</span><span class="token punctuation">:</span> CpuPercentage<span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre></div> <p>When observed, <a href="https://github.com/dotnet/extensions/blob/9974fbf7a3fede68d7e5f22b9b249aebd819a26d/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsSnapshotProvider.cs#L168">the <code>CpuPercentage()</code> method</a> is invoked, which returns a single value for the CPU usage as a value between <code>0</code> and <code>1</code>.</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">private</span> <span class="token return-type class-name"><span class="token keyword">double</span></span> <span class="token function">CpuPercentage</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token punctuation">{</span>
    <span class="token comment">// see above link for implementation</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>This <code>Instrument</code> is exposed in the <code>Microsoft.Extensions.Diagnostics.ResourceMonitoring</code> meter, and implemented in <a href="https://www.nuget.org/packages/Microsoft.Extensions.Diagnostics.ResourceMonitoring">the Microsoft.Extensions.Diagnostics.ResourceMonitoring NuGet package</a>.</p> <h3 id="histogramt" class="heading-with-anchor"><a href="https://andrewlock.net/creating-standard-and-observable-instruments/#histogramt" class="relative text-zinc-800 dark:text-white no-underline hover:underline"><code>Histogram&lt;T&gt;</code></a></h3> <p>The final instrument type is <code>Histogram&lt;T&gt;</code>, which is used to report arbitrary values, that you will typically want to aggregate using statistics.</p> <p>For example, <a href="https://github.com/dotnet/aspnetcore/blob/9a93048bd0afd7c2d09bdb5ce47ef7d78827c647/src/Hosting/Hosting/src/Internal/HostingMetrics.cs#L29">the <code>http.server.request.duration</code> metric</a> is a <code>Histogram&lt;double&gt;</code> which records the <code>"Duration of HTTP server requests."</code>. Durations and latencies are a classic example of where you might want to use a histogram, so that you can calculate the p50, p90, p99 etc latencies, or to record <em>all</em> the values and plot them as a graph.</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp">_requestDuration <span class="token operator">=</span> _meter<span class="token punctuation">.</span><span class="token generic-method"><span class="token function">CreateHistogram</span><span class="token generic class-name"><span class="token punctuation">&lt;</span><span class="token keyword">double</span><span class="token punctuation">&gt;</span></span></span><span class="token punctuation">(</span>
    <span class="token string">"http.server.request.duration"</span><span class="token punctuation">,</span>
    <span class="token named-parameter punctuation">unit</span><span class="token punctuation">:</span> <span class="token string">"s"</span><span class="token punctuation">,</span>
    <span class="token named-parameter punctuation">description</span><span class="token punctuation">:</span> <span class="token string">"Duration of HTTP server requests."</span><span class="token punctuation">,</span>
    <span class="token named-parameter punctuation">advice</span><span class="token punctuation">:</span> <span class="token keyword">new</span> <span class="token constructor-invocation class-name">InstrumentAdvice<span class="token punctuation">&lt;</span><span class="token keyword">double</span><span class="token punctuation">&gt;</span></span> <span class="token punctuation">{</span> HistogramBucketBoundaries <span class="token operator">=</span> MetricsConstants<span class="token punctuation">.</span>ShortSecondsBucketBoundaries <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre></div> <blockquote> <p>The example above also shows our first example of <code>InstrumentAdvice&lt;T&gt;</code>. This type provides suggested configuration settings for consumers, indicating the best settings to use when processing <code>Instrument</code> values. In this case, the advice provides a suggested set of histogram bucket boundaries: <code>[0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10]</code>, which can be useful for consumers to know how best to plot the metric values.</p> </blockquote> <p>The <code>_requestDuration</code> histogram instrument is called <a href="https://github.com/dotnet/aspnetcore/blob/9ea8e2c28c695650c619a89b1edf2d6d6a75da67/src/Hosting/Hosting/src/Internal/HostingMetrics.cs#L45">whenever an ASP.NET Core request ends</a>, recording the duration of the request, and a large associated number of tags. I've reproduced all the code below for completeness (expanding tag constants for clarity) but it's basically just building up a collection of tags which are recorded along with the duration of the request.</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">public</span> <span class="token return-type class-name"><span class="token keyword">void</span></span> <span class="token function">RequestEnd</span><span class="token punctuation">(</span><span class="token class-name"><span class="token keyword">string</span></span> protocol<span class="token punctuation">,</span> <span class="token class-name"><span class="token keyword">string</span></span> scheme<span class="token punctuation">,</span> <span class="token class-name"><span class="token keyword">string</span></span> method<span class="token punctuation">,</span> <span class="token class-name"><span class="token keyword">string</span><span class="token punctuation">?</span></span> route<span class="token punctuation">,</span> <span class="token class-name"><span class="token keyword">int</span></span> statusCode<span class="token punctuation">,</span> <span class="token class-name"><span class="token keyword">bool</span></span> unhandledRequest<span class="token punctuation">,</span> <span class="token class-name">Exception<span class="token punctuation">?</span></span> exception<span class="token punctuation">,</span> <span class="token class-name">List<span class="token punctuation">&lt;</span>KeyValuePair<span class="token punctuation">&lt;</span><span class="token keyword">string</span><span class="token punctuation">,</span> <span class="token keyword">object</span><span class="token punctuation">?</span><span class="token punctuation">&gt;</span><span class="token punctuation">&gt;</span><span class="token punctuation">?</span></span> customTags<span class="token punctuation">,</span> <span class="token class-name"><span class="token keyword">long</span></span> startTimestamp<span class="token punctuation">,</span> <span class="token class-name"><span class="token keyword">long</span></span> currentTimestamp<span class="token punctuation">,</span> <span class="token class-name"><span class="token keyword">bool</span></span> disableHttpRequestDurationMetric<span class="token punctuation">)</span>
<span class="token punctuation">{</span>
    <span class="token class-name"><span class="token keyword">var</span></span> tags <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token constructor-invocation class-name">TagList</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token function">InitializeRequestTags</span><span class="token punctuation">(</span><span class="token keyword">ref</span> tags<span class="token punctuation">,</span> scheme<span class="token punctuation">,</span> method<span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>disableHttpRequestDurationMetric <span class="token operator">&amp;&amp;</span> _requestDuration<span class="token punctuation">.</span>Enabled<span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        <span class="token keyword">if</span> <span class="token punctuation">(</span>HostingTelemetryHelpers<span class="token punctuation">.</span><span class="token function">TryGetHttpVersion</span><span class="token punctuation">(</span>protocol<span class="token punctuation">,</span> <span class="token keyword">out</span> <span class="token class-name"><span class="token keyword">var</span></span> httpVersion<span class="token punctuation">)</span><span class="token punctuation">)</span>
        <span class="token punctuation">{</span>
            tags<span class="token punctuation">.</span><span class="token function">Add</span><span class="token punctuation">(</span><span class="token string">"network.protocol.version"</span><span class="token punctuation">,</span> httpVersion<span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token punctuation">}</span>
        <span class="token keyword">if</span> <span class="token punctuation">(</span>unhandledRequest<span class="token punctuation">)</span>
        <span class="token punctuation">{</span>
            tags<span class="token punctuation">.</span><span class="token function">Add</span><span class="token punctuation">(</span><span class="token string">"aspnetcore.request.is_unhandled"</span><span class="token punctuation">,</span> <span class="token boolean">true</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token punctuation">}</span>

        <span class="token comment">// Add information gathered during request.</span>
        tags<span class="token punctuation">.</span><span class="token function">Add</span><span class="token punctuation">(</span><span class="token string">"http.response.status_code"</span><span class="token punctuation">,</span> HostingTelemetryHelpers<span class="token punctuation">.</span><span class="token function">GetBoxedStatusCode</span><span class="token punctuation">(</span>statusCode<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token keyword">if</span> <span class="token punctuation">(</span>route <span class="token operator">!=</span> <span class="token keyword">null</span><span class="token punctuation">)</span>
        <span class="token punctuation">{</span>
            tags<span class="token punctuation">.</span><span class="token function">Add</span><span class="token punctuation">(</span><span class="token string">"http.route"</span><span class="token punctuation">,</span> RouteDiagnosticsHelpers<span class="token punctuation">.</span><span class="token function">ResolveHttpRoute</span><span class="token punctuation">(</span>route<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token punctuation">}</span>

        <span class="token comment">// Add before some built in tags so custom tags are prioritized when dealing with duplicates.</span>
        <span class="token keyword">if</span> <span class="token punctuation">(</span>customTags <span class="token operator">!=</span> <span class="token keyword">null</span><span class="token punctuation">)</span>
        <span class="token punctuation">{</span>
            <span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token class-name"><span class="token keyword">var</span></span> i <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span> i <span class="token operator">&lt;</span> customTags<span class="token punctuation">.</span>Count<span class="token punctuation">;</span> i<span class="token operator">++</span><span class="token punctuation">)</span>
            <span class="token punctuation">{</span>
                tags<span class="token punctuation">.</span><span class="token function">Add</span><span class="token punctuation">(</span>customTags<span class="token punctuation">[</span>i<span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
            <span class="token punctuation">}</span>
        <span class="token punctuation">}</span>

        <span class="token comment">// This exception is only present if there is an unhandled exception.</span>
        <span class="token comment">// An exception caught by ExceptionHandlerMiddleware and DeveloperExceptionMiddleware isn't thrown to here. Instead, those middleware add error.type to custom tags.</span>
        <span class="token keyword">if</span> <span class="token punctuation">(</span>exception <span class="token operator">!=</span> <span class="token keyword">null</span><span class="token punctuation">)</span>
        <span class="token punctuation">{</span>
            <span class="token comment">// Exception tag could have been added by middleware. If an exception is later thrown in request pipeline</span>
            <span class="token comment">// then we don't want to add a duplicate tag here because that breaks some metrics systems.</span>
            tags<span class="token punctuation">.</span><span class="token function">TryAddTag</span><span class="token punctuation">(</span><span class="token string">"error.type"</span><span class="token punctuation">,</span> exception<span class="token punctuation">.</span><span class="token function">GetType</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span>FullName<span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token punctuation">}</span>
        <span class="token keyword">else</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>HostingTelemetryHelpers<span class="token punctuation">.</span><span class="token function">IsErrorStatusCode</span><span class="token punctuation">(</span>statusCode<span class="token punctuation">)</span><span class="token punctuation">)</span>
        <span class="token punctuation">{</span>
            <span class="token comment">// Add error.type for 5xx status codes when there's no exception.</span>
            tags<span class="token punctuation">.</span><span class="token function">TryAddTag</span><span class="token punctuation">(</span><span class="token string">"error.type"</span><span class="token punctuation">,</span> statusCode<span class="token punctuation">.</span><span class="token function">ToString</span><span class="token punctuation">(</span>CultureInfo<span class="token punctuation">.</span>InvariantCulture<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token punctuation">}</span>

        <span class="token class-name"><span class="token keyword">var</span></span> duration <span class="token operator">=</span> Stopwatch<span class="token punctuation">.</span><span class="token function">GetElapsedTime</span><span class="token punctuation">(</span>startTimestamp<span class="token punctuation">,</span> currentTimestamp<span class="token punctuation">)</span><span class="token punctuation">;</span>
        _requestDuration<span class="token punctuation">.</span><span class="token function">Record</span><span class="token punctuation">(</span>duration<span class="token punctuation">.</span>TotalSeconds<span class="token punctuation">,</span> tags<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>It's an interesting point to note that while the histogram is strictly about request durations, the presence of the many tags could enable you to derive various other metrics. For example, you could determine the number of "successful" requests, the number of requests to a particular route, or with a given status code.</p> <p>And that's it, we've covered all of the <code>Insturment</code> types currently available in .NET 10. Note that there's no <code>ObservableHistogram&lt;T&gt;</code> type, as that generally wouldn't be practical to implement.</p> <p>We now know how to create all the different types of <code>Instrument</code>, and in the <a href="https://andrewlock.net/creating-and-consuming-metrics-with-system-diagnostics-metrics-apis/">first post of this series</a> I showed how to record the metrics using <code>dotnet-counters</code>. In the following post in this series, we'll look at how to record these values in-process instead.</p> <h2 id="summary" class="heading-with-anchor"><a href="https://andrewlock.net/creating-standard-and-observable-instruments/#summary" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Summary</a></h2> <p>In this post, I described each of the different <code>Instrument&lt;T&gt;</code> types exposed by the <em>System.Diagnostics.Metrics</em> APIs. For each type I described when you would use it and provided an example of both how to create the <code>Instrument&lt;T&gt;</code>, and how to record values, using examples from the .NET base class libraries and ASP.NET Core. In the next post we'll look at how to record values produced by <code>Instrument&lt;T&gt;</code> types in-process.</p> ]]></content:encoded><category><![CDATA[Observability;.NET Core]]></category></item><item><title><![CDATA[Exploring the (underwhelming) System.Diagnostics.Metrics source generators: System.Diagnostics.Metrics APIs - Part 2]]></title><description><![CDATA[In this post I explore the source generators shipped in Microsoft.Extensions.Telemetry.Abstractions, explore the code, and discuss whether I would use them]]></description><link>https://andrewlock.net/creating-strongly-typed-metics-with-a-source-generator/</link><guid isPermaLink="true">https://andrewlock.net/creating-strongly-typed-metics-with-a-source-generator/</guid><pubDate>Tue, 03 Feb 2026 10:00:00 GMT</pubDate><dc:creator><![CDATA[Andrew Lock]]></dc:creator><media:content url="https://andrewlock.net/content/images/2026/metrics_banner_2.png" medium="image" /><content:encoded><![CDATA[<img src="https://andrewlock.net/content/images/2026/metrics_banner_2.png" /><nav><p>This is the second post in the series: <a href="https://andrewlock.net/series/system-diagnostics-metrics-apis/">System.Diagnostics.Metrics APIs</a>. </p> <ol class="list-none"><li><a href="https://andrewlock.net/creating-and-consuming-metrics-with-system-diagnostics-metrics-apis/">Part 1 - Creating and consuming metrics with System.Diagnostics.Metrics APIs</a></li><li>Part 2 - Exploring the (underwhelming) System.Diagnostics.Metrics source generators (this post) </li><li><a href="https://andrewlock.net/creating-standard-and-observable-instruments/">Part 3 - Creating standard and "observable" instruments</a></li><li><a href="https://andrewlock.net/recording-metrics-in-process-using-meterlistener/">Part 4 - Recording metrics in-process using MeterListener</a></li></ol></nav><p>In my <a href="https://andrewlock.net/creating-and-consuming-metrics-with-system-diagnostics-metrics-apis/">previous post</a> I provided an introduction to the <em>System.Diagnostics.Metrics</em> APIs introduced in .NET 6. In this post I show how to use the <a href="https://www.nuget.org/packages/Microsoft.Extensions.Telemetry.Abstractions">Microsoft.Extensions.Telemetry.Abstractions</a> source generator, explore how it changes the code you need to write, and explore the generated code.</p> <p>I start the post with a quick refresher on the basics of the <em>System.Diagnostics.Metrics</em> APIs and the sample app we wrote <a href="https://andrewlock.net/creating-and-consuming-metrics-with-system-diagnostics-metrics-apis/">last time</a>. I then show how we can update this code to use the <em>Microsoft.Extensions.Telemetry.Abstractions</em> source generator instead. Finally, I show how we can also update our metric definitions to use strongly-typed tag objects for additional type-safety. In both cases, we'll update our sample app to use the new approach, and explore the generated code.</p> <blockquote> <p>You can read about the source generators I discuss in this post in the Microsoft documentation <a href="https://learn.microsoft.com/en-us/dotnet/core/diagnostics/metrics-generator">here</a> and <a href="https://learn.microsoft.com/en-us/dotnet/core/diagnostics/metrics-strongly-typed">here</a>.</p> </blockquote> <h2 id="background-system-diagnostics-metrics-apis" class="heading-with-anchor"><a href="https://andrewlock.net/creating-strongly-typed-metics-with-a-source-generator/#background-system-diagnostics-metrics-apis" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Background: System.Diagnostics.Metrics APIs</a></h2> <p>The <em>System.Diagnostics.Metrics</em> APIs were introduced in .NET 6 but are available in earlier runtimes (including .NET Framework) by using the <a href="https://www.nuget.org/packages/System.Diagnostics.DiagnosticSource/"><em>System.Diagnostics.DiagnosticSource</em></a> NuGet package. There are two primary concepts exposed by these APIs; <code>Instrument</code> and <code>Meter</code>:</p> <ul><li><code>Instrument</code>: An instrument records the values for a single metric of interest. You might have separate <code>Instrument</code>s for "products sold", "invoices created", "invoice total", or "GC heap size".</li> <li><code>Meter</code>: A <code>Meter</code> is a logical grouping of multiple instruments. For example, the <a href="https://learn.microsoft.com/en-us/dotnet/core/diagnostics/built-in-metrics-runtime"><code>System.Runtime</code> <code>Meter</code></a> contains multiple <code>Instrument</code>s about the workings of the runtime, while <a href="https://learn.microsoft.com/en-us/aspnet/core/log-mon/metrics/built-in?view=aspnetcore-10.0#microsoftaspnetcorehosting">the <code>Microsoft.AspNetCore.Hosting</code> <code>Meter</code></a> contains <code>Instrument</code>s about the HTTP requests received by ASP.NET Core.</li></ul> <p>There are also multiple types of <code>Instrument</code>: <code>Counter&lt;T&gt;</code>, <code>UpDownCounter&lt;T&gt;</code>, <code>Gauge&lt;T&gt;</code>, and <code>Histogram&lt;T&gt;</code> (as well as "observable" versions, which I'll cover in a future post). To create a custom metric, you need to choose the type of <code>Instrument</code> to use, and associate it with a <code>Meter</code>. In my <a href="https://andrewlock.net/creating-and-consuming-metrics-with-system-diagnostics-metrics-apis/">previous post</a> I created a simple <code>Counter&lt;T&gt;</code> for tracking how often a product page was viewed.</p> <h2 id="background-sample-app-with-manual-boilerplate" class="heading-with-anchor"><a href="https://andrewlock.net/creating-strongly-typed-metics-with-a-source-generator/#background-sample-app-with-manual-boilerplate" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Background: sample app with manual boilerplate</a></h2> <p>In this post I'm going to start from where we left off in the previous post, and update it to use a source generator instead. So that we know where we're coming from, the full code for that sample is shown below, annotated to explain what's going on; for the full details, see my <a href="https://andrewlock.net/creating-and-consuming-metrics-with-system-diagnostics-metrics-apis/">previous post</a></p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">using</span> <span class="token namespace">System<span class="token punctuation">.</span>Diagnostics<span class="token punctuation">.</span>Metrics</span><span class="token punctuation">;</span>
<span class="token keyword">using</span> <span class="token namespace">Microsoft<span class="token punctuation">.</span>Extensions<span class="token punctuation">.</span>Diagnostics<span class="token punctuation">.</span>Metrics</span><span class="token punctuation">;</span>

<span class="token class-name"><span class="token keyword">var</span></span> builder <span class="token operator">=</span> WebApplication<span class="token punctuation">.</span><span class="token function">CreateBuilder</span><span class="token punctuation">(</span>args<span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token comment">// 👇 Register our "metrics helper" in DI</span>
builder<span class="token punctuation">.</span>Services<span class="token punctuation">.</span><span class="token generic-method"><span class="token function">AddSingleton</span><span class="token generic class-name"><span class="token punctuation">&lt;</span>ProductMetrics<span class="token punctuation">&gt;</span></span></span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token class-name"><span class="token keyword">var</span></span> app <span class="token operator">=</span> builder<span class="token punctuation">.</span><span class="token function">Build</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token comment">// Inject the "metrics helper" into the API handler 👇 </span>
app<span class="token punctuation">.</span><span class="token function">MapGet</span><span class="token punctuation">(</span><span class="token string">"/product/{id}"</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token class-name"><span class="token keyword">int</span></span> id<span class="token punctuation">,</span> <span class="token class-name">ProductMetrics</span> metrics<span class="token punctuation">)</span> <span class="token operator">=&gt;</span>
<span class="token punctuation">{</span>
    metrics<span class="token punctuation">.</span><span class="token function">PricingPageViewed</span><span class="token punctuation">(</span>id<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// 👈 Record the metric</span>
    <span class="token keyword">return</span> <span class="token interpolation-string"><span class="token string">$"Details for product </span><span class="token interpolation"><span class="token punctuation">{</span><span class="token expression language-csharp">id</span><span class="token punctuation">}</span></span><span class="token string">"</span></span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

app<span class="token punctuation">.</span><span class="token function">Run</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>


<span class="token comment">// The "metrics helper" class for our metrics</span>
<span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">ProductMetrics</span>
<span class="token punctuation">{</span>
    <span class="token keyword">private</span> <span class="token keyword">readonly</span> <span class="token class-name">Counter<span class="token punctuation">&lt;</span><span class="token keyword">long</span><span class="token punctuation">&gt;</span></span> _pricingDetailsViewed<span class="token punctuation">;</span>

    <span class="token keyword">public</span> <span class="token function">ProductMetrics</span><span class="token punctuation">(</span><span class="token class-name">IMeterFactory</span> meterFactory<span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        <span class="token comment">// Create a meter called MyApp.Products</span>
        <span class="token class-name"><span class="token keyword">var</span></span> meter <span class="token operator">=</span> meterFactory<span class="token punctuation">.</span><span class="token function">Create</span><span class="token punctuation">(</span><span class="token string">"MyApp.Products"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

        <span class="token comment">// Create an instrument, and associate it with our meter</span>
        _pricingDetailsViewed <span class="token operator">=</span> meter<span class="token punctuation">.</span><span class="token generic-method"><span class="token function">CreateCounter</span><span class="token generic class-name"><span class="token punctuation">&lt;</span><span class="token keyword">int</span><span class="token punctuation">&gt;</span></span></span><span class="token punctuation">(</span>
            <span class="token string">"myapp.products.pricing_page_requests"</span><span class="token punctuation">,</span>
            <span class="token named-parameter punctuation">unit</span><span class="token punctuation">:</span> <span class="token string">"requests"</span><span class="token punctuation">,</span>
            <span class="token named-parameter punctuation">description</span><span class="token punctuation">:</span> <span class="token string">"The number of requests to the pricing details page for the product with the given product_id"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token punctuation">}</span>

    <span class="token comment">// A convenience method for adding to the metric</span>
    <span class="token keyword">public</span> <span class="token return-type class-name"><span class="token keyword">void</span></span> <span class="token function">PricingPageViewed</span><span class="token punctuation">(</span><span class="token class-name"><span class="token keyword">int</span></span> id<span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        <span class="token comment">// Ensure we add the correct tag to the metric</span>
        _pricingDetailsViewed<span class="token punctuation">.</span><span class="token function">Add</span><span class="token punctuation">(</span><span class="token named-parameter punctuation">delta</span><span class="token punctuation">:</span> <span class="token number">1</span><span class="token punctuation">,</span> <span class="token keyword">new</span> <span class="token constructor-invocation class-name">KeyValuePair<span class="token punctuation">&lt;</span><span class="token keyword">string</span><span class="token punctuation">,</span> <span class="token keyword">object</span><span class="token punctuation">?</span><span class="token punctuation">&gt;</span></span><span class="token punctuation">(</span><span class="token string">"product_id"</span><span class="token punctuation">,</span> id<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>In summary, we have a <code>ProductMetrics</code> "metrics helper" class which is responsible for creating the <code>Meter</code> and <code>Instrument</code> definitions, as well as providing helper methods for recording page views.</p> <p>When we run the app and <a href="https://andrewlock.net/creating-and-consuming-metrics-with-system-diagnostics-metrics-apis/#collecting-metrics-with-dotnet-counters">monitor it with <code>dotnet-counters</code></a> we can see our metric being recorded:</p> <p><img src="https://andrewlock.net/content/images/2026/metrics.png" alt="Showing the metrics being reported using dotnet-counters"></p> <p>Now that we have our sample app ready, lets explore replacing some of the boilerplate with a source generator.</p> <h2 id="replacing-boiler-plate-with-a-source-generator" class="heading-with-anchor"><a href="https://andrewlock.net/creating-strongly-typed-metics-with-a-source-generator/#replacing-boiler-plate-with-a-source-generator" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Replacing boiler plate with a source generator</a></h2> <p>The <a href="https://www.nuget.org/packages/Microsoft.Extensions.Telemetry.Abstractions">Microsoft.Extensions.Telemetry.Abstractions</a> NuGet package includes a source generator which, according to <a href="https://learn.microsoft.com/en-us/dotnet/core/diagnostics/metrics-generator?tabs=dotnet-cli">the documentation</a>, generates code which:</p> <blockquote> <p>…exposes strongly typed metering types and methods that you can invoke to record metric values. The generated methods are implemented in a highly efficient form, which reduces computation overhead as compared to traditional metering solutions.</p> </blockquote> <p>In this section we'll replace some of the code we wrote above with the source generated equivalent!</p> <p>First you'll need to install the <a href="https://www.nuget.org/packages/Microsoft.Extensions.Telemetry.Abstractions">Microsoft.Extensions.Telemetry.Abstractions</a> package in your project using:</p> <div class="pre-code-wrapper"><pre class="language-bash"><code class="language-bash">dotnet <span class="token function">add</span> package Microsoft.Extensions.Telemetry.Abstractions
</code></pre></div> <p>Alternatively, update your project with a <code>&lt;PackageReference&gt;</code>:</p> <div class="pre-code-wrapper"><pre class="language-xml"><code class="language-xml"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>ItemGroup</span><span class="token punctuation">&gt;</span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PackageReference</span> <span class="token attr-name">Include</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>Microsoft.Extensions.Telemetry.Abstractions<span class="token punctuation">"</span></span> <span class="token attr-name">Version</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>10.2.0<span class="token punctuation">"</span></span> <span class="token punctuation">/&gt;</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>ItemGroup</span><span class="token punctuation">&gt;</span></span>
</code></pre></div> <blockquote> <p>Note that in this post I'm using the latest stable version of the package, 10.2.0.</p> </blockquote> <p>Now that we have the source generator running in our app, we can put it to use.</p> <h3 id="creating-the-metrics-helper-class" class="heading-with-anchor"><a href="https://andrewlock.net/creating-strongly-typed-metics-with-a-source-generator/#creating-the-metrics-helper-class" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Creating the "metrics helper" class</a></h3> <p>The main difference when you switch to the source generator is in the "metrics helper" class. There's a lot of different ways you <em>could</em> structure these—what I've shown below is a relatively close direct conversion of the previous code. But as I'll discuss later, this isn't necessarily the way you'll always want to use them.</p> <p>As is typical for source generators, the metrics generator is driven by specific attributes. There's a different attribute for each <code>Instrument</code> type, and you apply them to a <code>partial</code> method definition which creates a strongly-typed metric, called <code>PricingPageViewed</code> in this case:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">private</span> <span class="token keyword">static</span> <span class="token keyword">partial</span> <span class="token keyword">class</span> <span class="token class-name">Factory</span>
<span class="token punctuation">{</span>
    <span class="token punctuation">[</span><span class="token generic-method"><span class="token function">Counter</span><span class="token generic class-name"><span class="token punctuation">&lt;</span><span class="token keyword">int</span><span class="token punctuation">&gt;</span></span></span><span class="token punctuation">(</span><span class="token string">"product_id"</span><span class="token punctuation">,</span> Name <span class="token operator">=</span> <span class="token string">"myapp.products.pricing_page_requests"</span><span class="token punctuation">)</span><span class="token punctuation">]</span>
    <span class="token keyword">internal</span> <span class="token keyword">static</span> <span class="token keyword">partial</span> <span class="token return-type class-name">PricingPageViewed</span> <span class="token function">CreatePricingPageViewed</span><span class="token punctuation">(</span><span class="token class-name">Meter</span> meter<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>The example above uses the <code>[Counter&lt;T&gt;]</code> attribute, but there are equivalent versions for <code>[Gauge&lt;T&gt;]</code> and <code>[Histogram&lt;T&gt;]</code> too.</p> <p>This creates the "factory" methods for defining a metric, but we still need to update the <code>ProductMetrics</code> type to <em>use</em> this factory method instead of our hand-rolled versions:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token comment">// Note, must be partial</span>
<span class="token keyword">public</span> <span class="token keyword">partial</span> <span class="token keyword">class</span> <span class="token class-name">ProductMetrics</span>
<span class="token punctuation">{</span>
    <span class="token keyword">public</span> <span class="token function">ProductMetrics</span><span class="token punctuation">(</span><span class="token class-name">IMeterFactory</span> meterFactory<span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        <span class="token class-name"><span class="token keyword">var</span></span> meter <span class="token operator">=</span> meterFactory<span class="token punctuation">.</span><span class="token function">Create</span><span class="token punctuation">(</span><span class="token string">"MyApp.Products"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        PricingPageViewed <span class="token operator">=</span> Factory<span class="token punctuation">.</span><span class="token function">CreatePricingPageViewed</span><span class="token punctuation">(</span>meter<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>

    <span class="token keyword">internal</span> <span class="token return-type class-name">PricingPageViewed</span> PricingPageViewed <span class="token punctuation">{</span> <span class="token keyword">get</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>

    <span class="token keyword">private</span> <span class="token keyword">static</span> <span class="token keyword">partial</span> <span class="token keyword">class</span> <span class="token class-name">Factory</span>
    <span class="token punctuation">{</span>
        <span class="token punctuation">[</span><span class="token generic-method"><span class="token function">Counter</span><span class="token generic class-name"><span class="token punctuation">&lt;</span><span class="token keyword">int</span><span class="token punctuation">&gt;</span></span></span><span class="token punctuation">(</span><span class="token string">"product_id"</span><span class="token punctuation">,</span> Name <span class="token operator">=</span> <span class="token string">"myapp.products.pricing_page_requests"</span><span class="token punctuation">)</span><span class="token punctuation">]</span>
        <span class="token keyword">internal</span> <span class="token keyword">static</span> <span class="token keyword">partial</span> <span class="token return-type class-name">PricingPageViewed</span> <span class="token function">CreatePricingPageViewed</span><span class="token punctuation">(</span><span class="token class-name">Meter</span> meter<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>If you compare that to the code we wrote previously, there are two main differences:</p> <ul><li>The <code>[Counter&lt;T&gt;]</code> attribute is missing the "description" and "units" that we previously added.</li> <li>The <code>PricingPageViewed</code> metric is exposed directly (which we'll look at shortly), instead of exposing a <code>PricingPageViewed()</code> method for recording values.</li></ul> <p>The first point is just a limitation of the current API. We actually <em>can</em> specify the units on the attribute, but if we do, we need to add a <code>#pragma</code> as this API is currently experimental:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">private</span> <span class="token keyword">static</span> <span class="token keyword">partial</span> <span class="token keyword">class</span> <span class="token class-name">Factory</span>
<span class="token punctuation">{</span>
    <span class="token preprocessor property">#<span class="token directive keyword">pragma</span> warning disable EXTEXP0003 </span><span class="token comment">// Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.</span>

                                                        <span class="token comment">//   Add the Unit here 👇</span>
    <span class="token punctuation">[</span><span class="token generic-method"><span class="token function">Counter</span><span class="token generic class-name"><span class="token punctuation">&lt;</span><span class="token keyword">int</span><span class="token punctuation">&gt;</span></span></span><span class="token punctuation">(</span><span class="token string">"product_id"</span><span class="token punctuation">,</span> Name <span class="token operator">=</span> <span class="token string">"myapp.products.pricing_page_requests"</span><span class="token punctuation">,</span> Unit <span class="token operator">=</span> <span class="token string">"views"</span><span class="token punctuation">)</span><span class="token punctuation">]</span>
    <span class="token keyword">internal</span> <span class="token keyword">static</span> <span class="token keyword">partial</span> <span class="token return-type class-name">PricingPageViewed</span> <span class="token function">CreatePricingPageViewed</span><span class="token punctuation">(</span><span class="token class-name">Meter</span> meter<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>The second point is more interesting, and we'll dig into it when we look at the generated code.</p> <h3 id="updating-our-app" class="heading-with-anchor"><a href="https://andrewlock.net/creating-strongly-typed-metics-with-a-source-generator/#updating-our-app" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Updating our app</a></h3> <p>Before we get to the generated code, lets look at how we use our updated <code>ProductMetrics</code>. We keep the existing DI registration of our <code>ProductMetrics</code> type, the only change is how we <em>record</em> a view of the page</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">using</span> <span class="token namespace">System<span class="token punctuation">.</span>Diagnostics<span class="token punctuation">.</span>Metrics</span><span class="token punctuation">;</span>
<span class="token keyword">using</span> <span class="token namespace">System<span class="token punctuation">.</span>Globalization</span><span class="token punctuation">;</span>
<span class="token keyword">using</span> <span class="token namespace">Microsoft<span class="token punctuation">.</span>Extensions<span class="token punctuation">.</span>Diagnostics<span class="token punctuation">.</span>Metrics</span><span class="token punctuation">;</span>

<span class="token class-name"><span class="token keyword">var</span></span> builder <span class="token operator">=</span> WebApplication<span class="token punctuation">.</span><span class="token function">CreateBuilder</span><span class="token punctuation">(</span>args<span class="token punctuation">)</span><span class="token punctuation">;</span>
builder<span class="token punctuation">.</span>Services<span class="token punctuation">.</span><span class="token generic-method"><span class="token function">AddSingleton</span><span class="token generic class-name"><span class="token punctuation">&lt;</span>ProductMetrics<span class="token punctuation">&gt;</span></span></span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token class-name"><span class="token keyword">var</span></span> app <span class="token operator">=</span> builder<span class="token punctuation">.</span><span class="token function">Build</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

app<span class="token punctuation">.</span><span class="token function">MapGet</span><span class="token punctuation">(</span><span class="token string">"/product/{id}"</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token class-name"><span class="token keyword">int</span></span> id<span class="token punctuation">,</span> <span class="token class-name">ProductMetrics</span> metrics<span class="token punctuation">)</span> <span class="token operator">=&gt;</span>
<span class="token punctuation">{</span>
    <span class="token comment">// Update to call PricingPageViewed.Add() instead of PricingPageViewed(id)</span>
    metrics<span class="token punctuation">.</span>PricingPageViewed<span class="token punctuation">.</span><span class="token function">Add</span><span class="token punctuation">(</span><span class="token keyword">value</span><span class="token punctuation">:</span> <span class="token number">1</span><span class="token punctuation">,</span> <span class="token named-parameter punctuation">product_id</span><span class="token punctuation">:</span> id<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">return</span> <span class="token interpolation-string"><span class="token string">$"Details for product </span><span class="token interpolation"><span class="token punctuation">{</span><span class="token expression language-csharp">id</span><span class="token punctuation">}</span></span><span class="token string">"</span></span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

app<span class="token punctuation">.</span><span class="token function">Run</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre></div> <p>As you can see, there's not much change there. Instead of calling <code>PricingPageViewed(id)</code>, which internally adds a metric and tag, we call the <code>Add()</code> method, which is a source-generated method on the <code>PricingPageViewed</code> type. Let's take a look at all that generated code now, so we can see what's going on behind the scenes.</p> <h3 id="exploring-the-generated-code" class="heading-with-anchor"><a href="https://andrewlock.net/creating-strongly-typed-metics-with-a-source-generator/#exploring-the-generated-code" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Exploring the generated code</a></h3> <p>We have various generated methods to look at, so we'll start with our factory methods and work our way through from there.</p> <blockquote> <p>Note that in most IDEs you can navigate to the definitions of these partial methods and they'll show you the generated code.</p> </blockquote> <p>Starting with our <code>Factory</code> method, the generated code looks like this:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">public</span> <span class="token keyword">partial</span> <span class="token keyword">class</span> <span class="token class-name">ProductMetrics</span> 
<span class="token punctuation">{</span>
    <span class="token keyword">private</span> <span class="token keyword">static</span> <span class="token keyword">partial</span> <span class="token keyword">class</span> <span class="token class-name">Factory</span> 
    <span class="token punctuation">{</span>
        <span class="token keyword">internal</span> <span class="token keyword">static</span> <span class="token keyword">partial</span> <span class="token return-type class-name">PricingPageViewed</span> <span class="token function">CreatePricingPageViewed</span><span class="token punctuation">(</span><span class="token class-name">Meter</span> meter<span class="token punctuation">)</span>
            <span class="token operator">=&gt;</span> GeneratedInstrumentsFactory<span class="token punctuation">.</span><span class="token function">CreatePricingPageViewed</span><span class="token punctuation">(</span>meter<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>So the generated code is calling a <em>different</em> generated type, which looks like this:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">internal</span> <span class="token keyword">static</span> <span class="token keyword">partial</span> <span class="token keyword">class</span> <span class="token class-name">GeneratedInstrumentsFactory</span>
<span class="token punctuation">{</span>
    <span class="token keyword">private</span> <span class="token keyword">static</span> <span class="token class-name">ConcurrentDictionary<span class="token punctuation">&lt;</span>Meter<span class="token punctuation">,</span> PricingPageViewed<span class="token punctuation">&gt;</span></span> _pricingPageViewedInstruments <span class="token operator">=</span> <span class="token keyword">new</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token keyword">internal</span> <span class="token keyword">static</span> <span class="token return-type class-name">PricingPageViewed</span> <span class="token function">CreatePricingPageViewed</span><span class="token punctuation">(</span><span class="token class-name">Meter</span> meter<span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        <span class="token keyword">return</span> _pricingPageViewedInstruments<span class="token punctuation">.</span><span class="token function">GetOrAdd</span><span class="token punctuation">(</span>meter<span class="token punctuation">,</span> <span class="token keyword">static</span> _meter <span class="token operator">=&gt;</span>
            <span class="token punctuation">{</span>
                <span class="token class-name"><span class="token keyword">var</span></span> instrument <span class="token operator">=</span> _meter<span class="token punctuation">.</span><span class="token generic-method"><span class="token function">CreateCounter</span><span class="token generic class-name"><span class="token punctuation">&lt;</span><span class="token keyword">int</span><span class="token punctuation">&gt;</span></span></span><span class="token punctuation">(</span><span class="token string">@"myapp.products.pricing_page_requests"</span><span class="token punctuation">,</span> <span class="token string">@"views"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
                <span class="token keyword">return</span> <span class="token keyword">new</span> <span class="token constructor-invocation class-name">PricingPageViewed</span><span class="token punctuation">(</span>instrument<span class="token punctuation">)</span><span class="token punctuation">;</span>
            <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>This definition shows something interesting, in that it shows the source generator is catering to a pattern I was somewhat surprised to see. This code seems to be catering to adding the same <code>Instrument</code> to <em>multiple</em> <code>Meter</code>s.</p> <blockquote> <p>That seems a little surprising to me, but that's possibly because I'm used to thinking in terms of OpenTelemetry expectations, which doesn't have the concept of <code>Meter</code>s (as far as I know), and completely ignores it. It seems like you would get some weird duplication issues if you tried to use this source-generator-suggested pattern with OpenTelemetry, so I personally wouldn't recommend it.</p> </blockquote> <p>Other than the "dictionary" aspect, this generated code is basically creating the <code>Counter</code> instance, just as we were doing before, but is then passing it to a different generated type, the <code>PricingPageViewed</code> type:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">internal</span> <span class="token keyword">sealed</span> <span class="token keyword">class</span> <span class="token class-name">PricingPageViewed</span>
<span class="token punctuation">{</span>
    <span class="token keyword">private</span> <span class="token keyword">readonly</span> <span class="token class-name">Counter<span class="token punctuation">&lt;</span><span class="token keyword">int</span><span class="token punctuation">&gt;</span></span> _counter<span class="token punctuation">;</span>
    <span class="token keyword">public</span> <span class="token function">PricingPageViewed</span><span class="token punctuation">(</span><span class="token class-name">Counter<span class="token punctuation">&lt;</span><span class="token keyword">int</span><span class="token punctuation">&gt;</span></span> counter<span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        _counter <span class="token operator">=</span> counter<span class="token punctuation">;</span>
    <span class="token punctuation">}</span>

    <span class="token keyword">public</span> <span class="token return-type class-name"><span class="token keyword">void</span></span> <span class="token function">Add</span><span class="token punctuation">(</span><span class="token class-name"><span class="token keyword">int</span></span> <span class="token keyword">value</span><span class="token punctuation">,</span> <span class="token class-name"><span class="token keyword">object</span><span class="token punctuation">?</span></span> product_id<span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        <span class="token class-name"><span class="token keyword">var</span></span> tagList <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token constructor-invocation class-name">TagList</span>
        <span class="token punctuation">{</span>
            <span class="token keyword">new</span> <span class="token constructor-invocation class-name">KeyValuePair<span class="token punctuation">&lt;</span><span class="token keyword">string</span><span class="token punctuation">,</span> <span class="token keyword">object</span><span class="token punctuation">?</span><span class="token punctuation">&gt;</span></span><span class="token punctuation">(</span><span class="token string">"product_id"</span><span class="token punctuation">,</span> product_id<span class="token punctuation">)</span><span class="token punctuation">,</span>
        <span class="token punctuation">}</span><span class="token punctuation">;</span>

        _counter<span class="token punctuation">.</span><span class="token function">Add</span><span class="token punctuation">(</span><span class="token keyword">value</span><span class="token punctuation">,</span> tagList<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>This generated type provides roughly the same "public" API for recording metrics as we provided before:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">ProductMetrics</span>
<span class="token punctuation">{</span>
    <span class="token comment">// Previous implementation</span>
    <span class="token keyword">public</span> <span class="token return-type class-name"><span class="token keyword">void</span></span> <span class="token function">PricingPageViewed</span><span class="token punctuation">(</span><span class="token class-name"><span class="token keyword">int</span></span> id<span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        _pricingDetailsViewed<span class="token punctuation">.</span><span class="token function">Add</span><span class="token punctuation">(</span><span class="token named-parameter punctuation">delta</span><span class="token punctuation">:</span> <span class="token number">1</span><span class="token punctuation">,</span> <span class="token keyword">new</span> <span class="token constructor-invocation class-name">KeyValuePair<span class="token punctuation">&lt;</span><span class="token keyword">string</span><span class="token punctuation">,</span> <span class="token keyword">object</span><span class="token punctuation">?</span><span class="token punctuation">&gt;</span></span><span class="token punctuation">(</span><span class="token string">"product_id"</span><span class="token punctuation">,</span> id<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>However, there are some differences. The generated code uses a more "generic" version that wraps the type in a <code>TagList</code>. This is a <code>struct</code>, which can support adding multiple tags without needing to allocate an array on the heap, so it's <em>generally</em> very efficient. But in this case, it doesn't add anything over the "manual" version I implemented.</p> <p>So given all that, is this generated code actually <em>useful</em>?</p> <h3 id="is-the-generated-code-worth-it-" class="heading-with-anchor"><a href="https://andrewlock.net/creating-strongly-typed-metics-with-a-source-generator/#is-the-generated-code-worth-it-" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Is the generated code worth it?</a></h3> <p>I love source generators, I think they're a great way to reduce boilerplate and make code easier to read and write in many cases, but frankly, I don't really see the value of this metrics source generator.</p> <p>For a start, the source generator is only really changing how we define and create metrics. Which is generally 1 line of code to create the metric, and then a helper method for defining the tags etc (i.e. the <code>PricingPageViewed()</code> method). Is a source generator <em>really</em> necessary for that?</p> <p>Also, the generator is limited in the API it provides compared to calling the <em>System.Diagnostics.Metrics</em> APIs directly. You can't provide a <code>Description</code> for a metric, for example, and providing a <code>Unit</code> needs a <code>#pragma</code>…</p> <p>What's more, the fact that the generated code is generic, means that the resulting usability is actually <em>worse</em> in my example, because you have to call:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp">metrics<span class="token punctuation">.</span>PricingPageViewed<span class="token punctuation">.</span><span class="token function">Add</span><span class="token punctuation">(</span><span class="token keyword">value</span><span class="token punctuation">:</span> <span class="token number">1</span><span class="token punctuation">,</span> <span class="token named-parameter punctuation">product_id</span><span class="token punctuation">:</span> id<span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre></div> <p>and specify an "increment" value, as opposed to simply being</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp">metrics<span class="token punctuation">.</span><span class="token function">PricingPageViewed</span><span class="token punctuation">(</span><span class="token named-parameter punctuation">productId</span><span class="token punctuation">:</span> id<span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre></div> <p>(also note the "correct" argument names in my "manual case"). The source generator also seems to support scenarios that I don't envision needing (the same <code>Instrument</code> registered with multiple <code>Meter</code>), so that's extra work that need not happen in the source generated case.</p> <p>So unfortunately, in this simple example, the source generator seems like a net loss. But there's an additional scenario it supports: strongly-typed tag objects</p> <h2 id="using-strongly-typed-tag-objects" class="heading-with-anchor"><a href="https://andrewlock.net/creating-strongly-typed-metics-with-a-source-generator/#using-strongly-typed-tag-objects" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Using strongly-typed tag objects</a></h2> <p>There's a common programming bug when calling methods that have multiple parameters of the same type: accidentally passing values in the wrong position:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token function">Add</span><span class="token punctuation">(</span>order<span class="token punctuation">.</span>Id<span class="token punctuation">,</span> product<span class="token punctuation">.</span>Id<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// Oops, those are wrong, but it's not obvious!</span>

<span class="token keyword">public</span> <span class="token return-type class-name"><span class="token keyword">void</span></span> <span class="token function">Add</span><span class="token punctuation">(</span><span class="token class-name"><span class="token keyword">int</span></span> productId<span class="token punctuation">,</span> <span class="token class-name"><span class="token keyword">int</span></span> orderId<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">/* */</span> <span class="token punctuation">}</span>
</code></pre></div> <p>One partial solution to this issue is to use strongly-typed objects to try to make the mistake more obvious. For example, if the method above instead took an object:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">public</span> <span class="token return-type class-name"><span class="token keyword">void</span></span> <span class="token function">Add</span><span class="token punctuation">(</span><span class="token class-name">Details</span> details<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">/* */</span> <span class="token punctuation">}</span>

<span class="token keyword">public</span> <span class="token keyword">readonly</span> <span class="token keyword">struct</span> <span class="token class-name">Details</span>
<span class="token punctuation">{</span>
    <span class="token keyword">public</span> required <span class="token return-type class-name"><span class="token keyword">int</span></span> OrderId <span class="token punctuation">{</span> <span class="token keyword">get</span><span class="token punctuation">;</span> <span class="token keyword">init</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
    <span class="token keyword">public</span> required <span class="token return-type class-name"><span class="token keyword">int</span></span> ProductId <span class="token punctuation">{</span> <span class="token keyword">get</span><span class="token punctuation">;</span> <span class="token keyword">init</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>Then at the callsite, you're <em>less</em> likely to make the same mistake:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token comment">// Still wrong, but the error is more obvious! 😅</span>
<span class="token function">Add</span><span class="token punctuation">(</span><span class="token keyword">new</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token punctuation">{</span>
    OrderId <span class="token operator">=</span> product<span class="token punctuation">.</span>Id<span class="token punctuation">,</span>
    ProductId <span class="token operator">=</span> order<span class="token punctuation">.</span>Id<span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre></div> <p>It turns out that passing lots of similar values is exactly the issue you run into when you need to add multiple tags when recording a value with an <code>Instrument</code>. To help with this, the source generator code can optionally use strongly-typed tag objects instead of a list of parameters.</p> <h3 id="updating-the-holder-class-with-strongly-typed-tags" class="heading-with-anchor"><a href="https://andrewlock.net/creating-strongly-typed-metics-with-a-source-generator/#updating-the-holder-class-with-strongly-typed-tags" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Updating the holder class with strongly-typed tags</a></h3> <p>In the examples I've shown so far, I've only been attaching a single tag to the <code>PricingPageViewed</code> metric, but I'll add an additional one, <code>environment</code> just for demonstration purposes.</p> <p>Let's again start by updating the <code>Factory</code> class to use a strongly-typed object instead of "manually" defining the tags:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">private</span> <span class="token keyword">static</span> <span class="token keyword">partial</span> <span class="token keyword">class</span> <span class="token class-name">Factory</span>
<span class="token punctuation">{</span>
    <span class="token comment">// A Type that defines the tags 👇</span>
    <span class="token punctuation">[</span><span class="token generic-method"><span class="token function">Counter</span><span class="token generic class-name"><span class="token punctuation">&lt;</span><span class="token keyword">int</span><span class="token punctuation">&gt;</span></span></span><span class="token punctuation">(</span><span class="token keyword">typeof</span><span class="token punctuation">(</span><span class="token type-expression class-name">PricingPageTags</span><span class="token punctuation">)</span><span class="token punctuation">,</span> Name <span class="token operator">=</span> <span class="token string">"myapp.products.pricing_page_requests"</span><span class="token punctuation">)</span><span class="token punctuation">]</span>
    <span class="token keyword">internal</span> <span class="token keyword">static</span> <span class="token keyword">partial</span> <span class="token return-type class-name">PricingPageViewed</span> <span class="token function">CreatePricingPageViewed</span><span class="token punctuation">(</span><span class="token class-name">Meter</span> meter<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token comment">// previously:</span>
    <span class="token comment">// [Counter&lt;int&gt;("product_id", Name = "myapp.products.pricing_page_requests")]</span>
    <span class="token comment">// internal static partial PricingPageViewed CreatePricingPageViewed(Meter meter);</span>
<span class="token punctuation">}</span>

<span class="token keyword">public</span> <span class="token keyword">readonly</span> <span class="token keyword">struct</span> <span class="token class-name">PricingPageTags</span>
<span class="token punctuation">{</span>
    <span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">TagName</span><span class="token attribute-arguments"><span class="token punctuation">(</span><span class="token string">"product_id"</span><span class="token punctuation">)</span></span></span><span class="token punctuation">]</span>
    <span class="token keyword">public</span> required <span class="token return-type class-name"><span class="token keyword">string</span></span> ProductId <span class="token punctuation">{</span> <span class="token keyword">get</span><span class="token punctuation">;</span> <span class="token keyword">init</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
    <span class="token keyword">public</span> required <span class="token return-type class-name">Environment</span> Environment <span class="token punctuation">{</span> <span class="token keyword">get</span><span class="token punctuation">;</span> <span class="token keyword">init</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
<span class="token punctuation">}</span>

<span class="token keyword">public</span> <span class="token keyword">enum</span> <span class="token class-name">Environment</span>
<span class="token punctuation">{</span>
    Development<span class="token punctuation">,</span>
    QA<span class="token punctuation">,</span>
    Production<span class="token punctuation">,</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>So we have two changes:</p> <ul><li>We're passing a <code>Type</code> in the <code>[Counter&lt;T&gt;]</code> attribute, instead of a list of tag arguments.</li> <li>We've defined a struct type that includes all the tags we want to add to a value. <ul><li>This is defined as a <code>readonly struct</code> to avoid additional allocations.</li> <li>We specific the tag name for <code>ProductId</code>. By default, <code>Environment</code> uses the name <code>"Environment"</code> (which may not be what you want, but this is for demo reasons!).</li> <li>We can only use <code>string</code> or <code>enum</code> types in the tags</li></ul> </li></ul> <p>The source generator then does its thing, and so we need to update our API callsite to this:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp">app<span class="token punctuation">.</span><span class="token function">MapGet</span><span class="token punctuation">(</span><span class="token string">"/product/{id}"</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token class-name"><span class="token keyword">int</span></span> id<span class="token punctuation">,</span> <span class="token class-name">ProductMetrics</span> metrics<span class="token punctuation">)</span> <span class="token operator">=&gt;</span>
<span class="token punctuation">{</span>
    metrics<span class="token punctuation">.</span>PricingPageViewed<span class="token punctuation">.</span><span class="token function">Add</span><span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">,</span> <span class="token keyword">new</span> <span class="token constructor-invocation class-name">PricingPageTags</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
         ProductId <span class="token operator">=</span> id<span class="token punctuation">.</span><span class="token function">ToString</span><span class="token punctuation">(</span>CultureInfo<span class="token punctuation">.</span>InvariantCulture<span class="token punctuation">)</span><span class="token punctuation">,</span>
         Environment <span class="token operator">=</span> ProductMetrics<span class="token punctuation">.</span>Environment<span class="token punctuation">.</span>Production<span class="token punctuation">,</span>
    <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">return</span> <span class="token interpolation-string"><span class="token string">$"Details for product </span><span class="token interpolation"><span class="token punctuation">{</span><span class="token expression language-csharp">id</span><span class="token punctuation">}</span></span><span class="token string">"</span></span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre></div> <p>In the generated code we need to pass a <code>PricingPageTags</code> object into the <code>Add()</code> method, instead of individually passing each tag value.</p> <blockquote> <p>Note that we had to pass a <code>string</code> for <code>ProductId</code>, we can't use an <code>int</code> like we were before. That's not <em>great</em> perf wise, but previously we were boxing the <code>int</code> to an <code>object?</code> so <em>that</em> wasn't great either😅 Avoiding this allocation would be recommended if possible, but that's out of the scope for this post!</p> </blockquote> <p>As before, let's take a look at the generated code.</p> <h3 id="exploring-the-generated-code-1" class="heading-with-anchor"><a href="https://andrewlock.net/creating-strongly-typed-metics-with-a-source-generator/#exploring-the-generated-code-1" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Exploring the generated code</a></h3> <p>The generated code in this case is almost identical to before. The only difference is in the generated <code>Add</code> method:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">internal</span> <span class="token keyword">sealed</span> <span class="token keyword">class</span> <span class="token class-name">PricingPageViewed</span>
<span class="token punctuation">{</span>
    <span class="token keyword">private</span> <span class="token keyword">readonly</span> <span class="token class-name">Counter<span class="token punctuation">&lt;</span><span class="token keyword">int</span><span class="token punctuation">&gt;</span></span> _counter<span class="token punctuation">;</span>

    <span class="token keyword">public</span> <span class="token function">PricingPageViewed</span><span class="token punctuation">(</span><span class="token class-name">Counter<span class="token punctuation">&lt;</span><span class="token keyword">int</span><span class="token punctuation">&gt;</span></span> counter<span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        _counter <span class="token operator">=</span> counter<span class="token punctuation">;</span>
    <span class="token punctuation">}</span>

    <span class="token keyword">public</span> <span class="token return-type class-name"><span class="token keyword">void</span></span> <span class="token function">Add</span><span class="token punctuation">(</span><span class="token class-name"><span class="token keyword">int</span></span> <span class="token keyword">value</span><span class="token punctuation">,</span> <span class="token class-name">PricingPageTags</span> o<span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        <span class="token class-name"><span class="token keyword">var</span></span> tagList <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token constructor-invocation class-name">TagList</span>
        <span class="token punctuation">{</span>
            <span class="token keyword">new</span> <span class="token constructor-invocation class-name">KeyValuePair<span class="token punctuation">&lt;</span><span class="token keyword">string</span><span class="token punctuation">,</span> <span class="token keyword">object</span><span class="token punctuation">?</span><span class="token punctuation">&gt;</span></span><span class="token punctuation">(</span><span class="token string">"product_id"</span><span class="token punctuation">,</span> o<span class="token punctuation">.</span>ProductId<span class="token operator">!</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
            <span class="token keyword">new</span> <span class="token constructor-invocation class-name">KeyValuePair<span class="token punctuation">&lt;</span><span class="token keyword">string</span><span class="token punctuation">,</span> <span class="token keyword">object</span><span class="token punctuation">?</span><span class="token punctuation">&gt;</span></span><span class="token punctuation">(</span><span class="token string">"Environment"</span><span class="token punctuation">,</span> o<span class="token punctuation">.</span>Environment<span class="token punctuation">.</span><span class="token function">ToString</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
        <span class="token punctuation">}</span><span class="token punctuation">;</span>

        _counter<span class="token punctuation">.</span><span class="token function">Add</span><span class="token punctuation">(</span><span class="token keyword">value</span><span class="token punctuation">,</span> tagList<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>This generated code is <em>almost</em> the same as before. The only difference is that it's "splatting" the <code>PricingPageTags</code> object as individual tags in a <code>TagList</code>. So, does <em>this</em> mean the source generator is worth it?</p> <h2 id="are-the-source-generators-worth-using-" class="heading-with-anchor"><a href="https://andrewlock.net/creating-strongly-typed-metics-with-a-source-generator/#are-the-source-generators-worth-using-" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Are the source generators worth using?</a></h2> <p>From my point of view, the strongly-typed tags scenario doesn't change any of the arguments I raised previously against the source generator. It's still mostly obfuscating otherwise simple APIs, not adding anything performance-wise as far as I can tell, and it still supports the "<code>Instrument</code> in multiple <code>Meter</code> scenario" that seems unlikely to be useful (to me, anyway).</p> <p>The strongly-typed tags approach shown here, while nice, can just as easily be implemented manually. The generated code isn't really <em>adding</em> much. And in fact, given that it's calling <code>ToString()</code> on an <code>enum</code> (<a href="https://andrewlock.net/updates-to-netescapaades-enumgenerators-new-apis-and-system-memory-support/#why-should-you-use-an-enum-source-generator-">which is known to be slow</a>), the "manual" version can <em>likely</em> also provide better opportunities for performance optimizations.</p> <p>About the only argument I can see in favour of using the source generator is if you're using the "<code>Instrument</code> in multiple <code>Meter</code>" approach (let me know in the comments if you are, I feel like I'm missing something!). Or, I guess, if you just <em>like</em> the attribute-based generator approach and aren't worried about the points I raised. I'm a fan of source generators in general, but in this case, I don't think I would bother with them personally.</p> <p>Overall, the fact the generators don't really add much maybe just points to the <em>System.Diagnostics.Metrics</em> APIs being well defined? If you don't need much boilerplate to create the metrics, and you get the "best performance" by default, <em>without</em> needing a generator, then that seems like a <em>good</em> thing 😄</p> <h2 id="summary" class="heading-with-anchor"><a href="https://andrewlock.net/creating-strongly-typed-metics-with-a-source-generator/#summary" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Summary</a></h2> <p>In this post I showed how to use the source generators that ship in the <a href="https://www.nuget.org/packages/Microsoft.Extensions.Telemetry.Abstractions">Microsoft.Extensions.Telemetry.Abstractions</a> to help generating metrics with the <em>System.Diagnostics.Metrics</em> APIs. I show how the source generator changes the way you define your metric, but fundamentally generates roughly the same code as <a href="https://andrewlock.net/creating-and-consuming-metrics-with-system-diagnostics-metrics-apis/">in my previous post</a>. I then show how you can also create strongly-typed tags, which helps avoid a typical class of bugs.</p> <p>Overall, I didn't feel like the source generator saved much in the way of the code you write or provides performance benefits, unlike many other built-in source generators. The generated code caters to additional scenarios, such as registering the same <code>Instrument</code> with multiple <code>Meter</code>s, but that seems like a niche scenario.</p> ]]></content:encoded><category><![CDATA[Observability;.NET Core]]></category></item><item><title><![CDATA[Creating and consuming metrics with System.Diagnostics.Metrics APIs: System.Diagnostics.Metrics APIs - Part 1]]></title><description><![CDATA[In this post I provide an introduction to the System.Diagnostics.Metrics API, and show how to create a custom metric and read it with dotnet-counters]]></description><link>https://andrewlock.net/creating-and-consuming-metrics-with-system-diagnostics-metrics-apis/</link><guid isPermaLink="true">https://andrewlock.net/creating-and-consuming-metrics-with-system-diagnostics-metrics-apis/</guid><pubDate>Tue, 27 Jan 2026 10:00:00 GMT</pubDate><dc:creator><![CDATA[Andrew Lock]]></dc:creator><media:content url="https://andrewlock.net/content/images/2026/metrics_banner.png" medium="image" /><content:encoded><![CDATA[<img src="https://andrewlock.net/content/images/2026/metrics_banner.png" /><nav><p>This is the first post in the series: <a href="https://andrewlock.net/series/system-diagnostics-metrics-apis/">System.Diagnostics.Metrics APIs</a>. </p> <ol class="list-none"><li>Part 1 - Creating and consuming metrics with System.Diagnostics.Metrics APIs (this post) </li><li><a href="https://andrewlock.net/creating-strongly-typed-metics-with-a-source-generator/">Part 2 - Exploring the (underwhelming) System.Diagnostics.Metrics source generators</a></li><li><a href="https://andrewlock.net/creating-standard-and-observable-instruments/">Part 3 - Creating standard and "observable" instruments</a></li><li><a href="https://andrewlock.net/recording-metrics-in-process-using-meterlistener/">Part 4 - Recording metrics in-process using MeterListener</a></li></ol></nav><p>In this post I provide an introduction to the <em>System.Diagnostics.Metrics</em> API, show how to use <code>dotnet-counters</code> for local monitoring of metrics, and show how to add a custom metric to your application.</p> <h2 id="the-system-diagnostics-metrics-apis" class="heading-with-anchor"><a href="https://andrewlock.net/creating-and-consuming-metrics-with-system-diagnostics-metrics-apis/#the-system-diagnostics-metrics-apis" class="relative text-zinc-800 dark:text-white no-underline hover:underline">The <em>System.Diagnostics.Metrics</em> APIs</a></h2> <p>The <em>System.Diagnostics.Metrics</em> APIs were originally introduced as a built-in in feature in .NET 6, but are also supported in earlier versions of .NET Core and .NET Framework using the <a href="https://www.nuget.org/packages/System.Diagnostics.DiagnosticSource/"><em>System.Diagnostics.DiagnosticSource</em></a> NuGet package. The metrics APIs provide a way to both create and report on metrics generated by an application, such as simple counters, gauges, or histograms of values. I'll describe each of the available metric types later.</p> <p>The <em>System.Diagnostics.Metrics</em> APIs are designed to easily interoperate with OpenTelemetry, and so can be consumed by a large range of applications. You can also read the metrics using .NET SDK tools like <code>dotnet-counters</code>.</p> <blockquote> <p>The word "metric" is often used in multiple different ways. Is it a single "point" with associated "tags"? Is it the full set of the recorded values for a single concept? Is it the "aggregated" statistics for all of these points? It's common to see both meanings. In this post I mainly use "metric" to mean a stream of recordings for a single concept.</p> </blockquote> <p>There are two core concepts exposed by the <em>System.Diagnostics.Metrics</em> APIs. These are <code>Instrument</code>s and <code>Meter</code>s:</p> <ul><li><code>Instrument</code>: An instrument records the values for a single metric of interest. You might have separate <code>Instrument</code>s for "products sold", "invoices created", "invoice total", and "GC heap size".</li> <li><code>Meter</code>: A <code>Meter</code> is a logical grouping of multiple instruments. For example, the <a href="https://learn.microsoft.com/en-us/dotnet/core/diagnostics/built-in-metrics-runtime"><code>System.Runtime</code> <code>Meter</code></a> contains multiple <code>Instrument</code>s about the workings of the runtime, while <a href="https://learn.microsoft.com/en-us/aspnet/core/log-mon/metrics/built-in?view=aspnetcore-10.0#microsoftaspnetcorehosting">the <code>Microsoft.AspNetCore.Hosting</code> <code>Meter</code></a> contains <code>Instrument</code>s about the HTTP requests received by ASP.NET Core.</li></ul> <p>There are also several different <em>types</em> of <code>Instrument</code>:</p> <ul><li><code>Counter&lt;T&gt;</code>/ <code>ObservableCounter&lt;T&gt;</code>: These represent a count of occurrences, so return a non-negative values. For example, the number of requests received might be a <code>Counter&lt;T&gt;</code>.</li> <li><code>UpDownCounter&lt;T&gt;</code> / <code>ObservableUpDownCounter&lt;T&gt;</code>: These are similar to a counter, but can be used to record both positive and negative values. This may be used to report the change in queue size or the number of <em>active</em> requests.</li> <li><code>Gauge&lt;T&gt;</code> / <code>ObservableGauge&lt;T&gt;</code>: These return a value that represents the "current value". The values it emits effectively "replace" the previous value. For example, the amount of memory used might be a <code>Gauge&lt;T&gt;</code>.</li> <li><code>Histogram&lt;T&gt;</code>: Reports arbitrary values, which could be subsequently processed to calculate further statistics, or plot as a graph. For example, the duration of each request might be recorded as a <code>Histogram</code>.</li></ul> <p>You'll note that the <code>Counter&lt;T&gt;</code>, <code>UpDownCounter&lt;T&gt;</code>, and <code>Gauge&lt;T&gt;</code> all have <em>observable</em> versions. This difference relates to how the <code>Instrument</code> records and emits values; observable instruments only retrieve their values when explicitly requested, whereas the non-observable versions emit a value as soon as that value is recorded.</p> <blockquote> <p>The choice of whether an <code>Instrument</code> should be implemented as <code>Observable*</code> is driven partly by performance considerations, and partly by how the value is obtained. I'll cover more about the implementation differences with observable <code>Instrument</code>s in a future post.</p> </blockquote> <h2 id="collecting-metrics-with-dotnet-counters" class="heading-with-anchor"><a href="https://andrewlock.net/creating-and-consuming-metrics-with-system-diagnostics-metrics-apis/#collecting-metrics-with-dotnet-counters" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Collecting metrics with <code>dotnet-counters</code></a></h2> <p>When running in production, you'll likely want to collect your metrics using <a href="https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/docs/metrics/getting-started-prometheus-grafana#collect-metrics-using-prometheus">an <code>OpenTelemetry</code> exporter integration</a> or another solution (e.g. Datadog can collect these metrics without requiring application changes), but for local testing <code>dotnet-counters</code> is a very convenient tool.</p> <p><code>dotnet-counters</code> is a .NET tool shipped by Microsoft that you can install by running:</p> <div class="pre-code-wrapper"><pre class="language-bash"><code class="language-bash">dotnet tool <span class="token function">install</span> <span class="token parameter variable">-g</span> dotnet-counters
</code></pre></div> <p>You can then run the tool by specifying a process ID or process name to monitor, using:</p> <div class="pre-code-wrapper"><pre class="language-bash"><code class="language-bash">dotnet-counters monitor <span class="token parameter variable">-n</span> MyApp
<span class="token comment"># or </span>
dotnet-counters monitor <span class="token parameter variable">-p</span> <span class="token number">123</span>
</code></pre></div> <p>Alternatively, you can specify a command to run when starting the tool, and it will monitor the target process:</p> <div class="pre-code-wrapper"><pre class="language-bash"><code class="language-bash">dotnet-counters monitor -- dotnet MyApp.dll
</code></pre></div> <p>When you run <code>dotnet-counters</code> in this "monitor" mode, the counter values are written to the console and periodically refresh:</p> <div class="pre-code-wrapper"><pre class="language-bash"><code class="language-bash">Press p to pause, r to resume, q to quit.
    Status: Running
Name                                                                          Current Value
<span class="token punctuation">[</span>System.Runtime<span class="token punctuation">]</span>
    dotnet.assembly.count <span class="token punctuation">(</span><span class="token punctuation">{</span>assembly<span class="token punctuation">}</span><span class="token punctuation">)</span>                                              <span class="token number">100</span>
    dotnet.gc.collections <span class="token punctuation">(</span><span class="token punctuation">{</span>collection<span class="token punctuation">}</span><span class="token punctuation">)</span>
        gc.heap.generation
        ------------------
        gen0                                                                         <span class="token number">67</span>
        gen1                                                                          <span class="token number">6</span>
        gen2                                                                          <span class="token number">1</span>
    dotnet.gc.heap.total_allocated <span class="token punctuation">(</span>By<span class="token punctuation">)</span>                                       <span class="token number">4,134</span>,656
    dotnet.gc.last_collection.heap.fragmentation.size <span class="token punctuation">(</span>By<span class="token punctuation">)</span>
        gc.heap.generation
        ------------------
        gen0                                                                    <span class="token number">911,896</span>
        gen1                                                                      <span class="token number">5,544</span>
        gen2                                                                      <span class="token number">1,656</span>
        loh                                                                           <span class="token number">0</span>
        poh                                                                           <span class="token number">0</span>
    dotnet.gc.last_collection.heap.size <span class="token punctuation">(</span>By<span class="token punctuation">)</span>
        gc.heap.generation
        ------------------
        gen0                                                                    <span class="token number">943,560</span>
        gen1                                                                    <span class="token number">271,288</span>
        gen2                                                                    <span class="token number">840,136</span>
        loh                                                                           <span class="token number">0</span>
        poh                                                                      <span class="token number">24,528</span>
    dotnet.gc.last_collection.memory.committed_size <span class="token punctuation">(</span>By<span class="token punctuation">)</span>                      <span class="token number">3,981</span>,312
    dotnet.gc.pause.time <span class="token punctuation">(</span>s<span class="token punctuation">)</span>                                                          <span class="token number">0.106</span>
    dotnet.jit.compilation.time <span class="token punctuation">(</span>s<span class="token punctuation">)</span>                                                   <span class="token number">1.096</span>
    dotnet.jit.compiled_il.size <span class="token punctuation">(</span>By<span class="token punctuation">)</span>                                            <span class="token number">199,280</span>
    dotnet.jit.compiled_methods <span class="token punctuation">(</span><span class="token punctuation">{</span>method<span class="token punctuation">}</span><span class="token punctuation">)</span>                                        <span class="token number">2,126</span>
    dotnet.monitor.lock_contentions <span class="token punctuation">(</span><span class="token punctuation">{</span>contention<span class="token punctuation">}</span><span class="token punctuation">)</span>                                    <span class="token number">1</span>
    dotnet.process.cpu.count <span class="token punctuation">(</span><span class="token punctuation">{</span>cpu<span class="token punctuation">}</span><span class="token punctuation">)</span>                                                  <span class="token number">4</span>
    dotnet.process.cpu.time <span class="token punctuation">(</span>s<span class="token punctuation">)</span>
        cpu.mode
        --------
        system                                                                        <span class="token number">5.453</span>
        user                                                                          <span class="token number">9.313</span>
    dotnet.process.memory.working_set <span class="token punctuation">(</span>By<span class="token punctuation">)</span>                                   <span class="token number">51,384</span>,320
    dotnet.thread_pool.queue.length <span class="token punctuation">(</span><span class="token punctuation">{</span>work_item<span class="token punctuation">}</span><span class="token punctuation">)</span>                                     <span class="token number">0</span>
    dotnet.thread_pool.thread.count <span class="token punctuation">(</span><span class="token punctuation">{</span>thread<span class="token punctuation">}</span><span class="token punctuation">)</span>                                        <span class="token number">4</span>
    dotnet.thread_pool.work_item.count <span class="token punctuation">(</span><span class="token punctuation">{</span>work_item<span class="token punctuation">}</span><span class="token punctuation">)</span>                             <span class="token number">61,911</span>
    dotnet.timer.count <span class="token punctuation">(</span><span class="token punctuation">{</span>timer<span class="token punctuation">}</span><span class="token punctuation">)</span>                                                      <span class="token number">0</span>
</code></pre></div> <p>You can choose which <code>Meter</code>s to display by passing a comma-separated list of counters using <code>--counters</code>, for example to show the <code>Microsoft.AspNetCore.Hosting</code> <code>Meter</code>, you would use:</p> <div class="pre-code-wrapper"><pre class="language-bash"><code class="language-bash"><span class="token operator">&gt;</span> dotnet-counters monitor <span class="token parameter variable">--counters</span> <span class="token string">'Microsoft.AspNetCore.Hosting'</span> -- dotnet MyApp.dll

Press p to pause, r to resume, q to quit.
    Status: Running

Name                                                                                                             Current Value
<span class="token punctuation">[</span>Microsoft.AspNetCore.Hosting<span class="token punctuation">]</span>
    http.server.active_requests <span class="token punctuation">(</span><span class="token punctuation">{</span>request<span class="token punctuation">}</span><span class="token punctuation">)</span>
        http.request.method url.scheme
        ------------------- ----------
        GET                 http                                                                                          <span class="token number">0</span>
    http.server.request.duration <span class="token punctuation">(</span>s<span class="token punctuation">)</span>
        http.request.method http.response.status_code http.route network.protocol.version url.scheme Percentile
        ------------------- ------------------------- ---------- ------------------------ ---------- ----------
        GET                 <span class="token number">200</span>                       /          <span class="token number">1.1</span>                      http       <span class="token number">50</span>                   <span class="token number">0</span>
        GET                 <span class="token number">200</span>                       /          <span class="token number">1.1</span>                      http       <span class="token number">95</span>                   <span class="token number">0</span>
        GET                 <span class="token number">200</span>                       /          <span class="token number">1.1</span>                      http       <span class="token number">99</span>                   <span class="token number">0</span>
</code></pre></div> <p>The metrics in the above image were created by hitting the same endpoint in a sample app several times, but they show some of the different features of the <code>Instrument</code>s available. Each metric has an associated unit (<code>{request}</code> and <code>s</code>), and also an associated set of <em>tags</em>. Tags are an important aspect when recording metrics, as they allow you to more easily group and segregate data.</p> <p>For example, the <code>http.server.active_requests</code> up/down counter has tags for <code>http.request.method</code> and <code>url.scheme</code>. Seeing as I only made <code>GET</code> requests to <code>http://localhost:5000</code>, you only see one set of tags. But if I had made <code>POST</code> requests, or requests using <code>https</code> then you would have seen other values there. Similarly, the values in the <code>http.server.request.duration</code> histogram include tags for each value.</p> <blockquote> <p>Managing tag <em>cardinality</em> (the number of possible values) is an important aspect of dealing with tags in all observability data. Depending on how your data is stored, large tag cardinality could cause large data storage costs and an impact on performance. Those limits will generally be controlled by whatever system you're exporting your metrics to.</p> </blockquote> <p>As well as "immediate" monitoring approaches like the example above, which just outputs to the console, <code>dotnet-counters</code> also has options for just collecting the metrics and <a href="https://learn.microsoft.com/en-us/dotnet/core/diagnostics/dotnet-counters#dotnet-counters-collYouect">exporting them in a variety of formats</a>. You <em>could</em> drive a production monitoring system this way, but I suspect most usages of <code>dotnet-counters</code> are for the local testing scenario.</p> <h2 id="creating-your-own-metrics" class="heading-with-anchor"><a href="https://andrewlock.net/creating-and-consuming-metrics-with-system-diagnostics-metrics-apis/#creating-your-own-metrics" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Creating your own metrics</a></h2> <p>The <code>dotnet-counters</code> example above demonstrates some of the built-in metrics available in .NET 10. The <code>System.Runtime</code> meter is available since .NET 9, and the <code>Microsoft.AspNetCore.Routing</code> meter is available since .NET 8, but there are many other additional built-in metrics available in different versions of .NET. You can find what's available here:</p> <ul><li><a href="https://learn.microsoft.com/en-us/aspnet/core/log-mon/metrics/built-in">ASP.NET Core metrics</a></li> <li><a href="https://learn.microsoft.com/en-us/dotnet/core/diagnostics/built-in-metrics-runtime">.NET Runtime metrics</a></li> <li><a href="https://learn.microsoft.com/en-us/dotnet/core/diagnostics/built-in-metrics-system-net">System.Net metrics</a></li> <li><a href="https://learn.microsoft.com/en-us/dotnet/core/diagnostics/built-in-metrics-diagnostics">.NET extensions metrics</a></li></ul> <p>These metrics can provide a reasonable overview of how your system is operating in general, but there might also be application-specific or business related metrics that would be useful to record from the application itself.</p> <p>As an example, we'll create a very simple counter metric that just records the number of requests sent to a particular API. To make it slightly less abstract, we'll imagine this to be a product pricing endpoint, and we want to track how often the details are checked for a given product.</p> <h3 id="creating-the-initial-app" class="heading-with-anchor"><a href="https://andrewlock.net/creating-and-consuming-metrics-with-system-diagnostics-metrics-apis/#creating-the-initial-app" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Creating the initial app</a></h3> <p>We'll start by creating the basic app using</p> <div class="pre-code-wrapper"><pre class="language-bash"><code class="language-bash">dotnet new web
</code></pre></div> <p>and updating the application to the following:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token class-name"><span class="token keyword">var</span></span> builder <span class="token operator">=</span> WebApplication<span class="token punctuation">.</span><span class="token function">CreateBuilder</span><span class="token punctuation">(</span>args<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token class-name"><span class="token keyword">var</span></span> app <span class="token operator">=</span> builder<span class="token punctuation">.</span><span class="token function">Build</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

app<span class="token punctuation">.</span><span class="token function">MapGet</span><span class="token punctuation">(</span><span class="token string">"/product/{id}"</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token class-name"><span class="token keyword">int</span></span> id<span class="token punctuation">)</span> <span class="token operator">=&gt;</span>
<span class="token punctuation">{</span>
    <span class="token comment">// This would return the real details</span>
    <span class="token comment">// TODO: add metrics</span>
    <span class="token keyword">return</span> <span class="token interpolation-string"><span class="token string">$"Pricing for product </span><span class="token interpolation"><span class="token punctuation">{</span><span class="token expression language-csharp">id</span><span class="token punctuation">}</span></span><span class="token string">"</span></span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

app<span class="token punctuation">.</span><span class="token function">Run</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre></div> <p>We would obviously return real pricing details from this API, but this is just a demo after all.</p> <h3 id="creating-our-instrument-and-meter" class="heading-with-anchor"><a href="https://andrewlock.net/creating-and-consuming-metrics-with-system-diagnostics-metrics-apis/#creating-our-instrument-and-meter" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Creating our <code>Instrument</code> and <code>Meter</code></a></h3> <p>Now let's add our metrics. We need to create two things:</p> <ul><li>An <code>Instrument</code> to track the number of requests.</li> <li>A <code>Meter</code> to hold our instrument (and any future related instruments).</li></ul> <p>We need to be careful about the naming of both of these, as they essentially serve as the public API for subsequent consumers of our metrics.</p> <p>Seeing as this is an ASP.NET Core application and we generally avoid global <code>static</code> variables, the example below shows how we would create a class to encapsulate our <code>Instrument</code> and <code>Meter</code>, so that we can register it with the dependency injection container later. If you were creating an app that doesn't use DI, you could just as easily use <code>new Meter()</code>, and save the variable in a global variable.</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">ProductMetrics</span>
<span class="token punctuation">{</span>
    <span class="token keyword">private</span> <span class="token keyword">readonly</span> <span class="token class-name">Counter<span class="token punctuation">&lt;</span><span class="token keyword">long</span><span class="token punctuation">&gt;</span></span> _pricingDetailsViewed<span class="token punctuation">;</span>

    <span class="token keyword">public</span> <span class="token function">ProductMetrics</span><span class="token punctuation">(</span><span class="token class-name">IMeterFactory</span> meterFactory<span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        <span class="token class-name"><span class="token keyword">var</span></span> meter <span class="token operator">=</span> meterFactory<span class="token punctuation">.</span><span class="token function">Create</span><span class="token punctuation">(</span><span class="token string">"MyApp.Products"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        _pricingDetailsViewed <span class="token operator">=</span> meter<span class="token punctuation">.</span><span class="token generic-method"><span class="token function">CreateCounter</span><span class="token generic class-name"><span class="token punctuation">&lt;</span><span class="token keyword">long</span><span class="token punctuation">&gt;</span></span></span><span class="token punctuation">(</span><span class="token string">"myapp.products.pricing_page_requests"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>

    <span class="token keyword">public</span> <span class="token return-type class-name"><span class="token keyword">void</span></span> <span class="token function">PricingPageViewed</span><span class="token punctuation">(</span><span class="token class-name"><span class="token keyword">int</span></span> id<span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        _pricingDetailsViewed<span class="token punctuation">.</span><span class="token function">Add</span><span class="token punctuation">(</span><span class="token named-parameter punctuation">delta</span><span class="token punctuation">:</span> <span class="token number">1</span><span class="token punctuation">,</span> <span class="token keyword">new</span> <span class="token constructor-invocation class-name">KeyValuePair<span class="token punctuation">&lt;</span><span class="token keyword">string</span><span class="token punctuation">,</span> <span class="token keyword">object</span><span class="token punctuation">?</span><span class="token punctuation">&gt;</span></span><span class="token punctuation">(</span><span class="token string">"product_id"</span><span class="token punctuation">,</span> id<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>In the code above we:</p> <ul><li>Create a new <code>Meter</code> called <code>MyApp.Products</code>. This is named following similar guidelines to the built-in meters; we have "namespaced" using our app's name, and the broad category of the instruments it will include.</li> <li>We create a <code>Counter&lt;long&gt;</code> called <code>myapp.products.pricing_page_requests</code>. This is named using the <a href="https://github.com/open-telemetry/semantic-conventions/blob/main/docs/general/metrics.md#general-guidelines">OpenTelemetry naming guidelines</a>. I opted for <code>long</code> because I anticipate that some pages will get a <em>lot</em> of reviews in the lifetime of the app (more than <code>int.MaxValue</code>).</li> <li>We added a convenience method for recording a view of a product's pricing page, tagging the view with the ID of the product we're viewing. We could add other tags&amp;mash;maybe the product name would be more useful for example&amp;smash;but this tag will do for our purposes.</li></ul> <p>If we want to add additional <code>Instrument</code>s to the same <code>Meter</code> later, we would create them here, and likely add similar convenience methods.</p> <h3 id="hooking-up-the-instrument-in-the-app" class="heading-with-anchor"><a href="https://andrewlock.net/creating-and-consuming-metrics-with-system-diagnostics-metrics-apis/#hooking-up-the-instrument-in-the-app" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Hooking up the <code>Instrument</code> in the app</a></h3> <p>Now that we have our metrics helper, we need to make use of it in our app. This involves both registering the helper in DI, and using it in our API:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">using</span> <span class="token namespace">System<span class="token punctuation">.</span>Diagnostics<span class="token punctuation">.</span>Metrics</span><span class="token punctuation">;</span>
<span class="token keyword">using</span> <span class="token namespace">Microsoft<span class="token punctuation">.</span>Extensions<span class="token punctuation">.</span>Diagnostics<span class="token punctuation">.</span>Metrics</span><span class="token punctuation">;</span>

<span class="token class-name"><span class="token keyword">var</span></span> builder <span class="token operator">=</span> WebApplication<span class="token punctuation">.</span><span class="token function">CreateBuilder</span><span class="token punctuation">(</span>args<span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token comment">// 👇 Register in DI</span>
builder<span class="token punctuation">.</span>Services<span class="token punctuation">.</span><span class="token generic-method"><span class="token function">AddSingleton</span><span class="token generic class-name"><span class="token punctuation">&lt;</span>ProductMetrics<span class="token punctuation">&gt;</span></span></span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token class-name"><span class="token keyword">var</span></span> app <span class="token operator">=</span> builder<span class="token punctuation">.</span><span class="token function">Build</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token comment">// Inject in API handler    👇 </span>
app<span class="token punctuation">.</span><span class="token function">MapGet</span><span class="token punctuation">(</span><span class="token string">"/product/{id}"</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token class-name"><span class="token keyword">int</span></span> id<span class="token punctuation">,</span> <span class="token class-name">ProductMetrics</span> metrics<span class="token punctuation">)</span> <span class="token operator">=&gt;</span>
<span class="token punctuation">{</span>
    metrics<span class="token punctuation">.</span><span class="token function">PricingPageViewed</span><span class="token punctuation">(</span>id<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// 👈 Record</span>
    <span class="token keyword">return</span> <span class="token interpolation-string"><span class="token string">$"Details for product </span><span class="token interpolation"><span class="token punctuation">{</span><span class="token expression language-csharp">id</span><span class="token punctuation">}</span></span><span class="token string">"</span></span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

app<span class="token punctuation">.</span><span class="token function">Run</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre></div> <p>We can now try it out using <code>dotnet-counters</code> to view the metrics.</p> <h3 id="testing-our-new-metric" class="heading-with-anchor"><a href="https://andrewlock.net/creating-and-consuming-metrics-with-system-diagnostics-metrics-apis/#testing-our-new-metric" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Testing our new metric</a></h3> <p>We'll start by running our app:</p> <div class="pre-code-wrapper"><pre class="language-bash"><code class="language-bash">dotnet run
</code></pre></div> <p>and then in a separate terminal window, we'll set <code>dotnet-counters</code> running using</p> <div class="pre-code-wrapper"><pre class="language-bash"><code class="language-bash">dotnet-counters monitor <span class="token parameter variable">-n</span> MyApp <span class="token parameter variable">--counters</span> MyApp.Products
</code></pre></div> <p>I've used the <code>-n</code> option to find the app by name, <code>MyApp</code>, and made sure to only show the <code>MyApp.Products</code> instrument.</p> <p>If we hit the product endpoint a few times with various IDs, we can see that the metrics are reported to <code>dotnet-counters</code> as expected!</p> <p><img src="https://andrewlock.net/content/images/2026/metrics.png" alt="Showing the metrics being reported using dotnet-counters"></p> <p>With that, we have confirmed that we have a custom metric being successfully recorded 🎉</p> <h3 id="adding-extra-information-for-consumers" class="heading-with-anchor"><a href="https://andrewlock.net/creating-and-consuming-metrics-with-system-diagnostics-metrics-apis/#adding-extra-information-for-consumers" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Adding extra information for consumers</a></h3> <p>In the <code>dotnet-counters</code> output above, we can see that the Instrument is reported with the unit <code>Count</code>, inferred from the instrument type. That's fine, but the <code>Instrument</code> API lets us provide additional details that can be optionally used by consumers to customise the display or metrics.</p> <p>For example, we could add some additional details to our instrument, as follows:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp">_pricingDetailsViewed <span class="token operator">=</span> meter<span class="token punctuation">.</span><span class="token generic-method"><span class="token function">CreateCounter</span><span class="token generic class-name"><span class="token punctuation">&lt;</span><span class="token keyword">int</span><span class="token punctuation">&gt;</span></span></span><span class="token punctuation">(</span>
    <span class="token string">"myapp.products.pricing_page_requests"</span><span class="token punctuation">,</span>
    <span class="token named-parameter punctuation">unit</span><span class="token punctuation">:</span> <span class="token string">"requests"</span><span class="token punctuation">,</span>
    <span class="token named-parameter punctuation">description</span><span class="token punctuation">:</span> <span class="token string">"The number of requests to the pricing details page for the product with the given product_id"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre></div> <p>If we run and monitor the app again, the <code>dotnet-counters</code> output has changed slightly. The unit for <code>myapp.products.pricing_page_requests</code> has changed to <code>requests</code> instead of <code>Count</code>:</p> <div class="pre-code-wrapper"><pre class="language-bash"><code class="language-bash">Name                                                        Current Value
<span class="token punctuation">[</span>MyApp.Products<span class="token punctuation">]</span>
    myapp.products.pricing_page_requests <span class="token punctuation">(</span>requests<span class="token punctuation">)</span>
        product_id
        ----------
        <span class="token number">1</span>                                                           <span class="token number">1</span>
        <span class="token number">234</span>                                                         <span class="token number">1</span>
        <span class="token number">5</span>                                                           <span class="token number">4</span>
</code></pre></div> <p>That's a small nicity, and the description isn't used anywhere by <code>dotnet-counters</code>, but other exporters might choose to use it. Depending on how you're exporting your metrics out of process, your metric should now be available everywhere!</p> <h2 id="summary" class="heading-with-anchor"><a href="https://andrewlock.net/creating-and-consuming-metrics-with-system-diagnostics-metrics-apis/#summary" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Summary</a></h2> <p>In this post, I provided an introduction to the <em>System.Diagnostics.Metrics</em> APIs. I described some of the terminology used, such as <code>Meter</code> and <code>Instrument</code>, and the various different types of <code>Instrument</code> available. I then showed how you can use <code>dotnet-counters</code> to monitor the metrics produced by your app, primarily for local investigation. Finally, I showed how you could create a custom metric, customize it, hook it up to dependency injection, and report it in <code>dotnet-counters</code>.</p> ]]></content:encoded><category><![CDATA[Observability;.NET Core;Datadog]]></category></item><item><title><![CDATA[Making foreach on an IEnumerable allocation-free using reflection and dynamic methods]]></title><description><![CDATA[In this post I describe why foreach sometimes allocates, and show how you can use DynamicMethod and Reflection.Emit to go allocation-free]]></description><link>https://andrewlock.net/making-foreach-on-an-ienumerable-allocation-free-using-reflection-and-dynamic-methods/</link><guid isPermaLink="true">https://andrewlock.net/making-foreach-on-an-ienumerable-allocation-free-using-reflection-and-dynamic-methods/</guid><pubDate>Tue, 20 Jan 2026 10:00:00 GMT</pubDate><dc:creator><![CDATA[Andrew Lock]]></dc:creator><media:content url="https://andrewlock.net/content/images/2026/enumeration_banner.png" medium="image" /><content:encoded><![CDATA[<img src="https://andrewlock.net/content/images/2026/enumeration_banner.png" /><p>In this post I describe a technique for reducing the allocation associated with calling <code>foreach</code> on an <code>IEnumerable&lt;T&gt;</code>. This has been <a href="https://www.macrosssoftware.com/2020/07/13/enumerator-performance-surprises/">described</a> and <a href="https://github.com/open-telemetry/opentelemetry-dotnet/blob/73bff75ef653f81fe6877299435b21131be36dc0/src/OpenTelemetry/Internal/EnumerationHelper.cs#L58">used</a> previously by others, but I was recently optimizing some code in my day job working on the .NET SDK at Datadog and used the technique, so decided to explain it in more detail.</p> <h2 id="background-when-foreach-allocates" class="heading-with-anchor"><a href="https://andrewlock.net/making-foreach-on-an-ienumerable-allocation-free-using-reflection-and-dynamic-methods/#background-when-foreach-allocates" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Background: when <code>foreach</code> allocates</a></h2> <p><code>foreach</code> is one of the most commonly used patterns in C#; it's literally used all over the place. A quick, crude, <a href="https://github.com/search?q=repo%3Adotnet%2Fruntime+%2F%28%3F-i%29foreach%2F+language%3AC%23&amp;type=code&amp;l=C%23">search of the dotnet/runtime</a> repository reveals 3.9 <em>thousand</em> instances! The vast majority of those cases are enumerating built-in types from the .NET base class library, such as <code>List&lt;T&gt;</code> and arrays, but you can easily <code>foreach</code> over your own custom types too.</p> <p>Interestingly, the way that <em>most</em> people likely think or are taught about <code>foreach</code> is that you need to implement <code>IEnumerable</code> (or <code>IEnumerable&lt;T&gt;</code>), and then you can enumerate the collection. This is correct, but there's actually an interesting subtlety. <em>Technically</em> <a href="https://ericlippert.com/2011/06/30/following-the-pattern/">the compiler uses pattern matching</a>, and looks for a <code>GetEnumerator()</code> method that returns an <code>Enumerator</code>-like type with a <code>Current</code> property and <code>MoveNext</code> method. That pattern requirement is <a href="https://learn.microsoft.com/en-us/dotnet/api/system.collections.ienumerable.getenumerator?view=net-10.0">the <em>same</em> as what <code>IEnumerable</code> defines</a>, so what's the difference?</p> <p>Before we dig into that, it's worth taking a look at a quick benchmark which demonstrates the difference.</p> <h3 id="creating-a-benchmark-to-compare-foreach" class="heading-with-anchor"><a href="https://andrewlock.net/making-foreach-on-an-ienumerable-allocation-free-using-reflection-and-dynamic-methods/#creating-a-benchmark-to-compare-foreach" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Creating a benchmark to compare <code>foreach</code></a></h3> <p>I started by creating a new BenchmarkDotNet project <a href="https://benchmarkdotnet.org/articles/guides/dotnet-new-templates.html">using their templates</a> by running</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp">dotnet <span class="token keyword">new</span> benchmark
</code></pre></div> <p>I then updated the <code>Benchmarks</code> file as shown below. This simple benchmark just calls <code>foreach</code> on a <code>List&lt;int&gt;</code> instance, and then runs the same <code>foreach</code> loop on the <em>same</em> <code>List&lt;int&gt;</code>, but this time stored as an <code>IEnumerable&lt;int&gt;</code> variable:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">using</span> <span class="token namespace">System<span class="token punctuation">.</span>Collections<span class="token punctuation">.</span>Generic</span><span class="token punctuation">;</span>
<span class="token keyword">using</span> <span class="token namespace">System<span class="token punctuation">.</span>Linq</span><span class="token punctuation">;</span>
<span class="token keyword">using</span> <span class="token namespace">BenchmarkDotNet<span class="token punctuation">.</span>Attributes</span><span class="token punctuation">;</span>

<span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">MemoryDiagnoser</span></span><span class="token punctuation">]</span>
<span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">Benchmarks</span>
<span class="token punctuation">{</span>
    <span class="token keyword">private</span> <span class="token class-name">List<span class="token punctuation">&lt;</span><span class="token keyword">int</span><span class="token punctuation">&gt;</span></span> _list<span class="token punctuation">;</span>
    <span class="token keyword">private</span> <span class="token class-name">IEnumerable<span class="token punctuation">&lt;</span><span class="token keyword">int</span><span class="token punctuation">&gt;</span></span> _enumerable<span class="token punctuation">;</span>

    <span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">GlobalSetup</span></span><span class="token punctuation">]</span>
    <span class="token keyword">public</span> <span class="token return-type class-name"><span class="token keyword">void</span></span> <span class="token function">GlobalSetup</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        _list <span class="token operator">=</span> Enumerable<span class="token punctuation">.</span><span class="token function">Range</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">,</span> <span class="token number">10_000</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">ToList</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        _enumerable <span class="token operator">=</span> _list<span class="token punctuation">;</span>
    <span class="token punctuation">}</span>

    <span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">Benchmark</span></span><span class="token punctuation">]</span>
    <span class="token keyword">public</span> <span class="token return-type class-name"><span class="token keyword">long</span></span> <span class="token function">List</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        <span class="token class-name"><span class="token keyword">var</span></span> <span class="token keyword">value</span> <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span>
        <span class="token keyword">foreach</span> <span class="token punctuation">(</span><span class="token class-name"><span class="token keyword">int</span></span> i <span class="token keyword">in</span> _list<span class="token punctuation">)</span>
        <span class="token punctuation">{</span>
            <span class="token keyword">value</span> <span class="token operator">+=</span> i<span class="token punctuation">;</span>
        <span class="token punctuation">}</span>

        <span class="token keyword">return</span> <span class="token keyword">value</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>

    <span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">Benchmark</span></span><span class="token punctuation">]</span>
    <span class="token keyword">public</span> <span class="token return-type class-name"><span class="token keyword">long</span></span> <span class="token function">IEnumerable</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        <span class="token class-name"><span class="token keyword">var</span></span> <span class="token keyword">value</span> <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span>
        <span class="token keyword">foreach</span> <span class="token punctuation">(</span><span class="token class-name"><span class="token keyword">int</span></span> i <span class="token keyword">in</span> _enumerable<span class="token punctuation">)</span>
        <span class="token punctuation">{</span>
            <span class="token keyword">value</span> <span class="token operator">+=</span> i<span class="token punctuation">;</span>
        <span class="token punctuation">}</span>

        <span class="token keyword">return</span> <span class="token keyword">value</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>You might think that both these benchmarks would give the same results. Afterall, they're running the <em>same</em> <code>foreach</code> loop on the <em>same</em> <code>List&lt;T&gt;</code> instance. The only difference is whether the variable is stored as a <code>List&lt;int&gt;</code> or an <code>IEumerable&lt;int&gt;</code>, that can't make much difference, right?</p> <p>If we run the benchmarks (I ran them against both .NET Framework and .NET 9), then we can see there actually <em>is</em> a difference; the <code>IEnumerable</code> version is both slower <em>and</em> it allocates:</p> <table><thead><tr><th>Method</th><th>Runtime</th><th style="text-align:right">Mean</th><th style="text-align:right">Error</th><th style="text-align:right">StdDev</th><th style="text-align:right">Allocated</th></tr></thead><tbody><tr><td>List</td><td>.NET Framework 4.8</td><td style="text-align:right">8.245 us</td><td style="text-align:right">0.1582 us</td><td style="text-align:right">0.1480 us</td><td style="text-align:right">-</td></tr><tr><td>IEnumerable</td><td>.NET Framework 4.8</td><td style="text-align:right">25.433 us</td><td style="text-align:right">0.4977 us</td><td style="text-align:right">0.6644 us</td><td style="text-align:right">40 B</td></tr><tr><td></td><td></td><td style="text-align:right"></td><td style="text-align:right"></td><td style="text-align:right"></td><td style="text-align:right"></td></tr><tr><td>List</td><td>.NET 9.0</td><td style="text-align:right">2.951 us</td><td style="text-align:right">0.0587 us</td><td style="text-align:right">0.0861 us</td><td style="text-align:right">-</td></tr><tr><td>IEnumerable</td><td>.NET 9.0</td><td style="text-align:right">8.032 us</td><td style="text-align:right">0.1520 us</td><td style="text-align:right">0.1422 us</td><td style="text-align:right">40 B</td></tr></tbody></table> <p>So the question is, <em>why</em>?</p> <h3 id="foreach-as-lowered-c-" class="heading-with-anchor"><a href="https://andrewlock.net/making-foreach-on-an-ienumerable-allocation-free-using-reflection-and-dynamic-methods/#foreach-as-lowered-c-" class="relative text-zinc-800 dark:text-white no-underline hover:underline"><code>foreach</code> as lowered C#</a></h3> <p>It helps initially to understand exactly what the <code>foreach</code> construct looks like in "lowered" C#. This is effectively what the compiler converts the <code>foreach</code> loop into before converting it to IL. If we take the <code>EnumerateList()</code> method above and <a href="https://sharplab.io/#v2:CYLg1APgAgDABFAjAFgNwFgBQsGIHQAyAlgHYCOGmWUAzAgExwBCApiQMYAWAtgIYBOAawDOWAN5Y4UuAAd+RAG68ALizjFhygDyllAPjgB9ADZFNlSdNpxjAexIBzOAFESAV24t+KlhuUAKAEpLKQlMaQi4JX4o3mM3NQBeOBhKSOkAM1t+Fl4uOH9dOCJikiNTTWDw9LgwmsileLUwZKI0moBfLBDIqAB2WKb2qS7MDqA=">run it through sharplab.io</a>, you get the following:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">private</span> <span class="token class-name">List<span class="token punctuation">&lt;</span><span class="token keyword">int</span><span class="token punctuation">&gt;</span></span> _list<span class="token punctuation">;</span>

<span class="token keyword">public</span> <span class="token return-type class-name"><span class="token keyword">long</span></span> <span class="token function">EnumerateList</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token punctuation">{</span>
    <span class="token class-name"><span class="token keyword">int</span></span> num <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span>
    <span class="token class-name">List<span class="token punctuation">&lt;</span><span class="token keyword">int</span><span class="token punctuation">&gt;</span><span class="token punctuation">.</span>Enumerator</span> enumerator <span class="token operator">=</span> _list<span class="token punctuation">.</span><span class="token function">GetEnumerator</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">try</span>
    <span class="token punctuation">{</span>
        <span class="token keyword">while</span> <span class="token punctuation">(</span>enumerator<span class="token punctuation">.</span><span class="token function">MoveNext</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span>
        <span class="token punctuation">{</span>
            <span class="token class-name"><span class="token keyword">int</span></span> current <span class="token operator">=</span> enumerator<span class="token punctuation">.</span>Current<span class="token punctuation">;</span>
            num <span class="token operator">+=</span> current<span class="token punctuation">;</span>
        <span class="token punctuation">}</span>
    <span class="token punctuation">}</span>
    <span class="token keyword">finally</span>
    <span class="token punctuation">{</span>
        <span class="token punctuation">(</span><span class="token punctuation">(</span>IDisposable<span class="token punctuation">)</span>enumerator<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">Dispose</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
    <span class="token keyword">return</span> num<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>As you can see, in this example, the <code>GetEnumerator()</code> method returns a <code>List&lt;int&gt;.Enumerator</code> instance, which exposes a <code>MoveNext()</code> method, a <code>Current</code> property, and implements <code>IDisposable</code>. If we compare that to <code>EnumerateIEnumerable()</code> we <a href="https://sharplab.io/#v2:CYLg1APgAgDABFAjAFgNwFgBQsGIHQAyAlgHYCOGmWUAzAgExwBCApiQMYAWAtgIYBOAawDOWAN5Y4UuAAd+RAG68ALi1w0APKWUA+OAH02AV24t+vAEYAbFpUnTacKwHsSAczgBREibMqWSDQAFACU9lISmNLRcEr8sbxWRmoAvHAwlDHSAGbO/Cy8XHBB2nBEZSQGxqbm1ixhUVlwkU0xSklqYGlEmU0AvljhMVAA7AkdvVIDmH1AA">get <em>almost</em> the same code</a>:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">private</span> <span class="token class-name">IEnumerable<span class="token punctuation">&lt;</span><span class="token keyword">int</span><span class="token punctuation">&gt;</span></span> _enumerable<span class="token punctuation">;</span>

<span class="token keyword">public</span> <span class="token return-type class-name"><span class="token keyword">long</span></span> <span class="token function">EnumerateIEnumerable</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token punctuation">{</span>
    <span class="token class-name"><span class="token keyword">int</span></span> num <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span>
    <span class="token class-name">IEnumerator<span class="token punctuation">&lt;</span><span class="token keyword">int</span><span class="token punctuation">&gt;</span></span> enumerator <span class="token operator">=</span> _enumerable<span class="token punctuation">.</span><span class="token function">GetEnumerator</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">try</span>
    <span class="token punctuation">{</span>
        <span class="token keyword">while</span> <span class="token punctuation">(</span>enumerator<span class="token punctuation">.</span><span class="token function">MoveNext</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span>
        <span class="token punctuation">{</span>
            <span class="token class-name"><span class="token keyword">int</span></span> current <span class="token operator">=</span> enumerator<span class="token punctuation">.</span>Current<span class="token punctuation">;</span>
            num <span class="token operator">+=</span> current<span class="token punctuation">;</span>
        <span class="token punctuation">}</span>
    <span class="token punctuation">}</span>
    <span class="token keyword">finally</span>
    <span class="token punctuation">{</span>
        <span class="token keyword">if</span> <span class="token punctuation">(</span>enumerator <span class="token operator">!=</span> <span class="token keyword">null</span><span class="token punctuation">)</span>
        <span class="token punctuation">{</span>
            enumerator<span class="token punctuation">.</span><span class="token function">Dispose</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token punctuation">}</span>
    <span class="token punctuation">}</span>
    <span class="token keyword">return</span> num<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>The main difference in the code above is that the <code>GetEnumerator()</code> method returns an <code>IEnumerator&lt;int&gt;</code> instance instead of a concrete <code>List&lt;int&gt;.Enumerator</code> instance. If we look at the implementation details <a href="https://github.com/dotnet/dotnet/blob/b0f34d51fccc69fd334253924abd8d6853fad7aa/src/runtime/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/List.cs#L665">of <code>List&lt;T&gt;</code>'s enumeration </a>methods, we can see there actually 3 different implementations, but they all ultimately delegate to the <code>GetEnumerator()</code> method that returns an <code>List&lt;T&gt;.Enumerator</code> instance.</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">List<span class="token punctuation">&lt;</span>T<span class="token punctuation">&gt;</span></span>
<span class="token punctuation">{</span>
    <span class="token keyword">public</span> <span class="token return-type class-name">Enumerator</span> <span class="token function">GetEnumerator</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=&gt;</span> <span class="token keyword">new</span> <span class="token constructor-invocation class-name">Enumerator</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token return-type class-name">IEnumerator<span class="token punctuation">&lt;</span>T<span class="token punctuation">&gt;</span></span> IEnumerable<span class="token operator">&lt;</span>T<span class="token operator">&gt;</span><span class="token punctuation">.</span><span class="token function">GetEnumerator</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=&gt;</span> <span class="token function">GetEnumerator</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token return-type class-name">IEnumerator</span> IEnumerable<span class="token punctuation">.</span><span class="token function">GetEnumerator</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=&gt;</span> <span class="token punctuation">(</span><span class="token punctuation">(</span>IEnumerable<span class="token operator">&lt;</span>T<span class="token operator">&gt;</span><span class="token punctuation">)</span><span class="token keyword">this</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">GetEnumerator</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token keyword">public</span> <span class="token keyword">struct</span> <span class="token class-name">Enumerator</span> <span class="token punctuation">:</span> <span class="token type-list"><span class="token class-name">IEnumerator<span class="token punctuation">&lt;</span>T<span class="token punctuation">&gt;</span></span><span class="token punctuation">,</span> <span class="token class-name">IEnumerator</span></span>
    <span class="token punctuation">{</span>
        <span class="token comment">// details hidden for brevity</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>And importantly, the <code>List&lt;T&gt;.Enumerator</code> is defined as a <code>struct</code> type.</p> <h3 id="struct-enumerators" class="heading-with-anchor"><a href="https://andrewlock.net/making-foreach-on-an-ienumerable-allocation-free-using-reflection-and-dynamic-methods/#struct-enumerators" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Struct enumerators</a></h3> <p>The <code>struct</code> enumerator is the key to the difference in allocation. By returning a mutable <code>struct</code> implementation of the <code>Enumerator</code> instead of a <code>class</code>, the <code>List&lt;T&gt;.Enumerator</code> type can be allocated on the stack, avoid any allocation on the heap, and so avoid adding pressure on the garbage collector. That's <em>as long</em> as the compiler can call the <code>GetEnumerator()</code> method directly…</p> <p>However, when calling <code>foreach</code> on the <code>IEnumerable</code> variable, we need to return an <code>IEnumerator</code> (or <code>IEnumerator&lt;T&gt;</code>) to satisfy the interface. The only way to do that is for the <code>List&lt;T&gt;.Enumerator</code> object to <a href="https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/types/boxing-and-unboxing">be boxed onto the heap</a>. This is the source of the allocation we saw in the benchmark for the <code>IEnumerable</code> variable.</p> <p>In general, this limitation is all a little unfortunate and kind of annoying. Returning basic interface types like <code>IEnumerable&lt;T&gt;</code> or <code>ICollection&lt;T&gt;</code> rather than their concrete types is a standard method of encapsulation, which allows for later evolution without disrupting the public API, and is generally, <em>rightly</em>, encouraged. It's just a shame that results in allocation. Unless, that is, you're using .NET 10…</p> <h3 id="a-net-10-caveat-deabstraction" class="heading-with-anchor"><a href="https://andrewlock.net/making-foreach-on-an-ienumerable-allocation-free-using-reflection-and-dynamic-methods/#a-net-10-caveat-deabstraction" class="relative text-zinc-800 dark:text-white no-underline hover:underline">A .NET 10 caveat: deabstraction</a></h3> <p>If I run the same benchmark above on .NET 10, I get some interesting results:</p> <table><thead><tr><th>Method</th><th>Runtime</th><th style="text-align:right">Mean</th><th style="text-align:right">Error</th><th style="text-align:right">StdDev</th><th style="text-align:right">Allocated</th></tr></thead><tbody><tr><td>List</td><td>.NET 10.0</td><td style="text-align:right">2.895 us</td><td style="text-align:right">0.0527 us</td><td style="text-align:right">0.0493 us</td><td style="text-align:right">-</td></tr><tr><td>IEnumerable</td><td>.NET 10.0</td><td style="text-align:right">3.016 us</td><td style="text-align:right">0.0590 us</td><td style="text-align:right">0.0725 us</td><td style="text-align:right">-</td></tr></tbody></table> <p><em>Both</em> benchmarks are essentially the same. There's no allocation, and the execution time is essentially the same! So what's going on here? the short answer is that .NET 10 <a href="https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-10/#deabstraction">introduced a bunch of techniques</a> to make this sort of pattern faster. There's devertualization, so the runtime can see that it's always a <code>List&lt;T&gt;</code> and call the <code>struct</code> enumerator, and there's also Object Stack Allocation, where objects which would otherwise be allocated to the heap are actually allocated to the stack if the compiler can prove the object won't "escape". Add to that <a href="https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-10/#collections"><em>additional</em> work to fix the <code>List&lt;T&gt;.Enumerator</code></a>, and you get the glorious results above!</p> <p>Which is all great if you're using .NET 10. Unfortunately, in my work on the Datadog .NET SDK, we have customers that run on all sorts of older versions of .NET (including .NET Framework), and as we are often in the hot path for apps, we need to be as efficient as possible. And all those 40 byte allocations add up!</p> <h2 id="avoiding-foreach-allocation-for-known-return-types" class="heading-with-anchor"><a href="https://andrewlock.net/making-foreach-on-an-ienumerable-allocation-free-using-reflection-and-dynamic-methods/#avoiding-foreach-allocation-for-known-return-types" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Avoiding <code>foreach</code> allocation for known return types</a></h2> <p>These days, most collection types that are exposed by the BCL or by popular libraries will use the same pattern of a <code>stack</code>-based enumerator. But you lose these performance benefits when the collection is exposed as an <code>IEnumerable</code> collection.</p> <p>One way to avoid this regression (if you know what the return type of an API will be) is to simply cast to that type, so the compiler can "find" the better <code>GetEnumerator()</code> method:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token class-name">IEnumerable<span class="token punctuation">&lt;</span><span class="token keyword">int</span><span class="token punctuation">&gt;</span></span> someCollection <span class="token operator">=</span> <span class="token function">SomeApiThatReturnsAList</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token comment">// If we know that someCollection always returns List&lt;T&gt;, we can "help" the compiler</span>
<span class="token keyword">if</span><span class="token punctuation">(</span>someCollection <span class="token keyword">is</span> <span class="token class-name">List<span class="token punctuation">&lt;</span><span class="token keyword">int</span><span class="token punctuation">&gt;</span></span> list<span class="token punctuation">)</span>
<span class="token punctuation">{</span>
    <span class="token comment">// The compiler can call `List&lt;T&gt;.GetEnumerator()`, allocate</span>
    <span class="token comment">// on the stack, and avoid the boxing allocation</span>
    <span class="token keyword">foreach</span><span class="token punctuation">(</span><span class="token class-name"><span class="token keyword">var</span></span> <span class="token keyword">value</span> <span class="token keyword">in</span> list<span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
<span class="token keyword">else</span>
<span class="token punctuation">{</span>
    <span class="token comment">// Optionally Keep a fallback case for safety, in case our assumptions are wrong</span>
    <span class="token comment">// or it changes in the future</span>
    <span class="token keyword">foreach</span><span class="token punctuation">(</span><span class="token class-name"><span class="token keyword">var</span></span> <span class="token keyword">value</span> <span class="token keyword">in</span> someCollection<span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>It feels a bit clumsy but it works to avoid the allocations, and when you're trying to be efficient, every byte counts!</p> <h2 id="avoiding-foreach-allocation-when-you-can-t-reference-the-return-type" class="heading-with-anchor"><a href="https://andrewlock.net/making-foreach-on-an-ienumerable-allocation-free-using-reflection-and-dynamic-methods/#avoiding-foreach-allocation-when-you-can-t-reference-the-return-type" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Avoiding <code>foreach</code> allocation when you can't reference the return type</a></h2> <p>The above approach is easy and works well if</p> <ol><li>You know what type is going to be returned by an API. Obviously this may change (that's the whole point of using <code>IEnumerable</code> after all!) so you must make sure to handle this scenario.</li> <li>That type is public, so you can reference it.</li></ol> <p>That second point is often a problem for us in the Datadog SDK, because we instrument many different libraries, and can't reference them at compile time. So if we want to avoid allocation from enumerators, we need to do something else.</p> <p>Take for example <a href="https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.activity.tagobjects?view=net-10.0">the <code>Activity.TagObjects</code> property</a>. This API returns an <code>IEnumerable&lt;KeyValuePair&lt;string, object&gt;&gt;</code>, but <a href="https://github.com/dotnet/dotnet/blob/b0f34d51fccc69fd334253924abd8d6853fad7aa/src/runtime/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Activity.cs#L109">the concrete type is <code>TagsLinkedList</code></a>, which is <a href="https://github.com/dotnet/dotnet/blob/b0f34d51fccc69fd334253924abd8d6853fad7aa/src/runtime/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Activity.cs#L1632">an internal type</a>, with a <code>struct</code> enumerator. We can't use the <code>is</code> trick above because <code>TagsLinkedList</code> isn't public (and <a href="https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.activity.enumeratetagobjects?view=net-10.0">we can't use the <code>EnumerateTagObjects()</code> method</a>, because that's not available in all runtimes we support). So how can we avoid the allocation?</p> <p>The answer was to use an approach that we use in various other places: use <em>Reflection.Emit</em> capabilities to create a <code>DynamicMethod</code> that explicitly uses the struct enumerator.</p> <blockquote> <p>As I mentioned at the start of this post, this approach isn't novel, and has been <a href="https://www.macrosssoftware.com/2020/07/13/enumerator-performance-surprises/">described</a> and <a href="https://github.com/open-telemetry/opentelemetry-dotnet/blob/73bff75ef653f81fe6877299435b21131be36dc0/src/OpenTelemetry/Internal/EnumerationHelper.cs#L58">used</a> previously by others. I mostly took that prior art and tweaked it for my purposes, so kudos to them for doing the hard work!</p> </blockquote> <h3 id="designing-our-reflection-emit-dynamicmethod" class="heading-with-anchor"><a href="https://andrewlock.net/making-foreach-on-an-ienumerable-allocation-free-using-reflection-and-dynamic-methods/#designing-our-reflection-emit-dynamicmethod" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Designing our <em>Reflection.Emit</em> <code>DynamicMethod</code></a></h3> <p>Reflection.Emit refers to the <em>System.Reflection.Emit</em> namespace, which contains various methods for creating <em>new</em> intermediate language (IL) in your application. IL instructions are the "assembly code" that the compiler outputs when you compile your application. The JIT in the .NET runtime converts these IL instructions into real assembly code when your application runs.</p> <p><em>Reflection.Emit</em> is primarily used by libraries and frameworks that are either trying to wild things or are trying to eek out performance wherever they can, so it's definitely an "advanced" API. If you haven't used it before, or you find it confusing, don't worry about it!</p> <p>In the implementation coming below, we're basically going to "manually" construct a method that contains a "lowered" <code>foreach</code> loop, but making sure we call the <code>struct</code>-based <code>GetEnumerator()</code> on the object. Something like this:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token comment">// This is effectively the method we're going to create</span>
<span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token return-type class-name"><span class="token keyword">void</span></span> <span class="token function">AllocationFreeForEach</span><span class="token punctuation">(</span>
    <span class="token class-name">TagsLinkedList</span> list<span class="token punctuation">,</span> <span class="token comment">// The object to enumerate</span>
     <span class="token keyword">ref</span> <span class="token class-name">SomeState</span> state<span class="token punctuation">,</span> <span class="token comment">// A state object the callback object can use </span>
      <span class="token class-name">Func<span class="token punctuation">&lt;</span>SomeState<span class="token punctuation">,</span> KeyValuePair<span class="token punctuation">&lt;</span><span class="token keyword">string</span><span class="token punctuation">,</span> <span class="token keyword">object</span><span class="token punctuation">&gt;</span><span class="token punctuation">,</span> <span class="token keyword">bool</span><span class="token punctuation">&gt;</span></span> callback<span class="token punctuation">)</span> <span class="token comment">// The callback to execute</span>
<span class="token punctuation">{</span>
    <span class="token comment">// We create a lowered version of this code:</span>
    <span class="token comment">// foreach(var item in list)</span>
    <span class="token comment">// {</span>
    <span class="token comment">//     if (!callback(ref state, item))</span>
    <span class="token comment">//         break;</span>
    <span class="token comment">// }</span>
    <span class="token keyword">using</span> <span class="token punctuation">(</span><span class="token class-name">TagsLinkedList<span class="token punctuation">.</span>Enumerator</span> enumerator <span class="token operator">=</span> list<span class="token punctuation">.</span><span class="token function">GetEnumerator</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        <span class="token keyword">while</span> <span class="token punctuation">(</span>enumerator<span class="token punctuation">.</span><span class="token function">MoveNext</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span>
        <span class="token punctuation">{</span>
            <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span><span class="token function">callback</span><span class="token punctuation">(</span><span class="token keyword">ref</span> state<span class="token punctuation">,</span> enumerator<span class="token punctuation">.</span>Current<span class="token punctuation">)</span><span class="token punctuation">)</span>
                <span class="token keyword">break</span><span class="token punctuation">;</span>
        <span class="token punctuation">}</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>We have to create the "lowered" version of the code when constructing our Dynamic Method, which means we <em>also</em> need to lower the <code>using</code> block, so we're actually looking at something more like this instead:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token return-type class-name"><span class="token keyword">void</span></span> <span class="token function">AllocationFreeForEach</span><span class="token punctuation">(</span>
    <span class="token class-name">TagsLinkedList</span> list<span class="token punctuation">,</span>
     <span class="token keyword">ref</span> <span class="token class-name">SomeState</span> state<span class="token punctuation">,</span>
      <span class="token class-name">Func<span class="token punctuation">&lt;</span>SomeState<span class="token punctuation">,</span> KeyValuePair<span class="token punctuation">&lt;</span><span class="token keyword">string</span><span class="token punctuation">,</span> <span class="token keyword">object</span><span class="token punctuation">&gt;</span><span class="token punctuation">,</span> <span class="token keyword">bool</span><span class="token punctuation">&gt;</span></span> callback<span class="token punctuation">)</span>
<span class="token punctuation">{</span>
    <span class="token class-name">TagsLinkedList<span class="token punctuation">.</span>Enumerator</span> enumerator <span class="token operator">=</span> list<span class="token punctuation">.</span><span class="token function">GetEnumerator</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token keyword">try</span>
    <span class="token punctuation">{</span>
        <span class="token keyword">while</span> <span class="token punctuation">(</span>enumerator<span class="token punctuation">.</span><span class="token function">MoveNext</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span>
        <span class="token punctuation">{</span>
            <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span><span class="token function">callback</span><span class="token punctuation">(</span><span class="token keyword">ref</span> state<span class="token punctuation">,</span> enumerator<span class="token punctuation">.</span>Current<span class="token punctuation">)</span><span class="token punctuation">)</span>
                <span class="token keyword">break</span><span class="token punctuation">;</span>
        <span class="token punctuation">}</span>

    <span class="token punctuation">}</span>
    <span class="token keyword">finally</span>
    <span class="token punctuation">{</span>
        enumerator<span class="token punctuation">.</span><span class="token function">Dispose</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>That covers pretty much what we want to emit, all we need to do now is to generate our <code>DynamicMethod</code>.</p> <h3 id="generating-the-dynamicmethod" class="heading-with-anchor"><a href="https://andrewlock.net/making-foreach-on-an-ienumerable-allocation-free-using-reflection-and-dynamic-methods/#generating-the-dynamicmethod" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Generating the <code>DynamicMethod</code></a></h3> <p>We'll emit a method similar to the code above, but as a generalized version that can be called with many different enumeration types, and with many different item types.</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">internal</span> <span class="token keyword">static</span> <span class="token keyword">class</span> <span class="token class-name">AllocationFreeEnumerator<span class="token punctuation">&lt;</span>TEnumerable<span class="token punctuation">,</span> TItem<span class="token punctuation">,</span> TState<span class="token punctuation">&gt;</span></span>
    <span class="token keyword">where</span> <span class="token class-name">TEnumerable</span> <span class="token punctuation">:</span> <span class="token type-list"><span class="token class-name">IEnumerable<span class="token punctuation">&lt;</span>TItem<span class="token punctuation">&gt;</span></span></span>
    <span class="token keyword">where</span> <span class="token class-name">TState</span> <span class="token punctuation">:</span> <span class="token type-list"><span class="token keyword">struct</span></span>
<span class="token punctuation">{</span>
    <span class="token comment">// Use reflection to references to the methods we need to call</span>
    <span class="token keyword">private</span> <span class="token keyword">static</span> <span class="token keyword">readonly</span> <span class="token class-name">MethodInfo</span> GenericGetEnumeratorMethod <span class="token operator">=</span> <span class="token keyword">typeof</span><span class="token punctuation">(</span><span class="token type-expression class-name">IEnumerable<span class="token punctuation">&lt;</span>TItem<span class="token punctuation">&gt;</span></span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">GetMethod</span><span class="token punctuation">(</span><span class="token string">"GetEnumerator"</span><span class="token punctuation">)</span><span class="token operator">!</span><span class="token punctuation">;</span>
    <span class="token keyword">private</span> <span class="token keyword">static</span> <span class="token keyword">readonly</span> <span class="token class-name">MethodInfo</span> GenericCurrentGetMethod <span class="token operator">=</span> <span class="token keyword">typeof</span><span class="token punctuation">(</span><span class="token type-expression class-name">IEnumerator<span class="token punctuation">&lt;</span>TItem<span class="token punctuation">&gt;</span></span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">GetProperty</span><span class="token punctuation">(</span><span class="token string">"Current"</span><span class="token punctuation">)</span><span class="token operator">!</span><span class="token punctuation">.</span>GetMethod<span class="token operator">!</span><span class="token punctuation">;</span>
    <span class="token keyword">private</span> <span class="token keyword">static</span> <span class="token keyword">readonly</span> <span class="token class-name">MethodInfo</span> MoveNextMethod <span class="token operator">=</span> <span class="token keyword">typeof</span><span class="token punctuation">(</span><span class="token type-expression class-name">IEnumerator</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">GetMethod</span><span class="token punctuation">(</span><span class="token string">"MoveNext"</span><span class="token punctuation">)</span><span class="token operator">!</span><span class="token punctuation">;</span>
    <span class="token keyword">private</span> <span class="token keyword">static</span> <span class="token keyword">readonly</span> <span class="token class-name">MethodInfo</span> DisposeMethod <span class="token operator">=</span> <span class="token keyword">typeof</span><span class="token punctuation">(</span><span class="token type-expression class-name">IDisposable</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">GetMethod</span><span class="token punctuation">(</span><span class="token string">"Dispose"</span><span class="token punctuation">)</span><span class="token operator">!</span><span class="token punctuation">;</span>

    <span class="token comment">// This is the method we're going to invoke</span>
    <span class="token keyword">public</span> <span class="token keyword">delegate</span> <span class="token return-type class-name"><span class="token keyword">void</span></span> <span class="token function">AllocationFreeForEachDelegate</span><span class="token punctuation">(</span><span class="token class-name">TEnumerable</span> instance<span class="token punctuation">,</span> <span class="token keyword">ref</span> <span class="token class-name">TState</span> state<span class="token punctuation">,</span> <span class="token class-name">CallbackDelegate</span> itemCallback<span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token comment">// This is the callback which is invoked for each item</span>
    <span class="token keyword">public</span> <span class="token keyword">delegate</span> <span class="token return-type class-name"><span class="token keyword">bool</span></span> <span class="token function">CallbackDelegate</span><span class="token punctuation">(</span><span class="token keyword">ref</span> <span class="token class-name">TState</span> state<span class="token punctuation">,</span> <span class="token class-name">TItem</span> item<span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token comment">// Build an allocation-free enumerator</span>
    <span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token return-type class-name">AllocationFreeForEachDelegate</span> <span class="token function">BuildAllocationFreeForEachDelegate</span><span class="token punctuation">(</span><span class="token class-name">Type</span> enumerableType<span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        <span class="token class-name"><span class="token keyword">var</span></span> itemCallbackType <span class="token operator">=</span> <span class="token keyword">typeof</span><span class="token punctuation">(</span><span class="token type-expression class-name">CallbackDelegate</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

        <span class="token comment">// Try to find a non-interface returning GetEnumerator() method</span>
        <span class="token class-name"><span class="token keyword">var</span></span> getEnumeratorMethod <span class="token operator">=</span> <span class="token function">ResolveGetEnumeratorMethodForType</span><span class="token punctuation">(</span>enumerableType<span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token keyword">if</span> <span class="token punctuation">(</span>getEnumeratorMethod <span class="token operator">==</span> <span class="token keyword">null</span><span class="token punctuation">)</span>
        <span class="token punctuation">{</span>
            <span class="token comment">// We couldn't find a non-interface GetEnumerator() method, so</span>
            <span class="token comment">// fallback to allocation mode and use IEnumerable&lt;TItem&gt;.GetEnumerator</span>
            getEnumeratorMethod <span class="token operator">=</span> GenericGetEnumeratorMethod<span class="token punctuation">;</span>
        <span class="token punctuation">}</span>

        <span class="token class-name"><span class="token keyword">var</span></span> enumeratorType <span class="token operator">=</span> getEnumeratorMethod<span class="token punctuation">.</span>ReturnType<span class="token punctuation">;</span>

        <span class="token comment">// build the Dynamic method (our AllocationFreeForEachDelegate)</span>
        <span class="token class-name"><span class="token keyword">var</span></span> dynamicMethod <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token constructor-invocation class-name">DynamicMethod</span><span class="token punctuation">(</span>
            <span class="token string">"AllocationFreeForEach"</span><span class="token punctuation">,</span>
            <span class="token keyword">null</span><span class="token punctuation">,</span>
            <span class="token punctuation">[</span><span class="token keyword">typeof</span><span class="token punctuation">(</span><span class="token type-expression class-name">TEnumerable</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token keyword">typeof</span><span class="token punctuation">(</span><span class="token type-expression class-name">TState</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">MakeByRefType</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span> itemCallbackType<span class="token punctuation">]</span><span class="token punctuation">,</span>
            <span class="token keyword">typeof</span><span class="token punctuation">(</span><span class="token type-expression class-name">AllocationFreeForEachDelegate</span><span class="token punctuation">)</span><span class="token punctuation">.</span>Module<span class="token punctuation">,</span>
            <span class="token named-parameter punctuation">skipVisibility</span><span class="token punctuation">:</span> <span class="token boolean">true</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

        <span class="token class-name"><span class="token keyword">var</span></span> generator <span class="token operator">=</span> dynamicMethod<span class="token punctuation">.</span><span class="token function">GetILGenerator</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

        <span class="token comment">// TagsLinkedList.Enumerator enumerator</span>
        generator<span class="token punctuation">.</span><span class="token function">DeclareLocal</span><span class="token punctuation">(</span>enumeratorType<span class="token punctuation">)</span><span class="token punctuation">;</span>

        <span class="token class-name"><span class="token keyword">var</span></span> beginLoopLabel <span class="token operator">=</span> generator<span class="token punctuation">.</span><span class="token function">DefineLabel</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token class-name"><span class="token keyword">var</span></span> processCurrentLabel <span class="token operator">=</span> generator<span class="token punctuation">.</span><span class="token function">DefineLabel</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token class-name"><span class="token keyword">var</span></span> returnLabel <span class="token operator">=</span> generator<span class="token punctuation">.</span><span class="token function">DefineLabel</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token class-name"><span class="token keyword">var</span></span> breakLoopLabel <span class="token operator">=</span> generator<span class="token punctuation">.</span><span class="token function">DefineLabel</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

        <span class="token comment">// enumerator = arg0.GetEnumerator();</span>
        generator<span class="token punctuation">.</span><span class="token function">Emit</span><span class="token punctuation">(</span>OpCodes<span class="token punctuation">.</span>Ldarg_0<span class="token punctuation">)</span><span class="token punctuation">;</span>
        generator<span class="token punctuation">.</span><span class="token function">Emit</span><span class="token punctuation">(</span>OpCodes<span class="token punctuation">.</span>Callvirt<span class="token punctuation">,</span> getEnumeratorMethod<span class="token punctuation">)</span><span class="token punctuation">;</span>
        generator<span class="token punctuation">.</span><span class="token function">Emit</span><span class="token punctuation">(</span>OpCodes<span class="token punctuation">.</span>Stloc_0<span class="token punctuation">)</span><span class="token punctuation">;</span>

        <span class="token comment">// try</span>
        generator<span class="token punctuation">.</span><span class="token function">BeginExceptionBlock</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token punctuation">{</span>
            <span class="token comment">// while()</span>
            generator<span class="token punctuation">.</span><span class="token function">Emit</span><span class="token punctuation">(</span>OpCodes<span class="token punctuation">.</span>Br_S<span class="token punctuation">,</span> beginLoopLabel<span class="token punctuation">)</span><span class="token punctuation">;</span>

            generator<span class="token punctuation">.</span><span class="token function">MarkLabel</span><span class="token punctuation">(</span>processCurrentLabel<span class="token punctuation">)</span><span class="token punctuation">;</span>

            <span class="token comment">// bool shouldContinue = callback(arg1, enumerator.Current);</span>
            generator<span class="token punctuation">.</span><span class="token function">Emit</span><span class="token punctuation">(</span>OpCodes<span class="token punctuation">.</span>Ldarg_2<span class="token punctuation">)</span><span class="token punctuation">;</span>
            generator<span class="token punctuation">.</span><span class="token function">Emit</span><span class="token punctuation">(</span>OpCodes<span class="token punctuation">.</span>Ldarg_1<span class="token punctuation">)</span><span class="token punctuation">;</span>
            generator<span class="token punctuation">.</span><span class="token function">Emit</span><span class="token punctuation">(</span>OpCodes<span class="token punctuation">.</span>Ldloca_S<span class="token punctuation">,</span> <span class="token number">0</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
            generator<span class="token punctuation">.</span><span class="token function">Emit</span><span class="token punctuation">(</span>OpCodes<span class="token punctuation">.</span>Constrained<span class="token punctuation">,</span> enumeratorType<span class="token punctuation">)</span><span class="token punctuation">;</span>
            generator<span class="token punctuation">.</span><span class="token function">Emit</span><span class="token punctuation">(</span>OpCodes<span class="token punctuation">.</span>Callvirt<span class="token punctuation">,</span> GenericCurrentGetMethod<span class="token punctuation">)</span><span class="token punctuation">;</span>

            generator<span class="token punctuation">.</span><span class="token function">Emit</span><span class="token punctuation">(</span>OpCodes<span class="token punctuation">.</span>Callvirt<span class="token punctuation">,</span> itemCallbackType<span class="token punctuation">.</span><span class="token function">GetMethod</span><span class="token punctuation">(</span><span class="token string">"Invoke"</span><span class="token punctuation">)</span><span class="token operator">!</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

            <span class="token comment">// if (!continue)</span>
            <span class="token comment">//     break;</span>
            generator<span class="token punctuation">.</span><span class="token function">Emit</span><span class="token punctuation">(</span>OpCodes<span class="token punctuation">.</span>Brtrue_S<span class="token punctuation">,</span> beginLoopLabel<span class="token punctuation">)</span><span class="token punctuation">;</span>
            generator<span class="token punctuation">.</span><span class="token function">Emit</span><span class="token punctuation">(</span>OpCodes<span class="token punctuation">.</span>Leave_S<span class="token punctuation">,</span> returnLabel<span class="token punctuation">)</span><span class="token punctuation">;</span>

            <span class="token comment">// if (enumerator.MoveNext())</span>
            <span class="token comment">//    goto: start of while loop</span>
            generator<span class="token punctuation">.</span><span class="token function">MarkLabel</span><span class="token punctuation">(</span>beginLoopLabel<span class="token punctuation">)</span><span class="token punctuation">;</span>
            generator<span class="token punctuation">.</span><span class="token function">Emit</span><span class="token punctuation">(</span>OpCodes<span class="token punctuation">.</span>Ldloca_S<span class="token punctuation">,</span> <span class="token number">0</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
            generator<span class="token punctuation">.</span><span class="token function">Emit</span><span class="token punctuation">(</span>OpCodes<span class="token punctuation">.</span>Constrained<span class="token punctuation">,</span> enumeratorType<span class="token punctuation">)</span><span class="token punctuation">;</span>
            generator<span class="token punctuation">.</span><span class="token function">Emit</span><span class="token punctuation">(</span>OpCodes<span class="token punctuation">.</span>Callvirt<span class="token punctuation">,</span> MoveNextMethod<span class="token punctuation">)</span><span class="token punctuation">;</span>
            generator<span class="token punctuation">.</span><span class="token function">Emit</span><span class="token punctuation">(</span>OpCodes<span class="token punctuation">.</span>Brtrue_S<span class="token punctuation">,</span> processCurrentLabel<span class="token punctuation">)</span><span class="token punctuation">;</span>

            <span class="token comment">// close while loop</span>
            generator<span class="token punctuation">.</span><span class="token function">MarkLabel</span><span class="token punctuation">(</span>breakLoopLabel<span class="token punctuation">)</span><span class="token punctuation">;</span>
            generator<span class="token punctuation">.</span><span class="token function">Emit</span><span class="token punctuation">(</span>OpCodes<span class="token punctuation">.</span>Leave_S<span class="token punctuation">,</span> returnLabel<span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token punctuation">}</span>

        <span class="token comment">// finally</span>
        generator<span class="token punctuation">.</span><span class="token function">BeginFinallyBlock</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token punctuation">{</span>
            <span class="token comment">// enumerator.Dispose();</span>
            <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token keyword">typeof</span><span class="token punctuation">(</span><span class="token type-expression class-name">IDisposable</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">IsAssignableFrom</span><span class="token punctuation">(</span>enumeratorType<span class="token punctuation">)</span><span class="token punctuation">)</span>
            <span class="token punctuation">{</span>
                generator<span class="token punctuation">.</span><span class="token function">Emit</span><span class="token punctuation">(</span>OpCodes<span class="token punctuation">.</span>Ldloca_S<span class="token punctuation">,</span> <span class="token number">0</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
                generator<span class="token punctuation">.</span><span class="token function">Emit</span><span class="token punctuation">(</span>OpCodes<span class="token punctuation">.</span>Constrained<span class="token punctuation">,</span> enumeratorType<span class="token punctuation">)</span><span class="token punctuation">;</span>
                generator<span class="token punctuation">.</span><span class="token function">Emit</span><span class="token punctuation">(</span>OpCodes<span class="token punctuation">.</span>Callvirt<span class="token punctuation">,</span> DisposeMethod<span class="token punctuation">)</span><span class="token punctuation">;</span>
            <span class="token punctuation">}</span>
        <span class="token punctuation">}</span>

        generator<span class="token punctuation">.</span><span class="token function">EndExceptionBlock</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

        generator<span class="token punctuation">.</span><span class="token function">MarkLabel</span><span class="token punctuation">(</span>returnLabel<span class="token punctuation">)</span><span class="token punctuation">;</span>

        <span class="token comment">// return</span>
        generator<span class="token punctuation">.</span><span class="token function">Emit</span><span class="token punctuation">(</span>OpCodes<span class="token punctuation">.</span>Ret<span class="token punctuation">)</span><span class="token punctuation">;</span>

        <span class="token keyword">return</span> <span class="token punctuation">(</span>AllocationFreeForEachDelegate<span class="token punctuation">)</span>dynamicMethod<span class="token punctuation">.</span><span class="token function">CreateDelegate</span><span class="token punctuation">(</span><span class="token keyword">typeof</span><span class="token punctuation">(</span><span class="token type-expression class-name">AllocationFreeForEachDelegate</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>

    <span class="token keyword">private</span> <span class="token keyword">static</span> <span class="token return-type class-name">MethodInfo<span class="token punctuation">?</span></span> <span class="token function">ResolveGetEnumeratorMethodForType</span><span class="token punctuation">(</span><span class="token class-name">Type</span> type<span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        <span class="token comment">// Look for a `GetEnumerator()` method that _doesn't_ return an</span>
        <span class="token comment">// interface. This doesn't _guarantee_ a struct-based enumerator,</span>
        <span class="token comment">// but it's the standard pattern so catches most cases</span>
        <span class="token class-name"><span class="token keyword">var</span></span> methods <span class="token operator">=</span> type<span class="token punctuation">.</span><span class="token function">GetMethods</span><span class="token punctuation">(</span>BindingFlags<span class="token punctuation">.</span>Instance <span class="token operator">|</span> BindingFlags<span class="token punctuation">.</span>Public <span class="token operator">|</span> BindingFlags<span class="token punctuation">.</span>NonPublic<span class="token punctuation">)</span><span class="token punctuation">;</span>

        <span class="token keyword">foreach</span> <span class="token punctuation">(</span><span class="token class-name"><span class="token keyword">var</span></span> method <span class="token keyword">in</span> methods<span class="token punctuation">)</span>
        <span class="token punctuation">{</span>
            <span class="token keyword">if</span> <span class="token punctuation">(</span>method<span class="token punctuation">.</span>Name <span class="token operator">==</span> <span class="token string">"GetEnumerator"</span> <span class="token operator">&amp;&amp;</span> <span class="token operator">!</span>method<span class="token punctuation">.</span>ReturnType<span class="token punctuation">.</span>IsInterface<span class="token punctuation">)</span>
            <span class="token punctuation">{</span>
                <span class="token keyword">return</span> method<span class="token punctuation">;</span>
            <span class="token punctuation">}</span>
        <span class="token punctuation">}</span>

        <span class="token keyword">return</span> <span class="token keyword">null</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>There's a lot of code there, and if you struggle to follow IL then it will no doubt be confusing 😅 The only small piece of advice I have if you're trying to <em>write</em> this code, is to use an IL generator to show the IL that you <em>should</em> be trying to generate. I tend to use the one built into Rider when I'm working on this stuff:</p> <p><img src="https://andrewlock.net/content/images/2026/enumeration_il.png" alt="The output of Rider's IL window"></p> <p>Now that we have this dynamic method generator, we can put it to the test and check the results.</p> <h3 id="benchmarking-the-dynamicmethod-on-listt" class="heading-with-anchor"><a href="https://andrewlock.net/making-foreach-on-an-ienumerable-allocation-free-using-reflection-and-dynamic-methods/#benchmarking-the-dynamicmethod-on-listt" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Benchmarking the <code>DynamicMethod</code> on <code>List&lt;T&gt;</code></a></h3> <p>To test it out, I initially updated the benchmark to test 3 different scenarios</p> <ul><li>A <code>List&lt;int&gt;</code> saved in a <code>List&lt;int&gt;</code> variable</li> <li>A <code>List&lt;int&gt;</code> saved in an <code>IEnumerable&lt;int&gt;</code> variable</li> <li>A <code>List&lt;int&gt;</code> saved in an <code>IEnumerable&lt;int&gt;</code> variable, using the <code>DynamicMethod</code> above</li></ul> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">using</span> <span class="token namespace">System</span><span class="token punctuation">;</span>
<span class="token keyword">using</span> <span class="token namespace">System<span class="token punctuation">.</span>Collections</span><span class="token punctuation">;</span>
<span class="token keyword">using</span> <span class="token namespace">System<span class="token punctuation">.</span>Collections<span class="token punctuation">.</span>Generic</span><span class="token punctuation">;</span>
<span class="token keyword">using</span> <span class="token namespace">System<span class="token punctuation">.</span>Linq</span><span class="token punctuation">;</span>
<span class="token keyword">using</span> <span class="token namespace">BenchmarkDotNet<span class="token punctuation">.</span>Attributes</span><span class="token punctuation">;</span>

<span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">MemoryDiagnoser</span></span><span class="token punctuation">]</span>
<span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">Benchmarks</span>
<span class="token punctuation">{</span>
    <span class="token keyword">private</span> <span class="token class-name">List<span class="token punctuation">&lt;</span><span class="token keyword">int</span><span class="token punctuation">&gt;</span><span class="token punctuation">?</span></span> _list<span class="token punctuation">;</span>
    <span class="token keyword">private</span> <span class="token class-name">IEnumerable<span class="token punctuation">&lt;</span><span class="token keyword">int</span><span class="token punctuation">&gt;</span><span class="token punctuation">?</span></span> _listEnumerable<span class="token punctuation">;</span>
    <span class="token keyword">private</span> <span class="token class-name">AllocationFreeEnumerator<span class="token punctuation">&lt;</span>IEnumerable<span class="token punctuation">&lt;</span><span class="token keyword">int</span><span class="token punctuation">&gt;</span><span class="token punctuation">,</span> <span class="token keyword">int</span><span class="token punctuation">,</span> <span class="token keyword">long</span><span class="token punctuation">&gt;</span><span class="token punctuation">.</span>AllocationFreeForEachDelegate</span> _listEnumerator<span class="token punctuation">;</span>

    <span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">GlobalSetup</span></span><span class="token punctuation">]</span>
    <span class="token keyword">public</span> <span class="token return-type class-name"><span class="token keyword">void</span></span> <span class="token function">GlobalSetup</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        _list <span class="token operator">=</span> Enumerable<span class="token punctuation">.</span><span class="token function">Range</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">,</span> <span class="token number">10_000</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">ToList</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        _listEnumerable <span class="token operator">=</span> _list<span class="token punctuation">;</span>
        _listEnumerator <span class="token operator">=</span> AllocationFreeEnumerator<span class="token operator">&lt;</span>IEnumerable<span class="token operator">&lt;</span><span class="token keyword">int</span><span class="token operator">&gt;</span><span class="token punctuation">,</span> <span class="token keyword">int</span><span class="token punctuation">,</span> <span class="token keyword">long</span><span class="token operator">&gt;</span><span class="token punctuation">.</span><span class="token function">BuildAllocationFreeForEachDelegate</span><span class="token punctuation">(</span>_list<span class="token punctuation">.</span><span class="token function">GetType</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>

    <span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">Benchmark</span></span><span class="token punctuation">]</span>
    <span class="token keyword">public</span> <span class="token return-type class-name"><span class="token keyword">long</span></span> <span class="token function">List</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        <span class="token class-name"><span class="token keyword">long</span></span> <span class="token keyword">value</span> <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span>
        <span class="token keyword">foreach</span> <span class="token punctuation">(</span><span class="token class-name"><span class="token keyword">int</span></span> i <span class="token keyword">in</span> _list<span class="token operator">!</span><span class="token punctuation">)</span>
        <span class="token punctuation">{</span>
            <span class="token keyword">value</span> <span class="token operator">+=</span> i<span class="token punctuation">;</span>
        <span class="token punctuation">}</span>

        <span class="token keyword">return</span> <span class="token keyword">value</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>

    <span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">Benchmark</span></span><span class="token punctuation">]</span>
    <span class="token keyword">public</span> <span class="token return-type class-name"><span class="token keyword">long</span></span> <span class="token function">IEnumerable</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        <span class="token class-name"><span class="token keyword">long</span></span> <span class="token keyword">value</span> <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span>
        <span class="token keyword">foreach</span> <span class="token punctuation">(</span><span class="token class-name"><span class="token keyword">int</span></span> i <span class="token keyword">in</span> _listEnumerable<span class="token operator">!</span><span class="token punctuation">)</span>
        <span class="token punctuation">{</span>
            <span class="token keyword">value</span> <span class="token operator">+=</span> i<span class="token punctuation">;</span>
        <span class="token punctuation">}</span>

        <span class="token keyword">return</span> <span class="token keyword">value</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>

    <span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">Benchmark</span></span><span class="token punctuation">]</span>
    <span class="token keyword">public</span> <span class="token return-type class-name"><span class="token keyword">long</span></span> <span class="token function">IEnumerableDynamicMethod</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        <span class="token class-name"><span class="token keyword">long</span></span> <span class="token keyword">value</span> <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span>
        <span class="token function">_listEnumerator</span><span class="token punctuation">(</span>_list<span class="token operator">!</span><span class="token punctuation">,</span> <span class="token keyword">ref</span> <span class="token keyword">value</span><span class="token punctuation">,</span> <span class="token keyword">static</span> <span class="token punctuation">(</span><span class="token keyword">ref</span> state<span class="token punctuation">,</span> i<span class="token punctuation">)</span> <span class="token operator">=&gt;</span>
        <span class="token punctuation">{</span>
            state <span class="token operator">+=</span> i<span class="token punctuation">;</span>
            <span class="token keyword">return</span> <span class="token boolean">true</span><span class="token punctuation">;</span>
        <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

        <span class="token keyword">return</span> <span class="token keyword">value</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>The results from running this against .NET Framework 4.8 and .NET 9 are a bit of a mixed bag:</p> <table><thead><tr><th>Method</th><th>Runtime</th><th style="text-align:right">Mean</th><th style="text-align:right">Error</th><th style="text-align:right">StdDev</th><th style="text-align:right">Allocated</th></tr></thead><tbody><tr><td>List</td><td>.NET 9.0</td><td style="text-align:right">3.120 us</td><td style="text-align:right">0.0573 us</td><td style="text-align:right">0.0536 us</td><td style="text-align:right">-</td></tr><tr><td>IEnumerable</td><td>.NET 9.0</td><td style="text-align:right">7.554 us</td><td style="text-align:right">0.0935 us</td><td style="text-align:right">0.0828 us</td><td style="text-align:right">40 B</td></tr><tr><td>IEnumerableDynamicMethod</td><td>.NET 9.0</td><td style="text-align:right">15.436 us</td><td style="text-align:right">0.1631 us</td><td style="text-align:right">0.1446 us</td><td style="text-align:right">-</td></tr><tr><td></td><td></td><td style="text-align:right"></td><td style="text-align:right"></td><td style="text-align:right"></td><td style="text-align:right"></td></tr><tr><td>List</td><td>.NET Framework 4.8</td><td style="text-align:right">7.789 us</td><td style="text-align:right">0.0560 us</td><td style="text-align:right">0.0496 us</td><td style="text-align:right">-</td></tr><tr><td>IEnumerable</td><td>.NET Framework 4.8</td><td style="text-align:right">23.181 us</td><td style="text-align:right">0.1515 us</td><td style="text-align:right">0.1417 us</td><td style="text-align:right">40 B</td></tr><tr><td>IEnumerableDynamicMethod</td><td>.NET Framework 4.8</td><td style="text-align:right">14.894 us</td><td style="text-align:right">0.1978 us</td><td style="text-align:right">0.1754 us</td><td style="text-align:right">-</td></tr></tbody></table> <p>For .NET Framework, we're clearly onto a winner. We see reduced execution time <em>and</em> we're now allocation-free, so that's great.</p> <p>For .NET 9, we're now allocation free, but execution time has doubled, which is unfortunate, but likely comes from the fact that <code>List&lt;T&gt;</code> has seen a huge number of performance attention over the years, and we're likely stomping over that somewhat with our DynamicMethod. Whether the performance hit is worth it will likely come down to what the limiting factor is for you here. Bear in mind that the allocation cost is fixed regardless of the size of the list, whereas execution time for this case obviously scales approximately linearly with list size.</p> <p>For .NET 10, somewhat unsurprisingly, our <code>DynamicMethod</code> approach comes out worse than just using <code>IEnumerable&lt;int&gt;</code>:</p> <table><thead><tr><th>Method</th><th>Runtime</th><th style="text-align:right">Mean</th><th style="text-align:right">Error</th><th style="text-align:right">StdDev</th><th style="text-align:right">Allocated</th></tr></thead><tbody><tr><td>List</td><td>.NET 10.0</td><td style="text-align:right">3.105 us</td><td style="text-align:right">0.0442 us</td><td style="text-align:right">0.0413 us</td><td style="text-align:right">-</td></tr><tr><td>IEnumerable</td><td>.NET 10.0</td><td style="text-align:right">3.162 us</td><td style="text-align:right">0.0365 us</td><td style="text-align:right">0.0341 us</td><td style="text-align:right">-</td></tr><tr><td>IEnumerableDynamicMethod</td><td>.NET 10.0</td><td style="text-align:right">15.448 us</td><td style="text-align:right">0.2034 us</td><td style="text-align:right">0.1903 us</td><td style="text-align:right">-</td></tr></tbody></table> <p>This is what we'd expect, given all the performance improvements over the years, and the attention that's been given to <code>List&lt;T&gt;</code>. Given that enumerating <code>IEnumerable&lt;T&gt;</code> is <em>already</em> allocation free in .NET 10, there's no good reason to use it in this case.</p> <h3 id="benchmarking-the-dynamicmethod-with-a-custom-ienumerablet" class="heading-with-anchor"><a href="https://andrewlock.net/making-foreach-on-an-ienumerable-allocation-free-using-reflection-and-dynamic-methods/#benchmarking-the-dynamicmethod-with-a-custom-ienumerablet" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Benchmarking the <code>DynamicMethod</code> with a custom <code>IEnumerable&lt;T&gt;</code></a></h3> <p>My initial reason for looking into the <code>DynamicMethod</code> approach was for handling types that <em>aren't</em> built into the BCL, so I took a look at benchmarking a custom <code>IEnumerable&lt;T&gt;</code> implementation. The following linked list implementation is <em>super</em> basic, and is a heavily stripped-down version of an implementation <a href="https://github.com/dotnet/dotnet/blob/b0f34d51fccc69fd334253924abd8d6853fad7aa/src/runtime/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Activity.cs#L1632">used internally by <code>Activity</code></a>. These details aren't really important, I just include it below for completeness:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">internal</span> <span class="token keyword">sealed</span> <span class="token keyword">class</span> <span class="token class-name">CustomLinkedList<span class="token punctuation">&lt;</span>T<span class="token punctuation">&gt;</span></span> <span class="token punctuation">:</span> <span class="token type-list"><span class="token class-name">IEnumerable<span class="token punctuation">&lt;</span>T<span class="token punctuation">&gt;</span></span></span>
<span class="token punctuation">{</span>
    <span class="token keyword">private</span> <span class="token class-name">Node<span class="token punctuation">&lt;</span>T<span class="token punctuation">&gt;</span><span class="token punctuation">?</span></span> _first<span class="token punctuation">;</span>
    <span class="token keyword">private</span> <span class="token class-name">Node<span class="token punctuation">&lt;</span>T<span class="token punctuation">&gt;</span><span class="token punctuation">?</span></span> _last<span class="token punctuation">;</span>

    <span class="token keyword">public</span> <span class="token function">CustomLinkedList</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
    <span class="token punctuation">}</span>

    <span class="token keyword">public</span> <span class="token function">CustomLinkedList</span><span class="token punctuation">(</span><span class="token class-name">T</span> firstValue<span class="token punctuation">)</span> <span class="token operator">=&gt;</span> _last <span class="token operator">=</span> _first <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token constructor-invocation class-name">Node<span class="token punctuation">&lt;</span>T<span class="token punctuation">&gt;</span></span><span class="token punctuation">(</span>firstValue<span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token keyword">public</span> <span class="token function">CustomLinkedList</span><span class="token punctuation">(</span><span class="token class-name">IEnumerator<span class="token punctuation">&lt;</span>T<span class="token punctuation">&gt;</span></span> e<span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        _last <span class="token operator">=</span> _first <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token constructor-invocation class-name">Node<span class="token punctuation">&lt;</span>T<span class="token punctuation">&gt;</span></span><span class="token punctuation">(</span>e<span class="token punctuation">.</span>Current<span class="token punctuation">)</span><span class="token punctuation">;</span>

        <span class="token keyword">while</span> <span class="token punctuation">(</span>e<span class="token punctuation">.</span><span class="token function">MoveNext</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span>
        <span class="token punctuation">{</span>
            _last<span class="token punctuation">.</span>Next <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token constructor-invocation class-name">Node<span class="token punctuation">&lt;</span>T<span class="token punctuation">&gt;</span></span><span class="token punctuation">(</span>e<span class="token punctuation">.</span>Current<span class="token punctuation">)</span><span class="token punctuation">;</span>
            _last <span class="token operator">=</span> _last<span class="token punctuation">.</span>Next<span class="token punctuation">;</span>
        <span class="token punctuation">}</span>
    <span class="token punctuation">}</span>

    <span class="token keyword">public</span> <span class="token return-type class-name">Node<span class="token punctuation">&lt;</span>T<span class="token punctuation">&gt;</span><span class="token punctuation">?</span></span> First <span class="token operator">=&gt;</span> _first<span class="token punctuation">;</span>

    <span class="token keyword">public</span> <span class="token return-type class-name"><span class="token keyword">void</span></span> <span class="token function">Add</span><span class="token punctuation">(</span><span class="token class-name">T</span> <span class="token keyword">value</span><span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        <span class="token class-name">Node<span class="token punctuation">&lt;</span>T<span class="token punctuation">&gt;</span></span> newNode <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token constructor-invocation class-name">Node<span class="token punctuation">&lt;</span>T<span class="token punctuation">&gt;</span></span><span class="token punctuation">(</span><span class="token keyword">value</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token keyword">if</span> <span class="token punctuation">(</span>_first <span class="token keyword">is</span> <span class="token keyword">null</span><span class="token punctuation">)</span>
        <span class="token punctuation">{</span>
            _first <span class="token operator">=</span> _last <span class="token operator">=</span> newNode<span class="token punctuation">;</span>
            <span class="token keyword">return</span><span class="token punctuation">;</span>
        <span class="token punctuation">}</span>

        _last<span class="token operator">!</span><span class="token punctuation">.</span>Next <span class="token operator">=</span> newNode<span class="token punctuation">;</span>
        _last <span class="token operator">=</span> newNode<span class="token punctuation">;</span>
    <span class="token punctuation">}</span>

    <span class="token keyword">public</span> <span class="token return-type class-name">Enumerator<span class="token punctuation">&lt;</span>T<span class="token punctuation">&gt;</span></span> <span class="token function">GetEnumerator</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=&gt;</span> <span class="token keyword">new</span> <span class="token constructor-invocation class-name">Enumerator<span class="token punctuation">&lt;</span>T<span class="token punctuation">&gt;</span></span><span class="token punctuation">(</span>_first<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token return-type class-name">IEnumerator<span class="token punctuation">&lt;</span>T<span class="token punctuation">&gt;</span></span> IEnumerable<span class="token operator">&lt;</span>T<span class="token operator">&gt;</span><span class="token punctuation">.</span><span class="token function">GetEnumerator</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=&gt;</span> <span class="token function">GetEnumerator</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token return-type class-name">IEnumerator</span> IEnumerable<span class="token punctuation">.</span><span class="token function">GetEnumerator</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=&gt;</span> <span class="token function">GetEnumerator</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>


    <span class="token keyword">internal</span> <span class="token keyword">struct</span> <span class="token class-name">Enumerator<span class="token punctuation">&lt;</span>T<span class="token punctuation">&gt;</span></span> <span class="token punctuation">:</span> <span class="token type-list"><span class="token class-name">IEnumerator<span class="token punctuation">&lt;</span>T<span class="token punctuation">&gt;</span></span></span>
    <span class="token punctuation">{</span>
        <span class="token keyword">private</span> <span class="token keyword">static</span> <span class="token keyword">readonly</span> <span class="token class-name">Node<span class="token punctuation">&lt;</span>T<span class="token punctuation">&gt;</span></span> s_Empty <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token constructor-invocation class-name">Node<span class="token punctuation">&lt;</span>T<span class="token punctuation">&gt;</span></span><span class="token punctuation">(</span><span class="token keyword">default</span><span class="token operator">!</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

        <span class="token keyword">private</span> <span class="token class-name">Node<span class="token punctuation">&lt;</span>T<span class="token punctuation">&gt;</span><span class="token punctuation">?</span></span> _nextNode<span class="token punctuation">;</span>
        <span class="token keyword">private</span> <span class="token class-name">Node<span class="token punctuation">&lt;</span>T<span class="token punctuation">&gt;</span></span> _currentNode<span class="token punctuation">;</span>

        <span class="token keyword">public</span> <span class="token function">Enumerator</span><span class="token punctuation">(</span><span class="token class-name">Node<span class="token punctuation">&lt;</span>T<span class="token punctuation">&gt;</span><span class="token punctuation">?</span></span> head<span class="token punctuation">)</span>
        <span class="token punctuation">{</span>
            _nextNode <span class="token operator">=</span> head<span class="token punctuation">;</span>
            _currentNode <span class="token operator">=</span> s_Empty<span class="token punctuation">;</span>
        <span class="token punctuation">}</span>

        <span class="token keyword">public</span> <span class="token return-type class-name">T</span> Current <span class="token operator">=&gt;</span> _currentNode<span class="token punctuation">.</span>Value<span class="token punctuation">;</span>

        <span class="token return-type class-name"><span class="token keyword">object</span><span class="token punctuation">?</span></span> IEnumerator<span class="token punctuation">.</span>Current <span class="token operator">=&gt;</span> Current<span class="token punctuation">;</span>

        <span class="token keyword">public</span> <span class="token return-type class-name"><span class="token keyword">bool</span></span> <span class="token function">MoveNext</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
        <span class="token punctuation">{</span>
            <span class="token keyword">if</span> <span class="token punctuation">(</span>_nextNode <span class="token operator">==</span> <span class="token keyword">null</span><span class="token punctuation">)</span>
            <span class="token punctuation">{</span>
                _currentNode <span class="token operator">=</span> s_Empty<span class="token punctuation">;</span>
                <span class="token keyword">return</span> <span class="token boolean">false</span><span class="token punctuation">;</span>
            <span class="token punctuation">}</span>

            _currentNode <span class="token operator">=</span> _nextNode<span class="token punctuation">;</span>
            _nextNode <span class="token operator">=</span> _nextNode<span class="token punctuation">.</span>Next<span class="token punctuation">;</span>
            <span class="token keyword">return</span> <span class="token boolean">true</span><span class="token punctuation">;</span>
        <span class="token punctuation">}</span>

        <span class="token keyword">public</span> <span class="token return-type class-name"><span class="token keyword">void</span></span> <span class="token function">Reset</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=&gt;</span> <span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token constructor-invocation class-name">Exception</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

        <span class="token keyword">public</span> <span class="token return-type class-name"><span class="token keyword">void</span></span> <span class="token function">Dispose</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
        <span class="token punctuation">{</span>
        <span class="token punctuation">}</span>
    <span class="token punctuation">}</span>
    
    <span class="token keyword">internal</span> <span class="token keyword">sealed</span> <span class="token keyword">partial</span> <span class="token keyword">class</span> <span class="token class-name">Node<span class="token punctuation">&lt;</span>T<span class="token punctuation">&gt;</span></span>
    <span class="token punctuation">{</span>
        <span class="token keyword">public</span> <span class="token function">Node</span><span class="token punctuation">(</span><span class="token class-name">T</span> <span class="token keyword">value</span><span class="token punctuation">)</span> <span class="token operator">=&gt;</span> Value <span class="token operator">=</span> <span class="token keyword">value</span><span class="token punctuation">;</span>
        <span class="token keyword">public</span> <span class="token class-name">T</span> Value<span class="token punctuation">;</span>
        <span class="token keyword">public</span> <span class="token class-name">Node<span class="token punctuation">&lt;</span>T<span class="token punctuation">&gt;</span><span class="token punctuation">?</span></span> Next<span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>I then updated the benchmark to run the same set of tests with the <code>CustomLinkedList</code> implementation instead:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">using</span> <span class="token namespace">System</span><span class="token punctuation">;</span>
<span class="token keyword">using</span> <span class="token namespace">System<span class="token punctuation">.</span>Collections</span><span class="token punctuation">;</span>
<span class="token keyword">using</span> <span class="token namespace">System<span class="token punctuation">.</span>Collections<span class="token punctuation">.</span>Generic</span><span class="token punctuation">;</span>
<span class="token keyword">using</span> <span class="token namespace">System<span class="token punctuation">.</span>Linq</span><span class="token punctuation">;</span>
<span class="token keyword">using</span> <span class="token namespace">BenchmarkDotNet<span class="token punctuation">.</span>Attributes</span><span class="token punctuation">;</span>

<span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">MemoryDiagnoser</span></span><span class="token punctuation">]</span>
<span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">Benchmarks</span>
<span class="token punctuation">{</span>
    <span class="token keyword">private</span> <span class="token class-name">CustomLinkedList<span class="token punctuation">&lt;</span><span class="token keyword">int</span><span class="token punctuation">&gt;</span><span class="token punctuation">?</span></span> _linkedList<span class="token punctuation">;</span>
    <span class="token keyword">private</span> <span class="token class-name">IEnumerable<span class="token punctuation">&lt;</span><span class="token keyword">int</span><span class="token punctuation">&gt;</span><span class="token punctuation">?</span></span> _linkedListEnumerable<span class="token punctuation">;</span>
    <span class="token keyword">private</span> <span class="token class-name">AllocationFreeEnumerator<span class="token punctuation">&lt;</span>IEnumerable<span class="token punctuation">&lt;</span><span class="token keyword">int</span><span class="token punctuation">&gt;</span><span class="token punctuation">,</span> <span class="token keyword">int</span><span class="token punctuation">,</span> <span class="token keyword">long</span><span class="token punctuation">&gt;</span><span class="token punctuation">.</span>AllocationFreeForEachDelegate</span> _linkedListEnumerator<span class="token punctuation">;</span>

    <span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">GlobalSetup</span></span><span class="token punctuation">]</span>
    <span class="token keyword">public</span> <span class="token return-type class-name"><span class="token keyword">void</span></span> <span class="token function">GlobalSetup</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        _linkedList <span class="token operator">=</span> <span class="token keyword">new</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token keyword">foreach</span> <span class="token punctuation">(</span><span class="token class-name"><span class="token keyword">var</span></span> i <span class="token keyword">in</span> Enumerable<span class="token punctuation">.</span><span class="token function">Range</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">,</span> <span class="token number">10_000</span><span class="token punctuation">)</span><span class="token punctuation">)</span>
        <span class="token punctuation">{</span>
            _linkedList<span class="token punctuation">.</span><span class="token function">Add</span><span class="token punctuation">(</span>i<span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token punctuation">}</span>

        _linkedListEnumerable <span class="token operator">=</span> _linkedList<span class="token punctuation">;</span>
        _linkedListEnumerator <span class="token operator">=</span>
            AllocationFreeEnumerator<span class="token operator">&lt;</span>IEnumerable<span class="token operator">&lt;</span><span class="token keyword">int</span><span class="token operator">&gt;</span><span class="token punctuation">,</span> <span class="token keyword">int</span><span class="token punctuation">,</span> <span class="token keyword">long</span><span class="token operator">&gt;</span><span class="token punctuation">.</span><span class="token function">BuildAllocationFreeForEachDelegate</span><span class="token punctuation">(</span>
                _linkedList<span class="token punctuation">.</span><span class="token function">GetType</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>

    <span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">Benchmark</span></span><span class="token punctuation">]</span>
    <span class="token keyword">public</span> <span class="token return-type class-name"><span class="token keyword">long</span></span> <span class="token function">LinkedList</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        <span class="token class-name"><span class="token keyword">long</span></span> <span class="token keyword">value</span> <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span>
        <span class="token keyword">foreach</span> <span class="token punctuation">(</span><span class="token class-name"><span class="token keyword">int</span></span> i <span class="token keyword">in</span> _linkedList<span class="token operator">!</span><span class="token punctuation">)</span>
        <span class="token punctuation">{</span>
            <span class="token keyword">value</span> <span class="token operator">+=</span> i<span class="token punctuation">;</span>
        <span class="token punctuation">}</span>

        <span class="token keyword">return</span> <span class="token keyword">value</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>

    <span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">Benchmark</span></span><span class="token punctuation">]</span>
    <span class="token keyword">public</span> <span class="token return-type class-name"><span class="token keyword">long</span></span> <span class="token function">IEnumerableLinkedList</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        <span class="token class-name"><span class="token keyword">long</span></span> <span class="token keyword">value</span> <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span>
        <span class="token keyword">foreach</span> <span class="token punctuation">(</span><span class="token class-name"><span class="token keyword">int</span></span> i <span class="token keyword">in</span> _linkedListEnumerable<span class="token operator">!</span><span class="token punctuation">)</span>
        <span class="token punctuation">{</span>
            <span class="token keyword">value</span> <span class="token operator">+=</span> i<span class="token punctuation">;</span>
        <span class="token punctuation">}</span>

        <span class="token keyword">return</span> <span class="token keyword">value</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>

    <span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">Benchmark</span></span><span class="token punctuation">]</span>
    <span class="token keyword">public</span> <span class="token return-type class-name"><span class="token keyword">long</span></span> <span class="token function">IEnumerableLinkedListDynamicMethod</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        <span class="token class-name"><span class="token keyword">long</span></span> <span class="token keyword">value</span> <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span>
        <span class="token function">_linkedListEnumerator</span><span class="token punctuation">(</span>_linkedList<span class="token operator">!</span><span class="token punctuation">,</span> <span class="token keyword">ref</span> <span class="token keyword">value</span><span class="token punctuation">,</span> <span class="token keyword">static</span> <span class="token punctuation">(</span><span class="token keyword">ref</span> state<span class="token punctuation">,</span> i<span class="token punctuation">)</span> <span class="token operator">=&gt;</span>
        <span class="token punctuation">{</span>
            state <span class="token operator">+=</span> i<span class="token punctuation">;</span>
            <span class="token keyword">return</span> <span class="token boolean">true</span><span class="token punctuation">;</span>
        <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

        <span class="token keyword">return</span> <span class="token keyword">value</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>The results from these <code>CustomLinkedList&lt;T&gt;</code> benchmarks are <em>pretty</em> similar to the ones for <code>List&lt;T&gt;</code>, but with one main caveat: the <code>DynamicMethod</code> approach is now faster on .NET 9 <em>as well</em> as not allocating, so it becomes a clear winner in this case. The speed up for .NET Framework is also quite substantial:</p> <table><thead><tr><th>Method</th><th>Runtime</th><th style="text-align:right">Mean</th><th style="text-align:right">Error</th><th style="text-align:right">StdDev</th><th style="text-align:right">Allocated</th></tr></thead><tbody><tr><td>LinkedList</td><td>.NET 9.0</td><td style="text-align:right">7.844 us</td><td style="text-align:right">0.1340 us</td><td style="text-align:right">0.1254 us</td><td style="text-align:right">-</td></tr><tr><td>IEnumerableLinkedList</td><td>.NET 9.0</td><td style="text-align:right">18.892 us</td><td style="text-align:right">0.3430 us</td><td style="text-align:right">0.3209 us</td><td style="text-align:right">32 B</td></tr><tr><td>IEnumerableLinkedListDynamicMethod</td><td>.NET 9.0</td><td style="text-align:right">15.148 us</td><td style="text-align:right">0.2613 us</td><td style="text-align:right">0.2445 us</td><td style="text-align:right">-</td></tr><tr><td></td><td></td><td style="text-align:right"></td><td style="text-align:right"></td><td style="text-align:right"></td><td style="text-align:right"></td></tr><tr><td>LinkedList</td><td>.NET Framework 4.8</td><td style="text-align:right">7.914 us</td><td style="text-align:right">0.1295 us</td><td style="text-align:right">0.1212 us</td><td style="text-align:right">-</td></tr><tr><td>IEnumerableLinkedList</td><td>.NET Framework 4.8</td><td style="text-align:right">42.272 us</td><td style="text-align:right">0.8344 us</td><td style="text-align:right">0.9933 us</td><td style="text-align:right">32 B</td></tr><tr><td>IEnumerableLinkedListDynamicMethod</td><td>.NET Framework 4.8</td><td style="text-align:right">13.480 us</td><td style="text-align:right">0.2430 us</td><td style="text-align:right">0.2273 us</td><td style="text-align:right">-</td></tr></tbody></table> <p>As before, with .NET 10, the results for the <code>DynamicMethod</code> are worse than the plain <code>IEnumerable&lt;&lt;T&gt;</code>. This is actually really quite impressive—.NET 10 manages to treat the <code>LinkedList</code> and <code>IEnumerableLinkedList</code> benchmarks as essentially indistinguishable. Very cool 😎</p> <table><thead><tr><th>Method</th><th>Runtime</th><th style="text-align:right">Mean</th><th style="text-align:right">Error</th><th style="text-align:right">StdDev</th><th style="text-align:right">Allocated</th></tr></thead><tbody><tr><td>LinkedList</td><td>.NET 10.0</td><td style="text-align:right">7.944 us</td><td style="text-align:right">0.1570 us</td><td style="text-align:right">0.1542 us</td><td style="text-align:right">-</td></tr><tr><td>IEnumerableLinkedList</td><td>.NET 10.0</td><td style="text-align:right">7.798 us</td><td style="text-align:right">0.0745 us</td><td style="text-align:right">0.0622 us</td><td style="text-align:right">-</td></tr><tr><td>IEnumerableLinkedListDynamicMethod</td><td>.NET 10.0</td><td style="text-align:right">14.990 us</td><td style="text-align:right">0.2606 us</td><td style="text-align:right">0.2559 us</td><td style="text-align:right">-</td></tr></tbody></table> <p>So there you have it—a way to do allocation free enumeration of collection types. Obviously the question of whether you <em>should</em> do this is entirely context-dependent. If the enumeration is in a hot path, you're <em>not</em> on .NET 10, and these allocations are showing up in your profiling, then, well, <em>maybe</em> you should consider it 😅</p> <h2 id="summary" class="heading-with-anchor"><a href="https://andrewlock.net/making-foreach-on-an-ienumerable-allocation-free-using-reflection-and-dynamic-methods/#summary" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Summary</a></h2> <p>In the first part of this post I provide some background on how and when a <code>foreach</code> loop might cause allocations. I create a simple benchmark to demonstrate the problem, show the "lowered" C#, and describe that the allocation comes from boxing a <code>struct</code> enumerator.</p> <p>In the second part of the post, I describe how you can avoid this allocation, for scenarios where you <em>can't</em> simply cast to a known type, by creating a <code>DynamicMethod</code> using <em>Reflection.Emit</em>. This is a pretty advanced technique, but it shows how you can completely remove the allocations from enumeration.</p> <p>Finally, I showed how this approach performs in benchmarks. If you're using .NET 10, then you have no need for the <code>DynamicMethod</code> and don't need to worry at all 😀 On earlier runtimes, including .NET Framework, the <code>DynamicMethod</code> approach eliminates allocations, and in many cases improves execution time, particularly for "custom" collection types.</p> <p>Whether you should use this approach is very context dependent. In most scenarios, allocating 40 bytes is not a big deal. But if it <em>is</em> a problem for you, now you have a tool in your toolbelt!</p> ]]></content:encoded><category><![CDATA[Performance;.NET Core;Datadog]]></category></item><item><title><![CDATA[The Windows File Explorer replacement, File Pilot, is awesome]]></title><description><![CDATA[In this post I describe my experience with the Windows File Explorer replacement, File Pilot. It's blazingly fast, feature rich, and has hotkeys everywhere]]></description><link>https://andrewlock.net/windows-explorer-replacement-filepilot-is-awesome/</link><guid isPermaLink="true">https://andrewlock.net/windows-explorer-replacement-filepilot-is-awesome/</guid><pubDate>Tue, 13 Jan 2026 09:00:00 GMT</pubDate><dc:creator><![CDATA[Andrew Lock]]></dc:creator><media:content url="https://andrewlock.net/content/images/2026/filepilot_banner.png" medium="image" /><content:encoded><![CDATA[<img src="https://andrewlock.net/content/images/2026/filepilot_banner.png" /><p>In this post I show off the Windows File Explorer replacement tool I've been trying for the last couple of weeks, File Pilot. I discuss why you might want to replace File Explorer and show some of the many features File Pilot has (my number one feature: it's <em>fast</em>!). I then show how you can install File Pilot yourself and discuss some of the things that are currently missing.</p> <h2 id="why-you-need-a-file-explorer-replacement" class="heading-with-anchor"><a href="https://andrewlock.net/windows-explorer-replacement-filepilot-is-awesome/#why-you-need-a-file-explorer-replacement" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Why you need a File Explorer replacement</a></h2> <p>Windows' File Explorer (previously Windows Explorer) is the Windows-native way to manage your files. Anyone who's ever used Windows will have interacted with it in some way, and it's one of those apps that's only lightly changed, even looking all the way back to Windows XP😅 Sure the graphics have got slightly more modern, HomeGroup came and went, and Windows 11 <em>finally</em> introduced a tabbed interface, nearly <em>30 years</em> after explorer was released 🙄</p> <p><img src="https://andrewlock.net/content/images/2026/filepilot_explorer.png" alt="Windows/File Explorer in Windows 11"></p> <p>In many ways, it's not surprising that Explorer has ossified some what. It's used by literally every person that uses Windows, that's <em>billions</em> of people, most of whom are not tech-savvy. Any changes you make to the explorer better be incremental at best, otherwise you're going to have millions of unhappy users.</p> <p><code>explorer.exe</code> likely also suffers from many of the same problems that <code>cmd.exe</code> does; namely that there are thousands of applications that depend on the <em>exact</em> behaviour of the app. Any changes to <code>explorer.exe</code> could break those apps, which have been running perfectly well for decades, and it's just not worth the risk.</p> <blockquote> <p>Of course, Windows now has <a href="https://github.com/microsoft/terminal">Windows Terminal</a>, which is finally a <em>modern</em>, native terminal for Windows. But the main reason this is a completely different application is for precisely the reasons described above—changing <code>cmd.exe</code> was simply too risky for backwards compatibility.</p> </blockquote> <p>Despite all this, in general, File Explorer is <em>fine</em> in my experience. I've been a Windows user all my life, and I know all the shortcuts, so I know how to move around and get things done.</p> <blockquote> <p>That's in contrast to when I had to use a mac for 5 years. Man, I hated using that machine 😅 All the shortcuts were <em>wrong</em>, my muscle memory was completely shot, and Finder sucked. Yes, I realise this is 100% just a matter of familiarity, but it amazed me how even after 5 years I was constantly infuriated by it😂</p> </blockquote> <p>I never <em>really</em> considered about how annoying I've been finding File Explorer until recently. And then I heard Scott Hanselmen mention <a href="https://filepilot.tech/">File Pilot</a> a couple of times, including his interview with the creator, Vjekoslav Krajačić, <a href="https://www.hanselminutes.com/1030/vjekoslav-krajai-on-file-pilot-and-a-return-to-fast-uis">on The Hanselminutes Podcast</a>. There was a big emphasis on how fast File Pilot is, so the next time I was waiting 3 seconds for an Explorer window to finish rendering, I finally cracked and gave it a try.</p> <p>And damn, it's <em>so</em> fast.</p> <h2 id="installing-file-pilot" class="heading-with-anchor"><a href="https://andrewlock.net/windows-explorer-replacement-filepilot-is-awesome/#installing-file-pilot" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Installing File Pilot</a></h2> <p>You can install File Pilot for free at the moment while it's in beta by going to <a href="https://filepilot.tech/">https://filepilot.tech/</a>:</p> <p><img src="https://andrewlock.net/content/images/2026/filepilot_install.webp" alt="The File Pilot home page at filepilot.tech"></p> <p>It's worth noting though that File Pilot <em>isn't</em> going to be permanently free, and is instead aiming to be sustainable once they release a stable version. You can read more on <a href="https://filepilot.tech/pricing">their pricing page</a>, which at the moment looks like this for individual licenses (they also offer business licences)</p> <p><img src="https://andrewlock.net/content/images/2026/filepilot_pricing.png" alt="File Pilot pricing page"></p> <p>If those prices give you an initial sticker-shock, I totally understand. £40 seems like a lot for a tool which is included for free with Windows. So for that reason I <em>strongly</em> suggest giving the Beta a try for free now which you can. Because after using it myself for a couple of weeks, I'm almost certainly going to buy a license for myself!</p> <blockquote> <p>I should mention that there are also free, open source, File Explorer options such as Files at <a href="https://files.community/">https://files.community/</a> if File Pilot is too steep for you. I did try Files, but it was missing the sheer speed that File Pilot has, which was the main feature I wanted.</p> </blockquote> <p>When you download File Pilot, you get a 2MB executable. 2MB! 😅 You can run the app as a standalone tool if you wish, or you can go the extra step (as I did) and install it to your profile, and <em>replace</em> the built in File Explorer:</p> <p><img src="https://andrewlock.net/content/images/2026/filepilot_installer.png" alt="The File Pilot installer after install"></p> <p>The installer is pretty simple, allowing you to choose the install path, and four options:</p> <ul><li>Create a shortcut for File Pilot on the Desktop.</li> <li>Add "Open in File Pilot" and "File Pilot here" to right-click menu for files, folders, and desktop.</li> <li>Open folders by default with File Pilot and assign it to the <kbd>Win</kbd>+<kbd>E</kbd> shortcut.</li> <li>Add File Pilot to your Path, for access from the command line.</li></ul> <p>I chose to have File Pilot replace <em>all</em> of my File Explorer usage (where possible, more on that later), which is about the deepest you can integrate. With that, when an app tries to open a folder (e.g. if you click Downloads in Chrome, the <kbd>Win</kbd>+<kbd>E</kbd> shortcut, or run <code>ii .</code> in PowerShell) you'll get File Pilot instead:</p> <p><img src="https://andrewlock.net/content/images/2026/filepilot_initial.png" alt="The File Pilot interface"></p> <p>The initial view of File Pilot is very similar to the traditional File Explorer so you'll likely feel quite at home.</p> <h2 id="first-impressions-of-file-pilot" class="heading-with-anchor"><a href="https://andrewlock.net/windows-explorer-replacement-filepilot-is-awesome/#first-impressions-of-file-pilot" class="relative text-zinc-800 dark:text-white no-underline hover:underline">First impressions of File Pilot</a></h2> <p>One of the first things you'll almost certainly notice about File Pilot is just how fast it is. <em>Everything</em> about it is fast. It's hard to overstate this; File Pilot is fast to load; it's fast to search; opening a tab is instantaneous; scrolling is buttery smooth; scaling thumbnails is seamless. <em>Everything</em> is just lightning fast!</p> <p>Once you get over the speed, you'll probably see that <em>in general</em> it feels quite familiar to File Explorer. The styling isn't entirely in the <a href="https://en.wikipedia.org/wiki/Fluent_Design_System">Windows standard "Fluent"</a> design, but it doesn't feel out of place, and is quite clean general. There's also a bunch of configuration options, so you have dark mode, colour schemes, optional rounded corners, and a bunch of other options:</p> <p><img src="https://andrewlock.net/content/images/2026/filepilot_options.png" alt="The options screen for File Pilot"></p> <p>This includes most of the standard options that are part of File Explorer, like hiding/showing hidden/system files and file extensions, as well as a variety of other options.</p> <h2 id="exploring-file-pilot-s-features" class="heading-with-anchor"><a href="https://andrewlock.net/windows-explorer-replacement-filepilot-is-awesome/#exploring-file-pilot-s-features" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Exploring File Pilot's features</a></h2> <p>The options panel is really only the tip of the iceberg however. File Pilot has a ton of other features, which are all easily discoverable either through just clicking in the UI, or using the command palette.</p> <h3 id="command-palette" class="heading-with-anchor"><a href="https://andrewlock.net/windows-explorer-replacement-filepilot-is-awesome/#command-palette" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Command Palette</a></h3> <p>The "command palette" approach, which many developers will recognise from VS Code, seems to be finding its way into more and more apps these days, and File Pilot is no exception. The command palette can be accessed with <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>P</kbd> (the same as VS Code!) and provides access to all of File Pilot's features:</p> <p><img src="https://andrewlock.net/content/images/2026/filepilot_commands.png" alt="The File Pilot command palette"></p> <p>The File Pilot command palette contains a huge number of commands, split into multiple sections (which I've partially collapsed in the above image, to give a sense of what's available). You can instantly search for the command you want by just typing, or just scroll through and browse if you want to get a feeling for what's available.</p> <h3 id="shortcuts-hotkeys" class="heading-with-anchor"><a href="https://andrewlock.net/windows-explorer-replacement-filepilot-is-awesome/#shortcuts-hotkeys" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Shortcuts/Hotkeys</a></h3> <p>One of the really nice things about the File Pilot UI, which you can also see in the command palette image above, is that File Pilot has shortcuts for almost everything you can do. An importantly (IMO), File Pilot <em>shows</em> these shortcuts all around the UI when you click or right-click on UI items, which is a great way to get to learn what an application can do.</p> <p><img src="https://andrewlock.net/content/images/2026/filepilot_shortcuts.png" alt="Shortcuts are displayed prominently in File Pilot"></p> <p>The other great thing I've found with these shortcuts is that they're exactly what I'm used to as a long-time Windows user and developer. All the shortcuts that you would use in File Explorer, such as <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>N</kbd> for a new folder, or <kbd>Ctrl</kbd>+<kbd>T</kbd> for a new tab, do exactly what you expect. And add-in being a VS Code user, and the muscle-memory is there instantly, which is often a barrier to me adopting new tools.</p> <p>Of course, you might not want the same shortcuts as me, and luckily all the hotkeys are completely customizable. But after my experience <a href="https://andrewlock.net/trying-out-the-zed-editor-on-windows-for-dotnet-and-markdown/">trying out Zed</a> and butting up against "incorrect" shortcuts for several weeks, I was pleasantly surprised by how well I gelled with File Pilot.</p> <h3 id="split-screen-and-inspector" class="heading-with-anchor"><a href="https://andrewlock.net/windows-explorer-replacement-filepilot-is-awesome/#split-screen-and-inspector" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Split screen and inspector</a></h3> <p>A very common task when working with a file explorer is moving files between two different folders, or comparing the files in different folders. Typically I would open two instances of File Explorer, use Snap (<kbd>Win</kbd>+<kbd>←</kbd> or <kbd>Win</kbd>+<kbd>→</kbd>) to snap them side-by-side, and then <kbd>Alt</kbd>+<kbd>Tab</kbd> to switch between the different instances. This is ok, but it's a bit clunky, and means you end up having multiple instances of explorer hanging around.</p> <p>With File Pilot, you can have split panels, just as you might in your terminals, IDE, or editor of choice:</p> <p><img src="https://andrewlock.net/content/images/2026/filepilot_split.png" alt="The File Pilot split panels"></p> <p>Switching between panels is just a <kbd>Tab</kbd> away, you can move the split divider position and you can split to the bottom instead if you prefer.</p> <p>Alternatively, you can show the "inspector" on the right hand panel by hitting <kbd>Space</kbd> on a file to see a preview of it:</p> <p><img src="https://andrewlock.net/content/images/2026/filepilot_inspector.png" alt="The file pilot inspector shows images and text files"></p> <p>As with everything in File Pilot, the preview for each file is super-fast, though this is an area where the File Explorer is actually a little more feature rich, thanks to Power Toys add-ins that enhance File Explorer's preview pane with support for other files like PDFs or SVGs.</p> <p>There's a huge number of other features in File Pilot that I could talk about, but seeing as I've mentioned the lack of PDF-preview support, it's probably worth moving on to discuss a couple of aspects of File Pilot that don't quite work like I would like, or which are missing features.</p> <h2 id="missing-features-and-sub-optimal-behaviours" class="heading-with-anchor"><a href="https://andrewlock.net/windows-explorer-replacement-filepilot-is-awesome/#missing-features-and-sub-optimal-behaviours" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Missing features and sub-optimal behaviours</a></h2> <p>Seeing as I've mentioned it already, the inspector only seems to support previewing files that are images or text files. For the most part, that's fine, but both PDF support and SVG-rendering would be really nice additions. These aren't deal breakers, but with <a href="https://learn.microsoft.com/en-us/windows/powertoys/peek">Power Toy's Peek</a> utility, as well as their <a href="https://learn.microsoft.com/en-us/windows/powertoys/file-explorer">additional preview pane plugins</a> I've been a bit spoiled recently!</p> <p>Another feature that File Explorer has is the ability to group by file/folder recency, e.g. Today, Last Week, Last Month, and then apply sorting to each group independently. I primarily use this in my Downloads folder, which gives a really nice experience, seeing as when I'm opening things in my Downloads folder it's <em>normally</em> stuff I've downloaded recently. But once you get to a week out, ordering by Name gives more natural grouping than by strict descending file date:</p> <p><img src="https://andrewlock.net/content/images/2026/filepilot_grouping.png" alt="File explorer allows grouping by date period"></p> <p>The remaining niggles I have with File Pilot are <em>probably</em> not File Pilot's fault, and are rather a Windows limitation, because they are behaviours that the built-in File Explorer has too. In particular, it's annoying when you have a single instance of File Pilot instance open, with multiple tabs, and then you open a folder from some other app (e.g. opening Downloads from Chrome) and it starts a <em>new</em> instance of <code>FilePilot.exe</code>. I <em>always</em> want it to just open this as a tab in an existing folder, not in a new instance.</p> <blockquote> <p>Like I say, I suspect this is a limitation with how these folders are opened, seeing as File Explorer does the exact same thing, even if you've enabled "Open each folder in new Tab".</p> </blockquote> <p>The other thing I've noticed is basically any scenario in which I see a "native" File Explorer window instead of File Pilot 😅 In particular I'm thinking of file-picker dialogs. They're so slow compared to File Pilot, I just wish we could replace those too. And no, I'm not <em>really</em> suggesting that should be possible, but you know…it would be pretty nice if it was.</p> <p>And that's about it. There's really not many downsides that I've found. Which is why I'm almost certainly going to buy a license soon, despite the fact it's proprietary and relatively expensive. Would I prefer it was open source? Sure. But also, I have to respect a software developer that's made something which is great to use, and is trying to make a business out of that software.</p> <p>If the fact File Pilot isn't open source is a blocker for you then I totally understand and respect that attitude. But if not, I would <em>definitely</em> recommend giving File Pilot a try. The Beta I've been trying has been rock solid—I've not seen any bugs or crashes—and it's really low effort to try it out. Give it a look, and marvel at the speed! 😀</p> <h2 id="summary" class="heading-with-anchor"><a href="https://andrewlock.net/windows-explorer-replacement-filepilot-is-awesome/#summary" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Summary</a></h2> <p>In this post I described my experience with the Windows File Explorer replacement, File Pilot. File Pilot is blazingly fast, and includes a bunch of features like split screen, hotkeys for everything, a command palette, and themes and customisation. I've been using it for a couple of weeks now and I'm pretty sure it's going to completely replace my use of File Explorer thanks to the thoughtful shortcut configuration, extra features, and above all <em>fast</em> experience. File Pilot is free to use while it's in Beta, so I recommend giving it a try!</p> ]]></content:encoded><category><![CDATA[Tools]]></category></item><item><title><![CDATA[Recent updates to NetEscapades.EnumGenerators: new APIs and System.Memory support]]></title><description><![CDATA[In this post I describe some recent changes to the NetEscapades.EnumGenerators source generator, including support for the System.Memory package and new APIs]]></description><link>https://andrewlock.net/updates-to-netescapaades-enumgenerators-new-apis-and-system-memory-support/</link><guid isPermaLink="true">https://andrewlock.net/updates-to-netescapaades-enumgenerators-new-apis-and-system-memory-support/</guid><pubDate>Fri, 02 Jan 2026 09:00:00 GMT</pubDate><dc:creator><![CDATA[Andrew Lock]]></dc:creator><media:content url="https://andrewlock.net/content/images/2022/enumgenerators_banner.png" medium="image" /><content:encoded><![CDATA[<img src="https://andrewlock.net/content/images/2022/enumgenerators_banner.png" /><p>In this post I describe some of the recent updates added in version 1.0.0-beta19 of my source generator NuGet package <a href="https://github.com/andrewlock/NetEscapades.EnumGenerators">NetEscapades.EnumGenerators</a> which you can use to add fast methods for working with <code>enum</code>s. I start by briefly describing why the package exists and what you can use it for, then I walk through some of the changes in the latest release.</p> <h2 id="why-should-you-use-an-enum-source-generator-" class="heading-with-anchor"><a href="https://andrewlock.net/updates-to-netescapaades-enumgenerators-new-apis-and-system-memory-support/#why-should-you-use-an-enum-source-generator-" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Why should you use an enum source generator?</a></h2> <p><a href="https://github.com/andrewlock/NetEscapades.EnumGenerators">NetEscapades.EnumGenerators</a> provides a source generator that is designed to work around an annoying characteristic of working with enums: some operations are surprisingly slow.</p> <p>As an example, let's say you have the following enum:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">public</span> <span class="token keyword">enum</span> <span class="token class-name">Colour</span>
<span class="token punctuation">{</span>
    Red <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">,</span>
    Blue <span class="token operator">=</span> <span class="token number">1</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>At some point, you want to print the name of a <code>Color</code> variable, so you create this helper method:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">public</span> <span class="token return-type class-name"><span class="token keyword">void</span></span> <span class="token function">PrintColour</span><span class="token punctuation">(</span><span class="token class-name">Colour</span> colour<span class="token punctuation">)</span>
<span class="token punctuation">{</span>
    Console<span class="token punctuation">.</span><span class="token function">WriteLine</span><span class="token punctuation">(</span><span class="token string">"You chose "</span><span class="token operator">+</span> colour<span class="token punctuation">.</span><span class="token function">ToString</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// You chose Red</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>While this <em>looks</em> like it should be fast, it's really not. <em>NetEscapades.EnumGenerators</em> works by automatically generating an implementation that <em>is</em> fast. It generates a <code>ToStringFast()</code> method that looks something like this:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token keyword">class</span> <span class="token class-name">ColourExtensions</span>
<span class="token punctuation">{</span>
    <span class="token keyword">public</span> <span class="token return-type class-name"><span class="token keyword">string</span></span> <span class="token function">ToStringFast</span><span class="token punctuation">(</span><span class="token keyword">this</span> <span class="token class-name">Colour</span> colour<span class="token punctuation">)</span>
        <span class="token operator">=&gt;</span> colour <span class="token keyword">switch</span>
        <span class="token punctuation">{</span>
            Colour<span class="token punctuation">.</span>Red <span class="token operator">=&gt;</span> <span class="token keyword">nameof</span><span class="token punctuation">(</span>Colour<span class="token punctuation">.</span>Red<span class="token punctuation">)</span><span class="token punctuation">,</span>
            Colour<span class="token punctuation">.</span>Blue <span class="token operator">=&gt;</span> <span class="token keyword">nameof</span><span class="token punctuation">(</span>Colour<span class="token punctuation">.</span>Blue<span class="token punctuation">)</span><span class="token punctuation">,</span>
            _ <span class="token operator">=&gt;</span> colour<span class="token punctuation">.</span><span class="token function">ToString</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
        <span class="token punctuation">}</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>This simple switch statement checks for each of the known values of <code>Colour</code> and uses <code>nameof</code> to return the textual representation of the <code>enum</code>. If it's an unknown value, then it falls back to the built-in <code>ToString()</code> implementation for simplicity of handling of unknown values (for example this is valid C#: <code>PrintColour((Colour)123)</code>).</p> <p>If we compare these two implementations using <a href="https://benchmarkdotnet.org/">BenchmarkDotNet</a> for a known colour, you can see how much faster the <code>ToStringFast()</code> implementation is, even in .NET 10</p> <table><thead><tr><th>Method</th><th style="text-align:right">Mean</th><th style="text-align:right">Error</th><th style="text-align:right">StdDev</th><th style="text-align:right">Median</th><th style="text-align:right">Gen0</th><th style="text-align:right">Allocated</th></tr></thead><tbody><tr><td>ToString</td><td style="text-align:right">6.4389 ns</td><td style="text-align:right">0.1038 ns</td><td style="text-align:right">0.0971 ns</td><td style="text-align:right">6.4567 ns</td><td style="text-align:right">0.0038</td><td style="text-align:right">24 B</td></tr><tr><td>ToStringFast</td><td style="text-align:right">0.0050 ns</td><td style="text-align:right">0.0202 ns</td><td style="text-align:right">0.0189 ns</td><td style="text-align:right">0.0000 ns</td><td style="text-align:right">-</td><td style="text-align:right">-</td></tr></tbody></table> <p>Obviously your mileage may vary and the results will depend on the specific enum and which of the generated methods you're using, but in general, using the source generator should give you a free performance boost!</p> <blockquote> <p>If you want to learn more about all the features the package provides, check my <a href="https://andrewlock.net/tag/source-generators/">previous blog posts</a> or see the project <a href="https://github.com/andrewlock/NetEscapades.EnumGenerators">README</a>.</p> </blockquote> <p>That's the basics of why I think you should take a look at the source generator. Now let's take a look at the latest features added.</p> <h2 id="updates-in-1-0-0-beta19" class="heading-with-anchor"><a href="https://andrewlock.net/updates-to-netescapaades-enumgenerators-new-apis-and-system-memory-support/#updates-in-1-0-0-beta19" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Updates in 1.0.0-beta19</a></h2> <p>Version <a href="https://www.nuget.org/packages/NetEscapades.EnumGenerators/">1.0.0-beta19 of NetEscapades.EnumGenerators</a> was released to nuget.org recently and includes a number of new features. I'll describe each of the updates in more detail below, covering the following:</p> <ul><li>Support for disabling number parsing.</li> <li>Support for automatically calling <code>ToLowerInvariant()</code> or <code>ToUpperInvariant()</code> on the serialized enum.</li> <li>Add support for <code>ReadOnlySpan&lt;T&gt;</code> APIs when using <a href="https://www.nuget.org/packages/System.Memory">the <code>System.Memory</code> NuGet package</a>.</li></ul> <p>There are other fixes and features in 1.0.0-beta19, but these are the ones I'm focusing on in this post. I'll show some of the other features in the next post!</p> <h3 id="support-for-disabling-number-parsing-and-additional-options" class="heading-with-anchor"><a href="https://andrewlock.net/updates-to-netescapaades-enumgenerators-new-apis-and-system-memory-support/#support-for-disabling-number-parsing-and-additional-options" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Support for disabling number parsing and additional options</a></h3> <p>The first feature addresses a <a href="https://github.com/andrewlock/NetEscapades.EnumGenerators/issues/80">long-standing request</a>, disabling the fallback "number parsing" implemented in <code>Parse()</code> and <code>TryParse()</code>. For clarity I'll provide a brief example. Let's take the <code>Color</code> example again:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">EnumExtensions</span></span><span class="token punctuation">]</span>
<span class="token keyword">public</span> <span class="token keyword">enum</span> <span class="token class-name">Colour</span>
<span class="token punctuation">{</span>
    Red <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">,</span>
    Blue <span class="token operator">=</span> <span class="token number">1</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>As well as <code>ToStringFast()</code>, we generate similar "fast" <code>Parse()</code>, <code>TryParse()</code>, and most of the other <code>System.Enum</code> static methods that you might expect. The <code>TryParse()</code> method for the above code looks something like this:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token return-type class-name"><span class="token keyword">bool</span></span> <span class="token function">TryParse</span><span class="token punctuation">(</span><span class="token class-name"><span class="token keyword">string</span><span class="token punctuation">?</span></span> name<span class="token punctuation">,</span> <span class="token keyword">out</span> <span class="token class-name">Colour</span> <span class="token keyword">value</span><span class="token punctuation">)</span>
<span class="token punctuation">{</span>
    <span class="token keyword">switch</span> <span class="token punctuation">(</span>name<span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        <span class="token keyword">case</span> <span class="token keyword">nameof</span><span class="token punctuation">(</span>Colour<span class="token punctuation">.</span>Red<span class="token punctuation">)</span><span class="token punctuation">:</span>
            <span class="token keyword">value</span> <span class="token operator">=</span> Colour<span class="token punctuation">.</span>Red<span class="token punctuation">;</span>
            <span class="token keyword">return</span> <span class="token boolean">true</span><span class="token punctuation">;</span>
        <span class="token keyword">case</span> <span class="token keyword">nameof</span><span class="token punctuation">(</span>Colour<span class="token punctuation">.</span>Blue<span class="token punctuation">)</span><span class="token punctuation">:</span>
            <span class="token keyword">value</span> <span class="token operator">=</span> Colour<span class="token punctuation">.</span>Blue<span class="token punctuation">;</span>
            <span class="token keyword">return</span> <span class="token boolean">true</span><span class="token punctuation">;</span>
        <span class="token keyword">case</span> <span class="token class-name"><span class="token keyword">string</span></span> s <span class="token keyword">when</span> <span class="token keyword">int</span><span class="token punctuation">.</span><span class="token function">TryParse</span><span class="token punctuation">(</span>name<span class="token punctuation">,</span> <span class="token keyword">out</span> <span class="token class-name"><span class="token keyword">var</span></span> val<span class="token punctuation">)</span><span class="token punctuation">:</span>
            <span class="token keyword">value</span> <span class="token operator">=</span> <span class="token punctuation">(</span>Colour<span class="token punctuation">)</span>val<span class="token punctuation">;</span>
            <span class="token keyword">return</span> <span class="token boolean">true</span><span class="token punctuation">;</span>
        <span class="token keyword">default</span><span class="token punctuation">:</span>
            <span class="token keyword">value</span> <span class="token operator">=</span> <span class="token keyword">default</span><span class="token punctuation">;</span>
            <span class="token keyword">return</span> <span class="token boolean">false</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>The first two branches in this example are what you might expect; the source generator generates an explicit switch statement for the <code>Colour</code> enum. However, the third case may look a little odd. The problem is that <code>enum</code>s in C# are not a closed list of values, you can always do something like this:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token class-name">Colour</span> valid <span class="token operator">=</span> <span class="token punctuation">(</span>Colour<span class="token punctuation">)</span><span class="token number">123</span><span class="token punctuation">;</span>
<span class="token class-name"><span class="token keyword">string</span></span> stillValid <span class="token operator">=</span> valid<span class="token punctuation">.</span><span class="token function">ToString</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// "123"</span>
<span class="token class-name">Colour</span> parsed <span class="token operator">=</span> Enum<span class="token punctuation">.</span><span class="token generic-method"><span class="token function">Parse</span><span class="token generic class-name"><span class="token punctuation">&lt;</span>Colour<span class="token punctuation">&gt;</span></span></span><span class="token punctuation">(</span>stillValid<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// 123</span>
Console<span class="token punctuation">.</span><span class="token function">WriteLine</span><span class="token punctuation">(</span>valid <span class="token operator">==</span> parsed<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// true</span>
</code></pre></div> <p>Essentially, you can pretty much parse <em>any</em> integer that has been <code>ToString()</code>ed as <em>any</em> enum. That's why the source generated code includes the case statement to try parsing as an integer—it's to ensure compatibility with the "built-in" <code>Enum.Parse()</code> and <code>TryParse()</code> behaviour.</p> <p>However, that behaviour isn't always what you want. Arguably, it's <em>rarely</em> what you want, hence <a href="https://github.com/andrewlock/NetEscapades.EnumGenerators/issues/80">the request</a> to allow disabling it.</p> <p>Given it makes so much sense to allow disabling it, it's perhaps a little embarrassing that it took so long to address. I dragged my feet on it for several reasons:</p> <ul><li>The built-in <code>System.Enum</code> works like this, and I want to be as drop-in compatible as possible.</li> <li>There's a trivial "fix" by checking first that the first digit is not a number. e.g. <code>if (!char.IsDigit(text[0]) &amp;&amp; ColourExtensions.TryParse(text, out var value))</code></li> <li>It's <em>another</em> configuration switch that would need to be added to <code>Parse</code> and <code>TryParse</code></li></ul> <p>Of all the reasons, that latter point is the one that vexed me. There were already two configuration knobs for <code>Parse</code> and <code>TryParse</code>, as well as versions that accept both <code>string</code> and <code>ReadOnlySpan&lt;char&gt;</code>:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token keyword">partial</span> <span class="token keyword">class</span> <span class="token class-name">ColourExtensions</span>
<span class="token punctuation">{</span>
    <span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token return-type class-name">Colour</span> <span class="token function">Parse</span><span class="token punctuation">(</span><span class="token class-name"><span class="token keyword">string</span><span class="token punctuation">?</span></span> name<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token return-type class-name">Colour</span> <span class="token function">Parse</span><span class="token punctuation">(</span><span class="token class-name"><span class="token keyword">string</span><span class="token punctuation">?</span></span> name<span class="token punctuation">,</span> <span class="token class-name"><span class="token keyword">bool</span></span> ignoreCase<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token return-type class-name">Colour</span> <span class="token function">Parse</span><span class="token punctuation">(</span><span class="token class-name"><span class="token keyword">string</span><span class="token punctuation">?</span></span> name<span class="token punctuation">,</span> <span class="token class-name"><span class="token keyword">bool</span></span> ignoreCase<span class="token punctuation">,</span> <span class="token class-name"><span class="token keyword">bool</span></span> allowMatchingMetadataAttribute<span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token return-type class-name">Colour</span> <span class="token function">Parse</span><span class="token punctuation">(</span><span class="token class-name">ReadOnlySpan<span class="token punctuation">&lt;</span><span class="token keyword">char</span><span class="token punctuation">&gt;</span></span> name<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token return-type class-name">Colour</span> <span class="token function">Parse</span><span class="token punctuation">(</span><span class="token class-name">ReadOnlySpan<span class="token punctuation">&lt;</span><span class="token keyword">char</span><span class="token punctuation">&gt;</span></span> name<span class="token punctuation">,</span> <span class="token class-name"><span class="token keyword">bool</span></span> ignoreCase<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token return-type class-name">Colour</span> <span class="token function">Parse</span><span class="token punctuation">(</span><span class="token class-name">ReadOnlySpan<span class="token punctuation">&lt;</span><span class="token keyword">char</span><span class="token punctuation">&gt;</span></span> name<span class="token punctuation">,</span> <span class="token class-name"><span class="token keyword">bool</span></span> ignoreCase<span class="token punctuation">,</span> <span class="token class-name"><span class="token keyword">bool</span></span> allowMatchingMetadataAttribute<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>My concern was adding more and more parameters or overloads would make the generated API harder to understand. The simple answer was to take the classic approach of introducing an "options" object. This encapsulates all the available options in a single parameter, and can be extended without increasing the complexity of the generated APIs:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token keyword">partial</span> <span class="token keyword">class</span> <span class="token class-name">ColourExtensions</span>
<span class="token punctuation">{</span>
    <span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token return-type class-name">Colour</span> <span class="token function">Parse</span><span class="token punctuation">(</span><span class="token class-name"><span class="token keyword">string</span><span class="token punctuation">?</span></span> name<span class="token punctuation">,</span> <span class="token class-name">EnumParseOptions</span> options<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token return-type class-name">Colour</span> <span class="token function">Parse</span><span class="token punctuation">(</span><span class="token class-name">ReadOnlySpan<span class="token punctuation">&lt;</span><span class="token keyword">char</span><span class="token punctuation">&gt;</span></span> name<span class="token punctuation">,</span> <span class="token class-name">EnumParseOptions</span> options<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre></div> <p><a href="https://github.com/andrewlock/NetEscapades.EnumGenerators/blob/master/src/NetEscapades.EnumGenerators.Attributes/EnumParseOptions.cs">The <code>EnumParseOptions</code> object</a> is defined in the source generator dll that's referenced by your application, so it becomes part of the public API. It's defined as a <code>readonly struct</code> to avoid allocating an object on the heap just to call <code>Parse</code> (which would negate some of the benefits of these APIs).</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">public</span> <span class="token keyword">readonly</span> <span class="token keyword">struct</span> <span class="token class-name">EnumParseOptions</span>
<span class="token punctuation">{</span>
    <span class="token keyword">private</span> <span class="token keyword">readonly</span> <span class="token class-name">StringComparison<span class="token punctuation">?</span></span> _comparisonType<span class="token punctuation">;</span>
    <span class="token keyword">private</span> <span class="token keyword">readonly</span> <span class="token class-name"><span class="token keyword">bool</span></span> _blockNumberParsing<span class="token punctuation">;</span>

    <span class="token keyword">public</span> <span class="token function">EnumParseOptions</span><span class="token punctuation">(</span>
        <span class="token class-name">StringComparison</span> comparisonType <span class="token operator">=</span> StringComparison<span class="token punctuation">.</span>Ordinal<span class="token punctuation">,</span>
        <span class="token class-name"><span class="token keyword">bool</span></span> allowMatchingMetadataAttribute <span class="token operator">=</span> <span class="token boolean">false</span><span class="token punctuation">,</span>
        <span class="token class-name"><span class="token keyword">bool</span></span> enableNumberParsing <span class="token operator">=</span> <span class="token boolean">true</span><span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        _comparisonType <span class="token operator">=</span> comparisonType<span class="token punctuation">;</span>
        AllowMatchingMetadataAttribute <span class="token operator">=</span> allowMatchingMetadataAttribute<span class="token punctuation">;</span>
        _blockNumberParsing <span class="token operator">=</span> <span class="token operator">!</span>enableNumberParsing<span class="token punctuation">;</span>
    <span class="token punctuation">}</span>

    <span class="token keyword">public</span> <span class="token return-type class-name">StringComparison</span> ComparisonType <span class="token operator">=&gt;</span> _comparisonType <span class="token operator">??</span> StringComparison<span class="token punctuation">.</span>Ordinal<span class="token punctuation">;</span>
    <span class="token keyword">public</span> <span class="token return-type class-name"><span class="token keyword">bool</span></span> AllowMatchingMetadataAttribute <span class="token punctuation">{</span> <span class="token keyword">get</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
    <span class="token keyword">public</span> <span class="token return-type class-name"><span class="token keyword">bool</span></span> EnableNumberParsing <span class="token operator">=&gt;</span> <span class="token operator">!</span>_blockNumberParsing<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>The main difficulty in designing this object was that the default values (i.e. <code>(EnumParseOptions)default</code>) had to match the "default" values used in other APIs, i.e.</p> <ul><li><em>Not</em> case sensitive</li> <li>Number parsing <em>enabled</em></li> <li><em>Don't</em> match metadata attributes (see my <a href="https://andrewlock.net/recent-updates-to-netescapaades-enumgenerators/">previous post</a> about recent changes to metadata attributes!).</li></ul> <p>The final object ticks all those boxes, and it means you can now disable number parsing, using code like this:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token class-name"><span class="token keyword">string</span></span> someNumber <span class="token operator">=</span> <span class="token string">"123"</span><span class="token punctuation">;</span>
ColourExtensions<span class="token punctuation">.</span><span class="token function">Parse</span><span class="token punctuation">(</span>someNumer<span class="token punctuation">,</span> <span class="token keyword">new</span> <span class="token constructor-invocation class-name">EnumParseOptions</span><span class="token punctuation">(</span><span class="token named-parameter punctuation">enableNumberParsing</span><span class="token punctuation">:</span> <span class="token boolean">false</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// throws ArgumentException</span>
</code></pre></div> <p>As well as introducing number parsing, this also provided a way to sneak in the ability to use <em>any</em> type of <code>StringComparison</code> during <code>Parse</code> or <code>TryParse</code> methods, instead of only supporting <code>Ordinal</code> and <code>OrdinalIgnoreCase</code>.</p> <h3 id="support-for-automatic-tolowerinvariant-calls" class="heading-with-anchor"><a href="https://andrewlock.net/updates-to-netescapaades-enumgenerators-new-apis-and-system-memory-support/#support-for-automatic-tolowerinvariant-calls" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Support for automatic <code>ToLowerInvariant()</code> calls</a></h3> <p>The <a href="https://github.com/andrewlock/NetEscapades.EnumGenerators/pull/177">next feature</a> was <em>also</em> <a href="https://github.com/andrewlock/NetEscapades.EnumGenerators/issues/36">a feature request that's been around for a long time</a>, the ability to do the equivalent of <code>ToString().ToLowerInvariant()</code> but without the intermediate allocation, and with the transformation done at compile time, essentially generating something similar to these:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token keyword">class</span> <span class="token class-name">ColourExtensions</span>
<span class="token punctuation">{</span>
    <span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token return-type class-name"><span class="token keyword">string</span></span> <span class="token function">ToStringLowerInvariant</span><span class="token punctuation">(</span><span class="token keyword">this</span> <span class="token class-name">Colour</span> colour<span class="token punctuation">)</span> <span class="token operator">=&gt;</span> colour <span class="token keyword">switch</span>
    <span class="token punctuation">{</span>
        Colour<span class="token punctuation">.</span>Red <span class="token operator">=&gt;</span> <span class="token string">"red"</span><span class="token punctuation">,</span>
        Colour<span class="token punctuation">.</span>Blue <span class="token operator">=&gt;</span> <span class="token string">"blue"</span><span class="token punctuation">,</span>
        _ <span class="token operator">=&gt;</span> colour<span class="token punctuation">.</span><span class="token function">ToString</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">ToLowerInvariant</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
    <span class="token punctuation">}</span><span class="token punctuation">;</span>

    <span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token return-type class-name"><span class="token keyword">string</span></span> <span class="token function">ToStringUpperInvariant</span><span class="token punctuation">(</span><span class="token keyword">this</span> <span class="token class-name">Colour</span> colour<span class="token punctuation">)</span> <span class="token operator">=&gt;</span> colour <span class="token keyword">switch</span>
    <span class="token punctuation">{</span>
        Colour<span class="token punctuation">.</span>Red <span class="token operator">=&gt;</span> <span class="token string">"RED"</span><span class="token punctuation">,</span>
        Colour<span class="token punctuation">.</span>Blue <span class="token operator">=&gt;</span> <span class="token string">"BLUE"</span><span class="token punctuation">,</span>
        _ <span class="token operator">=&gt;</span> colour<span class="token punctuation">.</span><span class="token function">ToString</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">ToUpperInvariant</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>There are various reasons you might want to do that, for example if third-party APIs require that you use upper/lower for your enums, but you want to keep your definitions as canonical C# naming. This was another case where I could see the value, but I didn't really <em>want</em> to add it, as it looked like it would be a headache. <code>ToStringLower()</code> is kind of ugly, and there would be a bunch of extra overloads required again.</p> <p>Just as for the number parsing scenario, the solution I settled on was to add <a href="https://github.com/andrewlock/NetEscapades.EnumGenerators/blob/master/src/NetEscapades.EnumGenerators.Attributes/SerializationOptions.cs#L6">a <code>SerializationOptions</code> object</a> that supports a <code>SerializationTransform</code>, which can potentially be extended in the future if required (though I'm not chomping at the bit to add more options right now!)</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">public</span> <span class="token keyword">readonly</span> <span class="token keyword">struct</span> <span class="token class-name">SerializationOptions</span>
<span class="token punctuation">{</span>
    <span class="token keyword">public</span> <span class="token function">SerializationOptions</span><span class="token punctuation">(</span>
        <span class="token class-name"><span class="token keyword">bool</span></span> useMetadataAttributes <span class="token operator">=</span> <span class="token boolean">false</span><span class="token punctuation">,</span>
        <span class="token class-name">SerializationTransform</span> transform <span class="token operator">=</span> SerializationTransform<span class="token punctuation">.</span>None<span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        UseMetadataAttributes <span class="token operator">=</span> useMetadataAttributes<span class="token punctuation">;</span>
        Transform <span class="token operator">=</span> transform<span class="token punctuation">;</span>
    <span class="token punctuation">}</span>

    <span class="token keyword">public</span> <span class="token return-type class-name"><span class="token keyword">bool</span></span> UseMetadataAttributes <span class="token punctuation">{</span> <span class="token keyword">get</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
    <span class="token keyword">public</span> <span class="token return-type class-name">SerializationTransform</span> Transform <span class="token punctuation">{</span> <span class="token keyword">get</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
<span class="token punctuation">}</span>

<span class="token keyword">public</span> <span class="token keyword">enum</span> <span class="token class-name">SerializationTransform</span>
<span class="token punctuation">{</span>
    None<span class="token punctuation">,</span>
    LowerInvariant<span class="token punctuation">,</span>
    UpperInvariant<span class="token punctuation">,</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>You can then use the <code>ToStringFast()</code> overload that takes a <code>SerializationOptions</code> object, and it will output the lower version of your enum, without needing the intermediate <code>ToStringFast</code> call:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token class-name"><span class="token keyword">var</span></span> colour <span class="token operator">=</span> Colour<span class="token punctuation">.</span>Red<span class="token punctuation">;</span>
Console<span class="token punctuation">.</span><span class="token function">WriteLine</span><span class="token punctuation">(</span>colour<span class="token punctuation">.</span><span class="token function">ToStringFast</span><span class="token punctuation">(</span><span class="token keyword">new</span><span class="token punctuation">(</span><span class="token named-parameter punctuation">transform</span><span class="token punctuation">:</span> SerializationTransform<span class="token punctuation">.</span>LowerInvariant<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// red</span>
</code></pre></div> <p>It's not the tersest of syntax, but there's ways to clean that up, and it means that adding additional options later if required should be less of an issue, but we shall see. In the short-term, it means that you can now use this feature if you find yourself needing to call <code>ToLowerInvariant()</code> or <code>ToUpperInvariant()</code> on your enums.</p> <h3 id="support-for-the-system-memory-nuget-package" class="heading-with-anchor"><a href="https://andrewlock.net/updates-to-netescapaades-enumgenerators-new-apis-and-system-memory-support/#support-for-the-system-memory-nuget-package" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Support for the <em>System.Memory</em> NuGet package</a></h3> <p>From the start, NetEscapades.EnumGenerators has had support for parsing values from <code>ReadOnlySpan&lt;char&gt;</code>, just like the modern APIs in <code>System.Enum</code> do (but faster 😉). However, these APIs have always been guarded by a pre-processor directive; if you're using .NET Core 2.1+ or .NET Standard 2.1 then they're available, but if you're using .NET Framework, or .NET Standard 2.0, then they're not available.</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token preprocessor property">#<span class="token directive keyword">if</span> NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER</span>
    <span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token return-type class-name"><span class="token keyword">bool</span></span> <span class="token function">IsDefined</span><span class="token punctuation">(</span><span class="token keyword">in</span> <span class="token class-name">ReadOnlySpan<span class="token punctuation">&lt;</span><span class="token keyword">char</span><span class="token punctuation">&gt;</span></span> name<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token return-type class-name"><span class="token keyword">bool</span></span> <span class="token function">IsDefined</span><span class="token punctuation">(</span><span class="token keyword">in</span> <span class="token class-name">ReadOnlySpan<span class="token punctuation">&lt;</span><span class="token keyword">char</span><span class="token punctuation">&gt;</span></span> name<span class="token punctuation">,</span> <span class="token class-name"><span class="token keyword">bool</span></span> allowMatchingMetadataAttribute<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token return-type class-name">Colour</span> <span class="token function">Parse</span><span class="token punctuation">(</span><span class="token keyword">in</span> <span class="token class-name">ReadOnlySpan<span class="token punctuation">&lt;</span><span class="token keyword">char</span><span class="token punctuation">&gt;</span></span> name<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token return-type class-name"><span class="token keyword">bool</span></span> <span class="token function">TryParse</span><span class="token punctuation">(</span><span class="token keyword">in</span> <span class="token class-name">ReadOnlySpan<span class="token punctuation">&lt;</span><span class="token keyword">char</span><span class="token punctuation">&gt;</span></span> name<span class="token punctuation">,</span> <span class="token keyword">out</span> <span class="token class-name">Colour</span> colour<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token comment">// etc</span>
<span class="token preprocessor property">#<span class="token directive keyword">endif</span></span>
</code></pre></div> <p>In my experience, this isn't a <em>massive</em> problem these days. If you're targeting any version of .NET Core or modern .NET, then you have the APIs. If, on the other hand, you're on .NET Framework, then the speed of <code>Enum.ToString()</code> will really be the least of your performance worries, and the lack of the APIs probably don't matter <em>that</em> much.</p> <p>Where it <em>could</em> still be an issue is .NET Standard 2.0. It's not recommended to use this target if you're creating libraries for .NET Core or modern .NET, but if you need to target <em>both</em> .NET Core and .NET Framework, then you don't necessarily have a lot of choice, you need to use .NET Standard.</p> <p>What's more, there's a <a href="https://www.nuget.org/packages/System.Memory"><em>System.Memory</em> NuGet package</a> that provides polyfills for <em>many</em> of the APIs, in particular <code>ReadOnlySpan&lt;char&gt;</code>.</p> <blockquote> <p>As I understand it the polyfill implementation isn't generally <em>as</em> fast as the built-in version, but it's still something of an improvement!</p> </blockquote> <p>So the new feature in 1.0.0-beta19 of NetEscapades.EnumGenerators is that you can define an MSBuild property, <code>EnumGenerator_UseSystemMemory=true</code>, and the <code>ReadOnlySpan&lt;char&gt;</code> APIs will be available where previously they wouldn't be. Note that you <em>only</em> need to define this if you're targeting .NET Framework or .NET Standard 2.0 and want the <code>ReadOnlySpan&lt;char&gt;</code> APIs.</p> <div class="pre-code-wrapper"><pre class="language-xml"><code class="language-xml"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>Project</span> <span class="token attr-name">Sdk</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>Microsoft.NET.Sdk<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>

  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PropertyGroup</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>TargetFrameworks</span><span class="token punctuation">&gt;</span></span>netstandard2.0<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>TargetFrameworks</span><span class="token punctuation">&gt;</span></span>
    <span class="token comment">&lt;!-- 👇Setting this in a .NET Standard 2.0 project enables the ReadOnlySpan&lt;char&gt; APIs--&gt;</span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>EnumGenerator_UseSystemMemory</span><span class="token punctuation">&gt;</span></span>true<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>EnumGenerator_UseSystemMemory</span><span class="token punctuation">&gt;</span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>PropertyGroup</span><span class="token punctuation">&gt;</span></span>

  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>ItemGroup</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PackageReference</span> <span class="token attr-name">Include</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>NetEscapades.EnumGenerators<span class="token punctuation">"</span></span> <span class="token attr-name">Version</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>1.0.0-beta19<span class="token punctuation">"</span></span> <span class="token punctuation">/&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PackageReference</span> <span class="token attr-name">Include</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>System.Memory<span class="token punctuation">"</span></span> <span class="token attr-name">Version</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>4.6.3<span class="token punctuation">"</span></span> <span class="token punctuation">/&gt;</span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>ItemGroup</span><span class="token punctuation">&gt;</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>Project</span><span class="token punctuation">&gt;</span></span>
</code></pre></div> <p>Setting this property defines a constant, <code>NETESCAPADES_ENUMGENERATORS_SYSTEM_MEMORY</code>, which the updated generated code similarly predicates on:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp">                                                          <span class="token comment">// 👇 New in 1.0.0-beta19</span>
<span class="token preprocessor property">#<span class="token directive keyword">if</span> NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER || NETESCAPADES_ENUMGENERATORS_SYSTEM_MEMORY</span>
    <span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token return-type class-name"><span class="token keyword">bool</span></span> <span class="token function">IsDefined</span><span class="token punctuation">(</span><span class="token keyword">in</span> <span class="token class-name">ReadOnlySpan<span class="token punctuation">&lt;</span><span class="token keyword">char</span><span class="token punctuation">&gt;</span></span> name<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token return-type class-name"><span class="token keyword">bool</span></span> <span class="token function">IsDefined</span><span class="token punctuation">(</span><span class="token keyword">in</span> <span class="token class-name">ReadOnlySpan<span class="token punctuation">&lt;</span><span class="token keyword">char</span><span class="token punctuation">&gt;</span></span> name<span class="token punctuation">,</span> <span class="token class-name"><span class="token keyword">bool</span></span> allowMatchingMetadataAttribute<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token return-type class-name">Colour</span> <span class="token function">Parse</span><span class="token punctuation">(</span><span class="token keyword">in</span> <span class="token class-name">ReadOnlySpan<span class="token punctuation">&lt;</span><span class="token keyword">char</span><span class="token punctuation">&gt;</span></span> name<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token return-type class-name"><span class="token keyword">bool</span></span> <span class="token function">TryParse</span><span class="token punctuation">(</span><span class="token keyword">in</span> <span class="token class-name">ReadOnlySpan<span class="token punctuation">&lt;</span><span class="token keyword">char</span><span class="token punctuation">&gt;</span></span> name<span class="token punctuation">,</span> <span class="token keyword">out</span> <span class="token class-name">Colour</span> colour<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token comment">// etc</span>
<span class="token preprocessor property">#<span class="token directive keyword">endif</span></span>
</code></pre></div> <p>There <em>are</em> some caveats:</p> <ul><li>The <em>System.Memory</em> package doesn't provide an implementation of <code>int.TryParse()</code> that works with <code>ReadOnlySpan&lt;char&gt;</code>, so: <ul><li><em>If</em> you're attempting to parse a <code>ReadOnlySpan&lt;char&gt;</code>,</li> <li><em>and</em> the <code>ReadOnlySpan&lt;char&gt;</code> <em>doesn't</em> represent one of your enum types</li> <li><em>and</em> you haven't disabled number parsing</li> <li><em>then</em> the APIs will potentially allocate, so that they can call <code>int.TryParse(string)</code>.</li></ul> </li> <li>Additional warnings are included in the XML docs to warn about the above scenario</li> <li>If you set the variable, and <em>haven't</em> added a reference to <em>System.Memory</em>, you'll get compilation warnings.</li></ul> <p>As part of shipping the feature, I've also tentatively added "detection" of referencing the <em>System.Memory</em> NuGet package in the package <code>.targets</code> file, so that <code>EnumGenerator_UseSystemMemory=true</code> should be <em>automatically</em> set, simply by referencing <em>System.Memory</em>. However, I consider this part somewhat experimental, as it's not something I've tried to do before, I'm not sure it's something you <em>should</em> do, and I'm a long way from thinking it'll work 100% of the time 😅 I'd be interested in feedback on how I <em>should</em> do this, and/or whether it works for you!</p> <blockquote> <p>I also experimented with a variety of other approaches. Instead of using a defined constant, you could also detect the availability of <code>ReadOnlySpan&lt;char&gt;</code> in the generator itself, and emit different code entirely. But you'd still need to detect whether the <code>int.TryParse()</code> overloads are available (i.e. is <code>ReadOnlySpan&lt;char&gt;</code> "built in" or package-provided), and overall it seemed way more complex to handle than the approach I settled on. I'm still somewhat torn though, and maye revert to this approach in the future. And I'm open to other suggestions!</p> </blockquote> <p>So in summary, if you're using NetEscapades.EnumGenerators in a <code>netstandard2.0</code> or <code>.NET Framework</code> package, and you're already referencing <em>System.Memory</em>, then <em>in theory</em> you should magically get additional <code>ReadOnlySpan&lt;char&gt;</code> APIs by updating to 1.0.0-beta19. If that's <em>not</em> the case, then I'd love if you could <a href="https://github.com/andrewlock/NetEscapades.EnumGenerators/issues">raise an issue</a> so we can understand why, but you can also simply set <code>EnumGenerator_UseSystemMemory=true</code> to get all that performance goodness guaranteed.</p> <p>Before I close, I'd like to say a big thank you to everyone who has raised issues and PRs for the project, especially <a href="https://github.com/paulomorgado">Paulo Morgado</a> for his discussion and work around the <em>System.Memory</em> feature! All feedback is greatly appreciated, so do <a href="https://github.com/andrewlock/NetEscapades.EnumGenerators/issues">raise an issue</a> if you have any problems.</p> <h2 id="when-is-a-non-pre-release-1-0-0-release-coming-" class="heading-with-anchor"><a href="https://andrewlock.net/updates-to-netescapaades-enumgenerators-new-apis-and-system-memory-support/#when-is-a-non-pre-release-1-0-0-release-coming-" class="relative text-zinc-800 dark:text-white no-underline hover:underline">When is a non-pre-release 1.0.0 release coming?</a></h2> <p>Soon. I promise 😅 So give the new version a try and flag any issues so they can be fixed before we settle on the final API for once and for all! 😀</p> <h2 id="summary" class="heading-with-anchor"><a href="https://andrewlock.net/updates-to-netescapaades-enumgenerators-new-apis-and-system-memory-support/#summary" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Summary</a></h2> <p>In this post I walked through some of the recent updates to <a href="https://github.com/andrewlock/NetEscapades.EnumGenerators">NetEscapades.EnumGenerators</a> shipped in version 1.0.0-beta18. I showed how introducing options objects for both the <code>Parse()</code>/<code>TryParse()</code> and <code>ToString()</code> methods allowed introducing new features such as disabling number parsing and serializing directly with <code>ToLowerInvariant()</code>. Finally, I showed the new support for <code>ReadOnlySpan&lt;char&gt;</code> APIs when using .NET Framework or .NET Standard 2.0 with <a href="https://www.nuget.org/packages/System.Memory">the <em>System.Memory</em> NuGet package</a> If you haven't already, I recommend updating and giving it a try! If you run into any problems, please do <a href="https://github.com/andrewlock/NetEscapades.EnumGenerators/issues">log an issue on GitHub</a>.🙂</p> ]]></content:encoded><category><![CDATA[.NET Core;Roslyn;Source Generators]]></category></item><item><title><![CDATA[Creating a .NET CLR profiler using C# and NativeAOT with Silhouette]]></title><description><![CDATA[In this post I look at how to create a simple .NET profiler. But instead of using C++, the profiler uses C# and NativeAOT with the Silhouette library]]></description><link>https://andrewlock.net/creating-a-dotnet-profiler-using-csharp-with-silhouette/</link><guid isPermaLink="true">https://andrewlock.net/creating-a-dotnet-profiler-using-csharp-with-silhouette/</guid><pubDate>Tue, 16 Dec 2025 10:00:00 GMT</pubDate><dc:creator><![CDATA[Andrew Lock]]></dc:creator><media:content url="https://andrewlock.net/content/images/2025/SilhouetteProf_banner.png" medium="image" /><content:encoded><![CDATA[<img src="https://andrewlock.net/content/images/2025/SilhouetteProf_banner.png" /><p>In this post I take <a href="https://minidump.net/">Kevin Gosse's</a> <a href="https://github.com/kevingosse/Silhouette">Silhouette library</a> for a spin, to see how easy it is to build a basic .NET CLR profiler. And when I say basic, I mean <em>basic</em>—in this post we'll simply log when an assembly is loaded and that's it. I was mostly interested in seeing how easy it was to get up and running.</p> <p>Kevin already has a <a href="https://minidump.net/writing-a-net-profiler-in-c-part-5/">5 part series</a> on writing a .NET profiler in C# in which he describes his <a href="https://github.com/kevingosse/Silhouette">Silhouette</a> library, as well as follow up posts using the library to do real work, like <a href="https://minidump.net/measuring-ui-responsiveness/">measuring UI responsiveness in Resharper</a> and <a href="https://minidump.net/using-function-hooks-with-silhouette/">using profiler function hooks</a>. This post is going to be <em>much</em> more basic than that, just a demonstration of the library in action.</p> <p>I'm also not going to go into great details about the profiling APIs themselves. Kevin covers some of this in his above posts, or alternatively you can see <a href="https://chnasarre.medium.com/">Christophe Nasarre</a>'s series of posts on <a href="https://chnasarre.medium.com/start-a-journey-into-the-net-profiling-apis-40c76e2e36cc">the profiling APIs</a>. In this post we're really <em>not</em> diving in deep like that, we're just going to dip a toe in and see what's possible!</p> <p>So before we get started, we should first recap: what <em>are</em> the .NET profiling APIs?</p> <h2 id="what-are-the-net-profiling-apis-" class="heading-with-anchor"><a href="https://andrewlock.net/creating-a-dotnet-profiler-using-csharp-with-silhouette/#what-are-the-net-profiling-apis-" class="relative text-zinc-800 dark:text-white no-underline hover:underline">What are the .NET profiling APIs?</a></h2> <p>For 99% of people, working with .NET means staying in a nice managed runtime and never having to worry about native code, other than perhaps the occasional <a href="https://learn.microsoft.com/en-us/dotnet/standard/native-interop/pinvoke">P/Invoke</a>. However, behind the scenes, the .NET base class libraries often interact with native libraries, and the .NET runtime <em>itself</em> <a href="https://andrewlock.net/exploring-the-dotnet-boot-process-via-host-tracing/">is a native application</a>. What's more, both .NET Core and .NET Framework expose a whole suite of <a href="https://learn.microsoft.com/en-us/dotnet/core/unmanaged-api/">unmanaged APIs</a> that you can invoke from native code.</p> <p>.NET Core documents three main categories of unmanaged APIs:</p> <ul><li><a href="https://learn.microsoft.com/en-us/dotnet/core/unmanaged-api/debugging/">Debugging APIs</a> for debugging code that runs in the common language runtime (CLR) environment.</li> <li><a href="https://learn.microsoft.com/en-us/dotnet/core/unmanaged-api/debugging/">Metadata APIs</a> for reading or generating details about modules and types without loading them in the CLR.</li> <li><a href="https://learn.microsoft.com/en-us/dotnet/core/unmanaged-api/profiling/">Profiling APIs</a> for monitoring a program's execution by the CLR.</li></ul> <p>In this post we're primarily looking at the final category, the profiling APIs, and will use the metadata APIs in a supporting role.</p> <p>When you think of "profiling", you probably think about the profiling tools built into <a href="https://learn.microsoft.com/en-us/dotnet/core/unmanaged-api/profiling/">Visual Studio</a>, JetBrain's <a href="https://www.jetbrains.com/profiler/">dotTrace</a>, or viewing <a href="https://learn.microsoft.com/en-us/dotnet/core/diagnostics/dotnet-trace"><code>dotnet-trace</code> traces</a> in <a href="https://github.com/microsoft/perfview">PerfView</a>. These "traditional" profilers can use the profiling APIs for their functionality (though there are other approaches too), but the profiling APIs are far more general than that. For example, we use them in the <a href="https://github.com/DataDog/dd-trace-dotnet">Datadog .NET client library</a> to <em>rewrite</em> methods to include our instrumentation.</p> <p>The profiling APIs are very powerful, but the real difficulty is that they're <em>unmanaged</em> APIs, which typically means writing C/C++ code to use them. Yuk. Well, that's where <a href="https://github.com/kevingosse/Silhouette">Silhouette</a> comes in.</p> <h2 id="who-needs-c-when-you-have-nativeaot-" class="heading-with-anchor"><a href="https://andrewlock.net/creating-a-dotnet-profiler-using-csharp-with-silhouette/#who-needs-c-when-you-have-nativeaot-" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Who needs C when you have NativeAOT?</a></h2> <p>Microsoft have been working on NativeAOT for many releases, and with each new version of .NET it gets a little bit better. With NativeAOT, you can compile your .NET application to a <em>native</em>, standalone binary. And a native standalone binary is all you need to write a .NET profiler!</p> <blockquote> <p>The key thing with a NativeAOT binary is that it's fully self-contained. That means that when your profiling binary is loaded, it's running a completely <em>separate</em> .NET runtime from the application being profiled. Yes, that technically means there's two .NET runtimes loaded in the process!</p> </blockquote> <p>Of course, just compiling .NET as a native binary isn't the only requirement. You also need to make sure your native binary exposes all the correct entrypoints and interfaces such that the .NET runtime can load your library <em>as though</em> it was built using C++. To quote from <a href="https://minidump.net/writing-a-net-profiler-in-c-part-1-d3978aae9b12/">Kevin's article on Silhouette</a>:</p> <blockquote> <p>In a nutshell, we need to expose a <code>DllGetClassObject</code> method that will return an instance of <code>IClassFactory</code>. The .NET runtime will call the <code>CreateInstance</code> method on the class factory, which will return an instance of <code>ICorProfilerCallback</code> (or <code>ICorProfilerCallback2</code>, <code>ICorProfilerCallback3</code>, …, depending on which version of the profiling API we want to support). Last but not least, the runtime will call the <code>Initialize</code> method on that instance with an <code>IUnknown</code> parameter that we can use to fetch an instance of <code>ICorProfilerInfo</code> (or <code>ICorProfilerInfo2</code>, <code>ICorProfilerInfo3</code>, …) that we will need to query the profiling API.</p> </blockquote> <p>If that went right over your head, that's normal😅 These are APIs that very few .NET engineers ever need to work with, and seeing as they're C++ APIs, that's even worse! But that's the point; with Native AOT and the Silhouette library, you don't need to understand <em>all</em> these interactions. The Silhouette library handles the messy work of setting up your entrypoint and <a href="https://minidump.net/writing-a-net-profiler-in-c-part-1-d3978aae9b12/#exposing-a-c-interface-kind-of">exposing .NET types as C++ interfaces</a>.</p> <p>Of course, Silhouette doesn't let you completely off the hook—you still need to know what the unmanaged APIs are <em>for</em>, how to use them, and how to chain them together. But Silhouette makes it easier to get started, and means you can write your logic in C#, the language you know best, instead of having to wrestle with C++.</p> <h2 id="writing-a-net-profiler-in-c-" class="heading-with-anchor"><a href="https://andrewlock.net/creating-a-dotnet-profiler-using-csharp-with-silhouette/#writing-a-net-profiler-in-c-" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Writing a .NET profiler in C#</a></h2> <p>As an example of how easy Silhouette makes getting started, for the rest of the post we're going to write a simple profiler using .NET that simply prints out the assemblies that are loaded to the console.</p> <h3 id="creating-the-profiler-and-test-projects" class="heading-with-anchor"><a href="https://andrewlock.net/creating-a-dotnet-profiler-using-csharp-with-silhouette/#creating-the-profiler-and-test-projects" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Creating the profiler and test projects</a></h3> <p>We'll start by creating a simple solution. This will consist of two projects: a class library which is our profiler, and a "hello world" test app, which we will profile:</p> <div class="pre-code-wrapper"><pre class="language-powershell"><code class="language-powershell"><span class="token comment"># Create the two projects</span>
dotnet new classlib <span class="token operator">-</span>o SilhouetteProf
dotnet new console <span class="token operator">-</span>o TestApp

<span class="token comment"># Add the projects to a sln file</span>
dotnet new sln
dotnet sln add <span class="token punctuation">.</span>\SilhouetteProf\
dotnet sln add <span class="token punctuation">.</span>\TestApp\
</code></pre></div> <p>This gives us our basic project structure. Now we'll add the <a href="https://github.com/kevingosse/Silhouette">Silhouette</a> library to our profiler project:</p> <div class="pre-code-wrapper"><pre class="language-bash"><code class="language-bash">dotnet <span class="token function">add</span> package Silhouette <span class="token parameter variable">--project</span> SilhouetteProf
</code></pre></div> <p>Next we need to ensure we publish our application using NativeAOT and allow <code>unsafe</code> code. We're not actually going to write any unsafe code ourselves in this test, but Silhouette includes a source generator which <em>does</em> use <code>unsafe</code>.</p> <p>Open up <em>SilhoutteProj.csproj</em>, and add the two properties</p> <div class="pre-code-wrapper"><pre class="language-xml"><code class="language-xml"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>Project</span> <span class="token attr-name">Sdk</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>Microsoft.NET.Sdk<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>

  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PropertyGroup</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>TargetFramework</span><span class="token punctuation">&gt;</span></span>net10.0<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>TargetFramework</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>RootNamespace</span><span class="token punctuation">&gt;</span></span>SilhouetteProf<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>RootNamespace</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>ImplicitUsings</span><span class="token punctuation">&gt;</span></span>enable<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>ImplicitUsings</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>Nullable</span><span class="token punctuation">&gt;</span></span>enable<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>Nullable</span><span class="token punctuation">&gt;</span></span>

    <span class="token comment">&lt;!-- 👇 Add these two  --&gt;</span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PublishAot</span><span class="token punctuation">&gt;</span></span>true<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>PublishAot</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>AllowUnsafeBlocks</span><span class="token punctuation">&gt;</span></span>true<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>AllowUnsafeBlocks</span><span class="token punctuation">&gt;</span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>PropertyGroup</span><span class="token punctuation">&gt;</span></span>

  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>ItemGroup</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PackageReference</span> <span class="token attr-name">Include</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>Silhouette<span class="token punctuation">"</span></span> <span class="token attr-name">Version</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>3.2.0<span class="token punctuation">"</span></span> <span class="token punctuation">/&gt;</span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>ItemGroup</span><span class="token punctuation">&gt;</span></span>

<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>Project</span><span class="token punctuation">&gt;</span></span>
</code></pre></div> <p>Now we have our prerequisites, we can start creating our profiler.</p> <h3 id="creating-the-basic-profiler" class="heading-with-anchor"><a href="https://andrewlock.net/creating-a-dotnet-profiler-using-csharp-with-silhouette/#creating-the-basic-profiler" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Creating the basic profiler</a></h3> <p>To create a .NET profiler with Silhouette, you create a class that derives from the Silhouette-provided <code>CorProfilerCallbackBase</code> (or <code>CorProfilerCallback2Base</code>, <code>CorProfilerCallback3Base</code> etc, depending on which functionality you need). You then decorate this class with a <code>[Profiler]</code> attribute and provide a <strong>unique</strong> <code>Guid</code>:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">using</span> <span class="token namespace">Silhouette</span><span class="token punctuation">;</span>

<span class="token keyword">namespace</span> <span class="token namespace">SilhouetteProf</span><span class="token punctuation">;</span>

<span class="token comment">// 👇 Use a new random Guid, don't just use this one! </span>
<span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">Profiler</span><span class="token attribute-arguments"><span class="token punctuation">(</span><span class="token string">"9FD62131-BF21-47C1-A4D4-3AEF5D7C75C6"</span><span class="token punctuation">)</span></span></span><span class="token punctuation">]</span>
<span class="token keyword">internal</span> <span class="token keyword">partial</span> <span class="token keyword">class</span> <span class="token class-name">MyCorProfilerCallback</span> <span class="token punctuation">:</span> <span class="token type-list"><span class="token class-name">CorProfilerCallback5Base</span></span>
<span class="token punctuation">{</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>In the example above I chose a random <code>Guid</code> for my profiler, and derived from <code>CorProfilerCallback5Base</code>. Nothing in this post actually needs the "5" from <code>ICorProfilerInfo5</code>, I'm just including it here to demonstrate the pattern.</p> <p>The <code>[Profiler]</code> attribute above drives a source generator included with Silhouette which generates the boilerplate necessary required by the .NET runtime for creating an <code>IClassFactory</code>. You don't <em>have</em> to use this generated code (if, for example, you need additional logic in your <code>DllGetClassObject</code> method); if you don't want this code, just omit the <code>[Profiler]</code> attribute. The generated code looks like this:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">namespace</span> <span class="token namespace">Silhouette<span class="token punctuation">.</span>_Generated</span>
<span class="token punctuation">{</span>
    <span class="token keyword">using</span> <span class="token namespace">System</span><span class="token punctuation">;</span>
    <span class="token keyword">using</span> <span class="token namespace">System<span class="token punctuation">.</span>Runtime<span class="token punctuation">.</span>InteropServices</span><span class="token punctuation">;</span>

    file <span class="token keyword">static</span> <span class="token keyword">class</span> <span class="token class-name">DllMain</span>
    <span class="token punctuation">{</span>
        <span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">UnmanagedCallersOnly</span><span class="token attribute-arguments"><span class="token punctuation">(</span>EntryPoint <span class="token operator">=</span> <span class="token string">"DllGetClassObject"</span><span class="token punctuation">)</span></span></span><span class="token punctuation">]</span>
        <span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token keyword">unsafe</span> <span class="token return-type class-name">HResult</span> <span class="token function">DllGetClassObject</span><span class="token punctuation">(</span>Guid<span class="token operator">*</span> rclsid<span class="token punctuation">,</span> Guid<span class="token operator">*</span> riid<span class="token punctuation">,</span> nint<span class="token operator">*</span> ppv<span class="token punctuation">)</span>
        <span class="token punctuation">{</span>
            <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">*</span>rclsid <span class="token operator">!=</span> <span class="token keyword">new</span> <span class="token constructor-invocation class-name">Guid</span><span class="token punctuation">(</span><span class="token string">"9fd62131-bf21-47c1-a4d4-3aef5d7c75c6"</span><span class="token punctuation">)</span><span class="token punctuation">)</span>
            <span class="token punctuation">{</span>
                <span class="token keyword">return</span> HResult<span class="token punctuation">.</span>CORPROF_E_PROFILER_CANCEL_ACTIVATION<span class="token punctuation">;</span>
            <span class="token punctuation">}</span>

            <span class="token operator">*</span>ppv <span class="token operator">=</span> ClassFactory<span class="token punctuation">.</span><span class="token function">For</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token keyword">global</span><span class="token punctuation">::</span>SilhouetteProf<span class="token punctuation">.</span><span class="token function">MyCorProfilerCallback</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
            <span class="token keyword">return</span> HResult<span class="token punctuation">.</span>S_OK<span class="token punctuation">;</span>
        <span class="token punctuation">}</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>We have the skeleton of our profiler, but before we can compile it, we need to implement the <code>Initialize</code> method:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">using</span> <span class="token namespace">Silhouette</span><span class="token punctuation">;</span>

<span class="token keyword">namespace</span> <span class="token namespace">SilhouetteProf</span><span class="token punctuation">;</span>

<span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">Profiler</span><span class="token attribute-arguments"><span class="token punctuation">(</span><span class="token string">"9FD62131-BF21-47C1-A4D4-3AEF5D7C75C6"</span><span class="token punctuation">)</span></span></span><span class="token punctuation">]</span>
<span class="token keyword">internal</span> <span class="token keyword">partial</span> <span class="token keyword">class</span> <span class="token class-name">MyCorProfilerCallback</span> <span class="token punctuation">:</span> <span class="token type-list"><span class="token class-name">CorProfilerCallback5Base</span></span>
<span class="token punctuation">{</span>
    <span class="token keyword">protected</span> <span class="token keyword">override</span> <span class="token return-type class-name">HResult</span> <span class="token function">Initialize</span><span class="token punctuation">(</span><span class="token class-name"><span class="token keyword">int</span></span> iCorProfilerInfoVersion<span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        Console<span class="token punctuation">.</span><span class="token function">WriteLine</span><span class="token punctuation">(</span><span class="token string">"[SilhouetteProf] Initialize"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token keyword">if</span> <span class="token punctuation">(</span>iCorProfilerInfoVersion <span class="token operator">&lt;</span> <span class="token number">5</span><span class="token punctuation">)</span>
        <span class="token punctuation">{</span>
            <span class="token comment">// we need at least ICorProfilerInfo5 and we got &lt; 5</span>
            <span class="token keyword">return</span> HResult<span class="token punctuation">.</span>E_FAIL<span class="token punctuation">;</span>
        <span class="token punctuation">}</span>

        <span class="token comment">// Call SetEventMask to tell the .NET runtime which events we're interested in</span>
        <span class="token keyword">return</span> ICorProfilerInfo5<span class="token punctuation">.</span><span class="token function">SetEventMask</span><span class="token punctuation">(</span>COR_PRF_MONITOR<span class="token punctuation">.</span>COR_PRF_MONITOR_ALL<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>The above code is about the most simple version <code>Initialize</code> method we can write. Silhouette takes care of working out which version of <code>ICorProfilerInfo</code> is available, and passes this as an <code>int</code> to the method. In the above code, we're making sure that we have <em>at least</em> <code>ICorProfilerInfo5</code> available, so we can call any methods exposed by <code>ICorProfilerInfo5</code>, <code>ICorProfilerInfo4</code>, <code>ICorProfilerInfo3</code> etc.</p> <blockquote> <p>You'll notice a lot of <code>HResult</code> values used as return values. Returning error codes is the predominant error handling approach with the native APIs, so you'll be dealing with these a lot. Luckily, Silhouette exposes this as a handy enum for you to use.</p> </blockquote> <p>Once we've confirmed that the current .NET runtime supports the features we need, we need to <a href="https://learn.microsoft.com/en-us/dotnet/framework/unmanaged-api/profiling/cor-prf-monitor-enumeration">tell the runtime which events we're interested in using the <code>COR_PRF_MONITOR</code> enum</a> and the <a href="https://learn.microsoft.com/en-us/dotnet/framework/unmanaged-api/profiling/icorprofilerinfo-seteventmask-method"><code>SetEventMask()</code></a> or <a href="https://learn.microsoft.com/en-us/dotnet/framework/unmanaged-api/profiling/icorprofilerinfo5-seteventmask2-method"><code>SetEventMask2()</code></a> methods. For simplicity I used <code>ICorProfilerInfo5.SetEventMask()</code> and just enabled all the features.</p> <blockquote> <p>The <code>ICorProfilerInfo5</code> field is initialized prior to <code>Initialize</code> being called, based on the available interface version. For example, if <code>iCorProfilerInfoVersion</code> is <code>7</code>, then all the <code>ICorProfilerInfo*</code> fields up to <code>ICorProfilerInfo7</code> will be initialized. It's <strong>very</strong> important you only call interface versions that have been initialized. So if <code>iCorProfilerInfoVersion</code> is <code>7</code>, <em>don't</em> call <code>ICorProfilerInfo8</code> or higher!</p> </blockquote> <p>At this point we could test our profiler, but I'm going to continue with the implementation a bit before we come to testing.</p> <h3 id="adding-functionality-to-our-profiler" class="heading-with-anchor"><a href="https://andrewlock.net/creating-a-dotnet-profiler-using-csharp-with-silhouette/#adding-functionality-to-our-profiler" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Adding functionality to our profiler</a></h3> <p>Responding to events with a Silhouette profiler is as easy as overriding a method in the base class. For example we could override the <code>Shutdown</code> method, called when the runtime is shutting down:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">protected</span> <span class="token keyword">override</span> <span class="token return-type class-name">HResult</span> <span class="token function">Shutdown</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token punctuation">{</span>
    Console<span class="token punctuation">.</span><span class="token function">WriteLine</span><span class="token punctuation">(</span><span class="token string">"[SilhouetteProf] Shutdown"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">return</span> HResult<span class="token punctuation">.</span>S_OK<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>To add a bit of interest, we're going to override the <code>AssemblyLoadFinished</code> method which is called when an assembly has finished loading (shocking, I know):</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">protected</span> <span class="token keyword">override</span> <span class="token return-type class-name">HResult</span> <span class="token function">AssemblyLoadFinished</span><span class="token punctuation">(</span><span class="token class-name">AssemblyId</span> assemblyId<span class="token punctuation">,</span> <span class="token class-name">HResult</span> hrStatus<span class="token punctuation">)</span>
<span class="token punctuation">{</span>
    <span class="token comment">// ...</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>The <code>AssemblyLoadFinished</code> method provides an <code>AssemblyId</code> which we can use to retrieve the name of the assembly by calling <em>another</em> method on <code>ICorProfilerInfo5</code>, <code>GetAssemblyInfo(AssemblyId)</code>.</p> <blockquote> <p>The <code>AssemblyId</code> type is a very thin wrapper around an <code>IntPtr</code>, acting as strongly-typed wrappers around the otherwise-ubiquitous <code>IntPtr</code>s used in the profiling APIs. I'm a <a href="https://github.com/andrewlock/StronglyTypedId">big fan of this approach</a> as it eliminates a whole class of mistakes that you could otherwise make of passing a "Class ID" <code>IntPtr</code> to a method expecting an "Assembly ID" <code>IntPtr</code> (for example).</p> </blockquote> <p>We can call <code>GetAssemblyInfo()</code> easily enough using the <code>ICorProfilerInfo5</code> field, but this is a good opportunity to look at a common pattern in the Silhouette library, the use of <code>HResult&lt;T&gt;</code>:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">protected</span> <span class="token keyword">override</span> <span class="token return-type class-name">HResult</span> <span class="token function">AssemblyLoadFinished</span><span class="token punctuation">(</span><span class="token class-name">AssemblyId</span> assemblyId<span class="token punctuation">,</span> <span class="token class-name">HResult</span> hrStatus<span class="token punctuation">)</span>
<span class="token punctuation">{</span>
     <span class="token class-name">HResult<span class="token punctuation">&lt;</span>AssemblyInfoWithName<span class="token punctuation">&gt;</span></span> assemblyInfo <span class="token operator">=</span> ICorProfilerInfo5<span class="token punctuation">.</span><span class="token function">GetAssemblyInfo</span><span class="token punctuation">(</span>assemblyId<span class="token punctuation">)</span>
    <span class="token comment">// ...</span>
<span class="token punctuation">}</span>
</code></pre></div> <p><code>HResult&lt;T&gt;</code> is effectively a simple <a href="https://andrewlock.net/series/working-with-the-result-pattern/">result pattern</a> discriminated union. It contains both an <code>HResult</code> and, if the <code>HResult</code> represents success, an object <code>T</code>. This approach is a way of avoiding the common pattern in the profiling APIs of having multiple "<code>out</code>" parameters, and an <code>HRESULT</code> to indicate whether it's valid to <em>use</em> those values, e.g.</p> <div class="pre-code-wrapper"><pre class="language-cpp"><code class="language-cpp">HRESULT GetAssemblyInfo(  
    [in]  AssemblyID  assemblyId,  
    [in]  ULONG       cchName,  
    [out] ULONG       *pcchName,  
    [out, size_is(cchName), length_is(*pcchName)]  
          WCHAR       szName[] ,  
    [out] AppDomainID *pAppDomainId,  
    [out] ModuleID    *pModuleId);
</code></pre></div> <p>The typical pattern when working with the profiling APIs directly is to make the call, check the return value, and then decide whether to continue or not. <code>HResult&lt;T&gt;</code> allows that pattern too, but you can also go YOLO mode. Instead of doing all the checks yourself, you can instead call <code>HResult&lt;T&gt;.ThrowIfFailed()</code> which returns the <code>T</code> if the call was successful, and throws a <code>Win32Exception</code> otherwise. This can make for some dramatically simpler code to read and write, so it's a real win.</p> <blockquote> <p>Of course, whether you would want to do this with a <em>production</em> grade profiler is a whole other thing. But then, should you really be using anything from this post for production? Probably not 😉</p> </blockquote> <p>Using the <code>ThrowIfFailed()</code> approach gives us the code below. We try to get the assembly name, and if it's available, print it:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">protected</span> <span class="token keyword">override</span> <span class="token return-type class-name">HResult</span> <span class="token function">AssemblyLoadFinished</span><span class="token punctuation">(</span><span class="token class-name">AssemblyId</span> assemblyId<span class="token punctuation">,</span> <span class="token class-name">HResult</span> hrStatus<span class="token punctuation">)</span>
<span class="token punctuation">{</span>
    <span class="token keyword">try</span>
    <span class="token punctuation">{</span>
        <span class="token comment">// Try to get the AssemblyInfoWithName, and if the HResult returns non-success, throw</span>
        <span class="token class-name">AssemblyInfoWithName</span> assemblyInfo <span class="token operator">=</span> ICorProfilerInfo5<span class="token punctuation">.</span><span class="token function">GetAssemblyInfo</span><span class="token punctuation">(</span>assemblyId<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">ThrowIfFailed</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

        Console<span class="token punctuation">.</span><span class="token function">WriteLine</span><span class="token punctuation">(</span><span class="token interpolation-string"><span class="token string">$"[SilhouetteProf] AssemblyLoadFinished: </span><span class="token interpolation"><span class="token punctuation">{</span><span class="token expression language-csharp">assemblyInfo<span class="token punctuation">.</span>AssemblyName</span><span class="token punctuation">}</span></span><span class="token string">"</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token keyword">return</span> HResult<span class="token punctuation">.</span>S_OK<span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
    <span class="token keyword">catch</span> <span class="token punctuation">(</span><span class="token class-name">Win32Exception</span> ex<span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        <span class="token comment">// GetAssemblyInfo() failed for some reason, weird.</span>
        Console<span class="token punctuation">.</span><span class="token function">WriteLine</span><span class="token punctuation">(</span><span class="token interpolation-string"><span class="token string">$"[SilhouetteProf] AssemblyLoadFinished failed: </span><span class="token interpolation"><span class="token punctuation">{</span><span class="token expression language-csharp">ex</span><span class="token punctuation">}</span></span><span class="token string">"</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token keyword">return</span> ex<span class="token punctuation">.</span>NativeErrorCode<span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>The gains of <code>ThrowIfFailed()</code> aren't particularly obvious if you just have a single call. Where it really shines is when you want to chain multiple calls. For example, if we wanted to implement <code>ClassLoadStarted</code>, we would need to chain multiple calls, and that's where <code>ThrowIfFailed()</code> comes into its own:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">protected</span> <span class="token keyword">override</span> <span class="token return-type class-name">HResult</span> <span class="token function">ClassLoadStarted</span><span class="token punctuation">(</span><span class="token class-name">ClassId</span> classId<span class="token punctuation">)</span>
<span class="token punctuation">{</span>
    <span class="token keyword">try</span>
    <span class="token punctuation">{</span>
        <span class="token class-name">ClassIdInfo</span> classIdInfo <span class="token operator">=</span> ICorProfilerInfo<span class="token punctuation">.</span><span class="token function">GetClassIdInfo</span><span class="token punctuation">(</span>classId<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">ThrowIfFailed</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

        <span class="token keyword">using</span> <span class="token class-name">ComPtr<span class="token punctuation">&lt;</span>IMetaDataImport<span class="token punctuation">&gt;</span><span class="token punctuation">?</span></span> metaDataImport <span class="token operator">=</span> ICorProfilerInfo2
                                                            <span class="token punctuation">.</span><span class="token function">GetModuleMetaDataImport</span><span class="token punctuation">(</span>classIdInfo<span class="token punctuation">.</span>ModuleId<span class="token punctuation">,</span> CorOpenFlags<span class="token punctuation">.</span>ofRead<span class="token punctuation">)</span>
                                                            <span class="token punctuation">.</span><span class="token function">ThrowIfFailed</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
                                                            <span class="token punctuation">.</span><span class="token function">Wrap</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token class-name">TypeDefPropsWithName</span> classProps <span class="token operator">=</span> metaDataImport<span class="token punctuation">.</span>Value<span class="token punctuation">.</span><span class="token function">GetTypeDefProps</span><span class="token punctuation">(</span>classIdInfo<span class="token punctuation">.</span>TypeDef<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">ThrowIfFailed</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

        Console<span class="token punctuation">.</span><span class="token function">WriteLine</span><span class="token punctuation">(</span><span class="token interpolation-string"><span class="token string">$"[SilhouetteProf] ClassLoadStarted: </span><span class="token interpolation"><span class="token punctuation">{</span><span class="token expression language-csharp">classProps<span class="token punctuation">.</span>TypeName</span><span class="token punctuation">}</span></span><span class="token string">"</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token keyword">return</span> HResult<span class="token punctuation">.</span>S_OK<span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
    <span class="token keyword">catch</span> <span class="token punctuation">(</span><span class="token class-name">Win32Exception</span> ex<span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        Console<span class="token punctuation">.</span><span class="token function">WriteLine</span><span class="token punctuation">(</span><span class="token interpolation-string"><span class="token string">$"[SilhouetteProf] ClassLoadStarted failed: </span><span class="token interpolation"><span class="token punctuation">{</span><span class="token expression language-csharp">ex</span><span class="token punctuation">}</span></span><span class="token string">"</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token keyword">return</span> ex<span class="token punctuation">.</span>NativeErrorCode<span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>We have three <code>ThrowIfFailed()</code> calls in the above code, which keeps the nice, procedural, flow. We could instead have added three additional <code>if (result != HResult.S_OK)</code> in the code, but that's harder to follow, particularly if you're writing something similar or just prototyping.</p> <p>OK, we now have enough functionality to take our profiler for a spin!</p> <h2 id="testing-our-new-profiler" class="heading-with-anchor"><a href="https://andrewlock.net/creating-a-dotnet-profiler-using-csharp-with-silhouette/#testing-our-new-profiler" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Testing our new profiler</a></h2> <p>To test our profiler, we need to do three things</p> <ul><li>Publish our test app.</li> <li>Publish our profiler.</li> <li>Set the required profiling environment variables.</li></ul> <h3 id="publish-the-test-app" class="heading-with-anchor"><a href="https://andrewlock.net/creating-a-dotnet-profiler-using-csharp-with-silhouette/#publish-the-test-app" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Publish the test app</a></h3> <p>We'll startup by publishing the test app. This isn't <em>technically</em> required, we <em>could</em> just run the app using <code>dotnet run</code> for example. The difficulty is that this invokes the .NET SDK, which <a href="https://andrewlock.net/exploring-the-dotnet-boot-process-via-host-tracing/#finding-the-sdk">is <em>itself</em> a .NET app</a>, which means we'd end up profiling that too. Which is fine, it's just not what we're <em>trying</em> to do.</p> <p>We can publish our hello world app using a simple <code>dotnet publish</code>:</p> <div class="pre-code-wrapper"><pre class="language-powershell"><code class="language-powershell">❯ dotnet publish <span class="token punctuation">.</span>\TestApp\ <span class="token operator">-</span>c Release 
Restore complete <span class="token punctuation">(</span>0<span class="token punctuation">.</span>6s<span class="token punctuation">)</span>
  TestApp net10<span class="token punctuation">.</span>0 succeeded <span class="token punctuation">(</span>0<span class="token punctuation">.</span>9s<span class="token punctuation">)</span> → TestApp\bin\Release\net10<span class="token punctuation">.</span>0\publish\
</code></pre></div> <h3 id="publish-our-profiler" class="heading-with-anchor"><a href="https://andrewlock.net/creating-a-dotnet-profiler-using-csharp-with-silhouette/#publish-our-profiler" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Publish our profiler</a></h3> <p>Publishing our profiler is similar, but as we're using NativeAOT, we also need to provide a runtime ID. In .NET 10, you can also use the <code>--use-current-runtime</code> option to publish for "whatever runtime you're currently using". As you can see below, the SDK used <code>win-x64</code> as I'm running on Windows:</p> <div class="pre-code-wrapper"><pre class="language-powershell"><code class="language-powershell">❯ dotnet publish <span class="token punctuation">.</span>\SilhouetteProf\ <span class="token operator">-</span>c Release <span class="token operator">--</span><span class="token function">use-current</span><span class="token operator">-</span>runtime
Restore complete <span class="token punctuation">(</span>0<span class="token punctuation">.</span>6s<span class="token punctuation">)</span>
  SilhouetteProf net10<span class="token punctuation">.</span>0 win-x64 succeeded <span class="token punctuation">(</span>4<span class="token punctuation">.</span>2s<span class="token punctuation">)</span> → SilhouetteProf\bin\Release\net10<span class="token punctuation">.</span>0\win-x64\publish\

Build succeeded in 5<span class="token punctuation">.</span>5s
</code></pre></div> <p>As we're using NativeAOT, the result is a single, self contained dll (plus separate debug symbols). This is our .NET app, compiled as a NativeAOT .NET profiler!</p> <p><img src="https://andrewlock.net/content/images/2025/SilhouetteProf.png" alt="The profiler dll"></p> <h3 id="setting-the-profiling-environment-variables" class="heading-with-anchor"><a href="https://andrewlock.net/creating-a-dotnet-profiler-using-csharp-with-silhouette/#setting-the-profiling-environment-variables" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Setting the profiling environment variables</a></h3> <p>To attach a profiler to the .NET runtime, you need to set some environment variables. These are different depending on whether you're profiling a .NET Framework or .NET Core app. There are three different variables to set:</p> <p>For profiling a .NET Framework app:</p> <ul><li><code>COR_ENABLE_PROFILING=1</code>—Enable profiling</li> <li><code>COR_PROFILER={9FD62131-BF21-47C1-A4D4-3AEF5D7C75C6}</code>—Set to the value of the GUID from the <code>[Profiler]</code> attribute.</li> <li><code>COR_PROFILER_PATH=c:\path\to\profiler</code>—Path to the profiler dll</li></ul> <p>For profiling a .NET Core/.NET 5+ app:</p> <ul><li><code>CORECLR_ENABLE_PROFILING=1</code>—Enable profiling</li> <li><code>CORECLR_PROFILER={9FD62131-BF21-47C1-A4D4-3AEF5D7C75C6}</code>—Set to the value of the GUID from the <code>[Profiler]</code> attribute.</li> <li><code>CORECLR_PROFILER_PATH=c:\path\to\profiler</code>—Path to the profiler dll</li></ul> <blockquote> <p>There are <a href="https://learn.microsoft.com/en-us/dotnet/core/runtime-config/debugging-profiling#environment-variable-cross-plat">additional platform-specific versions</a> of the path variable you can set if you need to support multiple platforms.</p> </blockquote> <p>After publishing the profiler and the app, I copied the absolute path to the profiler dll, and set the required environment variables using powershell.</p> <blockquote> <p>You technically don't <em>have</em> to use an absolute path for the dll, you can use a relative path, but is that relative to the target app? To the working directory? I prefer to use absolute paths as they're are unambiguous!</p> </blockquote> <div class="pre-code-wrapper"><pre class="language-powershell"><code class="language-powershell"><span class="token variable">$env</span>:CORECLR_ENABLE_PROFILING=1
<span class="token variable">$env</span>:CORECLR_PROFILER=<span class="token string">"{9FD62131-BF21-47C1-A4D4-3AEF5D7C75C6}"</span>
<span class="token variable">$env</span>:CORECLR_PROFILER_PATH=<span class="token string">"D:\repos\temp\silouette-prof\SilhouetteProf\bin\Release\net10.0\win-x64\publish\SilhouetteProf.dll"</span>
</code></pre></div> <p>Note that the GUID variable <em>does</em> include the <code>{}</code> surrounding braces. Once the variables are set, we can take our profiler for a spin!</p> <h3 id="testing-our-app-with-our-nativeaot-profiler" class="heading-with-anchor"><a href="https://andrewlock.net/creating-a-dotnet-profiler-using-csharp-with-silhouette/#testing-our-app-with-our-nativeaot-profiler" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Testing our app with our NativeAOT profiler</a></h3> <p>When we run our app, the .NET runtime checks the <code>CORECLR_</code> variables, and loads our NativeAOT profiler, emitting events as the application executes. As each event is raised, we write to the console, and we can see all the assemblies being loaded as the "Hello World!" application runs!</p> <div class="pre-code-wrapper"><pre class="language-powershell"><code class="language-powershell">❯ <span class="token punctuation">.</span>\TestApp<span class="token punctuation">.</span>exe
<span class="token namespace">[SilhouetteProf]</span> Initialize
<span class="token namespace">[SilhouetteProf]</span> AssemblyLoadFinished: System<span class="token punctuation">.</span>Private<span class="token punctuation">.</span>CoreLib
<span class="token namespace">[SilhouetteProf]</span> AssemblyLoadFinished: TestApp
<span class="token namespace">[SilhouetteProf]</span> AssemblyLoadFinished: System<span class="token punctuation">.</span>Runtime
<span class="token namespace">[SilhouetteProf]</span> AssemblyLoadFinished: System<span class="token punctuation">.</span>Console
<span class="token namespace">[SilhouetteProf]</span> AssemblyLoadFinished: System<span class="token punctuation">.</span>Threading
<span class="token namespace">[SilhouetteProf]</span> AssemblyLoadFinished: System<span class="token punctuation">.</span>Text<span class="token punctuation">.</span>Encoding<span class="token punctuation">.</span>Extensions
<span class="token namespace">[SilhouetteProf]</span> AssemblyLoadFinished: System<span class="token punctuation">.</span>Runtime<span class="token punctuation">.</span>InteropServices
Hello<span class="token punctuation">,</span> World!
<span class="token namespace">[SilhouetteProf]</span> Shutdown
</code></pre></div> <p>And there we have it, our .NET profiler, written in .NET, works as expected!🎉 Now, this was obviously a very simple implementation, but it showed me how easy it is to use the Silhouette library to get something up and running vastly quicker than if I had to mess with C++.</p> <p>One thing to bear in mind is that while Silhouette helps with the mechanics of listening to events and interoperating with the C++ interfaces, you still need to know <em>how</em> to use the native APIs. Silhouette helps with the learning curve there, but you'll likely still need to do research for how to achieve what you want.</p> <p>From my point of view, Silhouette is clearly a handy tool for fulfilling a specific need. You won't necessarily want to use it to produce a production-grade profiler, but for proof of concept or development work, it seems invaluable. Especially if Kevin continues <a href="https://minidump.net/measuring-ui-responsiveness/">posting practical examples</a> of using Silhouette himself!</p> <h2 id="summary" class="heading-with-anchor"><a href="https://andrewlock.net/creating-a-dotnet-profiler-using-csharp-with-silhouette/#summary" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Summary</a></h2> <p>In this post I gave a brief introduction to the unmanaged .NET profiling APIs, and how you would typically interact with these APIs using C++. I then described how you can use .NET to produce a binary that can interact with these APIs instead, giving all the benefits of working in .NET, while still being able to call native APIs.</p> <p>I then introduced <a href="https://minidump.net/">Kevin Gosse's</a> <a href="https://github.com/kevingosse/Silhouette">Silhouette library</a>, and showed how this library makes producing a profiler with NativeAOT simple, by deriving from a base class, and overriding the methods you're interested in. I produced a simple profiler, published it, and used it to show all the assemblies loaded by a hello world console application. Overall I was impressed with how simple it was to Silhouette and will likely explore it much more in the future too!</p> ]]></content:encoded><category><![CDATA[.NET Core;C#;Performance;Native AOT]]></category></item><item><title><![CDATA[Trying out the Zed editor on Windows for .NET and Markdown]]></title><description><![CDATA[In this post I try out Zed on Windows to see if it can replace my VS Code usages for quick edits of .NET projects and writing Markdown documents]]></description><link>https://andrewlock.net/trying-out-the-zed-editor-on-windows-for-dotnet-and-markdown/</link><guid isPermaLink="true">https://andrewlock.net/trying-out-the-zed-editor-on-windows-for-dotnet-and-markdown/</guid><pubDate>Tue, 09 Dec 2025 10:00:00 GMT</pubDate><dc:creator><![CDATA[Andrew Lock]]></dc:creator><media:content url="https://andrewlock.net/content/images/2025/zed_banner.webp" medium="image" /><content:encoded><![CDATA[<img src="https://andrewlock.net/content/images/2025/zed_banner.webp" /><p>In this post I provide my initial thoughts about the <a href="https://zed.dev/">Zed</a> editor. I provide my first impressions, some of the customizations I made to get comfortable with it, and my conclusions on using it for working with .NET and Markdown documents.</p> <h2 id="what-s-wrong-with-vs-code-" class="heading-with-anchor"><a href="https://andrewlock.net/trying-out-the-zed-editor-on-windows-for-dotnet-and-markdown/#what-s-wrong-with-vs-code-" class="relative text-zinc-800 dark:text-white no-underline hover:underline">What's wrong with VS Code?</a></h2> <p>Before I get to my actual impressions, I think it's probably worth discussing <em>why</em> I started looking at the <a href="https://zed.dev/">Zed editor</a> in the first place. After all, I currently have JetBrains Rider, Visual Studio, <em>and</em> Visual Studio Code installed on my laptop, do I <em>really</em> need another one?😅</p> <p>Of course, while you <em>can</em> use all these IDEs/editors for much of the same tasks, they have different sweet spots. For example, JetBrains Rider is my go-to IDE day to day. It's what I use for virtually all my "real" development these days. Visual Studio fills essentially the same role for me, and consequently I don't use it much, there's just occasional things where I find the deeper integration with Azure (among other things) come in handy.</p> <p>And then we have VS Code. VS Code always <em>used</em> to be a quick, lightweight editor. It had good-enough general syntax highlighting for all sorts of arbitrary languages, the .NET integration was "good enough" when all I want to do is hack together a quick console app for a blog post, and it's actually a pretty good markdown editor. But VS Code has been starting to bug me more and more recently. It's nothing that I can really put my finger on, it's the little things…</p> <p>First of all, and the main gripe to be honest, is that VS Code just doesn't feel as snappy as it used to. It's not terrible, but I go out of my way to try to <em>not</em> have too many extensions installed now, because it just exacerbates things. I want to be able to edit a file in explorer and have it pop up straight away, not to have to wait 5 seconds for the window to appear.</p> <p>The other thing that bugs me recently is, ironically, the "improved .NET support", by way of <a href="https://learn.microsoft.com/en-us/visualstudio/subscriptions/vs-c-sharp-dev-kit">the C# Dev Kit</a>. And the experience that irritates me is the same irritating feature Visual Studio insists on: creating <em>sln</em> files all over the place. There's a perfectly good <em>csproj</em> in the folder, that's good enough! Stop creating pointless <em>sln</em> files!</p> <p><img src="https://andrewlock.net/content/images/2025/zed_00.png" alt="Oh look, VS Code decided it would create a sln file for no good reason"></p> <p>Obviously I understand <em>why</em> it does this, but I literally never want this behaviour. I also don't care about the "solution explorer" window—if I need that view I'll use a real IDE like Rider. Why can't you just be happy as an editor, VS Code?!</p> <p>I have some other more minor gripes, but as none of that's particularly productive, let's move on. One thing that I <em>do</em> enjoy about VS code is the markdown editing experience. I use the <a href="https://marketplace.visualstudio.com/items?itemName=yzhang.markdown-all-in-one">Markdown All in One</a> extension, <a href="https://marketplace.visualstudio.com/items?itemName=ban.spellright">Spell Right</a>, and <a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode.wordcount">Word Count</a>, and it does everything I need.</p> <p>So in conclusion I was feeling a bit down on VS Code as an editor. It was still better than anything I had used before, but it wasn't quite the darling that I had once considered it to be. And that's when a colleague at work mentioned they'd been using (and enjoying) Zed.</p> <h2 id="maybe-zed-can-replace-vs-code-" class="heading-with-anchor"><a href="https://andrewlock.net/trying-out-the-zed-editor-on-windows-for-dotnet-and-markdown/#maybe-zed-can-replace-vs-code-" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Maybe Zed can replace VS Code?</a></h2> <p>Zed is an editor I'd <a href="https://changelog.com/podcast/531">heard about</a> a <a href="https://changelog.com/podcast/640">few times</a> on the <a href="https://changelog.com/podcast/640">changelog podcast</a>. It was interesting, as it was created by Nathan Sobo, a developer that previously worked on <a href="https://atom-editor.cc/">Atom</a> at GitHub, but it went in a completely different direction. While Atom used Chromium and Node (and spawned the Electron framework so many apps use today), Zed was very different. It was built in Rust, and designed to be very fast.</p> <p>Zed was interesting from an academic point of view, but not hugely practical for me, for several reasons:</p> <ol><li>It didn't support Windows, (until recently)!</li> <li>A big sell is about being "collaborative", something I wasn't interested in for an editor.</li> <li>They've largely pivoted Zed to be an AI play, again something I'm not interested in for an editor.</li></ol> <p>But then, in October 2025, Zed released a Windows build. After a colleague at work told me how much they were enjoying it, I decided to give it a go, and see if it could replace VS Code for my day to day usages.</p> <p>As a reminder, what I really wanted was and editor that:</p> <ul><li>Is <em>fast</em>.</li> <li>Works well with Markdown</li> <li>Works with small .NET projects (when opening an IDE is too slow!)</li></ul> <p>So I installed Zed onto my personal machine, and gave it a try!</p> <h2 id="installing-zed-for-windows" class="heading-with-anchor"><a href="https://andrewlock.net/trying-out-the-zed-editor-on-windows-for-dotnet-and-markdown/#installing-zed-for-windows" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Installing Zed for Windows</a></h2> <p>To install Zed, head to <a href="https://zed.dev/windows">https://zed.dev/windows</a>, and hit the download button (Or just press <kbd>W</kbd>!)</p> <p><img src="https://andrewlock.net/content/images/2025/zed_01.webp" alt="The Zed for Windows home page"></p> <p>This downloads the installer, which is your pretty standard affair, and it includes the usual options for adding Zed to the explorer context window for example (ideal for what I would be using it for).</p> <p><img src="https://andrewlock.net/content/images/2025/zed_02.png" alt="The Zed installer Additional Tasks wizard, allowing you to register Zed in the Windows Explorer Shell"></p> <p>On running Zed for the first time, you're presented with some base customization options:</p> <p><img src="https://andrewlock.net/content/images/2025/zed_03.png" alt="The customization options on the welcome screen of Zed"></p> <p>The ability to base your keymap on other well known editors <em>and</em> to import settings from other editors is a very neat onboarding trick. I chose to import my settings from VS Code:</p> <p><img src="https://andrewlock.net/content/images/2025/zed_05.png" alt="The screen after importing settings from VS Code"></p> <p>Once you're done with the setup, you're dropped into the default welcome screen, which overall looks pretty similar to the VS Code experience. You have your getting started actions, an explanation of the shortcuts available, and a bunch of toolbars around the place:</p> <p><img src="https://andrewlock.net/content/images/2025/zed_06.png" alt="The Zed start screen after first install"></p> <p>I tried generally just opening a few files at this point and looking around, typing a little bit, and I have to say, the first impression is very impressive. It's just <em>so</em> smooth and snappy. Seriously, it feels <em>so</em> fast compared to VS Code. It was a good start! 😄</p> <h2 id="adding-support-for-c-to-zed" class="heading-with-anchor"><a href="https://andrewlock.net/trying-out-the-zed-editor-on-windows-for-dotnet-and-markdown/#adding-support-for-c-to-zed" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Adding support for C# to Zed</a></h2> <p>Much like VS Code, Zed is built around various protocols, and delegates <a href="https://zed.dev/docs/languages">large parts of its core functionality</a> to extensions. There are extensions for themes, extensions for icon packs, extensions for languages…the list goes on! You can open the extension window using the same shortcut as VS Code, <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>X</kbd> (at least, assuming you're using the VS Code shortcuts!)</p> <p><img src="https://andrewlock.net/content/images/2025/zed_09.png" alt="The Zed extension window showing the extensions you can install"></p> <p>To add support for C#, open the extensions window, search for C#, and <a href="https://github.com/zed-extensions/csharp">install the C# extension</a> by <code>fminkowski</code>. This uses the <a href="https://github.com/omnisharp">omnisharp</a> Language Server to provide language support. This is essentially the same backing implementation as the <em>original</em> VS Code C# extension, before Microsoft moved to the C# Dev Kit extension.</p> <p>Once the extension is installed you get syntax highlighting for your C# code, as well as refactoring options, just as you would expect in VS Code, though these will likely be more limited than the options available as part of the VS Code C# Dev kit.</p> <p><img src="https://andrewlock.net/content/images/2025/zed_10.png" alt="C# file in Zed showing a refactoring popup"></p> <p>That's the most important part of getting .NET editing working in Zed, but I also tweaked the themes to be more familiar. I installed <a href="https://github.com/NarmadaWeb/jetbrains-rider-zed">the JetBrains Rider theme</a>, and also <a href="https://github.com/ankddev/zed-jetbrains-newui-icons">the JetBrains New UI Icon Theme</a>, though the latter didn't seem to have special icons for C# files, so that's a bit of a shame. Anyway, that's it for editing .NET for now; let's look at markdown instead.</p> <h2 id="adding-support-for-markdown" class="heading-with-anchor"><a href="https://andrewlock.net/trying-out-the-zed-editor-on-windows-for-dotnet-and-markdown/#adding-support-for-markdown" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Adding support for Markdown</a></h2> <p>The good news is that Zed has <a href="https://zed.dev/docs/languages/markdown">built-in support for Markdown</a>, so there's nothing you <em>need</em> to install. That said, it's relatively pretty bare bones:</p> <p><img src="https://andrewlock.net/content/images/2025/zed_11.png" alt="The markdown experience in Zed"></p> <p>You get syntax highlighting of the markdown text, and a preview pane, but there's not a huge number of other features (more on that later). For me, the one critical extension I added was a spell checker. I added <a href="https://github.com/blopker/codebook">CodeBook</a>, which is actually intended to be used as a spell checker inside <em>code</em> rather than markdown, but it does work with markdown at least:</p> <p><img src="https://andrewlock.net/content/images/2025/zed_12.png" alt="A markdown document with a flagged spelling mistake including suggestions">.</p> <p>But the question is, did I stick with it? Did I write this post in Zed?</p> <h2 id="my-first-impressions-after-trying-zed" class="heading-with-anchor"><a href="https://andrewlock.net/trying-out-the-zed-editor-on-windows-for-dotnet-and-markdown/#my-first-impressions-after-trying-zed" class="relative text-zinc-800 dark:text-white no-underline hover:underline">My first impressions after trying Zed</a></h2> <p>The short answer is, no, I didn't stick with Zed, and instead went back to VS Code. There's a lot of reasons for that which I'll get into, but before I start nipicking, I think it's worth highlighting the good bits.</p> <h3 id="the-good-bits" class="heading-with-anchor"><a href="https://andrewlock.net/trying-out-the-zed-editor-on-windows-for-dotnet-and-markdown/#the-good-bits" class="relative text-zinc-800 dark:text-white no-underline hover:underline">The good bits</a></h3> <p>Zed feels <em>really</em> smooth to use. It opens quickly, tabbing between documents is lighting fast, and even typing feels much faster than VS Code. Switching folders/workspaces is <em>crazy</em> fast😮 The performance is one of those things that you don't realise you're missing until you try it for yourself! 😀</p> <p>Also, I was really impressed with the care that's clearly gone into building and designing Zed. In the relatively short time I used it, I ran into literally no bugs or issues, and the attempt to accommodate onboarding VS Code and other editor users was a really nice touch. Pretty much all of my reasons for not sticking with Zed were missing features rather than anything else.</p> <p>So now let's talk about those missing features.</p> <h3 id="-net-is-good-but-missing-razor-support" class="heading-with-anchor"><a href="https://andrewlock.net/trying-out-the-zed-editor-on-windows-for-dotnet-and-markdown/#-net-is-good-but-missing-razor-support" class="relative text-zinc-800 dark:text-white no-underline hover:underline">.NET is good, but missing Razor support</a></h3> <p>First, editing .NET, for the most part, just worked. I mean, it's like working in VS Code, which is basically all I wanted or needed. I'm not looking to replace Rider with Zed, so I don't need a full IDE.</p> <p>The one piece that was missing is that there doesn't seem to be any support for Razor or cshtml files, which I <em>think</em> is an Omnisharp limitation. And if that's the case, it'll probably <em>never</em> be there unfortunately🙁</p> <p><img src="https://andrewlock.net/content/images/2025/zed_13.png" alt=".razor files don't have any syntax highlighting"></p> <p>This isn't really a knock against Zed or the Omnisharp project; Razor is a notoriously difficult format to handle as it's <em>multiple</em> languages in one: HTML, C#, Blazor <code>@</code> syntax, CSS, JavaScript etc. And it's not really a big deal for me; Zed is meant to be my editor and I'm generally not going to be messing with Razor pages in an editor.</p> <p>So overall, Zed is "good enough" with .NET to be my go-to editor. Unfortunately, that's not the case for Markdown.</p> <h3 id="markdown-is-missing-too-many-features" class="heading-with-anchor"><a href="https://andrewlock.net/trying-out-the-zed-editor-on-windows-for-dotnet-and-markdown/#markdown-is-missing-too-many-features" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Markdown is missing too many features</a></h3> <p>After checking out .NET I was pretty optimistic with Zed, as I assumed that would be the hard bit, but unfortunately I was ultimately let down by the Markdown support.</p> <p>Markdown has first-party support in Zed, but it's missing a lot of features that I'm used to having when writing in VS Code. Many of these are features provided by <a href="https://marketplace.visualstudio.com/items?itemName=yzhang.markdown-all-in-one">the extension I use</a>, but they make for a very smooth writing experience:</p> <ul><li>Shortcuts like <kbd>Ctrl</kbd>+<kbd>B</kbd> to bold the current word. <a href="https://github.com/zed-industries/zed/issues/13208">This has been requested for Zed</a>, but doesn't currently exist.</li> <li>"Code folding" for headings and other elements. These let you hide sections, just like you would when folding a method in code.</li> <li>"IntelliSense" for internal links and for images (which are added via a shortcut of course!).</li> <li>Images are rendered in the preview pane</li> <li>As you scroll the source page (without moving the cursor), the preview window scrolls too.</li></ul> <p>However, the biggest issue with markdown is the way Zed <em>constantly</em> pops up suggestions for the word you're currently writing. This is incredibly distracting, provides no value when writing markdown documents, and frankly made it unusable for me.</p> <p><img src="https://andrewlock.net/content/images/2025/zed_08.png" alt="The suggestion popups when writing Markdown in Zed are infuriating"></p> <p>These all seem like small things, and they are, but they add up to a slick experience, and importantly an experience I'm <em>used</em> to. I've been writing this blog for long enough now that it's just not worth throwing away nearly a decade of muscle memory😅</p> <p>That said, most of these are small features that absolutely <em>could</em> be added later or addressed, so I'm certainly going to keep an eye on it.</p> <h3 id="a-lot-of-shortcuts-don-t-port-across" class="heading-with-anchor"><a href="https://andrewlock.net/trying-out-the-zed-editor-on-windows-for-dotnet-and-markdown/#a-lot-of-shortcuts-don-t-port-across" class="relative text-zinc-800 dark:text-white no-underline hover:underline">A lot of shortcuts don't port across</a></h3> <p>On the theme of muscle memory, one of the big struggles I had initially was realising <em>quite</em> how many shortcuts I use instinctively. This can be very confusing when you hit a shortcut expecting it to (for example) create a new cursor on the line below, and instead it switches to a completely different document😅</p> <p>None of this is a big problem, as you can customize basically all of the keyboard shortcuts. I'm also not <em>entirely</em> sure if these were just not mapped in the "import Keymaps from VS Code" stage, or if these aren't part of the "core" VS Code or something. Anyway, I've added the following mappings so far to get something closer to what I have in VS Code:</p> <div class="pre-code-wrapper"><pre class="language-json"><code class="language-json"><span class="token comment">// Zed keymap</span>
<span class="token punctuation">[</span>
  <span class="token punctuation">{</span>
    <span class="token property">"context"</span><span class="token operator">:</span> <span class="token string">"Workspace"</span><span class="token punctuation">,</span>
    <span class="token property">"bindings"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
      <span class="token comment">// "shift shift": "file_finder::Toggle" </span>
    <span class="token punctuation">}</span>
  <span class="token punctuation">}</span><span class="token punctuation">,</span>
  <span class="token punctuation">{</span>
    <span class="token property">"context"</span><span class="token operator">:</span> <span class="token string">"Editor &amp;&amp; vim_mode == insert"</span><span class="token punctuation">,</span>
    <span class="token property">"bindings"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
      <span class="token comment">// "j k": "vim::NormalBefore"</span>
    <span class="token punctuation">}</span>
  <span class="token punctuation">}</span><span class="token punctuation">,</span>
  <span class="token punctuation">{</span>
    <span class="token property">"context"</span><span class="token operator">:</span> <span class="token string">"Editor"</span><span class="token punctuation">,</span>
    <span class="token property">"bindings"</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token property">"ctrl-shift-delete"</span><span class="token operator">:</span> <span class="token string">"editor::DeleteLine"</span> <span class="token punctuation">}</span>
  <span class="token punctuation">}</span><span class="token punctuation">,</span>
  <span class="token punctuation">{</span>
    <span class="token property">"bindings"</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token property">"ctrl-d"</span><span class="token operator">:</span> <span class="token string">"editor::DuplicateSelection"</span> <span class="token punctuation">}</span>
  <span class="token punctuation">}</span><span class="token punctuation">,</span>
  <span class="token punctuation">{</span>
    <span class="token property">"bindings"</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token property">"ctrl-k ctrl-f"</span><span class="token operator">:</span> <span class="token string">"editor::FormatSelections"</span> <span class="token punctuation">}</span>
  <span class="token punctuation">}</span><span class="token punctuation">,</span>
  <span class="token punctuation">{</span>
    <span class="token property">"context"</span><span class="token operator">:</span> <span class="token string">"Editor"</span><span class="token punctuation">,</span>
    <span class="token property">"bindings"</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token property">"ctrl-k ctrl-d"</span><span class="token operator">:</span> <span class="token string">"editor::Format"</span> <span class="token punctuation">}</span>
  <span class="token punctuation">}</span><span class="token punctuation">,</span>
  <span class="token punctuation">{</span>
    <span class="token property">"context"</span><span class="token operator">:</span> <span class="token string">"Editor"</span><span class="token punctuation">,</span>
    <span class="token property">"bindings"</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token property">"alt-enter"</span><span class="token operator">:</span> <span class="token string">"editor::ToggleCodeActions"</span> <span class="token punctuation">}</span>
  <span class="token punctuation">}</span><span class="token punctuation">,</span>
  <span class="token punctuation">{</span>
    <span class="token property">"context"</span><span class="token operator">:</span> <span class="token string">"Editor"</span><span class="token punctuation">,</span>
    <span class="token property">"bindings"</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token property">"shift-f12"</span><span class="token operator">:</span> <span class="token string">"editor::FindAllReferences"</span> <span class="token punctuation">}</span>
  <span class="token punctuation">}</span><span class="token punctuation">,</span>
  <span class="token punctuation">{</span>
    <span class="token property">"context"</span><span class="token operator">:</span> <span class="token string">"Editor"</span><span class="token punctuation">,</span>
    <span class="token property">"bindings"</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token property">"f3"</span><span class="token operator">:</span> <span class="token punctuation">[</span> <span class="token string">"editor::SelectNext"</span><span class="token punctuation">,</span> <span class="token punctuation">{</span> <span class="token property">"replace_newest"</span><span class="token operator">:</span> <span class="token boolean">false</span> <span class="token punctuation">}</span> <span class="token punctuation">]</span> <span class="token punctuation">}</span>
  <span class="token punctuation">}</span><span class="token punctuation">,</span>
  <span class="token punctuation">{</span>
    <span class="token property">"context"</span><span class="token operator">:</span> <span class="token string">"Editor"</span><span class="token punctuation">,</span>
    <span class="token property">"bindings"</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token property">"shift-f3"</span><span class="token operator">:</span> <span class="token punctuation">[</span> <span class="token string">"editor::SelectPrevious"</span><span class="token punctuation">,</span> <span class="token punctuation">{</span> <span class="token property">"replace_newest"</span><span class="token operator">:</span> <span class="token boolean">false</span> <span class="token punctuation">}</span> <span class="token punctuation">]</span> <span class="token punctuation">}</span>
  <span class="token punctuation">}</span><span class="token punctuation">,</span>
  <span class="token punctuation">{</span>
    <span class="token property">"context"</span><span class="token operator">:</span> <span class="token string">"Editor"</span><span class="token punctuation">,</span>
    <span class="token property">"bindings"</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token property">"ctrl-alt-down"</span><span class="token operator">:</span> <span class="token string">"editor::AddSelectionBelow"</span> <span class="token punctuation">}</span>
  <span class="token punctuation">}</span><span class="token punctuation">,</span>
  <span class="token punctuation">{</span>
    <span class="token property">"context"</span><span class="token operator">:</span> <span class="token string">"Editor"</span><span class="token punctuation">,</span>
    <span class="token property">"bindings"</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token property">"ctrl-alt-up"</span><span class="token operator">:</span> <span class="token string">"editor::AddSelectionAbove"</span> <span class="token punctuation">}</span>
  <span class="token punctuation">}</span><span class="token punctuation">,</span>
  <span class="token punctuation">{</span>
    <span class="token property">"context"</span><span class="token operator">:</span> <span class="token string">"Workspace"</span><span class="token punctuation">,</span>
    <span class="token property">"bindings"</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token property">"alt-s"</span><span class="token operator">:</span> <span class="token string">"workspace::ToggleLeftDock"</span> <span class="token punctuation">}</span>
  <span class="token punctuation">}</span><span class="token punctuation">,</span>
  <span class="token punctuation">{</span>
    <span class="token property">"context"</span><span class="token operator">:</span> <span class="token string">"Pane"</span><span class="token punctuation">,</span>
    <span class="token property">"bindings"</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token property">"alt-left"</span><span class="token operator">:</span> <span class="token string">"pane::GoBack"</span> <span class="token punctuation">}</span>
  <span class="token punctuation">}</span><span class="token punctuation">,</span>
  <span class="token punctuation">{</span>
    <span class="token property">"context"</span><span class="token operator">:</span> <span class="token string">"Pane"</span><span class="token punctuation">,</span>
    <span class="token property">"bindings"</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token property">"alt-right"</span><span class="token operator">:</span> <span class="token string">"pane::GoForward"</span> <span class="token punctuation">}</span>
  <span class="token punctuation">}</span><span class="token punctuation">,</span>
  <span class="token punctuation">{</span>
    <span class="token property">"context"</span><span class="token operator">:</span> <span class="token string">"Editor"</span><span class="token punctuation">,</span>
    <span class="token property">"bindings"</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token property">"ctrl-k ctrl-c"</span><span class="token operator">:</span> <span class="token punctuation">[</span> <span class="token string">"editor::ToggleComments"</span><span class="token punctuation">,</span> <span class="token punctuation">{</span> <span class="token property">"advance_downwards"</span><span class="token operator">:</span> <span class="token boolean">false</span> <span class="token punctuation">}</span> <span class="token punctuation">]</span> <span class="token punctuation">}</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">]</span>
</code></pre></div> <p>Which brings us to the final point.</p> <h3 id="the-ai-elephant-in-the-room" class="heading-with-anchor"><a href="https://andrewlock.net/trying-out-the-zed-editor-on-windows-for-dotnet-and-markdown/#the-ai-elephant-in-the-room" class="relative text-zinc-800 dark:text-white no-underline hover:underline">The AI elephant in the room</a></h3> <p>As I already mentioned, I don't want or need AI in my editor. And yet every company has to add AI to their product whether you want it or not. Such is the way of our times. VS Code and Visual Studio are the same. But the good thing about Zed is that you can simply disable it if you don't want it:</p> <p><img src="https://andrewlock.net/content/images/2025/zed_14.png" alt="You can disable AI in Zed if you don't want AI features"></p> <p>Alternatively you can edit all the settings as JSON. This is what I have currently:</p> <div class="pre-code-wrapper"><pre class="language-json"><code class="language-json"><span class="token comment">// Zed settings</span>
<span class="token punctuation">{</span>
  <span class="token property">"ensure_final_newline_on_save"</span><span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span>
  <span class="token property">"remove_trailing_whitespace_on_save"</span><span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span>
  <span class="token property">"format_on_save"</span><span class="token operator">:</span> <span class="token string">"off"</span><span class="token punctuation">,</span>
  <span class="token property">"when_closing_with_no_tabs"</span><span class="token operator">:</span> <span class="token string">"keep_window_open"</span><span class="token punctuation">,</span>
  <span class="token property">"disable_ai"</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span>
  <span class="token property">"telemetry"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
    <span class="token property">"diagnostics"</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span>
    <span class="token property">"metrics"</span><span class="token operator">:</span> <span class="token boolean">true</span>
  <span class="token punctuation">}</span><span class="token punctuation">,</span>
  <span class="token property">"project_panel"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
    <span class="token property">"auto_fold_dirs"</span><span class="token operator">:</span> <span class="token boolean">false</span>
  <span class="token punctuation">}</span><span class="token punctuation">,</span>
  <span class="token property">"base_keymap"</span><span class="token operator">:</span> <span class="token string">"VSCode"</span><span class="token punctuation">,</span>
  <span class="token property">"minimap"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
    <span class="token property">"show"</span><span class="token operator">:</span> <span class="token string">"never"</span>
  <span class="token punctuation">}</span><span class="token punctuation">,</span>
  <span class="token property">"file_types"</span><span class="token operator">:</span> <span class="token punctuation">{</span><span class="token punctuation">}</span><span class="token punctuation">,</span>
  <span class="token property">"show_whitespaces"</span><span class="token operator">:</span> <span class="token string">"trailing"</span><span class="token punctuation">,</span>
  <span class="token property">"icon_theme"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
    <span class="token property">"mode"</span><span class="token operator">:</span> <span class="token string">"dark"</span><span class="token punctuation">,</span>
    <span class="token property">"light"</span><span class="token operator">:</span> <span class="token string">"Zed (Default)"</span><span class="token punctuation">,</span>
    <span class="token property">"dark"</span><span class="token operator">:</span> <span class="token string">"JetBrains New UI Icons (Dark)"</span>
  <span class="token punctuation">}</span><span class="token punctuation">,</span>
  <span class="token property">"ui_font_size"</span><span class="token operator">:</span> <span class="token number">13.0</span><span class="token punctuation">,</span>
  <span class="token property">"buffer_font_size"</span><span class="token operator">:</span> <span class="token number">12.0</span><span class="token punctuation">,</span>
  <span class="token property">"theme"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
    <span class="token property">"mode"</span><span class="token operator">:</span> <span class="token string">"dark"</span><span class="token punctuation">,</span>
    <span class="token property">"light"</span><span class="token operator">:</span> <span class="token string">"One Light"</span><span class="token punctuation">,</span>
    <span class="token property">"dark"</span><span class="token operator">:</span> <span class="token string">"JetBrains Rider Dark"</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>And that's pretty much it. In conclusion, I won't be replacing VS Code with Zed today. But I can definitely see a time in the near future where enough of the missing markdown features I'm used to have been added that I'll make the switch. The Zed team are shipping updates regularly, and they have a really nice product, even if it's not quite there for me.</p> <h2 id="summary" class="heading-with-anchor"><a href="https://andrewlock.net/trying-out-the-zed-editor-on-windows-for-dotnet-and-markdown/#summary" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Summary</a></h2> <p>In this post I described my experience installing the Zed editor, and trying to replace my VS Code usages with Zed. In particular, I wanted to use Zed to edit .NET projects and to write markdown documents. The .NET experience was very similar to the VS Code experience with the Omnisharp plugin, which is mostly only lacking with regards to Razor/Blazor support. For Markdown I found the lack of features I'm used to in VS Code to be a deal breaker. Zed is an incredibly fast and smooth experience, but I won't be switching to it currently. If the features I'm missing are added however, I would seriously consider it.</p> ]]></content:encoded><category><![CDATA[IDEs;VS Code]]></category></item><item><title><![CDATA[Recent updates to NetEscapades.EnumGenerators: [EnumMember] support, analyzers, and bug fixes]]></title><description><![CDATA[In this post I describe some recent changes to the NetEscapades.EnumGenerators source generator, including support for [EnumMember] and new analyzers]]></description><link>https://andrewlock.net/recent-updates-to-netescapaades-enumgenerators/</link><guid isPermaLink="true">https://andrewlock.net/recent-updates-to-netescapaades-enumgenerators/</guid><pubDate>Tue, 02 Dec 2025 09:00:00 GMT</pubDate><dc:creator><![CDATA[Andrew Lock]]></dc:creator><media:content url="https://andrewlock.net/content/images/2022/enumgenerators_banner.png" medium="image" /><content:encoded><![CDATA[<img src="https://andrewlock.net/content/images/2022/enumgenerators_banner.png" /><p>In this post I describe some of the recent updates to my source generator NuGet package <a href="https://github.com/andrewlock/NetEscapades.EnumGenerators">NetEscapades.EnumGenerators</a> which you can use to add fast methods for working with <code>enum</code>s. I start by describing why the package exists and what you can use it for, then I walk through some of the recent changes.</p> <h2 id="why-should-you-use-an-enum-source-generator-" class="heading-with-anchor"><a href="https://andrewlock.net/recent-updates-to-netescapaades-enumgenerators/#why-should-you-use-an-enum-source-generator-" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Why should you use an enum source generator?</a></h2> <p><a href="https://github.com/andrewlock/NetEscapades.EnumGenerators">NetEscapades.EnumGenerators</a> was one of the first source generators I created using <a href="https://andrewlock.net/exploring-dotnet-6-part-9-source-generator-updates-incremental-generators/">the incremental generator support introduced in .NET 6</a>. I chose to create this package to work around an annoying characteristic of working with enums: some operations are surprisingly slow.</p> <blockquote> <p>Note that while this has <em>historically</em> been true, this fact won't necessarily remain true forever. In fact, .NET 8+ provided a bunch of improvements to enum handling in the runtime.</p> </blockquote> <p>As an example, let's say you have the following enum:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">public</span> <span class="token keyword">enum</span> <span class="token class-name">Colour</span>
<span class="token punctuation">{</span>
    Red <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">,</span>
    Blue <span class="token operator">=</span> <span class="token number">1</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>At some point, you want to print the name of a <code>Color</code> variable, so you create this helper method:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">public</span> <span class="token return-type class-name"><span class="token keyword">void</span></span> <span class="token function">PrintColour</span><span class="token punctuation">(</span><span class="token class-name">Colour</span> colour<span class="token punctuation">)</span>
<span class="token punctuation">{</span>
    Console<span class="token punctuation">.</span><span class="token function">WriteLine</span><span class="token punctuation">(</span><span class="token string">"You chose "</span><span class="token operator">+</span> colour<span class="token punctuation">.</span><span class="token function">ToString</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// You chose Red</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>While this <em>looks</em> like it should be fast, it's really not. <em>NetEscapades.EnumGenerators</em> works by automatically generating an implementation that <em>is</em> fast. It generates a <code>ToStringFast()</code> method that looks something like this:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token keyword">class</span> <span class="token class-name">ColourExtensions</span>
<span class="token punctuation">{</span>
    <span class="token keyword">public</span> <span class="token return-type class-name"><span class="token keyword">string</span></span> <span class="token function">ToStringFast</span><span class="token punctuation">(</span><span class="token keyword">this</span> <span class="token class-name">Colour</span> colour<span class="token punctuation">)</span>
        <span class="token operator">=&gt;</span> colour <span class="token keyword">switch</span>
        <span class="token punctuation">{</span>
            Colour<span class="token punctuation">.</span>Red <span class="token operator">=&gt;</span> <span class="token keyword">nameof</span><span class="token punctuation">(</span>Colour<span class="token punctuation">.</span>Red<span class="token punctuation">)</span><span class="token punctuation">,</span>
            Colour<span class="token punctuation">.</span>Blue <span class="token operator">=&gt;</span> <span class="token keyword">nameof</span><span class="token punctuation">(</span>Colour<span class="token punctuation">.</span>Blue<span class="token punctuation">)</span><span class="token punctuation">,</span>
            _ <span class="token operator">=&gt;</span> colour<span class="token punctuation">.</span><span class="token function">ToString</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
        <span class="token punctuation">}</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>This simple switch statement checks for each of the known values of <code>Colour</code> and uses <code>nameof</code> to return the textual representation of the <code>enum</code>. If it's an unknown value, then it falls back to the built-in <code>ToString()</code> implementation to ensure correct handling of unknown values (for example this is valid C#: <code>PrintColour((Colour)123)</code>).</p> <p>If we compare these two implementations using <a href="https://benchmarkdotnet.org/">BenchmarkDotNet</a> for a known colour, you can see how much faster <code>ToStringFast()</code> implementation is:</p> <table><thead><tr><th>Method</th><th>FX</th><th style="text-align:right">Mean</th><th style="text-align:right">Error</th><th style="text-align:right">StdDev</th><th style="text-align:right">Ratio</th><th style="text-align:right">Gen 0</th><th style="text-align:right">Allocated</th></tr></thead><tbody><tr><td>ToString</td><td><code>net48</code></td><td style="text-align:right">578.276 ns</td><td style="text-align:right">3.3109 ns</td><td style="text-align:right">3.0970 ns</td><td style="text-align:right">1.000</td><td style="text-align:right">0.0458</td><td style="text-align:right">96 B</td></tr><tr><td>ToStringFast</td><td><code>net48</code></td><td style="text-align:right">3.091 ns</td><td style="text-align:right">0.0567 ns</td><td style="text-align:right">0.0443 ns</td><td style="text-align:right">0.005</td><td style="text-align:right">-</td><td style="text-align:right">-</td></tr><tr><td>ToString</td><td><code>net6.0</code></td><td style="text-align:right">17.985 ns</td><td style="text-align:right">0.1230 ns</td><td style="text-align:right">0.1151 ns</td><td style="text-align:right">1.000</td><td style="text-align:right">0.0115</td><td style="text-align:right">24 B</td></tr><tr><td>ToStringFast</td><td><code>net6.0</code></td><td style="text-align:right">0.121 ns</td><td style="text-align:right">0.0225 ns</td><td style="text-align:right">0.0199 ns</td><td style="text-align:right">0.007</td><td style="text-align:right">-</td><td style="text-align:right">-</td></tr></tbody></table> <p>These numbers are obviously quite old now, but the overall pattern hasn't changed: .NET is <em>way</em> faster than .NET Framework, and the <code>ToStringFast()</code> implementation is way faster than the built-in <code>ToString()</code>. Obviously your mileage may vary and the results will depend on the specific enum you're using, but in general, using the source generator should give you a free performance boost.</p> <blockquote> <p>If you want to learn more about what the package provides, check my <a href="https://andrewlock.net/recent-updates-for-netescapades-enumgenerators-interceptors/">blog posts</a> or see the project <a href="https://github.com/andrewlock/NetEscapades.EnumGenerators">README</a>.</p> </blockquote> <p>That covers the basics, now let's look at what's new.</p> <h2 id="updates-in-1-0-0-beta-16" class="heading-with-anchor"><a href="https://andrewlock.net/recent-updates-to-netescapaades-enumgenerators/#updates-in-1-0-0-beta-16" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Updates in 1.0.0-beta.16</a></h2> <p>Version <a href="https://www.nuget.org/packages/NetEscapades.EnumGenerators/">1.0.0-beta16 of NetEscapades.EnumGenerators</a> was released to nuget.org on 4th November and included a number of quality of life features and bug fixes. I'll describe each of the updates in more detail below, but they fall into one of three categories:</p> <ul><li>Redesign of how "additional metadata attributes" such as <code>[Display]</code> and <code>[Description]</code> work.</li> <li>Additional analyzers to ensure <code>[EnumExtensions]</code> is used correctly</li> <li>Bug fixes for edge cases</li></ul> <p>Let's start by looking at the updated metadata attribute support.</p> <h3 id="updated-metadata-attribute-and-enummember-support" class="heading-with-anchor"><a href="https://andrewlock.net/recent-updates-to-netescapaades-enumgenerators/#updated-metadata-attribute-and-enummember-support" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Updated metadata attribute and <code>[EnumMember]</code> support</a></h3> <p>For a long time, you've been able to use <code>[Display]</code> or <code>[Description]</code> attributes applied to <code>enum</code> members to customize how <code>ToStringFast</code> or <code>Parse</code> works with the library. For example, if you have the following <code>enum</code>:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">EnumExtensions</span></span><span class="token punctuation">]</span>
<span class="token keyword">public</span> <span class="token keyword">enum</span> <span class="token class-name">MyEnum</span>
<span class="token punctuation">{</span>
    First<span class="token punctuation">,</span>

    <span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">Display</span><span class="token attribute-arguments"><span class="token punctuation">(</span>Name <span class="token operator">=</span> <span class="token string">"2nd"</span><span class="token punctuation">)</span></span></span><span class="token punctuation">]</span>
    Second<span class="token punctuation">,</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>Then three different <code>ToString</code> methods are generated: Two overloads of <code>ToStringFast()</code> and <code>ToStringFastWithMetadata()</code>:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token keyword">partial</span> <span class="token keyword">class</span> <span class="token class-name">MyEnumExtensions</span>
<span class="token punctuation">{</span>
    <span class="token comment">// Use a boolean to decide whether to use "metadata" attributes</span>
    <span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token return-type class-name"><span class="token keyword">string</span></span> <span class="token function">ToStringFast</span><span class="token punctuation">(</span><span class="token keyword">this</span> <span class="token class-name">MyEnum</span> <span class="token keyword">value</span><span class="token punctuation">,</span> <span class="token class-name"><span class="token keyword">bool</span></span> useMetadataAttributes<span class="token punctuation">)</span>
        <span class="token operator">=&gt;</span> useMetadataAttributes <span class="token punctuation">?</span> <span class="token keyword">value</span><span class="token punctuation">.</span><span class="token function">ToStringFastWithMetadata</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">:</span> <span class="token keyword">value</span><span class="token punctuation">.</span><span class="token function">ToStringFast</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token comment">// Use the raw enum member names</span>
    <span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token return-type class-name"><span class="token keyword">string</span></span> <span class="token function">ToStringFast</span><span class="token punctuation">(</span><span class="token keyword">this</span> <span class="token class-name">MyEnum</span> <span class="token keyword">value</span><span class="token punctuation">)</span>
        <span class="token operator">=&gt;</span> <span class="token keyword">value</span> <span class="token keyword">switch</span>
        <span class="token punctuation">{</span>
            MyEnum<span class="token punctuation">.</span>First <span class="token operator">=&gt;</span> <span class="token keyword">nameof</span><span class="token punctuation">(</span>MyEnum<span class="token punctuation">.</span>First<span class="token punctuation">)</span><span class="token punctuation">,</span>
            MyEnum<span class="token punctuation">.</span>Second <span class="token operator">=&gt;</span> <span class="token keyword">nameof</span><span class="token punctuation">(</span>MyEnum<span class="token punctuation">.</span>Second<span class="token punctuation">)</span><span class="token punctuation">,</span>
            _ <span class="token operator">=&gt;</span> <span class="token keyword">value</span><span class="token punctuation">.</span><span class="token function">ToString</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
        <span class="token punctuation">}</span><span class="token punctuation">;</span>

    <span class="token comment">// Use metadata attributes if provided, and fallback to raw enum member names</span>
    <span class="token keyword">private</span> <span class="token keyword">static</span> <span class="token return-type class-name"><span class="token keyword">string</span></span> <span class="token function">ToStringFastWithMetadata</span><span class="token punctuation">(</span><span class="token keyword">this</span> <span class="token class-name">MyEnum</span> <span class="token keyword">value</span><span class="token punctuation">)</span>
        <span class="token operator">=&gt;</span> <span class="token keyword">value</span> <span class="token keyword">switch</span>
        <span class="token punctuation">{</span>
            MyEnum<span class="token punctuation">.</span>First <span class="token operator">=&gt;</span> <span class="token keyword">nameof</span><span class="token punctuation">(</span>MyEnum<span class="token punctuation">.</span>First<span class="token punctuation">)</span><span class="token punctuation">,</span>
            MyEnum<span class="token punctuation">.</span>Second <span class="token operator">=&gt;</span> <span class="token string">"2nd"</span><span class="token punctuation">,</span> <span class="token comment">// 👈 from the metadata names</span>
            _ <span class="token operator">=&gt;</span> <span class="token keyword">value</span><span class="token punctuation">.</span><span class="token function">ToString</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
        <span class="token punctuation">}</span><span class="token punctuation">;</span>
    <span class="token comment">// ... more generated members</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>The ability to use these additional metadata values can be very useful, and I've used them frequently. For a long time I supported <code>[Display]</code> and <code>[Description]</code> attributes, but <a href="https://github.com/andrewlock/NetEscapades.EnumGenerators/issues/73">there was a request</a> to support <code>[EnumMember]</code> as well.</p> <p>The problem was when you had <em>multiple</em> metadata attributes on enum members—which one should the attribute use? Previously the generator arbitrarily chose <code>[Display]</code> preferentially, and fell back to <code>[Description]</code>. But there was no good reason for that ordering, it was entirely due to one being implemented before the other😬 And adding <code>[EnumMember]</code> as <em>another</em> fallback just felt too nasty.😅</p> <p>So instead, <a href="https://github.com/andrewlock/NetEscapades.EnumGenerators/pull/163">in #163</a>, I added explicit support for <code>[EnumMember]</code> but also updated the code so that you could only use a <em>single</em> metadata attribute source for a given enum. That means only a single <em>type</em> of metadata attribute is considered for a given enum.</p> <p>You can select the source to use by setting the <code>MetadataSource</code> property on the <code>[EnumExtensions]</code> attribute. In the example below, the generated source explicitly opts in to using <code>[Display]</code> attributes:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">EnumExtensions</span><span class="token attribute-arguments"><span class="token punctuation">(</span>MetadataSource <span class="token operator">=</span> MetadataSource<span class="token punctuation">.</span>DisplayAttribute<span class="token punctuation">)</span></span></span><span class="token punctuation">]</span>
<span class="token keyword">public</span> <span class="token keyword">enum</span> <span class="token class-name">EnumWithDisplayNameInNamespace</span>
<span class="token punctuation">{</span>
    First <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">,</span>
    <span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">Display</span><span class="token attribute-arguments"><span class="token punctuation">(</span>Name <span class="token operator">=</span> <span class="token string">"2nd"</span><span class="token punctuation">)</span></span></span><span class="token punctuation">]</span>
    Second <span class="token operator">=</span> <span class="token number">1</span><span class="token punctuation">,</span>
    Third <span class="token operator">=</span> <span class="token number">2</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>Any other metadata attributes (<code>[Description]</code>, <code>[EnumMember]</code>) applied to members in the above <code>enum</code> would be ignored.</p> <p>Alternatively, you can use <code>MetadataSource.None</code> to choose <em>none</em> of the metadata attributes. In this case, the overloads that take a <code>useMetadataAttributes</code> parameter will not be emitted.</p> <blockquote> <p>This was a breaking change on its own, but there was an even bigger change: the <em>default</em> metadata source has been changed to <code>[EnumMember]</code> as a better semantic choice for these attributes.</p> </blockquote> <p>You can change the default metadata source to use for a whole project by setting the <code>EnumGenerator_EnumMetadataSource</code> property in your project:</p> <div class="pre-code-wrapper"><pre class="language-xml"><code class="language-xml"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PropertyGroup</span><span class="token punctuation">&gt;</span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>EnumGenerator_EnumMetadataSource</span><span class="token punctuation">&gt;</span></span>DisplayAttribute<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>EnumGenerator_EnumMetadataSource</span><span class="token punctuation">&gt;</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>PropertyGroup</span><span class="token punctuation">&gt;</span></span>
</code></pre></div> <p>Just to reiterate, <strong>this is a breaking change, that <em>will</em> impact you</strong> if you're currently using metadata attributes. I may add an analyzer to try to warn about this potential issue in a subsequent release, which brings us to the next category: analyzers</p> <h3 id="new-analyzers-to-warn-of-incorrect-usage" class="heading-with-anchor"><a href="https://andrewlock.net/recent-updates-to-netescapaades-enumgenerators/#new-analyzers-to-warn-of-incorrect-usage" class="relative text-zinc-800 dark:text-white no-underline hover:underline">New analyzers to warn of incorrect usage</a></h3> <p>There are several scenarios in which the code generated by the NetEscapades.EnumGenerators package won't compile. These are often edge cases that are tricky to handle in the generator, but which can be very confusing if you hit them in your application.</p> <p>To work around the issue, I added several Roslyn analyzers to explain and warnabout cases that will cause problems.</p> <h4 id="flagging-generated-extension-class-name-clashes" class="heading-with-anchor"><a href="https://andrewlock.net/recent-updates-to-netescapaades-enumgenerators/#flagging-generated-extension-class-name-clashes" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Flagging generated extension class name clashes</a></h4> <p>Currently, you can decorate <code>enum</code>s with <code>[EnumExtension]</code> attributes in such a way that the same extension class name is used in both cases, which causes name clashes. For example, the following generates <code>SomeNamespace.MyEnumExtensions</code> twice, one for each <code>enum</code>:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">namespace</span> <span class="token namespace">SomeNamespace</span><span class="token punctuation">;</span>

<span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">EnumExtensions</span></span><span class="token punctuation">]</span>
<span class="token keyword">public</span> <span class="token keyword">enum</span> <span class="token class-name">MyEnum</span>
<span class="token punctuation">{</span>
    One<span class="token punctuation">,</span>
    Two
<span class="token punctuation">}</span>

<span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">Nested</span>
<span class="token punctuation">{</span>
    <span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">EnumExtensions</span></span><span class="token punctuation">]</span>
    <span class="token keyword">public</span> <span class="token keyword">enum</span> <span class="token class-name">MyEnum</span>
    <span class="token punctuation">{</span>
        One<span class="token punctuation">,</span>
        Two
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div> <p><em>Ideally</em> we would disambiguate by generating <code>SomeNamespace.Nested.MyEnumExtensions</code> as a nested class for the second case, but unfortunately extension method classes <em>can't</em> be nested classes.</p> <p>Another option would be to include the class name in the generated namespace, but then that runs into <em>another</em> issue that can generate clashes. Ultimately, there's always a way to get clashes, especially as you can explicitly set the name of the class to generate!</p> <p>Given that these types of clashes are going to be very rare, <a href="https://github.com/andrewlock/NetEscapades.EnumGenerators/pull/158">#158 added</a> an analyzer, with diagnostic ID <code>NEEG001</code>, which flags the fact there's a clash on the <code>[EnumExtensions]</code> attribute directly as an error diagnostic.</p> <p>This isn't strictly necessary, because generating duplicate extension classes results in a <em>lot</em> of compiler errors, but having an analyzer will hopefully make it more obvious exactly what's happened. 😅</p> <h4 id="handling-enums-nested-in-generic-types" class="heading-with-anchor"><a href="https://andrewlock.net/recent-updates-to-netescapaades-enumgenerators/#handling-enums-nested-in-generic-types" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Handling enums nested in generic types</a></h4> <p>Another case where we simply <em>can't</em> generate valid code is if you have an enum nested inside a generic type:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">using</span> <span class="token namespace">NetEscapades<span class="token punctuation">.</span>EnumGenerators</span><span class="token punctuation">;</span>

<span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">Nested<span class="token punctuation">&lt;</span>T<span class="token punctuation">&gt;</span></span> <span class="token comment">// Type is generic</span>
<span class="token punctuation">{</span>
    <span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">EnumExtensions</span></span><span class="token punctuation">]</span>
    <span class="token keyword">public</span> <span class="token keyword">enum</span> <span class="token class-name">MyEnum</span> <span class="token comment">// Enum is nested inside</span>
    <span class="token punctuation">{</span>
        First<span class="token punctuation">,</span>
        Second<span class="token punctuation">,</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>Unfortunately there's no easy way to generate a valid extension class in this case. We can't put the generated extension class inside <code>Nested&lt;T&gt;</code>, because extension methods can't be inside nested types. There's some things we <em>could</em> do with making the extension class itself generic, but that's all a bit confusing and opens the flood gates to some complexity.</p> <p>Instead, <a href="https://github.com/andrewlock/NetEscapades.EnumGenerators/pull/159">in #159</a> I opted to just not support this scenario. If you write code like the above, no extension method is generated, and instead the <code>NEEG002</code> diagnostic is applied to the <code>[EnumExtensions]</code> attribute to warn you that this isn't valid.</p> <h4 id="duplicate-case-labels-in-an-enum" class="heading-with-anchor"><a href="https://andrewlock.net/recent-updates-to-netescapaades-enumgenerators/#duplicate-case-labels-in-an-enum" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Duplicate case labels in an enum</a></h4> <p>The final analyzer added in this release handles the case where you have "duplicate" enum members, that is, enum members with the same "value" as others. For example in the code below, both <code>Failed</code> and <code>Error</code> have the same value:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">EnumExtensions</span></span><span class="token punctuation">]</span>
<span class="token keyword">public</span> <span class="token keyword">enum</span> <span class="token class-name">Status</span>
<span class="token punctuation">{</span>
    Unknown <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">,</span>
    Pending <span class="token operator">=</span> <span class="token number">1</span><span class="token punctuation">,</span>
    Failed <span class="token operator">=</span> <span class="token number">2</span><span class="token punctuation">,</span>
    Error <span class="token operator">=</span> <span class="token number">2</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>This is perfectly valid, but due to the way the enum generator works with switch expressions, it means you won't always get the value you expect if you call <code>ToStringFast()</code> (or other methods). This isn't an issue with the generator <em>per se</em>, as you see similar behaviour using the built-in <code>ToString()</code> method:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token class-name"><span class="token keyword">var</span></span> status <span class="token operator">=</span> Status<span class="token punctuation">.</span>Error<span class="token punctuation">;</span>
Console<span class="token punctuation">.</span><span class="token function">WriteLine</span><span class="token punctuation">(</span>status<span class="token punctuation">.</span><span class="token function">ToString</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// prints Failed</span>
</code></pre></div> <p>This is just an artifact of how <code>enum</code>s work behind the scenes in .NET, but it can be confusing, so <a href="https://github.com/andrewlock/NetEscapades.EnumGenerators/pull/162">#162</a> adds an analyzer that flags these problematic cases with a diagnostic <code>NEEG003</code>:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">EnumExtensions</span></span><span class="token punctuation">]</span>
<span class="token keyword">public</span> <span class="token keyword">enum</span> <span class="token class-name">Status</span>
<span class="token punctuation">{</span>
    Unknown <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">,</span>
    Pending <span class="token operator">=</span> <span class="token number">1</span><span class="token punctuation">,</span>
    Failed <span class="token operator">=</span> <span class="token number">2</span><span class="token punctuation">,</span>
    Error <span class="token operator">=</span> <span class="token number">2</span><span class="token punctuation">,</span>  <span class="token comment">// NEEG003: Enum has duplicate values and will give inconsistent values for ToStringFast()</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>This diagnostic is just <code>Info</code>, so it won't break your build, as it's still <em>valid</em> to use <code>[EnumExtensions]</code> with these cases, it's just important to be aware that the generated extensions <em>might</em> not work as you expect!</p> <p>That covers all the new analyzers, so finally we'll look at some of the fixes.</p> <h3 id="bug-fixes" class="heading-with-anchor"><a href="https://andrewlock.net/recent-updates-to-netescapaades-enumgenerators/#bug-fixes" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Bug fixes</a></h3> <p>The first fix, introduced <a href="https://github.com/andrewlock/NetEscapades.EnumGenerators/pull/165">in #165</a> and then fixed <em>properly</em> <a href="https://github.com/andrewlock/NetEscapades.EnumGenerators/pull/172">in #172</a> was to better handle the cases where users have set their project's <code>LangVersion</code> to <code>Preview</code>.</p> <p>In a previous release of NetEscapades.EnumGenerators <a href="https://andrewlock.net/exploring-dotnet-10-preview-features-3-csharp-14-extensions-members/#a-case-study-netescapades-enumgenerators">I added support for C#14 Extension Members</a>. This lets you call static extension members as though they're defined on the type itself. For example, lets say you have this <code>enum</code>:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">EnumExtensions</span></span><span class="token punctuation">]</span>
<span class="token keyword">public</span> <span class="token keyword">enum</span> <span class="token class-name">MyColours</span>
<span class="token punctuation">{</span>
    Red<span class="token punctuation">,</span>
    Green<span class="token punctuation">,</span>
    Blue<span class="token punctuation">,</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>The source generator generates a <code>MyColoursExtensions.Parse()</code> method, but with extension members, you can call it as though it's defined on the <code>MyColours</code> enum itself:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token class-name"><span class="token keyword">var</span></span> colour <span class="token operator">=</span> MyColours<span class="token punctuation">.</span><span class="token function">Parse</span><span class="token punctuation">(</span><span class="token string">"Red"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre></div> <p>I intended to only enable this when you're using C#14, but I made a mistake. I enabled it when you're using C#14 <em>or</em> when you've set the <code>LangVersion=Preview</code>. Long story short, <code>Preview</code> can mean practically anything depending on what you're targeting and what version of the SDK you're building with, so this was not a good idea 😅</p> <p>As a fix, I removed the generation of extension members unless you're explicitly targeting C#14 or higher (<em>ignoring</em> the <code>Preview</code> case). To allow opt-in to extension members when you're using <code>Preview</code>, I added a <code>EnumGenerator_ForceExtensionMembers</code> setting that you can set to <code>true</code> to explicitly opt-in when you wouldn't normally. Unfortunately I accidentally initially <em>defaulted</em> this to <code>true</code>, so <a href="https://github.com/andrewlock/NetEscapades.EnumGenerators/pull/172">#172</a> fixes this to be <code>false</code> by default instead 🙈</p> <p>The main other fix was for handling the case where <a href="https://github.com/andrewlock/NetEscapades.EnumGenerators/issues/166">enum member names are reserved words</a>, e.g.</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">EnumExtensions</span></span><span class="token punctuation">]</span>
<span class="token keyword">public</span> <span class="token keyword">enum</span> <span class="token class-name">AttributeFieldType</span>
<span class="token punctuation">{</span>
    number<span class="token punctuation">,</span>
    @<span class="token keyword">string</span><span class="token punctuation">,</span> <span class="token comment">// reserved, so escaped with @</span>
    date
<span class="token punctuation">}</span>
</code></pre></div> <p>Unfortunately, I wasn't handling this correctly, so the generator was generating invalid code:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token return-type class-name"><span class="token keyword">string</span></span> <span class="token function">ToStringFast</span><span class="token punctuation">(</span><span class="token keyword">this</span> <span class="token class-name">AttributeFieldType</span> <span class="token keyword">value</span><span class="token punctuation">)</span>
    <span class="token operator">=&gt;</span> <span class="token keyword">value</span> <span class="token keyword">switch</span>
    <span class="token punctuation">{</span>
        <span class="token keyword">global</span><span class="token punctuation">::</span>AttributeFieldType<span class="token punctuation">.</span>number <span class="token operator">=&gt;</span> <span class="token string">"number"</span><span class="token punctuation">,</span>
        <span class="token keyword">global</span><span class="token punctuation">::</span>AttributeFieldType<span class="token punctuation">.</span><span class="token keyword">string</span> <span class="token operator">=&gt;</span> <span class="token string">"string"</span><span class="token punctuation">,</span> <span class="token comment">// ❌ Does not compile</span>
        <span class="token keyword">global</span><span class="token punctuation">::</span>AttributeFieldType<span class="token punctuation">.</span>date <span class="token operator">=&gt;</span> <span class="token string">"date"</span><span class="token punctuation">,</span>
        _ <span class="token operator">=&gt;</span> <span class="token keyword">value</span><span class="token punctuation">.</span><span class="token function">AsUnderlyingType</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">ToString</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
    <span class="token punctuation">}</span><span class="token punctuation">;</span>
</code></pre></div> <p>The fix involved updating the generator with this handy function, to make sure we correctly escape the identifiers as necessary:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">private</span> <span class="token keyword">static</span> <span class="token return-type class-name"><span class="token keyword">string</span></span> <span class="token function">EscapeIdentifier</span><span class="token punctuation">(</span><span class="token class-name"><span class="token keyword">string</span></span> identifier<span class="token punctuation">)</span>
<span class="token punctuation">{</span>
    <span class="token keyword">return</span> SyntaxFacts<span class="token punctuation">.</span><span class="token function">GetKeywordKind</span><span class="token punctuation">(</span>identifier<span class="token punctuation">)</span> <span class="token operator">!=</span> SyntaxKind<span class="token punctuation">.</span>None
        <span class="token punctuation">?</span> <span class="token string">"@"</span> <span class="token operator">+</span> identifier
        <span class="token punctuation">:</span> identifier<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>So that the generated code is escaped correctly:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token return-type class-name"><span class="token keyword">string</span></span> <span class="token function">ToStringFast</span><span class="token punctuation">(</span><span class="token keyword">this</span> <span class="token class-name">AttributeFieldType</span> <span class="token keyword">value</span><span class="token punctuation">)</span>
    <span class="token operator">=&gt;</span> <span class="token keyword">value</span> <span class="token keyword">switch</span>
    <span class="token punctuation">{</span>
        <span class="token keyword">global</span><span class="token punctuation">::</span>AttributeFieldType<span class="token punctuation">.</span>number <span class="token operator">=&gt;</span> <span class="token string">"number"</span><span class="token punctuation">,</span>
        <span class="token keyword">global</span><span class="token punctuation">::</span>AttributeFieldType<span class="token punctuation">.</span>@<span class="token keyword">string</span> <span class="token operator">=&gt;</span> <span class="token string">"string"</span><span class="token punctuation">,</span> <span class="token comment">// ✅ Correctly escaped</span>
        <span class="token keyword">global</span><span class="token punctuation">::</span>AttributeFieldType<span class="token punctuation">.</span>date <span class="token operator">=&gt;</span> <span class="token string">"date"</span><span class="token punctuation">,</span>
        _ <span class="token operator">=&gt;</span> <span class="token keyword">value</span><span class="token punctuation">.</span><span class="token function">AsUnderlyingType</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">ToString</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
    <span class="token punctuation">}</span><span class="token punctuation">;</span>
</code></pre></div> <p>The final change was to remove the <code>NETESCAPADES_ENUMGENERATORS_EMBED_ATTRIBUTES</code> option in <a href="https://github.com/andrewlock/NetEscapades.EnumGenerators/pull/160">#160</a> which removes the ability to embed the marker attributes in the target dll. This is rarely the right thing to do, and the package is already doing the work to ship the attribute in a dedicated dll. This also reduces some of the duplication, removes a config combination to need to test, and opens up the ability to ship "helper" types in the "attributes" dll in the future.</p> <h2 id="summary" class="heading-with-anchor"><a href="https://andrewlock.net/recent-updates-to-netescapaades-enumgenerators/#summary" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Summary</a></h2> <p>In this post I walked through some of the recent updates to <a href="https://github.com/andrewlock/NetEscapades.EnumGenerators">NetEscapades.EnumGenerators</a> shipped in version 1.0.0-beta16. These quality of life updates add support for <code>[EnumMember]</code>, updates how metadata attributes are used, and adds additional analyzers to catch potential pitfalls. Finally it fixes a few edge-case bugs. If you haven't already, I recommend updating and giving it a try! If you run into any problems, please do <a href="https://github.com/andrewlock/NetEscapades.EnumGenerators/issues">log an issue on GitHub</a>.🙂</p> ]]></content:encoded><category><![CDATA[.NET Core;Roslyn;Source Generators]]></category></item><item><title><![CDATA[Exploring the .NET boot process via host tracing]]></title><description><![CDATA[In this post we enable host tracing and use that to understand how a .NET app boots up via the dotnet muxer, hostfxr, and hostpolicy.dll]]></description><link>https://andrewlock.net/exploring-the-dotnet-boot-process-via-host-tracing/</link><guid isPermaLink="true">https://andrewlock.net/exploring-the-dotnet-boot-process-via-host-tracing/</guid><pubDate>Tue, 25 Nov 2025 10:00:00 GMT</pubDate><dc:creator><![CDATA[Andrew Lock]]></dc:creator><media:content url="https://andrewlock.net/content/images/2025/hostfxr_banner.png" medium="image" /><content:encoded><![CDATA[<img src="https://andrewlock.net/content/images/2025/hostfxr_banner.png" /><p>In this post we take a look a look at how you can enable diagnostics for the .NET host itself that you can use to debug issues running your .NET applications. We then use the tracing diagnostics to explore the boot process of a simple .NET application.</p> <h2 id="understanding-the-boot-process-with-tracing" class="heading-with-anchor"><a href="https://andrewlock.net/exploring-the-dotnet-boot-process-via-host-tracing/#understanding-the-boot-process-with-tracing" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Understanding the boot process with tracing</a></h2> <p>The main focus of this post is to show the <a href="https://github.com/dotnet/runtime/blob/25cae043b11fa5e4fbda011376a7ad403438bd62/docs/design/features/host-tracing.md">the <em>host tracing</em> feature</a> available in modern .NET. This isn't "tracing" like OpenTelemetry or APM solutions with activities and spans, this is <em>old school</em> tracing, i.e. logging.😄</p> <p>Host tracing provides you detailed diagnostic information about the very early steps of a .NET application's "boot" process. This can be useful if you're trying to understand why your application is using the "wrong" version of .NET, for example. You won't need it often, but it can be invaluable when things aren't working the way you expect!</p> <p>In this post I'm going to explore the startup process for a simple .NET app by looking at the host tracing output. It's going to be intentionally verbose, but it will give you an idea of what's available.</p> <p>Enabling host tracing requires setting a single environment variable: <code>COREHOST_TRACE=1</code>. By default this writes the traces to <code>stderr</code>, but you can redirect that output to a file by setting <code>COREHOST_TRACEFILE</code> to one of two values:</p> <ul><li><code>COREHOST_TRACEFILE=&lt;file_path&gt;</code> appends the logs to the file <code>&lt;path&gt;</code>. The file is created if it doesn't already exist, but the <em>directory</em> it's in must exist. Relative paths are relative to the working directory.</li> <li><code>COREHOST_TRACEFILE=&lt;dir_path&gt;</code> (.NET 10+ only), if the directory <code>&lt;dir_path&gt;</code> exists, The file <code>&lt;exe_name&gt;.&lt;pid&gt;.log</code> is appended to.</li></ul> <p>You can also control the verbosity of the logs by setting <code>COREHOST_TRACE_VERBOSITY=&lt;level&gt;</code> where <code>&lt;level&gt;</code> is a value from <code>1</code> to <code>4</code>, <code>4</code> being the most verbose, and <code>1</code> being only errors.</p> <p>To test it out, I created a simple console app, built it, and ran it with tracing enabled:</p> <div class="pre-code-wrapper"><pre class="language-powershell"><code class="language-powershell">dotnet new console
dotnet build

<span class="token comment"># Enable tracing</span>
<span class="token variable">$env</span>:COREHOST_TRACE=1
<span class="token variable">$env</span>:COREHOST_TRACEFILE=<span class="token string">"host_trace.log"</span>
dotnet bin\Debug\net9<span class="token punctuation">.</span>0\MyApp<span class="token punctuation">.</span>dll
</code></pre></div> <p>With that in mind, let's explore the boot process of a .NET app.</p> <h2 id="loading-applications-with-modern-net" class="heading-with-anchor"><a href="https://andrewlock.net/exploring-the-dotnet-boot-process-via-host-tracing/#loading-applications-with-modern-net" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Loading applications with modern .NET</a></h2> <p>When I think about modern .NET applications, I often think of three main divisions:</p> <ul><li>The .NET <strong>runtime</strong>, the CoreCLR, which is running the JIT compiler, the garbage collector, and everything that make up a .NET application.</li> <li>The .NET <strong>base class libraries (BCL)</strong>, which are all the libraries shipped as part of .NET.</li> <li>Your .NET <strong>application</strong>, which is the code written by you, which may reference other .NET libraries, as well as libraries that make up the BCL.</li></ul> <p>However, there's also a whole "loading" process that has to happen to get the .NET runtime running!</p> <p>At a high level, when you run a .NET application using <code>dotnet myapp.dll</code>, your app goes through the following chain of components:</p> <ul><li>The <code>dotnet</code> app is a "multiplexer" (muxer) application that decides what you're trying to run.</li> <li><code>hostfxr</code> is a native library responsible for finding the correct .NET runtime to load.</li> <li><code>hostpolicy</code> is a native library responsible for <em>starting</em> the correct .NET runtime.</li></ul> <p>I explore each of these components in a little more detail in this post, but for a deeper dive (on the first two at least), I recommend <a href="https://www.stevejgordon.co.uk/a-brief-introduction-to-the-dotnet-muxer">Steve Gordon's posts looking at the internals</a>.</p> <h3 id="the-dotnet-muxer" class="heading-with-anchor"><a href="https://andrewlock.net/exploring-the-dotnet-boot-process-via-host-tracing/#the-dotnet-muxer" class="relative text-zinc-800 dark:text-white no-underline hover:underline">The <code>dotnet</code> muxer</a></h3> <p>The <code>dotnet</code> muxer is the entrypoint for most of the work you do as a .NET developer. Whether you're doing development with <code>dotnet build</code> and <code>dotnet publish</code>, or actually running an application using <code>dotnet MyApp.dll</code>, the <code>dotnet</code> muxer is your entrypoint.</p> <p>On Windows, the <code>dotnet</code> muxer is the executable that's installed by default at <em>C:\Program Files\dotnet\dotnet.exe</em>. There's a single entrypoint here, even if you have multiple versions of the .NET runtime or .NET SDK installed on your machine.</p> <blockquote> <p>When you install a new version of the SDK or runtime, you'll typically get a new version of the muxer, but there's still only one.</p> </blockquote> <p>The muxer is really just responsible for one thing: loading the <code>hostfxr</code> library and invoking it. That said, it still does a <em>bit</em> of preliminary validation. Calling <code>dotnet</code> without any arguments doesn't make any sense, so if you simply run <code>dotnet.exe</code>, <a href="https://github.com/dotnet/runtime/blob/f169b52556bc4769b4260b7a85c05c6f78911097/src/native/corehost/corehost.cpp#L187">the muxer itself</a> prints some basic usage information:</p> <div class="pre-code-wrapper"><pre class="language-log"><code class="language-log">Usage: dotnet [path-to-application]
Usage: dotnet [commands]

path-to-application:
  The path to an application .dll file to execute.

commands:
  -h|--help                         Display help.
  --info                            Display .NET information.
  --list-runtimes [--arch &lt;arch&gt;]   Display the installed runtimes matching the host or specified architecture. Example architectures: arm64, x64, x86.
  --list-sdks [--arch &lt;arch&gt;]       Display the installed SDKs matching the host or specified architecture. Example architectures: arm64, x64, x86.
</code></pre></div> <p>The next step is for the muxer to try to find and load the <em>.NET Host Framework Resolver</em> (<code>hostfxr</code>). This searches a subfolder <em>host\fxr</em> next to the <code>dotnet</code> executable, and reads all the folder versions listed there. If, like me, you have lots of runtimes installed, you'll have lots of entries:</p> <p><img src="https://andrewlock.net/content/images/2025/hostfxr.png" alt="The host/fxr folder"></p> <p>The muxer reads all these folders, does a SemVer comparison, and selects the highest one. Inside the folder you'll find the <code>hostfxr</code> library (<code>hostfxr.dll</code> on Windows, <code>libhostfxr.dylib</code> on mac, and <code>libhostfxr.so</code> on Linux). The muxer loads the <code>hostfxr</code> library into the process.</p> <blockquote> <p>Steve Gordon walks through the code the muxer uses to do this search and loading in <a href="https://www.stevejgordon.co.uk/how-dotnet-muxer-resolves-and-loads-the-hostfxr-library">his post on the hostfxr library</a> if you want to see the details!</p> </blockquote> <p>Once the muxer has loaded <code>hostfxr</code>, it resolves <a href="https://github.com/dotnet/runtime/blob/main/docs/design/features/hosting-layer-apis.md#net-core-21">the <code>hostfxr_main_startupinfo</code> function</a> and invokes it.</p> <p>Now, if we take a look at the tracing logs, we can see this all playing out:</p> <div class="pre-code-wrapper"><pre class="language-ps"><code class="language-ps">Tracing enabled @ Thu Oct 23 18:33:26 2025 GMT
--- Invoked dotnet [version: 10.0.0-rc.2.25502.107 @Commit: 89c8f6a112d37d2ea8b77821e56d170a1bccdc5a] main = {
C:\Program Files\dotnet\dotnet.exe
bin\Debug\net9.0\myapp.dll
}

.NET root search location options: 0
Reading fx resolver directory=[C:\Program Files\dotnet\host\fxr]
Considering fxr version=[10.0.0-rc.2.25502.107]...
Considering fxr version=[2.1.30]...
Considering fxr version=[3.1.32]...
Considering fxr version=[5.0.17]...
Considering fxr version=[6.0.36]...
Considering fxr version=[7.0.20]...
Considering fxr version=[9.0.10]...
Considering fxr version=[9.0.6]...
Detected latest fxr version=[C:\Program Files\dotnet\host\fxr\10.0.0-rc.2.25502.107]...

Resolved fxr [C:\Program Files\dotnet\host\fxr\10.0.0-rc.2.25502.107\hostfxr.dll]...
Loaded library from C:\Program Files\dotnet\host\fxr\10.0.0-rc.2.25502.107\hostfxr.dll

Invoking fx resolver [C:\Program Files\dotnet\host\fxr\10.0.0-rc.2.25502.107\hostfxr.dll] hostfxr_main_startupinfo
Host path: [C:\Program Files\dotnet\dotnet.exe]
Dotnet path: [C:\Program Files\dotnet\]
App path: [C:\Program Files\dotnet\dotnet.dll]
</code></pre></div> <p>These logs clearly show the muxer searching the <em>host\fxr</em> directory, finding the highest version, loading the <code>hostfxr.dll</code> library, and invoking the <code>hostfxr_main_startupinfo</code> function.</p> <blockquote> <p>There's a variation on the "muxer" as the standard entrypoint, which is the "apphost" model. When you publish your .NET application, you typically also get an executable produced next to your app's dll, e.g. <code>MyApp.exe</code> as well as <code>MyApp.dll</code>. This executable is essentially a modified version of the <code>dotnet</code> muxer, with various tweaks. I'm not going to look into the apphost in this post, just know that it exists!</p> </blockquote> <p>We've loaded the <code>hostfxr</code> library, so it's time to see what that does.</p> <h3 id="the-hostfxr-library" class="heading-with-anchor"><a href="https://andrewlock.net/exploring-the-dotnet-boot-process-via-host-tracing/#the-hostfxr-library" class="relative text-zinc-800 dark:text-white no-underline hover:underline">The <code>hostfxr</code> library</a></h3> <p>The <code>hostfxr</code> library has several responsibilities:</p> <ul><li>Parse the provided arguments to decide what to execute; is this a .NET SDK command like <code>dotnet build</code> and <code>dotnet publish</code>, or is it an app execution like <code>dotnet MyApp.dll</code>.</li> <li>If it's an SDK command, find the correct SDK to use.</li> <li>Decide which version of the .NET runtime to load.</li> <li>Load the <code>hostpolicy</code> library for the selected runtime.</li></ul> <p>We'll look at how each of those steps shows up in the tracing logs below.</p> <h4 id="parse-the-arguments-and-decide-behaviour" class="heading-with-anchor"><a href="https://andrewlock.net/exploring-the-dotnet-boot-process-via-host-tracing/#parse-the-arguments-and-decide-behaviour" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Parse the arguments and decide behaviour</a></h4> <p>The first step is <em>conceptually</em> part of the muxer in that it's about deciding the intention of the caller. Are they trying to execute SDK commands, or are they trying to execute an application? It's easiest to see this playing out in the tracing logs if we run an SDK command like <code>dotnet --info</code>:</p> <div class="pre-code-wrapper"><pre class="language-ps"><code class="language-ps">--- Executing in muxer mode...
Using the provided arguments to determine the application to execute.
Application '--info' is not a managed executable.
--- Resolving .NET SDK with working dir [D:\repos\temp\MyApp]
</code></pre></div> <p>In the above logs, you can see that <code>hostfxr</code> has established that <code>--info</code> is <em>not</em> an app to run, so it redirects to the .NET SDK. On the other hand, if we had run our app using <code>dotnet myapp.dll</code> we'd see something like this instead:</p> <div class="pre-code-wrapper"><pre class="language-ps"><code class="language-ps">--- Executing in muxer mode...
Using the provided arguments to determine the application to execute.
Using dotnet root path [C:\Program Files\dotnet\]
App runtimeconfig.json from [D:\repos\temp\myapp\bin\Debug\net9.0\myapp.dll]
</code></pre></div> <p>We'll come back to the application case in a second, for now we'll stick to the SDK scenario:</p> <h4 id="finding-the-sdk" class="heading-with-anchor"><a href="https://andrewlock.net/exploring-the-dotnet-boot-process-via-host-tracing/#finding-the-sdk" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Finding the SDK</a></h4> <p>Once <code>hostfxr</code> has decided that an SDK command was executed the next step is to work out <em>which</em> .NET SDK to load by reading any <em>global.json</em> files in the path:</p> <div class="pre-code-wrapper"><pre class="language-ps"><code class="language-ps">--- Resolving .NET SDK with working dir [D:\repos\temp\MyApp]
Probing path [D:\repos\temp\MyApp\global.json] for global.json
Probing path [D:\repos\temp\global.json] for global.json
Probing path [D:\repos\global.json] for global.json
Found global.json [D:\repos\global.json]

--- Resolving SDK information from global.json [D:\repos\global.json]
Value 'sdk/version' is missing or null in [D:\repos\global.json]
Value 'sdk/rollForward' is missing or null in [D:\repos\global.json]
Resolving SDKs with version = 'latest', rollForward = 'latestMajor', allowPrerelease = false
</code></pre></div> <p>In these logs we can see that <code>hostfxr</code> found a <em>global.json</em> folder in a parent path and parsed the rules for loading an SDK. Now it can search for the available SDKs and pick the one to run:</p> <div class="pre-code-wrapper"><pre class="language-ps"><code class="language-ps">Searching for SDK versions in [C:\Program Files\dotnet\sdk]
Ignoring version [10.0.100-preview.6.25358.103] because it does not match the roll-forward policy
Ignoring version [10.0.100-rc.2.25502.107] because it does not match the roll-forward policy
Version [9.0.301] is a better match than [none]
Version [9.0.306] is a better match than [9.0.301]
SDK path resolved to [C:\Program Files\dotnet\sdk\9.0.306]
Using .NET SDK dll=[C:\Program Files\dotnet\sdk\9.0.306\dotnet.dll]

Using the provided arguments to determine the application to execute.
Using dotnet root path [C:\Program Files\dotnet\]
App runtimeconfig.json from [C:\Program Files\dotnet\sdk\9.0.306\dotnet.dll]
</code></pre></div> <p>As you can see, it's resolved to the <code>9.0.306</code> version of the SDK and is executing the <code>dotnet.dll</code> SDK application. It's also interesting to see the final three logs, starting with <code>"Using the provided arguments"</code>—they're essentially the <em>same</em> logs we saw when we ran <code>dotnet myapp.dll</code>. The only difference is that in this case, the .NET app we're running is <code>dotnet.dll</code>, the .NET SDK.</p> <p>We'll switch back to the console app again now, and continue with the load process.</p> <h4 id="choosing-a-net-runtime-to-load" class="heading-with-anchor"><a href="https://andrewlock.net/exploring-the-dotnet-boot-process-via-host-tracing/#choosing-a-net-runtime-to-load" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Choosing a .NET runtime to load</a></h4> <p>At this point <code>hostfxr</code> knows which .NET <em>app</em> to load but it doesn't know which .NET <em>runtime</em> to load. It determines this by inspecting the <em>runtimeconfig.json</em> of the app. This file lives alongside the app and includes, among other things, the version of the runtime to use:</p> <div class="pre-code-wrapper"><pre class="language-json"><code class="language-json"><span class="token punctuation">{</span>
  <span class="token property">"runtimeOptions"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
    <span class="token property">"tfm"</span><span class="token operator">:</span> <span class="token string">"net9.0"</span><span class="token punctuation">,</span>
    <span class="token property">"framework"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
      <span class="token property">"name"</span><span class="token operator">:</span> <span class="token string">"Microsoft.NETCore.App"</span><span class="token punctuation">,</span>
      <span class="token property">"version"</span><span class="token operator">:</span> <span class="token string">"9.0.0"</span>
    <span class="token punctuation">}</span><span class="token punctuation">,</span>
    <span class="token property">"configProperties"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
      <span class="token property">"System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization"</span><span class="token operator">:</span> <span class="token boolean">false</span>
    <span class="token punctuation">}</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>If we check the tracing logs, we can see <code>hostfxr</code> probes for and finds this file, and reads the specified <code>framework</code> details:</p> <div class="pre-code-wrapper"><pre class="language-ps"><code class="language-ps">Using the provided arguments to determine the application to execute.
Using dotnet root path [C:\Program Files\dotnet\]
App runtimeconfig.json from [D:\repos\temp\myapp\bin\Debug\net9.0\myapp.dll]

Runtime config is cfg=D:\repos\temp\myapp\bin\Debug\net9.0\myapp.runtimeconfig.json dev=D:\repos\temp\myapp\bin\Debug\net9.0\myapp.runtimeconfig.dev.json
Attempting to read dev runtime config: D:\repos\temp\myapp\bin\Debug\net9.0\myapp.runtimeconfig.dev.json
Attempting to read runtime config: D:\repos\temp\myapp\bin\Debug\net9.0\myapp.runtimeconfig.json
Runtime config [D:\repos\temp\myapp\bin\Debug\net9.0\myapp.runtimeconfig.json] is valid=[1]

--- The specified framework 'Microsoft.NETCore.App', version '9.0.0', apply_patches=1, version_compatibility_range=minor is compatible with the previously referenced version '9.0.0'.
</code></pre></div> <p>With the requested version established, <code>hostfxr</code> sets about searching for which versions of the runtime are available by looking in <em>C:\Program Files\dotnet\shared\Microsoft.NETCore.App</em>. It applies whatever <a href="https://learn.microsoft.com/en-us/dotnet/core/versions/selection#control-roll-forward-behavior">roll forward policies</a> are configured for the app (<code>Minor</code> unless otherwise specified) and chooses the best match:</p> <div class="pre-code-wrapper"><pre class="language-ps"><code class="language-ps">--- Resolving FX directory, name 'Microsoft.NETCore.App' version '9.0.0'
Searching FX directory in [C:\Program Files\dotnet]
Attempting FX roll forward starting from version='[9.0.0]', apply_patches=1, version_compatibility_range=minor, roll_to_highest_version=0, prefer_release=1

'Roll forward' enabled with version_compatibility_range [minor]. Looking for the lowest release greater than or equal version to [9.0.0]
Found version [9.0.6]

Applying patch roll forward from [9.0.6] on release only
Inspecting version... [10.0.0-rc.2.25502.107]
Inspecting version... [2.1.30]
Inspecting version... [3.1.32]
Inspecting version... [5.0.17]
Inspecting version... [6.0.36]
Inspecting version... [7.0.20]
Inspecting version... [8.0.17]
Inspecting version... [8.0.21]
Inspecting version... [9.0.10]
Inspecting version... [9.0.6]
Changing Selected FX version from [] to [C:\Program Files\dotnet\shared\Microsoft.NETCore.App\9.0.10]

Chose FX version [C:\Program Files\dotnet\shared\Microsoft.NETCore.App\9.0.10]
</code></pre></div> <p>As you can see above, <code>hostfxr</code> found that <code>9.0.10</code> was the best version match for the app. If it <em>couldn't</em> find a match for some reason, you'd see a message something like this:</p> <div class="pre-code-wrapper"><pre class="language-ps"><code class="language-ps">No match greater than or equal to [10.0.0] found.
Framework reference didn't resolve to any available version.
It was not possible to find any compatible framework version
You must install or update .NET to run this application.
</code></pre></div> <p>Once a valid runtime version is found, <code>hostfxr</code> attempts to load the <em>runtimeconfig.json</em> for the <em>runtime</em>. This indicates if any other runtimes need to be resolved.</p> <blockquote> <p>The runtime is actually a shared "framework", called <code>Microsoft.NETCore.App</code>. Frameworks can reference <em>other</em> frameworks, for example the <code>Microsoft.AspNetCore.App</code> and <code>Microsoft.WindowsDesktop.App</code> "frameworks" can reference the <code>Microsoft.NETCore.App</code> framework. You can also create your own frameworks if you want! Everything is resolved recursively at this point.</p> </blockquote> <div class="pre-code-wrapper"><pre class="language-ps"><code class="language-ps">Runtime config is cfg=C:\Program Files\dotnet\shared\Microsoft.NETCore.App\9.0.10\Microsoft.NETCore.App.runtimeconfig.json dev=C:\Program Files\dotnet\shared\Microsoft.NETCore.App\9.0.10\Microsoft.NETCore.App.runtimeconfig.dev.json

Attempting to read dev runtime config: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\9.0.10\Microsoft.NETCore.App.runtimeconfig.dev.json
Attempting to read runtime config: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\9.0.10\Microsoft.NETCore.App.runtimeconfig.json
Runtime config [C:\Program Files\dotnet\shared\Microsoft.NETCore.App\9.0.10\Microsoft.NETCore.App.runtimeconfig.json] is valid=[1]

--- Summary of all frameworks:
     framework:'Microsoft.NETCore.App', lowest requested version='9.0.0', found version='9.0.10', effective reference version='9.0.0' apply_patches=1, version_compatibility_range=minor, roll_to_highest_version=0, folder=C:\Program Files\dotnet\shared\Microsoft.NETCore.App\9.0.10

Executing as a framework-dependent app as per config file [D:\repos\temp\myapp\bin\Debug\net9.0\myapp.runtimeconfig.json]
</code></pre></div> <p>Once all the frameworks are loaded (just the <code>Microsoft.NETCore.App</code> runtime in this case) we move onto the final responsibility of <code>hostfxr</code>, loading <code>hostpolicy</code>.</p> <h4 id="loading-hostpolicy" class="heading-with-anchor"><a href="https://andrewlock.net/exploring-the-dotnet-boot-process-via-host-tracing/#loading-hostpolicy" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Loading <code>hostpolicy</code></a></h4> <p>Once the .NET runtime is resolved, <code>hostfxr</code> needs to load the <code>hostpolicy</code> library for the specific chosen version of the runtime. It does this by reading the <em>deps.json</em> file of the chosen runtime and looking for a library called something like <code>runtime.win-x64.Microsoft.NETCore.DotNetHostPolicy</code>. If it doesn't find that entry (it didn't in the example below) then it just looks for it in the root framework folder:</p> <div class="pre-code-wrapper"><pre class="language-ps"><code class="language-ps">--- Resolving hostpolicy.dll version from deps json [C:\Program Files\dotnet\shared\Microsoft.NETCore.App\9.0.10\Microsoft.NETCore.App.deps.json]
Dependency manifest C:\Program Files\dotnet\shared\Microsoft.NETCore.App\9.0.10\Microsoft.NETCore.App.deps.json does not contain an entry for runtime.win-x64.Microsoft.NETCore.DotNetHostPolicy

The expected hostpolicy.dll directory is [C:\Program Files\dotnet\shared\Microsoft.NETCore.App\9.0.10]
Loaded library from C:\Program Files\dotnet\shared\Microsoft.NETCore.App\9.0.10\hostpolicy.dll
</code></pre></div> <p>And as you can see from the final line, <code>hostfxr</code> found <code>hostpolicy.dll</code> and loaded it, so it's time to look at the <code>hostpolicy</code> behaviour.</p> <h3 id="the-hostpolicy-library" class="heading-with-anchor"><a href="https://andrewlock.net/exploring-the-dotnet-boot-process-via-host-tracing/#the-hostpolicy-library" class="relative text-zinc-800 dark:text-white no-underline hover:underline">The <code>hostpolicy</code> library</a></h3> <p>The main responsibilities of <code>hostpolicy</code> are:</p> <ul><li>Building the Trusted Platform Assemblies list based on the application and framework <em>deps.json</em>.</li> <li>Setting up the context switches to run the application.</li> <li>Launching the .NET runtime to run your application.</li></ul> <h4 id="building-the-trusted-platform-assemblies-list" class="heading-with-anchor"><a href="https://andrewlock.net/exploring-the-dotnet-boot-process-via-host-tracing/#building-the-trusted-platform-assemblies-list" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Building the Trusted Platform Assemblies list</a></h4> <p>After printing a few logs that I'm going to skip over for the purposes of this post, we start to get a <em>lot</em> of logs printed. I truncate them to just a few entries below, just enough to give a taste of what's going on:</p> <div class="pre-code-wrapper"><pre class="language-ps"><code class="language-ps">Loading deps file... [C:\Program Files\dotnet\shared\Microsoft.NETCore.App\9.0.10\Microsoft.NETCore.App.deps.json]: is_framework_dependent=0, use_fallback_graph=0

Processing package Microsoft.NETCore.App.Runtime.win-x64/9.0.10
  Adding runtime assets
    System.Private.CoreLib.dll assemblyVersion=9.0.0.0 fileVersion=9.0.1025.47515
    Microsoft.VisualBasic.dll assemblyVersion=10.0.0.0 fileVersion=9.0.1025.47515
    Microsoft.Win32.Primitives.dll assemblyVersion=9.0.0.0 fileVersion=9.0.1025.47515
    mscorlib.dll assemblyVersion=4.0.0.0 fileVersion=9.0.1025.47515
    netstandard.dll assemblyVersion=2.1.0.0 fileVersion=9.0.1025.47515
    System.AppContext.dll assemblyVersion=9.0.0.0 fileVersion=9.0.1025.47515
    System.Buffers.dll assemblyVersion=9.0.0.0 fileVersion=9.0.1025.47515
    System.ComponentModel.DataAnnotations.dll assemblyVersion=4.0.0.0 fileVersion=9.0.1025.47515
# ...

  Adding native assets
    clrjit.dll assemblyVersion= fileVersion=9.0.1025.47515
    coreclr.dll assemblyVersion= fileVersion=9.0.1025.47515
    createdump.exe assemblyVersion= fileVersion=9.0.1025.47515
    System.IO.Compression.Native.dll assemblyVersion= fileVersion=9.0.1025.47515
# ...

Reconciling library Microsoft.NETCore.App.Runtime.win-x64/9.0.10
  package: Microsoft.NETCore.App.Runtime.win-x64, version: 9.0.10
  Adding runtime assets
    Entry 0 for asset name: System.Private.CoreLib, relpath: System.Private.CoreLib.dll, assemblyVersion 9.0.0.0, fileVersion 9.0.1025.47515
    Entry 1 for asset name: Microsoft.VisualBasic, relpath: Microsoft.VisualBasic.dll, assemblyVersion 10.0.0.0, fileVersion 9.0.1025.47515
    Entry 2 for asset name: Microsoft.Win32.Primitives, relpath: Microsoft.Win32.Primitives.dll, assemblyVersion 9.0.0.0, fileVersion 9.0.1025.47515
    Entry 3 for asset name: mscorlib, relpath: mscorlib.dll, assemblyVersion 4.0.0.0, fileVersion 9.0.1025.47515
# ...

  Adding native assets
    Entry 0 for asset name: clretwrc, relpath: clretwrc.dll, assemblyVersion , fileVersion 9.0.1025.47515
    Entry 1 for asset name: clrgc, relpath: clrgc.dll, assemblyVersion , fileVersion 9.0.1025.47515
    Entry 2 for asset name: clrgcexp, relpath: clrgcexp.dll, assemblyVersion , fileVersion 9.0.1025.47515
#...
</code></pre></div> <p>In the above logs, <code>hostpolicy</code> has read the <em>deps.json</em> file for the chosen runtime, and is loading all the libraries it lists. You can see these files all listed if you open the <em>deps.json</em> file yourself, for example:</p> <div class="pre-code-wrapper"><pre class="language-json"><code class="language-json"><span class="token punctuation">{</span>
  <span class="token property">"runtimeTarget"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
    <span class="token property">"name"</span><span class="token operator">:</span> <span class="token string">".NETCoreApp,Version=v9.0/win-x64"</span><span class="token punctuation">,</span>
    <span class="token property">"signature"</span><span class="token operator">:</span> <span class="token string">""</span>
  <span class="token punctuation">}</span><span class="token punctuation">,</span>
  <span class="token property">"compilationOptions"</span><span class="token operator">:</span> <span class="token punctuation">{</span><span class="token punctuation">}</span><span class="token punctuation">,</span>
  <span class="token property">"targets"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
    <span class="token property">".NETCoreApp,Version=v9.0"</span><span class="token operator">:</span> <span class="token punctuation">{</span><span class="token punctuation">}</span><span class="token punctuation">,</span>
    <span class="token property">".NETCoreApp,Version=v9.0/win-x64"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
      <span class="token property">"Microsoft.NETCore.App.Runtime.win-x64/9.0.10"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
        <span class="token property">"runtime"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
          <span class="token property">"System.Private.CoreLib.dll"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
            <span class="token property">"assemblyVersion"</span><span class="token operator">:</span> <span class="token string">"9.0.0.0"</span><span class="token punctuation">,</span>
            <span class="token property">"fileVersion"</span><span class="token operator">:</span> <span class="token string">"9.0.1025.47515"</span>
          <span class="token punctuation">}</span><span class="token punctuation">,</span>
          <span class="token property">"Microsoft.VisualBasic.dll"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
            <span class="token property">"assemblyVersion"</span><span class="token operator">:</span> <span class="token string">"10.0.0.0"</span><span class="token punctuation">,</span>
            <span class="token property">"fileVersion"</span><span class="token operator">:</span> <span class="token string">"9.0.1025.47515"</span>
          <span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token comment">//...          </span>
</code></pre></div> <p>After processing the framework <em>deps.json</em> file, <code>hostpolicy</code> moves onto your <em>apps</em> deps.json file, which is likely much simpler. In the simple console app case it will only contain a reference to the app dll itself:</p> <div class="pre-code-wrapper"><pre class="language-ps"><code class="language-ps">Processing package myapp/1.0.0
  Adding runtime assets
    myapp.dll assemblyVersion= fileVersion=

Reconciling library myapp/1.0.0
  project: myapp, version: 1.0.0
  Adding runtime assets
    Entry 0 for asset name: myapp, relpath: myapp.dll, assemblyVersion , fileVersion 
</code></pre></div> <p>With the list of assets created, <code>hostpolicy</code> sets about building up the Trusted Platform Assemblies (TPA) list. As per <a href="https://github.com/dotnet/runtime/blob/1e09fc169a2c4d0c54c483967b845b03d11215d5/docs/project/glossary.md">this glossary</a>:</p> <blockquote> <p>Trusted Platform Assemblies used to be a special set of assemblies that comprised the platform assemblies, when it was originally designed. As of today, it is simply the set of assemblies known to constitute the application.</p> </blockquote> <p>So <code>hostpolicy</code> simply walks through all those assemblies it discovered, and adds them to the TPA:</p> <div class="pre-code-wrapper"><pre class="language-ps"><code class="language-ps">-- Probe configurations:
  probe type=app
  probe type=framework dir=[C:\Program Files\dotnet\shared\Microsoft.NETCore.App\9.0.10] fx_level=1

Adding tpa entry: D:\repos\temp\myapp\bin\Debug\net9.0\myapp.dll, AssemblyVersion: , FileVersion: 

Processing TPA for deps entry [myapp, 1.0.0, myapp.dll] with fx level: 0
  Using probe config: type=app
    Local path query D:\repos\temp\myapp\bin\Debug\net9.0\myapp.dll (skipped file existence check)
    Probed deps dir and matched 'D:\repos\temp\myapp\bin\Debug\net9.0\myapp.dll'

Processing TPA for deps entry [Microsoft.NETCore.App.Runtime.win-x64, 9.0.10, System.Private.CoreLib.dll] with fx level: 1
  Using probe config: type=app
    Skipping... not app asset
  Using probe config: type=framework dir=[C:\Program Files\dotnet\shared\Microsoft.NETCore.App\9.0.10] fx_level=1
    Local path query C:\Program Files\dotnet\shared\Microsoft.NETCore.App\9.0.10\System.Private.CoreLib.dll (skipped file existence check)
    Probed deps json and matched 'C:\Program Files\dotnet\shared\Microsoft.NETCore.App\9.0.10\System.Private.CoreLib.dll'

Adding tpa entry: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\9.0.10\System.Private.CoreLib.dll, AssemblyVersion: 9.0.0.0, FileVersion: 9.0.1025.47515
#...
</code></pre></div> <p>That goes on for another 1000 lines, even in a basic console app, so we'll skip ahead 😅</p> <h4 id="creating-the-context-switches" class="heading-with-anchor"><a href="https://andrewlock.net/exploring-the-dotnet-boot-process-via-host-tracing/#creating-the-context-switches" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Creating the context switches</a></h4> <p>The next lines written by <code>hostpolicy</code> in the trace log are:</p> <div class="pre-code-wrapper"><pre class="language-ps"><code class="language-ps">Property FX_DEPS_FILE = C:\Program Files\dotnet\shared\Microsoft.NETCore.App\9.0.10\Microsoft.NETCore.App.deps.json
Property TRUSTED_PLATFORM_ASSEMBLIES = C:\Program Files\dotnet\shared\Microsoft.NETCore.App\9.0.10\System.Security.Cryptography.X509Certificates.dll;C:\Program Files\dotnet\shared\Microsoft.NETCore.App\9.0.10\Microsoft.CSharp.dll; # TRUNCATED!
Property NATIVE_DLL_SEARCH_DIRECTORIES = C:\Program Files\dotnet\shared\Microsoft.NETCore.App\9.0.10\;
Property PLATFORM_RESOURCE_ROOTS = 
Property APP_CONTEXT_BASE_DIRECTORY = D:\repos\temp\myapp\bin\Debug\net9.0\
Property APP_CONTEXT_DEPS_FILES = D:\repos\temp\myapp\bin\Debug\net9.0\myapp.deps.json;C:\Program Files\dotnet\shared\Microsoft.NETCore.App\9.0.10\Microsoft.NETCore.App.deps.json
Property PROBING_DIRECTORIES = 
Property RUNTIME_IDENTIFIER = win-x64
Property System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization = false
Property HOST_RUNTIME_CONTRACT = 0x1cd836084d8
</code></pre></div> <p>This shows the context properties which will be passed to the runtime when it's loaded. As you can see, it's primarily a set of configuration values loaded from the the environment, containing various details about paths to files used to initialize the runtime. It also contains the <code>configProperties</code> from the app's <em>runtimeconfig.json</em>, such as the <code>EnableUnsafeBinaryFormatterSerialization</code> setting.</p> <blockquote> <p>Note that I truncated the <code>TRUSTED_PLATFORM_ASSEMBLIES</code> property as it's a list of paths to <em>all</em> the assemblies in the TPA</p> </blockquote> <p>And <em>finally</em> <code>hostpolicy</code> loads the <code>coreclr.dll</code> .NET runtime and launches it!</p> <div class="pre-code-wrapper"><pre class="language-ps"><code class="language-ps">CoreCLR path = 'C:\Program Files\dotnet\shared\Microsoft.NETCore.App\9.0.10\coreclr.dll', CoreCLR dir = 'C:\Program Files\dotnet\shared\Microsoft.NETCore.App\9.0.10\'
Loaded library from C:\Program Files\dotnet\shared\Microsoft.NETCore.App\9.0.10\coreclr.dll

Launch host: C:\Program Files\dotnet\dotnet.exe, app: D:\repos\temp\myapp\bin\Debug\net9.0\myapp.dll, argc: 0, args: 
</code></pre></div> <p>And there we have it, from muxer, to <code>hostfxr</code>, to <code>hostpolicy.dll</code> to <code>coreclr.dll</code> and a running app! If you're running into difficulties early in the NET app booting process, then consider enabling tracing to see exactly what's going on.</p> <h2 id="summary" class="heading-with-anchor"><a href="https://andrewlock.net/exploring-the-dotnet-boot-process-via-host-tracing/#summary" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Summary</a></h2> <p>In this post I showed how you can enable host tracing by setting <code>COREHOST_TRACE=1</code> and setting <code>COREHOST_TRACEFILE</code> to a file path. I then ran a very simple app and explored the host tracing logs it produces. We then saw how the dotnet muxer is the entrypoint for the app, which locates and loads <code>hostfxr</code>. <code>hostfxr</code> is then responsible for finding the correct .NET runtime to load and for loading <code>hostpolicy.dll</code>. Finally <code>hostpolicy.dll</code> boots the .NET runtime and runs your application.</p> ]]></content:encoded><category><![CDATA[.NET Core;.NET 10;.NET CLI]]></category></item><item><title><![CDATA[Companies complaining .NET moves too fast should just pay for post-EOL support]]></title><description><![CDATA[In this post I describe a solution to .NET "releasing too quickly": just pay for support of older versions, such as HeroDevs' Never Ending Support for .NET 6]]></description><link>https://andrewlock.net/companies-using-dotnet-need-to-suck-it-up-and-pay-for-support/</link><guid isPermaLink="true">https://andrewlock.net/companies-using-dotnet-need-to-suck-it-up-and-pay-for-support/</guid><pubDate>Tue, 18 Nov 2025 10:00:00 GMT</pubDate><dc:creator><![CDATA[Andrew Lock]]></dc:creator><media:content url="https://andrewlock.net/content/images/2025/herodevs.png" medium="image" /><content:encoded><![CDATA[<img src="https://andrewlock.net/content/images/2025/herodevs.png" /><p>In this post I make a (potentially) spicy assertion: if you're worried about the speed that .NET is moving, and more specifically the fact that Microsoft only gives 2 or 3 years of support to new versions of .NET, <em>maybe</em> the answer isn't that .NET should slow down .NET. <em>Maybe</em> it isn't even that Microsoft should shoulder the burden of supporting more versions of .NET. <em>Maybe</em> the answer is that you should just <strong>pay for post-EOL support</strong> like other ecosystems do.</p> <blockquote> <p>This post is sponsored by <a href="https://www.herodevs.com/">HeroDevs</a>, and is the result of chatting with <a href="https://andrewlock.net/@unixterminal.bsky.social">Hayden Barnes</a> in the fallout on my post about <a href="https://andrewlock.net/understanding-the-worst-dotnet-vulnerability-request-smuggling-and-cve-2025-55315/">the worst .NET vulnerability ever</a>. That said, all the opinions expressed in this post are entirely my own—I just think HeroDevs' Never Ending Support for .NET could be a great option for many companies.</p> </blockquote> <p>In this post I describe the official support for .NET provided by Microsoft and what "support" actually means. I discuss the advantages and disadvantages of updating to a new major version and then provide an alternative: pay for EOL support instead. Finally I show how you can easily fix your EOL .NET 6 applications by using HeroDevs' Never Ending Support for .NET 6, demonstrating how it protects you from <a href="https://andrewlock.net/understanding-the-worst-dotnet-vulnerability-request-smuggling-and-cve-2025-55315/">the recent 9.9 severity CVE</a>.</p> <h2 id="-net-support-lifecycles-and-managing-vulnerabilities" class="heading-with-anchor"><a href="https://andrewlock.net/companies-using-dotnet-need-to-suck-it-up-and-pay-for-support/#-net-support-lifecycles-and-managing-vulnerabilities" class="relative text-zinc-800 dark:text-white no-underline hover:underline">.NET, support lifecycles, and managing vulnerabilities</a></h2> <p>.NET 10 has just been released, and while some people are excited to see the performance improvements and to use the new features, others will no doubt be worrying about the inevitable march of time before the <em>current</em> version of .NET they're using is out of support. A new version of .NET is released every November, and is either a Long Term Support (LTS) release or Standard Term Support (STS) release:</p> <ul><li>Odd number releases are Standard Term Support (STS), and receive 2 years of support from Microsoft.</li> <li>Even number releases are Long Term Support (LTS), and receive 3 years of support from Microsoft.</li></ul> <p>The following image shows how this works (adapted from the official <a href="https://dotnet.microsoft.com/en-us/platform/support/policy/dotnet-core">.NET support policy page</a>)</p> <p><img src="https://andrewlock.net/content/images/2025/dotnet-release-schedule.svg" alt="Illustration showing .NET 9 as an STS release that happened in November 2024 and all other releases happening in November, alternating between LTS and STS and .NET 10 as the latest"></p> <p>There's no difference in the quality bar between LTS and STS releases, the only difference is how long the release is supported by Microsoft until it becomes End of Life (EOL). But what do "supported" and "EOL" even mean? 🤔</p> <h3 id="what-does-supported-mean-" class="heading-with-anchor"><a href="https://andrewlock.net/companies-using-dotnet-need-to-suck-it-up-and-pay-for-support/#what-does-supported-mean-" class="relative text-zinc-800 dark:text-white no-underline hover:underline">What does "supported" mean?</a></h3> <p>Most people have an intrinsic feel for what "supported" means and it's typically something like "if there's a bug, it'll get fixed". But Microsoft is very specific about <em>exactly</em> what supported means in <a href="https://dotnet.microsoft.com/en-us/platform/support/policy/dotnet-core">their policy</a>. There are two "phases" to support for both LTS and STS releases:</p> <ul><li><strong>Active support</strong>: During the active support period, .NET releases are updated to improve functional capabilities and mitigate security vulnerabilities.</li> <li><strong>Maintenance support</strong>: During the maintenance support period, .NET releases are updated to mitigate security vulnerabilities, only. The maintenance support period is the final 6 months of support for any release.</li></ul> <p>So for the majority of the support timeline, you can expect to see fixes for security vulnerabilities, and "updates to improve functional capabilities". What counts as an "improvement to functional capability" is actually <a href="https://dotnet.microsoft.com/en-us/platform/support/policy/dotnet-core">pretty broad</a>:</p> <ul><li>Resolve reported crashes.</li> <li>Resolve severe performance issues.</li> <li>Resolve functional bugs in common scenarios.</li> <li>Add support for a new operating system version or new hardware platform.</li></ul> <blockquote> <p>Given how the first three points are all about fixing relatively severe issues, I was somewhat surprised to see "supporting a whole new OS version" on the list, but that somewhat makes sense. For example, if Debian releases a new version, it's likely the .NET team are going to want to make sure the latest released version of .NET works on it!</p> </blockquote> <p>6 months before a release goes EOL and stops receiving official support entirely, it enters "maintenance", in which <em>only</em> security vulnerabilities will be addressed:</p> <p><img src="https://andrewlock.net/content/images/2025/dotnet-release-schedule-maintenance.png" alt="Illustration showing that the last 6 months of a release is &quot;maintenance&quot; mode"></p> <p>Whatever your issue, you should be able to <a href="https://support.microsoft.com/supportforbusiness/productselection/?sapid=4fd4947b-15ea-ce01-080f-97f2ca3c76e8">contact a Microsoft Support Professional</a> to ask for support. This is particularly appealing to larger companies that like to have a single person they can shout at if things aren't working, and typically have an ongoing relationship with Microsoft anyway. Of course, you can also interact with the various teams on GitHub in <a href="http://github.com/dotnet">the dotnet org</a> too.</p> <p>If you do file an issue on GitHub, and assuming that you provide sufficient information that the issue can be reproduced, then I've found the team to be very receptive to <a href="https://github.com/dotnet/runtime/issues/112565">resolving issues</a>:</p> <p><img src="https://andrewlock.net/content/images/2025/herodevs_github.png" alt="Resolving an issue in the .NET runtime"></p> <p>Where you might struggle is where your issue is considered minor. If you find a security issue, or a common crashing bug, then sure, it'll probably be fixed in all supported versions of .NET. But if your issue is rare, or if it's risky (because it could have adverse impacts, for example) then you <em>might</em> have a harder time getting the fix implemented in all versions of .NET, <em>even if they're technically still supported</em>. This is particularly true in the last 6 months of support, when the release enters "maintenance" support. In this stage, you'll likely only get security vulnerability fixes.</p> <p>Another aspect to consider is that whenever you raise an issue, you'll be asked what version of .NET you're using. Which brings us to the next very important point: <em>only</em> the latest patch version of a .NET major version is supported.</p> <h3 id="you-patch-every-month-right-" class="heading-with-anchor"><a href="https://andrewlock.net/companies-using-dotnet-need-to-suck-it-up-and-pay-for-support/#you-patch-every-month-right-" class="relative text-zinc-800 dark:text-white no-underline hover:underline">You patch every month, right?</a></h3> <p>.NET 10 was released on 11th November, but in a month's time I'm sure there will be patches released for the runtime, for ASP.NET Core, for the .NET SDK, and for a variety of other packages that make up the base .NET ecosystem as provided by Microsoft.</p> <p>Even if you're using a .NET release which <em>is</em> actively supported, you're <em>only supported if you're using the latest patched version</em>. To make this concrete, the latest released versions of .NET 9 at the time of writing are:</p> <ul><li>.NET 9 SDK <code>9.0.307</code></li> <li>.NET Runtime <code>9.0.11</code></li></ul> <p>If you have an application today that's using .NET 9, and you're <em>not</em> running on the latest .NET 9 patch version (<code>9.0.11</code>), then you're using <em>an unsupported version of .NET</em>. And next month there will likely be a <code>9.0.12</code> version, which will supplant <code>9.0.11</code>!</p> <blockquote> <p>Only the <em>latest</em> patch versions of all the .NET components are supported. As of today, that means the latest patches for .NET 8, .NET 9, and .NET 10 respectively.</p> </blockquote> <p>Putting that all together, if you want to stick to using only Microsoft supported versions of .NET, that means that <em>every month</em>, you need to, at a minimum:</p> <ul><li>Update the .NET SDKs used to build .NET applications.</li> <li>Update the .NET runtime used to deploy and run your applications.</li></ul> <p>If you <em>can't</em> update these versions every month, then you won't be using a Microsoft supported version of .NET.</p> <p>The good news is that patch updates for .NET are <em>typically</em> very easy to apply. They rarely contain breaking issues or require significant effort. But eventually there won't be any more patch versions, the version of .NET you're using will go out of support, and <em>then</em> what?</p> <h3 id="running-an-unsupported-version-of-net-is-playing-with-fire" class="heading-with-anchor"><a href="https://andrewlock.net/companies-using-dotnet-need-to-suck-it-up-and-pay-for-support/#running-an-unsupported-version-of-net-is-playing-with-fire" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Running an unsupported version of .NET is playing with fire</a></h3> <p>Fundamentally, nothing happens from a <em>technical</em> point of view when a .NET version goes out of official support. Everything keeps working, just as it did before. Nevertheless, depending on your regulatory environment, you may find you're no longer compliant, with all the potential associated financial or legal implications.</p> <p>Even if you're not in a regulated environment, you still have the potential for a big problem to hit at any point: when you (or someone else) discovers an issue in your out-of-support version of .NET.</p> <p>This is the exact scenario many people found themselves in recently, when <a href="https://github.com/dotnet/aspnetcore/issues/64033">the request-smuggling security vulnerability CVE-2025-55315</a> was announced. This vulnerability is Very Bad™️ (a 9.9 severity), because it allows attackers to potentially bypass security controls, exfiltrate data, login as other users, perform injection attacks and more, all by relying on <a href="https://en.wikipedia.org/wiki/HTTP_request_smuggling">request smuggling</a>. Hopefully, you get the picture…Not Good.</p> <p><img src="https://andrewlock.net/content/images/2025/request_smuggling_05.svg" alt="A request smuggling attack exploiting differences between a proxy and server implementation"></p> <blockquote> <p>I <a href="https://andrewlock.net/understanding-the-worst-dotnet-vulnerability-request-smuggling-and-cve-2025-55315/">wrote an extensive blog post looking at this vulnerability</a>, explaining how request smuggling works in general, walking through the specific vulnerability in CVE-2025-55315, describing how to test if you're vulnerable and what to do to protect yourself.</p> </blockquote> <p>Patches were released for the vulnerability for .NET 8, .NET 9, and .NET 10, as these were in support. But basically <em>all</em> versions of .NET are vulnerable; <em>at least</em> .NET Core 3.1. NET 5, .NET 6, and .NET 7.</p> <p>All of which leaves those organisations running an unsupported version of .NET in a sticky situation. If you keep running an unsupported version of .NET, then your application has a known 9.9 severity vulnerability. The alternative is to perform a <em>major</em> version update of your applications to use a supported version of .NET, but that might also be easier said than done…</p> <blockquote> <p>If you're using .NET 6, then there's another option, <a href="https://www.herodevs.com/support/dot-net-nes">HeroDevs</a>, which we're going to come to shortly! 😅</p> </blockquote> <h2 id="the-difficulties-of-a-major-version-update" class="heading-with-anchor"><a href="https://andrewlock.net/companies-using-dotnet-need-to-suck-it-up-and-pay-for-support/#the-difficulties-of-a-major-version-update" class="relative text-zinc-800 dark:text-white no-underline hover:underline">The difficulties of a major version update</a></h2> <p>Many developers look forward to new major versions of .NET. A new major version of .NET typically means:</p> <ul><li><strong>Performance improvements</strong>. Incredibly, each new version of .NET is faster than the previous version.</li> <li><strong>New features</strong>. A major version typically means new features. Whether that's incremental improvements or entirely new features, most releases touch all parts of the .NET stack.</li> <li><strong>Support for new platforms</strong>. Some major releases include support for new operating systems and CPU architectures.</li></ul> <p>But a major upgrade isn't always for the feint of heart. We've come a long way from the huge shifts between .NET Core 1.0, .NET Core 2.0, and .NET Core 3.1, but there are still many potential risks associated with performing major version updates, particularly when viewed from an <em>organisation</em>'s point of view. For example:</p> <ul><li><strong>Breaking changes</strong>. Major version updates inevitably come with a variety <a href="https://learn.microsoft.com/en-us/dotnet/core/compatibility/10.0">of breaking changes</a>. How severely you're affected by these issues will depend on your specific application, but you should always review this list to establish which, if any, would impact you, and plan work to mitigate them as part of an upgrade.</li> <li><strong>Behavioural changes</strong>. Even if the changes in an update aren't considered <em>breaking</em>, they may still result in a difference in <em>behaviour</em>, which could cause your application to behave in a way you don't expect. Often the only way to identify these issues (if they're not marked as breaking changes) is to test your updated application extensively.</li> <li><strong>Tooling upgrades</strong>. Building a new version of .NET may require that you update various tooling, whether that's the SDK you use to build your application, the continuous integration (CI) runners you use to build your app, or the machines you deploy your application to. This work often takes a large proportion of the overall migration time, and is commonly overlooked.</li> <li><strong>Regulatory compliance</strong>. Depending on your environment, you may need to re-certify all the applications that you update to a new major version, to ensure they're compliant with any regulations. That can cost time and money.</li> <li><strong>Internal support</strong>. If you have a "platform" team which supports other teams, then there's a degree of experience and learning developers need to undertake to <a href="https://andrewlock.net/series/exploring-the-dotnet-10-preview/">learn about what's available</a>.</li> <li><strong>Opportunity cost</strong>. Time spent migrating to newer versions of .NET is time spent not implementing new features or addressing customer needs.</li></ul> <p>Updating to a new major version clearly has a number of advantages, but for some organisations and applications, the benefits of performance improvements and new features simply aren't enough to offset the risk and costs associated with performing a migration.</p> <p>But those <em>same</em> organizations <em>also</em> can't afford to be stuck on unsupported versions that will cause them regulatory problems, or be at risk of handling unpatched CVEs <a href="https://andrewlock.net/understanding-the-worst-dotnet-vulnerability-request-smuggling-and-cve-2025-55315/">like CVE-2025-55315</a>. So what's a poor .NET-using org to do?</p> <h2 id="why-not-just-pay-for-support-" class="heading-with-anchor"><a href="https://andrewlock.net/companies-using-dotnet-need-to-suck-it-up-and-pay-for-support/#why-not-just-pay-for-support-" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Why not just pay for support?</a></h2> <p>And now we get to my pitch: <strong>delay the major-version update if it's too painful, and just pay for support for old .NET versions</strong>.</p> <p>As I discussed in the previous section, organizations often don't <em>want</em> to do major version updates, due to the difficulties described previously. Organisations make these major updates because they <em>have</em> to, to ensure they can address any security issues that appear, and to ensure they're compliant with their regulatory requirements.</p> <blockquote> <p>A prime example is an application which is no longer being actively developed. The costs associated with performing a major version update in this case likely outweigh the benefits, especially if this triggers any sort of compliance or audit requirements. But leaving the app unsupported is also not an option.</p> </blockquote> <p>The fact that organisations don't <em>want</em> to update to new major versions of .NET sometimes manifests as pleas to Microsoft for them to extend the support windows for .NET. Many organisations yearn for the decades-long support of Windows or .NET Framework, and ask "why can't modern .NET be like that".</p> <p>But here's the thing, even with a <em>decade</em> of support, organizations don't want to upgrade! Windows 10 was no longer supported as of October 2025 (10 years after its release), and yet <a href="https://gs.statcounter.com/windows-version-market-share/desktop/worldwide">it still accounts for 40% of active Windows versions</a>! Organisations <em>hate</em> change…</p> <p>Of course, Windows 10 ended <em>standard</em> support in October 2025, but <a href="https://www.microsoft.com/en-gb/windows/extended-security-updates?r=1">you can still <em>pay</em> for extended support</a> for Windows 10 and receive security updates. So it seems like the simple answer is "if you don't want to update, just pay for support".</p> <h3 id="post-eol-support-in-other-ecosystems" class="heading-with-anchor"><a href="https://andrewlock.net/companies-using-dotnet-need-to-suck-it-up-and-pay-for-support/#post-eol-support-in-other-ecosystems" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Post-EOL support in other ecosystems</a></h3> <p>This model of paying for support after a product's official support has ended is also common in other ecosystems. Take Java for example. Multiple vendors produce Open JDK distributions, and provide substantial support durations for them, but you often need to pay for that:</p> <table><thead><tr><th>Vendor</th><th>Supported JDKs</th><th>Support Duration</th></tr></thead><tbody><tr><td>Red Hat)</td><td>8, 11, 17, 21 (<a href="https://access.redhat.com/articles/1299013">varies by RHEL release</a>)</td><td>Typically up to 8+ years (with RHEL subscription)</td></tr><tr><td>Microsoft)</td><td>11, 17, 21 (<a href="https://learn.microsoft.com/en-us/java/openjdk/support#release-and-servicing-roadmap">based on Microsoft support policy</a>)</td><td>Typically 6+ years</td></tr><tr><td>Ubuntu</td><td>8, 11, 17, 21 (<a href="https://ubuntu.com/toolchains/java">varies by Ubuntu release</a>)</td><td>12 years (with extended support)</td></tr></tbody></table> <p>Similarly, the <a href="https://spring.io/projects/spring-framework">Java Spring and Spring Boot frameworks</a> provide free support for releases initially, and then have paid options through VMware Tanzu, <a href="https://spring.io/projects/spring-framework#support">as shown on their support page</a>:</p> <p><img src="https://andrewlock.net/content/images/2025/spring.png" alt="The Spring support matrix from https://spring.io/projects/spring-framework#support "></p> <p>However there are also a whole load of third-party companies that will happily provide paid support for Spring Framework and Spring Boot even beyond these timelines (<a href="https://www.herodevs.com/support/spring-nes">including HeroDevs</a>!)</p> <p>And it's not like the willingness to pay for support after a product is officially EOL is restricted to the Java ecosystem. You can find EOL support for all <em>sorts</em> of front-end frameworks like Angular, AngularJS, Vue, heck, even <em>Bootstrap</em>.</p> <p><img src="https://andrewlock.net/content/images/2025/herodevs_frontend.png" alt="An example of the front-end frameworks supported by HeroDevs"></p> <p>So why not .NET too?</p> <h3 id="post-eol-support-in-net" class="heading-with-anchor"><a href="https://andrewlock.net/companies-using-dotnet-need-to-suck-it-up-and-pay-for-support/#post-eol-support-in-net" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Post-EOL support in .NET</a></h3> <p>So why does it seem like some companies are so hesitant to pay for post-EOL support for .NET? I would speculate that part of the reason is because <em>Microsoft</em> has typically been willing to provide <em>very</em> long support windows in general. Take .NET Framework for example; .NET Framework 3.5 is <em>still</em> supported—it's not EOL until 2029!</p> <p>I suspect another major reason is that some .NET organisations simply don't know that paying for post-EOL support is an option! as far as I can tell, Microsoft doesn't explicitly endorse or even <em>mention</em> that this is available for the ecosystem, nor do Microsoft-adjacent bodies like <a href="https://dotnetfoundation.org/">the .NET Foundation</a>. I really don't know why they wouldn't, it seems like a win-win scenario to me. 🤷</p> <p>So consider this my public service announcement:</p> <blockquote> <p>If you're still running applications on .NET 6, or if you're eyeing .NET 8's end of support next year with concern, don't worry. Switch to a post-EOL support build of .NET 6 (or .NET 8 next year), and you'll receive security fixes without needing to do go through a costly major version update.</p> </blockquote> <p>This is the pitch <a href="https://www.herodevs.com/support/dot-net-nes">HeroDevs make for their .NET support</a>:</p> <ul><li><strong>Security Fixes</strong>. A new version of NES for .NET will be released each time HeroDevs find, validate, and fix a security issue.</li> <li><strong>Drop-In Compatibility</strong>. A direct replacement for your framework—no migrations, no rewrites, just ongoing support.</li> <li><strong>SLA Compliance</strong>. HeroDevs <a href="https://docs.herodevs.com/legal/service-level-agreement">provides SLAs</a> that ensure compliance by providing incident response and remediation in accordance with industry-standard regulations, including SOC 2, FedRAMP, PCI, and HIPAA.</li></ul> <p>Of course, like me, you'll probably still have questions:</p> <ul><li>"Just <em>how</em> easy is to swap to a post-EOL supported version?"</li> <li>"Will I get fixes for security issues found in <em>other</em> versions of .NET?"</li> <li>"How much is this going to cost?"</li></ul> <p>The final point is going to vary depending on your requirements, so you'll need to <a href="https://www.herodevs.com/pricing">dig into that yourself</a>. But I wanted to get a feel for the technical side of the first two questions, so I gave HeroDevs' <a href="https://www.herodevs.com/support/dot-net-nes">Never Ending Support (NES) for .NET 6</a> a try!</p> <h2 id="trying-out-herodevs-never-ending-support-for-net" class="heading-with-anchor"><a href="https://andrewlock.net/companies-using-dotnet-need-to-suck-it-up-and-pay-for-support/#trying-out-herodevs-never-ending-support-for-net" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Trying out HeroDevs' Never Ending Support for .NET</a></h2> <p>HeroDevs provide a drop-in replacement for out-of-support .NET 6 versions as part of their <a href="https://www.herodevs.com/support/dot-net-nes">Never-Ending Support (NES) for .NET</a>. They provide both Linux and Windows binaries, so you can easily patch your existing applications, no matter how you deploy.</p> <p>I wanted to test out the NES binaries, and I thought the best way to show the benefits would be to test the binaries against the recent <a href="https://github.com/dotnet/aspnetcore/issues/64033">CVE-2025-55315</a> vulnerability. My plan was:</p> <ul><li>Create a simple docker image using the latest official .NET 6 version.</li> <li>Demonstrate the presence of the <a href="https://github.com/dotnet/aspnetcore/issues/64033">CVE-2025-55315</a> vulnerability.</li> <li>Update the docker image to use HeroDevs' NES version of .NET 6.</li> <li>Show that the vulnerability no longer exists.</li></ul> <p>Luckily, as I described in <a href="https://andrewlock.net/understanding-the-worst-dotnet-vulnerability-request-smuggling-and-cve-2025-55315/">my post about the CVE-2025-55315 vulnerability</a>, we can test for the presence of the vulnerability in .NET using <a href="https://github.com/sirredbeard/CVE-2025-55315-repro">this GitHub repo from Hayden Barnes</a>. The app in this repo tests for the CVE-2025-55315 by sending a malicious payload to an ASP.NET Core app. It then prints the result of the test to the output. For example, if we run the test against an up-to-date .NET 8.0 release, we get the following results:</p> <p><img src="https://andrewlock.net/content/images/2025/herodevs_repro.png" alt="Shows the result of the repro testing for the CVE-2025-55315 against .NET 8, and includes 2/2 tests passed"></p> <p>At the bottom of the output you can see the <code>2/2 tests passed</code>, showing that the latest .NET 8 release (8.0.22) is <em>not</em> vulnerable to CVE-2025-55315. Now we'll try it against .NET 6.</p> <h3 id="demonstrating-that-net-6-is-vulnerable-to-cve-2025-55315" class="heading-with-anchor"><a href="https://andrewlock.net/companies-using-dotnet-need-to-suck-it-up-and-pay-for-support/#demonstrating-that-net-6-is-vulnerable-to-cve-2025-55315" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Demonstrating that .NET 6 is vulnerable to CVE-2025-55315</a></h3> <p>The dockerfile below is how I tested against .NET 6. It uses the latest .NET 10 SDK to build the app from the reproduction GitHub repo, and then copies the published app into the latest <a href="https://github.com/dotnet/dotnet-docker/blob/main/README.aspnet.md">official .NET 6 docker image</a>.</p> <div class="pre-code-wrapper"><pre class="language-dockerfile"><code class="language-dockerfile"><span class="token comment"># ---------------Builder image---------------------- #</span>
<span class="token instruction"><span class="token keyword">FROM</span> mcr.microsoft.com/dotnet/sdk:10.0 <span class="token keyword">AS</span> builder</span>

<span class="token comment"># Clone the reproduction repo</span>
<span class="token instruction"><span class="token keyword">RUN</span> git clone https://github.com/sirredbeard/CVE-2025-55315-repro /app</span>

<span class="token comment"># Publish the app for .NET 6 to the /app/publish folder</span>
<span class="token instruction"><span class="token keyword">RUN</span> dotnet publish /app/Repro/Repro.csproj -c Release --framework net6.0 -o /app/publish</span>

<span class="token comment"># ---------------Final image---------------------- #</span>
<span class="token comment"># Use the latest official .NET 6 image</span>
<span class="token instruction"><span class="token keyword">FROM</span> mcr.microsoft.com/dotnet/aspnet:6.0</span>

<span class="token comment"># Copy the published app from the builder image</span>
<span class="token instruction"><span class="token keyword">WORKDIR</span> /app</span>
<span class="token instruction"><span class="token keyword">COPY</span> <span class="token options"><span class="token property">--from</span><span class="token punctuation">=</span><span class="token string">builder</span></span> /app/publish .</span>

<span class="token comment"># Run the app</span>
<span class="token instruction"><span class="token keyword">ENTRYPOINT</span> [<span class="token string">"dotnet"</span>, <span class="token string">"Repro.dll"</span>]</span>
</code></pre></div> <blockquote> <p>Note that I <em>built</em> the app with .NET 10, but you can always use a more recent version of the .NET SDK to build applications that target <em>older</em> versions of .NET. In many ways this is preferable to using an old .NET SDK, as you benefit from compiler improvements and the ability to use newer versions of C#.</p> </blockquote> <p>To test the above docker image, copy it into a file called <code>Dockerfile</code> and build and test it using the following:</p> <div class="pre-code-wrapper"><pre class="language-bash"><code class="language-bash"><span class="token function">docker</span> build <span class="token parameter variable">-t</span> andrewlock/repro-test:official-6.0 <span class="token builtin class-name">.</span>
<span class="token function">docker</span> run <span class="token parameter variable">--rm</span> <span class="token parameter variable">-it</span> andrewlock/repro-test:official-6.0
</code></pre></div> <p>This runs the same vulnerability reproduction tests as we ran for .NET 8, but this time the tests fail, and we can see that <a href="https://dotnet.microsoft.com/en-us/download/dotnet/6.0">the latest .NET 6 release, version 6.0.36,</a> <em>is</em> vulnerable to CVE-2025-55315, as shown by the <code>0/2 tests passed</code> in the logs:</p> <p><img src="https://andrewlock.net/content/images/2025/herodevs_repro_6.png" alt="=== Runtime Version Information === .NET Runtime: 6.0.36 Runtime Directory: /usr/share/dotnet/shared/Microsoft.NETCore.App/6.0.36/ warn: Microsoft.AspNetCore.Server.Kestrel       Overriding address(es) 'http://+:80'. Binding to endpoints defined via IConfiguration and/or UseKestrel() instead. info: Microsoft.Hosting.Lifetime[14]       Now listening on: http://localhost:5000 info: Microsoft.Hosting.Lifetime       Application started. Press Ctrl+C to shut down. Kestrel.Core Version: 6.0.36+64ea4108e7dcf1ca575f8dd2028363b0b1ef6ebc info: Microsoft.Hosting.Lifetime       Hosting environment: Production info: Microsoft.Hosting.Lifetime       Content root path: /app ASP.NET Core Version: 6.0.36+64ea4108e7dcf1ca575f8dd2028363b0b1ef6ebc =================================== Server started on http://localhost:5000 info: Microsoft.AspNetCore.Hosting.Diagnostics[1]       Request starting HTTP/1.1 GET http:/// - - info: Program       Bad chunk extension NOT detected. info: Microsoft.AspNetCore.Hosting.Diagnostics[2]       Request finished HTTP/1.1 GET http:/// - - - 408 - - 10915.5124ms Test 1 FAILED info: Microsoft.AspNetCore.Hosting.Diagnostics[1]       Request starting HTTP/1.1 GET http:/// - - info: Program       Bad chunk extension NOT detected. info: Microsoft.AspNetCore.Hosting.Diagnostics[2]       Request finished HTTP/1.1 GET http:/// - - - 408 - - 10977.8230ms Test 2 FAILED 0/2 tests passed info: Microsoft.Hosting.Lifetime       Application is shutting down..."></p> <p>This shows that if you're running .NET 6 today, you've vulnerable to CVE-2025-55315.</p> <h3 id="using-the-herodevs-nes-for-net-6-build" class="heading-with-anchor"><a href="https://andrewlock.net/companies-using-dotnet-need-to-suck-it-up-and-pay-for-support/#using-the-herodevs-nes-for-net-6-build" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Using the HeroDevs NES for .NET 6 build</a></h3> <p>To test out HeroDevs' NES for .NET 6 support, <a href="mailto:hbarnes@herodevs.com">Hayden Barnes</a> provided me with a docker image containing their patched NES version of .NET 6.</p> <blockquote> <p>This isn't necessarily the way you <em>need</em> to integrate HeroDevs into your build pipeline, it was just a convenient approach for me. HeroDevs provide multiple ways to integrate, so <a href="https://www.herodevs.com/contact">reach out to them</a> for more details!</p> </blockquote> <p>The only change I made was to switch out the "official" .NET 6 image with HeroDevs patched version of .NET 6. Note that you won't be able to use the docker image below as-is, you'll need to contact HeroDevs to get access to patched versions of .NET; I was provided this image directly by HeroDevs just for testing.</p> <div class="pre-code-wrapper"><pre class="language-dockerfile"><code class="language-dockerfile">
<span class="token comment"># ---------------Builder image---------------------- #</span>
<span class="token instruction"><span class="token keyword">FROM</span> mcr.microsoft.com/dotnet/sdk:10.0 <span class="token keyword">AS</span> builder</span>

<span class="token instruction"><span class="token keyword">RUN</span> git clone https://github.com/sirredbeard/CVE-2025-55315-repro /app</span>
<span class="token instruction"><span class="token keyword">RUN</span> dotnet publish /app/Repro/Repro.csproj -c Release --framework net6.0 -o /app/publish</span>

<span class="token comment"># ---------------Final image---------------------- #</span>
<span class="token comment">#  👇 Using HeroDevs NES for .NET 6 version</span>
<span class="token instruction"><span class="token keyword">FROM</span> registry.nes.herodevs.com/oci/dotnet-runtime:6.0.39-bullseye-slim</span>

<span class="token instruction"><span class="token keyword">WORKDIR</span> /app</span>
<span class="token instruction"><span class="token keyword">COPY</span> <span class="token options"><span class="token property">--from</span><span class="token punctuation">=</span><span class="token string">builder</span></span> /app/publish .</span>

<span class="token instruction"><span class="token keyword">ENTRYPOINT</span> [<span class="token string">"dotnet"</span>, <span class="token string">"Repro.dll"</span>]</span>
</code></pre></div> <p>I then built the docker image and ran it using:</p> <div class="pre-code-wrapper"><pre class="language-bash"><code class="language-bash"><span class="token function">docker</span> build <span class="token parameter variable">-t</span> andrewlock/repro-test:nes-6.0 <span class="token builtin class-name">.</span>
<span class="token function">docker</span> run <span class="token parameter variable">--rm</span> <span class="token parameter variable">-it</span> andrewlock/repro-test:nes-6.0
</code></pre></div> <p>and as you can see from the logs below, the NES version of .NET 6 is <em>not</em> vulnerable to CVE-2025-55315! 🎉</p> <p><img src="https://andrewlock.net/content/images/2025/herodevs_repro_nes.png" alt="=== Runtime Version Information === .NET Runtime: 6.0.39 Runtime Directory: /usr/share/dotnet/shared/Microsoft.NETCore.App/6.0.39/ info: Microsoft.Hosting.Lifetime[14]       Now listening on: http://localhost:5000 info: Microsoft.Hosting.Lifetime       Application started. Press Ctrl+C to shut down. info: Microsoft.Hosting.Lifetime       Hosting environment: Production info: Microsoft.Hosting.Lifetime       Content root path: /app Kestrel.Core Version: 6.0.39+c7c5c868b73b54f925f3a9d0ca7080b5910be4b9 ASP.NET Core Version: 6.0.39+c7c5c868b73b54f925f3a9d0ca7080b5910be4b9 =================================== Server started on http://localhost:5000 info: Microsoft.AspNetCore.Hosting.Diagnostics[1]       Request starting HTTP/1.1 GET http:/// - - info: Program       Bad chunk extension detected. info: Microsoft.AspNetCore.Hosting.Diagnostics[2]       Request finished HTTP/1.1 GET http:/// - - - 400 - - 82.6425ms Test 1 PASSED info: Microsoft.AspNetCore.Hosting.Diagnostics[1]       Request starting HTTP/1.1 GET http:/// - - info: Program       Bad chunk extension detected. info: Microsoft.AspNetCore.Hosting.Diagnostics[2]       Request finished HTTP/1.1 GET http:/// - - - 400 - - 0.4682ms Test 2 PASSED 2/2 tests passed info: Microsoft.Hosting.Lifetime       Application is shutting down..."></p> <p>The logs above show that the .NET version provided in the NES for .NET build is <code>6.0.39</code>, higher than the latest official version of <code>6.0.36</code>. This is thanks to the additional patches HeroDevs apply to the runtime and libraries on <em>top</em> of the 6.0.36 base.</p> <p>And there you have it. We easily replaced a vulnerable version of .NET 6 with HeroDevs' NES for .NET version and our app was no longer vulnerable. No costly or risky major version updates required, just support for what you're already using!</p> <blockquote> <p>One aspect I didn't strictly demonstrate was that we didn't even recompile the app—we simply swapped out the <em>runtime</em> image, not the build step. Even if you <em>can't</em> rebuild your app (perhaps you lost the source code, for example), the HeroDevs solution still works, while updating to a new major version clearly wouldn't be an option!</p> </blockquote> <p>I demonstrated an ASP.NET Core app in this example, but HeroDevs support many different components: the .NET SDK, the runtime, the ASP.NET Core runtime, WPF, and more! Just <a href="https://www.herodevs.com/support/dot-net-nes">reach out to the team at HeroDevs</a> and see how they can help you keep your applications protected.</p> <h2 id="conclusion" class="heading-with-anchor"><a href="https://andrewlock.net/companies-using-dotnet-need-to-suck-it-up-and-pay-for-support/#conclusion" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Conclusion</a></h2> <p>In this post I described the support that Microsoft provides for .NET, including what "supported" means, the requirement for you to patch every month to be supported, and the risks you're exposing yourself to if you <em>don't</em> regularly patch your applications.</p> <p>I then discussed some of the difficulties of staying up to date when a major version is no longer supported. A major version update can mean potentially time-consuming and costly updates, depending on the number and type of applications you need to support. Obviously there are benefits to updating to newer major versions, and I'm certainly not suggesting you <em>don't</em> update. But if you <em>can't</em> update, then there's another option: pay for support.</p> <p>In many other ecosystems, paying for support of end-of-life (EOL) products is common place. It's just an accepted maintenance cost, and it frees organisations from the regulatory and maintenance barriers of <em>having</em> to update all your applications when a framework goes EOL. For organisations that have dozens, hundreds, or even thousands of apps, performing a mass migration simply isn't practical, and post-EOL support is a very practical option.</p> <p>And I think .NET organisations should embrace this. Companies like <a href="https://www.herodevs.com/">HeroDevs</a> can provide patched versions of EOL runtimes that you can "drop in" to your existing apps and get instance support and protection, with very little work required on your side—you don't even need to recompile your apps, which my be crucial in some cases (oops, we lost the source code😅).</p> <p>At the end of this post I tried out HeroDevs support, by showing how easy it is to drop in their Never Ending Support (NES) for .NET 6 binary. After switching to NES for .NET, I demonstrated how an application that was vulnerable to <a href="https://andrewlock.net/understanding-the-worst-dotnet-vulnerability-request-smuggling-and-cve-2025-55315/">the critical CVE-2025-55315 vulnerability</a> was now protected.</p> <p>More organisations should be considering this as a path forward; if upgrading to a new major version is too costly, or simply isn't possible, take a look at <a href="https://www.herodevs.com/">HeroDevs</a>, and see if they can meet your needs!</p> ]]></content:encoded><category><![CDATA[.NET Core;ASP.NET Core;Security]]></category></item><item><title><![CDATA[Easier reflection with [UnsafeAccessorType] in .NET 10: Exploring the .NET 10 preview - Part 9]]></title><description><![CDATA[In this post I show how to work with [UnsafeAccessor] to do 'easier' reflection and how to use .NET 10's [UnsafeAccessorType] with types you can't reference]]></description><link>https://andrewlock.net/exploring-dotnet-10-preview-features-9-easier-reflection-with-unsafeaccessortype/</link><guid isPermaLink="true">https://andrewlock.net/exploring-dotnet-10-preview-features-9-easier-reflection-with-unsafeaccessortype/</guid><pubDate>Tue, 04 Nov 2025 10:00:00 GMT</pubDate><dc:creator><![CDATA[Andrew Lock]]></dc:creator><media:content url="https://andrewlock.net/content/images/2025/unsafe_accessor_banner.png" medium="image" /><content:encoded><![CDATA[<img src="https://andrewlock.net/content/images/2025/unsafe_accessor_banner.png" /><nav><p>This is the ninth post in the series: <a href="https://andrewlock.net/series/exploring-the-dotnet-10-preview/">Exploring the .NET 10 preview</a>. </p> <ol class="list-none"><li><a href="https://andrewlock.net/exploring-dotnet-10-preview-features-1-exploring-the-dotnet-run-app.cs/">Part 1 - Exploring the features of dotnet run app.cs</a></li><li><a href="https://andrewlock.net/exploring-dotnet-10-preview-features-2-behind-the-scenes-of-dotnet-run-app.cs/">Part 2 - Behind the scenes of dotnet run app.cs</a></li><li><a href="https://andrewlock.net/exploring-dotnet-10-preview-features-3-csharp-14-extensions-members/">Part 3 - C# 14 extension members; AKA extension everything</a></li><li><a href="https://andrewlock.net/exploring-dotnet-10-preview-features-4-solving-the-source-generator-marker-attribute-problem-in-dotnet-10/">Part 4 - Solving the source generator 'marker attribute' problem in .NET 10</a></li><li><a href="https://andrewlock.net/exploring-dotnet-10-preview-features-5-running-one-off-dotnet-tools-with-dnx/">Part 5 - Running one-off .NET tools with dnx</a></li><li><a href="https://andrewlock.net/exploring-dotnet-10-preview-features-6-passkey-support-for-aspnetcore-identity/">Part 6 - Passkey support for ASP.NET Core identity</a></li><li><a href="https://andrewlock.net/exploring-dotnet-10-preview-features-7-packaging-self-contained-and-native-aot-dotnet-tools-for-nuget/">Part 7 - Packaging self-contained and native AOT .NET tools for NuGet</a></li><li><a href="https://andrewlock.net/exploring-dotnet-10-preview-features-8-supporting-platform-specific-dotnet-tools-on-old-sdks/">Part 8 - Supporting platform-specific .NET tools on old .NET SDKs</a></li><li>Part 9 - Easier reflection with [UnsafeAccessorType] in .NET 10 (this post) </li></ol></nav><p>In this post I describe some of the improvements to the <code>[UnsafeAccessor]</code> mechanism that was introduced in .NET 8. <code>[UnsafeAccessor]</code> allows you to easily access private fields and invoke private methods of types, without needing to use the reflection APIs. In .NET 9 there are some limitations to the methods and types this will work with. In .NET 10, some of those gaps have been closed by the introduction of <code>[UnsafeAccessorType]</code>, and it's those improvements we'll look at in this post.</p> <h2 id="calling-private-members-with-unsafeaccessor-in-net-8-and-9" class="heading-with-anchor"><a href="https://andrewlock.net/exploring-dotnet-10-preview-features-9-easier-reflection-with-unsafeaccessortype/#calling-private-members-with-unsafeaccessor-in-net-8-and-9" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Calling private members with <code>[UnsafeAccessor]</code> in .NET 8 and 9</a></h2> <p>The <code>[UnsafeAccessor]</code> mechanism was introduced in .NET 8, with support for generics added in .NET 9.</p> <blockquote> <p>I'm going to ignore the question of <em>why</em> you might want to access private members like this as a tangential discussion. You've always been <em>able</em> to do this via the reflection APIs, <code>[UnsafeAccessor]</code> just makes it a bit easier and faster.</p> </blockquote> <p>For example, let's say you want to retrieve the private <code>_items</code> field in <code>List&lt;T&gt;</code>:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">List<span class="token punctuation">&lt;</span>T<span class="token punctuation">&gt;</span></span>
<span class="token punctuation">{</span>
    <span class="token class-name">T<span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">?</span></span> _items<span class="token punctuation">;</span>
    <span class="token comment">// .. other members</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>You could do this using reflection with code like this:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token comment">// Get a FieldInfo object for accessing the value</span>
<span class="token class-name"><span class="token keyword">var</span></span> itemsFieldInfo <span class="token operator">=</span> <span class="token keyword">typeof</span><span class="token punctuation">(</span><span class="token type-expression class-name">List<span class="token punctuation">&lt;</span><span class="token keyword">int</span><span class="token punctuation">&gt;</span></span><span class="token punctuation">)</span>
    <span class="token punctuation">.</span><span class="token function">GetField</span><span class="token punctuation">(</span><span class="token string">"_items"</span><span class="token punctuation">,</span> BindingFlags<span class="token punctuation">.</span>NonPublic <span class="token operator">|</span> BindingFlags<span class="token punctuation">.</span>Instance<span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token comment">// Create an instance of the list</span>
<span class="token class-name"><span class="token keyword">var</span></span> list <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token constructor-invocation class-name">List<span class="token punctuation">&lt;</span><span class="token keyword">int</span><span class="token punctuation">&gt;</span></span><span class="token punctuation">(</span><span class="token number">16</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token comment">// Retreive the list using reflection</span>
<span class="token class-name"><span class="token keyword">var</span></span> items <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token keyword">int</span><span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">)</span>itemsFieldInfo<span class="token punctuation">.</span><span class="token function">GetValue</span><span class="token punctuation">(</span>list<span class="token punctuation">)</span><span class="token punctuation">;</span>

Console<span class="token punctuation">.</span><span class="token function">WriteLine</span><span class="token punctuation">(</span><span class="token interpolation-string"><span class="token string">$"</span><span class="token interpolation"><span class="token punctuation">{</span><span class="token expression language-csharp">items<span class="token punctuation">.</span>Length</span><span class="token punctuation">}</span></span><span class="token string"> items"</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// Prints "16 items"</span>
</code></pre></div> <p>To use <code>[UnsafeAccessor]</code> you must create a special <code>extern</code> method, decorated with the attribute, that has the correct signature for accessing the member you want. In the case of a field, that means a method that takes a single parameter of the target type, and returns an instance of the field's target type as a <code>ref</code>. The name of the method itself is not important.</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token comment">// Create an instance of the list</span>
<span class="token class-name"><span class="token keyword">var</span></span> list <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token constructor-invocation class-name">List<span class="token punctuation">&lt;</span><span class="token keyword">int</span><span class="token punctuation">&gt;</span></span><span class="token punctuation">(</span><span class="token number">16</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token comment">// Invoke the method to retrieve the list</span>
<span class="token class-name"><span class="token keyword">int</span><span class="token punctuation">[</span><span class="token punctuation">]</span></span> items <span class="token operator">=</span> Accessors<span class="token operator">&lt;</span><span class="token keyword">int</span><span class="token operator">&gt;</span><span class="token punctuation">.</span><span class="token function">GetItems</span><span class="token punctuation">(</span>list<span class="token punctuation">)</span><span class="token punctuation">;</span>

Console<span class="token punctuation">.</span><span class="token function">WriteLine</span><span class="token punctuation">(</span><span class="token interpolation-string"><span class="token string">$"</span><span class="token interpolation"><span class="token punctuation">{</span><span class="token expression language-csharp">items<span class="token punctuation">.</span>Length</span><span class="token punctuation">}</span></span><span class="token string"> items"</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// Prints "16 items"</span>

<span class="token keyword">static</span> <span class="token keyword">class</span> <span class="token class-name">Accessors<span class="token punctuation">&lt;</span>T<span class="token punctuation">&gt;</span></span>
<span class="token punctuation">{</span>
    <span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">UnsafeAccessor</span><span class="token attribute-arguments"><span class="token punctuation">(</span>UnsafeAccessorKind<span class="token punctuation">.</span>Field<span class="token punctuation">,</span> Name <span class="token operator">=</span> <span class="token string">"_items"</span><span class="token punctuation">)</span></span></span><span class="token punctuation">]</span>
    <span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token keyword">extern</span> <span class="token keyword">ref</span> <span class="token return-type class-name">T<span class="token punctuation">[</span><span class="token punctuation">]</span></span> <span class="token function">GetItems</span><span class="token punctuation">(</span><span class="token class-name">List<span class="token punctuation">&lt;</span>T<span class="token punctuation">&gt;</span></span> list<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>Note that as <code>List&lt;T&gt;</code> is a generic type, we must use a "container" type to declare the accessor method with the correct signature. Making the method itself generic, (<code>GetItems&lt;T&gt;(List&lt;T&gt;)</code>) will not work, nor will using a closed type <code>GetItems(List&lt;int&gt;)</code>.</p> <blockquote> <p>There are some more examples later in this post on calling generic <em>methods</em>, as well as members on generic types.</p> </blockquote> <p>It's also worth noting that the <code>GetItems()</code> signature returns a <code>ref</code> (and it <em>must</em> when you're accessing a field), which means you can also <em>change</em> the field. The example below uses the same accessor method but this time <em>replaces</em> the field:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token comment">// Create an instance of the list</span>
<span class="token class-name"><span class="token keyword">var</span></span> list <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token constructor-invocation class-name">List<span class="token punctuation">&lt;</span><span class="token keyword">int</span><span class="token punctuation">&gt;</span></span><span class="token punctuation">(</span><span class="token number">16</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
Console<span class="token punctuation">.</span><span class="token function">WriteLine</span><span class="token punctuation">(</span><span class="token interpolation-string"><span class="token string">$"Capacity: </span><span class="token interpolation"><span class="token punctuation">{</span><span class="token expression language-csharp">list<span class="token punctuation">.</span>Capacity</span><span class="token punctuation">}</span></span><span class="token string">"</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// Prints "Capacity: 16"</span>

<span class="token comment">// Invoke the method to retrieve the field ref and set the value of the field to an empty array</span>
Accessors<span class="token operator">&lt;</span><span class="token keyword">int</span><span class="token operator">&gt;</span><span class="token punctuation">.</span><span class="token function">GetItems</span><span class="token punctuation">(</span>list<span class="token punctuation">)</span> <span class="token operator">=</span> Array<span class="token punctuation">.</span><span class="token generic-method"><span class="token function">Empty</span><span class="token generic class-name"><span class="token punctuation">&lt;</span><span class="token keyword">int</span><span class="token punctuation">&gt;</span></span></span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
Console<span class="token punctuation">.</span><span class="token function">WriteLine</span><span class="token punctuation">(</span><span class="token interpolation-string"><span class="token string">$"Capacity: </span><span class="token interpolation"><span class="token punctuation">{</span><span class="token expression language-csharp">list<span class="token punctuation">.</span>Capacity</span><span class="token punctuation">}</span></span><span class="token string">"</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// Prints "Capacity: 0"</span>

<span class="token comment">// Same accessor as before:</span>
<span class="token keyword">static</span> <span class="token keyword">class</span> <span class="token class-name">Accessors<span class="token punctuation">&lt;</span>T<span class="token punctuation">&gt;</span></span>
<span class="token punctuation">{</span>
    <span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">UnsafeAccessor</span><span class="token attribute-arguments"><span class="token punctuation">(</span>UnsafeAccessorKind<span class="token punctuation">.</span>Field<span class="token punctuation">,</span> Name <span class="token operator">=</span> <span class="token string">"_items"</span><span class="token punctuation">)</span></span></span><span class="token punctuation">]</span>
    <span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token keyword">extern</span> <span class="token keyword">ref</span> <span class="token return-type class-name">T<span class="token punctuation">[</span><span class="token punctuation">]</span></span> <span class="token function">GetItems</span><span class="token punctuation">(</span><span class="token class-name">List<span class="token punctuation">&lt;</span>T<span class="token punctuation">&gt;</span></span> list<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>Of course it's not just fields that you can invoke, you can also call methods (and therefore properties) and constructors; anything defined on <code>UnsafeAccesorType</code>:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">public</span> <span class="token keyword">enum</span> <span class="token class-name">UnsafeAccessorKind</span>
<span class="token punctuation">{</span>
  Constructor<span class="token punctuation">,</span>
  Method<span class="token punctuation">,</span>
  StaticMethod<span class="token punctuation">,</span>
  Field<span class="token punctuation">,</span>
  StaticField<span class="token punctuation">,</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>For example, the following shows an example of invoking a static method defined on <code>List&lt;T&gt;</code>:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token comment">// Invoking private static methods</span>
<span class="token comment">// We can pass `null` as the instance argument because these are static methods</span>
<span class="token class-name"><span class="token keyword">bool</span></span> isCompat1 <span class="token operator">=</span> Accessors<span class="token operator">&lt;</span><span class="token keyword">int</span><span class="token punctuation">?</span><span class="token operator">&gt;</span><span class="token punctuation">.</span><span class="token function">IsCompatibleObject</span><span class="token punctuation">(</span><span class="token keyword">null</span><span class="token punctuation">,</span> <span class="token number">123</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// true</span>
<span class="token class-name"><span class="token keyword">bool</span></span> isCompat2 <span class="token operator">=</span> Accessors<span class="token operator">&lt;</span><span class="token keyword">int</span><span class="token punctuation">?</span><span class="token operator">&gt;</span><span class="token punctuation">.</span><span class="token function">IsCompatibleObject</span><span class="token punctuation">(</span><span class="token keyword">null</span><span class="token punctuation">,</span> <span class="token keyword">null</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// true</span>
<span class="token class-name"><span class="token keyword">bool</span></span> isCompat3 <span class="token operator">=</span> Accessors<span class="token operator">&lt;</span><span class="token keyword">int</span><span class="token punctuation">?</span><span class="token operator">&gt;</span><span class="token punctuation">.</span><span class="token function">IsCompatibleObject</span><span class="token punctuation">(</span><span class="token keyword">null</span><span class="token punctuation">,</span> <span class="token number">1.23</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// false</span>

<span class="token keyword">static</span> <span class="token keyword">class</span> <span class="token class-name">Accessors<span class="token punctuation">&lt;</span>T<span class="token punctuation">&gt;</span></span>
<span class="token punctuation">{</span>
    <span class="token comment">// The method we're invoking has this signature:</span>
    <span class="token comment">//     private static bool IsCompatibleObject(object? value)</span>
    <span class="token comment">// </span>
    <span class="token comment">// Our extern signature must include the target type as the first method parameter</span>
    <span class="token punctuation">[</span><span class="token function">UnsafeAccessor</span><span class="token punctuation">(</span>UnsafeAccessorKind<span class="token punctuation">.</span>StaticMethod<span class="token punctuation">,</span> Name <span class="token operator">=</span> <span class="token string">"IsCompatibleObject"</span><span class="token punctuation">)</span><span class="token punctuation">]</span>
    <span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token keyword">extern</span> <span class="token return-type class-name"><span class="token keyword">bool</span></span> <span class="token function">CheckObject</span><span class="token punctuation">(</span><span class="token class-name">List<span class="token punctuation">&lt;</span>T<span class="token punctuation">&gt;</span></span> instance<span class="token punctuation">,</span> <span class="token class-name"><span class="token keyword">object</span><span class="token punctuation">?</span></span> <span class="token keyword">value</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>As you can see above, for both instance and static methods you include the "target" instance as the first parameter on the method (and pass <code>null</code> as the argument when invoking static methods). That way the runtime knows which <code>Type</code> it's working with: it's whatever the type of the first argument is. But what if you <em>can't</em> specify this type, because the type itself is private?</p> <h2 id="limitations-of-unsafeaccessor-in-net-9" class="heading-with-anchor"><a href="https://andrewlock.net/exploring-dotnet-10-preview-features-9-easier-reflection-with-unsafeaccessortype/#limitations-of-unsafeaccessor-in-net-9" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Limitations of <code>[UnsafeAccessor]</code> in .NET 9</a></h2> <p>The big limitation around using <code>[UnsafeAccessor]</code> in .NET 9 is that you must be able to directly reference all the types that are part of the target signature. As a concrete example, imagine a library you're using has a type that looks something like this.</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">PublicClass</span>
<span class="token punctuation">{</span>
    <span class="token keyword">private</span> <span class="token keyword">readonly</span> <span class="token class-name">PrivateClass</span> _private <span class="token operator">=</span> <span class="token keyword">new</span><span class="token punctuation">(</span><span class="token string">"Hello world!"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token keyword">internal</span> <span class="token return-type class-name">PrivateClass</span> <span class="token function">GetPrivate</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=&gt;</span> _private<span class="token punctuation">;</span>
<span class="token punctuation">}</span>

<span class="token keyword">internal</span> <span class="token keyword">class</span> <span class="token class-name">PrivateClass</span><span class="token punctuation">(</span><span class="token class-name"><span class="token keyword">string</span></span> someValue<span class="token punctuation">)</span>
<span class="token punctuation">{</span>
    <span class="token keyword">internal</span> <span class="token return-type class-name"><span class="token keyword">string</span></span> SomeValue <span class="token punctuation">{</span> <span class="token keyword">get</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token operator">=</span> someValue<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>It's very contrived, but it'll do for our purposes. Imagine you have an instance of <code>PublicClass</code> but what you really need is <code>SomeValue</code> that's held on the <code>_private</code> field. However, <code>PrivateClass</code> is marked <code>internal</code> so you can't directly reference it. That means that none of these accessors will work, because there's no way to use <code>PrivateClass</code> in the signature:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">UnsafeAccessor</span><span class="token attribute-arguments"><span class="token punctuation">(</span>UnsafeAccessorKind<span class="token punctuation">.</span>Field<span class="token punctuation">,</span> Name <span class="token operator">=</span> <span class="token string">"_private"</span><span class="token punctuation">)</span></span></span><span class="token punctuation">]</span>
<span class="token keyword">static</span> <span class="token keyword">extern</span> <span class="token keyword">ref</span> <span class="token keyword">readonly</span> <span class="token return-type class-name">PrivateClass</span> <span class="token function">GetByField</span><span class="token punctuation">(</span><span class="token class-name">PublicClass</span> instance<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">//                         👆 ❌ Can't reference PrivateClass</span>

<span class="token punctuation">[</span><span class="token function">UnsafeAccessor</span><span class="token punctuation">(</span>UnsafeAccessorKind<span class="token punctuation">.</span>Method<span class="token punctuation">,</span> Name <span class="token operator">=</span> <span class="token string">"GetPrivate"</span><span class="token punctuation">)</span><span class="token punctuation">]</span>
<span class="token keyword">static</span> <span class="token keyword">extern</span> <span class="token return-type class-name">PrivateClass</span> <span class="token function">GetByMethod</span><span class="token punctuation">(</span><span class="token class-name">PublicClass</span> instance<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">//            👆 ❌ Can't reference PrivateClass</span>

<span class="token punctuation">[</span><span class="token function">UnsafeAccessor</span><span class="token punctuation">(</span>UnsafeAccessorKind<span class="token punctuation">.</span>Method<span class="token punctuation">,</span> Name <span class="token operator">=</span> <span class="token string">"get_SomeValue"</span><span class="token punctuation">)</span><span class="token punctuation">]</span>
<span class="token keyword">static</span> <span class="token keyword">extern</span> <span class="token return-type class-name"><span class="token keyword">string</span></span> <span class="token function">GetSomeValue</span><span class="token punctuation">(</span><span class="token class-name">PrivateClass</span> instance<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">//                               👆 ❌ Can't reference PrivateClass</span>
</code></pre></div> <p>The example above is where you can't reference the type in your signature due to its visibility. There's an additional scenario that won't work in .NET 9: where you don't have access to the types you're working with at <em>compile</em> time.</p> <p>That second scenario will likely be rarer, but there's a couple of examples of this that come to mind:</p> <ul><li>The .NET runtime has this scenario due to circular-dependencies between different libraries, for example <a href="https://github.com/dotnet/runtime/pull/115583">the HTTP and Cryptography libraries</a>.</li> <li>The Datadog instrumentation libraries need to access internal properties of libraries we're instrumenting, but we <em>can't</em> reference those libraries directly due to version compatibility constraints.</li></ul> <p>In .NET 9, the only solution to these problems was to fallback to "normal" reflection, to use <code>System.Reflection.Emit</code>, or to use <code>System.Linq.Expressions</code>, all of which are significantly slower than <code>[UnsafeAccessor]</code>, and much more cumbersome.</p> <p>Luckily, in .NET 10, we have a way to get the best of both worlds: we can use <code>[UnsafeAccessor]</code> even with types we can't reference!</p> <h2 id="accessing-unreferenced-types-with-unsafeaccessortype-in-net-10" class="heading-with-anchor"><a href="https://andrewlock.net/exploring-dotnet-10-preview-features-9-easier-reflection-with-unsafeaccessortype/#accessing-unreferenced-types-with-unsafeaccessortype-in-net-10" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Accessing unreferenced types with <code>[UnsafeAccessorType]</code> in .NET 10</a></h2> <p>In .NET 9 you must be able to directly reference all the types used in the method signature of a <code>[UnsafeAccessor]</code> method. .NET 10 introduces the <code>[UnsafeAccessorType]</code> attribute, which lets us use <code>[UnsafeAccessor]</code> even with types we <em>can't</em> reference.</p> <h3 id="using-unsafeaccessortype-with-unsafeaccessor-" class="heading-with-anchor"><a href="https://andrewlock.net/exploring-dotnet-10-preview-features-9-easier-reflection-with-unsafeaccessortype/#using-unsafeaccessortype-with-unsafeaccessor-" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Using <code>[UnsafeAccessorType]</code> with <code>[UnsafeAccessor]</code></a></h3> <p>.NET 10 introduces a new attribute, <code>[UnsafeAccessorType]</code>, which allows you to specify the expected type for an <code>[UnsafeAccessor]</code> parameter as a <code>string</code>, which solves both of the scenarios described in the previous section. It's easiest to see this in action, so let's look at the same example as before. Let's say we have this hierarchy.</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">PublicClass</span>
<span class="token punctuation">{</span>
    <span class="token keyword">private</span> <span class="token keyword">readonly</span> <span class="token class-name">PrivateClass</span> _private <span class="token operator">=</span> <span class="token keyword">new</span><span class="token punctuation">(</span><span class="token string">"Hello world!"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token keyword">internal</span> <span class="token return-type class-name">PrivateClass</span> <span class="token function">GetPrivate</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=&gt;</span> _private<span class="token punctuation">;</span>
<span class="token punctuation">}</span>

<span class="token keyword">internal</span> <span class="token keyword">class</span> <span class="token class-name">PrivateClass</span><span class="token punctuation">(</span><span class="token class-name"><span class="token keyword">string</span></span> someValue<span class="token punctuation">)</span>
<span class="token punctuation">{</span>
    <span class="token keyword">internal</span> <span class="token return-type class-name"><span class="token keyword">string</span></span> SomeValue <span class="token punctuation">{</span> <span class="token keyword">get</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token operator">=</span> someValue<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>And we'll create some unsafe accessor methods to retrieve that pesky <code>SomeValue</code>:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">UnsafeAccessor</span><span class="token attribute-arguments"><span class="token punctuation">(</span>UnsafeAccessorKind<span class="token punctuation">.</span>Method<span class="token punctuation">,</span> Name <span class="token operator">=</span> <span class="token string">"GetPrivate"</span><span class="token punctuation">)</span></span></span><span class="token punctuation">]</span>
<span class="token punctuation">[</span><span class="token attribute"><span class="token target keyword">return</span><span class="token punctuation">:</span> <span class="token class-name">UnsafeAccessorType</span><span class="token attribute-arguments"><span class="token punctuation">(</span><span class="token string">"PrivateClass"</span><span class="token punctuation">)</span></span></span><span class="token punctuation">]</span> <span class="token comment">// 👈 Specify target return type as a string</span>
<span class="token keyword">static</span> <span class="token keyword">extern</span> <span class="token return-type class-name"><span class="token keyword">object</span></span> <span class="token function">GetByMethod</span><span class="token punctuation">(</span><span class="token class-name">PublicClass</span> instance<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">//            👆 use object as return type</span>

<span class="token punctuation">[</span><span class="token function">UnsafeAccessor</span><span class="token punctuation">(</span>UnsafeAccessorKind<span class="token punctuation">.</span>Method<span class="token punctuation">,</span> Name <span class="token operator">=</span> <span class="token string">"get_SomeValue"</span><span class="token punctuation">)</span><span class="token punctuation">]</span>
<span class="token keyword">static</span> <span class="token keyword">extern</span> <span class="token return-type class-name"><span class="token keyword">string</span></span> <span class="token function">GetSomeValue</span><span class="token punctuation">(</span><span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">UnsafeAccessorType</span><span class="token attribute-arguments"><span class="token punctuation">(</span><span class="token string">"PrivateClass"</span><span class="token punctuation">)</span></span></span><span class="token punctuation">]</span> <span class="token class-name"><span class="token keyword">object</span></span> instance<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">// Specify target type in attribute 👆 and use object as instance type 👆</span>
</code></pre></div> <p>As you can see in the examples above, instead of referencing the type directly, you use <code>object</code> instead and add an <code>[UnsafeAccessorType]</code> with the "real" type (more on that shortly).</p> <p>Once we have the above definitions, we can chain these two methods to retrieve the value stored in a <code>PrivateClass</code> instance, even though we can't reference it directly:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token comment">// Create the target instance</span>
<span class="token class-name"><span class="token keyword">var</span></span> publicClass <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token constructor-invocation class-name">PublicClass</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token comment">// Invoke GetPrivate(), and return the result as an object</span>
<span class="token class-name"><span class="token keyword">object</span></span> privateClass <span class="token operator">=</span> <span class="token function">GetByMethod</span><span class="token punctuation">(</span>publicClass<span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token comment">// Pass the object and invoke the SomeValue getter method</span>
<span class="token class-name"><span class="token keyword">string</span></span> <span class="token keyword">value</span> <span class="token operator">=</span> <span class="token function">GetSomeValue</span><span class="token punctuation">(</span>privateClass<span class="token punctuation">)</span><span class="token punctuation">;</span>
Console<span class="token punctuation">.</span><span class="token function">WriteLine</span><span class="token punctuation">(</span><span class="token keyword">value</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// Hello world!</span>
</code></pre></div> <p>And there you have it, the ability to use <code>[UnsafeAccessor]</code> with types that you can't reference.</p> <h3 id="specifying-type-names-with-unsafeaccessortype-" class="heading-with-anchor"><a href="https://andrewlock.net/exploring-dotnet-10-preview-features-9-easier-reflection-with-unsafeaccessortype/#specifying-type-names-with-unsafeaccessortype-" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Specifying type names with <code>[UnsafeAccessorType]</code></a></h3> <p>The type name I used in the previous example was very simple, just <code>PrivateClass</code>, but this hides the fact that this is actually a fully qualified type name, just as you might use in <code>Type.GetType(name)</code> for example. This needs to be <em>fully</em> qualified though it doesn't <em>have</em> to be <em>Assembly</em> qualified, though doing so is generally more robust.</p> <p>Also note that for generic types and methods, these strings must be in either the open or closed generic format, depending on their usage, e.g. <code>List``1[[!0]]</code>. Similarly, you need the <code>+</code> for handling nested classes.</p> <p>To make that all a little more concrete, I've included some examples below taken from <a href="https://github.com/dotnet/runtime/blob/main/src/tests/baseservices/compilerservices/UnsafeAccessors/UnsafeAccessorsTests.cs">the runtime's unit tests</a> for <code>[UnsafeAccessor]</code>, which demonstrates the use of <code>UnsafeAccessorType]</code> for referencing types in accessor methods. Note that all the types are defined in an assembly called <code>PrivateLib</code>, and all the classes and members are <code>internal</code>, so can't be referenced directly.</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">namespace</span> <span class="token namespace">PrivateLib</span><span class="token punctuation">;</span>

<span class="token keyword">internal</span> <span class="token keyword">class</span> <span class="token class-name">Class1</span>
<span class="token punctuation">{</span>
    <span class="token keyword">static</span> <span class="token class-name"><span class="token keyword">int</span></span> StaticField <span class="token operator">=</span> <span class="token number">123</span><span class="token punctuation">;</span>
    <span class="token class-name"><span class="token keyword">int</span></span> InstanceField <span class="token operator">=</span> <span class="token number">456</span><span class="token punctuation">;</span>

    <span class="token function">Class1</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token punctuation">}</span>

    <span class="token keyword">static</span> <span class="token return-type class-name">Class1</span> <span class="token function">GetClass</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=&gt;</span> <span class="token keyword">new</span> <span class="token constructor-invocation class-name">Class1</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token keyword">private</span> <span class="token return-type class-name">Class1<span class="token punctuation">[</span><span class="token punctuation">]</span></span> <span class="token function">GetArray</span><span class="token punctuation">(</span><span class="token keyword">ref</span> <span class="token class-name">Class1</span> a<span class="token punctuation">)</span> <span class="token operator">=&gt;</span> <span class="token keyword">new</span><span class="token punctuation">[</span><span class="token punctuation">]</span> <span class="token punctuation">{</span> a <span class="token punctuation">}</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>

<span class="token keyword">internal</span> <span class="token keyword">class</span> <span class="token class-name">GenericClass<span class="token punctuation">&lt;</span>T<span class="token punctuation">&gt;</span></span>
<span class="token punctuation">{</span>
    <span class="token return-type class-name">List<span class="token punctuation">&lt;</span>Class1<span class="token punctuation">&gt;</span></span> <span class="token function">ClosedGeneric</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=&gt;</span> <span class="token keyword">new</span> <span class="token constructor-invocation class-name">List<span class="token punctuation">&lt;</span>Class1<span class="token punctuation">&gt;</span></span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token return-type class-name">List<span class="token punctuation">&lt;</span>U<span class="token punctuation">&gt;</span></span> <span class="token generic-method"><span class="token function">GenericMethod</span><span class="token generic class-name"><span class="token punctuation">&lt;</span>U<span class="token punctuation">&gt;</span></span></span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=&gt;</span> <span class="token keyword">new</span> <span class="token constructor-invocation class-name">List<span class="token punctuation">&lt;</span>U<span class="token punctuation">&gt;</span></span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token return-type class-name"><span class="token keyword">bool</span></span> <span class="token generic-method"><span class="token function">GenericWithConstraints</span><span class="token generic class-name"><span class="token punctuation">&lt;</span>V<span class="token punctuation">,</span> W<span class="token punctuation">&gt;</span></span></span><span class="token punctuation">(</span><span class="token class-name">List<span class="token punctuation">&lt;</span>T<span class="token punctuation">&gt;</span></span> a<span class="token punctuation">,</span> <span class="token class-name">List<span class="token punctuation">&lt;</span>V<span class="token punctuation">&gt;</span></span> b<span class="token punctuation">,</span> <span class="token class-name">List<span class="token punctuation">&lt;</span>W<span class="token punctuation">&gt;</span></span> c<span class="token punctuation">,</span> <span class="token class-name">List<span class="token punctuation">&lt;</span>Class1<span class="token punctuation">&gt;</span></span> d<span class="token punctuation">)</span>
        <span class="token keyword">where</span> <span class="token class-name">W</span> <span class="token punctuation">:</span> <span class="token type-list"><span class="token class-name">T</span></span>
         <span class="token operator">=&gt;</span> <span class="token boolean">true</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>The following are a bunch of representative examples of accessors, in increasing complexity. Each one shows a different example of an <code>[UnsafeAccessorType]</code> usage. This also demonstrates different <em>kinds</em> of accessors, such as constructors.</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token comment">// 1. Calling the Class1 constructor, returned type name is assembly qualified</span>
<span class="token punctuation">[</span><span class="token function">UnsafeAccessor</span><span class="token punctuation">(</span>UnsafeAccessorKind<span class="token punctuation">.</span>Constructor<span class="token punctuation">)</span><span class="token punctuation">]</span>
<span class="token punctuation">[</span><span class="token attribute"><span class="token target keyword">return</span><span class="token punctuation">:</span> <span class="token class-name">UnsafeAccessorType</span><span class="token attribute-arguments"><span class="token punctuation">(</span><span class="token string">"PrivateLib.Class1, PrivateLib"</span><span class="token punctuation">)</span></span></span><span class="token punctuation">]</span>
<span class="token keyword">extern</span> <span class="token keyword">static</span> <span class="token return-type class-name"><span class="token keyword">object</span></span> <span class="token function">CreateClass</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token comment">// 2. Calling a static method. Both the return type and the "target" parameter are assembly qualified</span>
<span class="token punctuation">[</span><span class="token function">UnsafeAccessor</span><span class="token punctuation">(</span>UnsafeAccessorKind<span class="token punctuation">.</span>StaticMethod<span class="token punctuation">,</span> Name <span class="token operator">=</span> <span class="token string">"GetClass"</span><span class="token punctuation">)</span><span class="token punctuation">]</span>
<span class="token punctuation">[</span><span class="token attribute"><span class="token target keyword">return</span><span class="token punctuation">:</span> <span class="token class-name">UnsafeAccessorType</span><span class="token attribute-arguments"><span class="token punctuation">(</span><span class="token string">"PrivateLib.Class1, PrivateLib"</span><span class="token punctuation">)</span></span></span><span class="token punctuation">]</span>
<span class="token keyword">extern</span> <span class="token keyword">static</span> <span class="token return-type class-name"><span class="token keyword">object</span></span> <span class="token function">CallGetClass</span><span class="token punctuation">(</span><span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">UnsafeAccessorType</span><span class="token attribute-arguments"><span class="token punctuation">(</span><span class="token string">"PrivateLib.Class1, PrivateLib"</span><span class="token punctuation">)</span></span></span><span class="token punctuation">]</span> <span class="token class-name"><span class="token keyword">object</span></span> a<span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token comment">// 3. Returning a ref to the static field</span>
<span class="token punctuation">[</span><span class="token function">UnsafeAccessor</span><span class="token punctuation">(</span>UnsafeAccessorKind<span class="token punctuation">.</span>StaticField<span class="token punctuation">,</span> Name <span class="token operator">=</span> <span class="token string">"StaticField"</span><span class="token punctuation">)</span><span class="token punctuation">]</span>
<span class="token keyword">extern</span> <span class="token keyword">static</span> <span class="token keyword">ref</span> <span class="token return-type class-name"><span class="token keyword">int</span></span> <span class="token function">GetStaticField</span><span class="token punctuation">(</span><span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">UnsafeAccessorType</span><span class="token attribute-arguments"><span class="token punctuation">(</span><span class="token string">"PrivateLib.Class1, PrivateLib"</span><span class="token punctuation">)</span></span></span><span class="token punctuation">]</span> <span class="token class-name"><span class="token keyword">object</span></span> a<span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token comment">// 4. Returning a ref to an instance field</span>
<span class="token comment">// Note that we cannot use [UnsafeAccessorType] on the return type. More on that later.</span>
<span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">UnsafeAccessor</span><span class="token attribute-arguments"><span class="token punctuation">(</span>UnsafeAccessorKind<span class="token punctuation">.</span>Field<span class="token punctuation">,</span> Name <span class="token operator">=</span> <span class="token string">"InstanceField"</span><span class="token punctuation">)</span></span></span><span class="token punctuation">]</span>
<span class="token keyword">extern</span> <span class="token keyword">static</span> <span class="token keyword">ref</span> <span class="token return-type class-name"><span class="token keyword">int</span></span> <span class="token function">GetInstanceField</span><span class="token punctuation">(</span><span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">UnsafeAccessorType</span><span class="token attribute-arguments"><span class="token punctuation">(</span><span class="token string">"PrivateLib.Class1, PrivateLib"</span><span class="token punctuation">)</span></span></span><span class="token punctuation">]</span> <span class="token class-name"><span class="token keyword">object</span></span> a<span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token comment">// 5. Passing an object by reference and returning an array</span>
<span class="token comment">// Note the `&amp;` in the signature when passing by reference.</span>
<span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">UnsafeAccessor</span><span class="token attribute-arguments"><span class="token punctuation">(</span>UnsafeAccessorKind<span class="token punctuation">.</span>Method<span class="token punctuation">,</span> Name <span class="token operator">=</span> <span class="token string">"GetArray"</span><span class="token punctuation">)</span></span></span><span class="token punctuation">]</span>
<span class="token punctuation">[</span><span class="token attribute"><span class="token target keyword">return</span><span class="token punctuation">:</span> <span class="token class-name">UnsafeAccessorType</span><span class="token attribute-arguments"><span class="token punctuation">(</span><span class="token string">"PrivateLib.Class1[], PrivateLib"</span><span class="token punctuation">)</span></span></span><span class="token punctuation">]</span>
<span class="token keyword">extern</span> <span class="token keyword">static</span> <span class="token return-type class-name"><span class="token keyword">object</span></span> <span class="token function">CallM_RC1</span><span class="token punctuation">(</span><span class="token class-name">TargetClass</span> tgt<span class="token punctuation">,</span> <span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">UnsafeAccessorType</span><span class="token attribute-arguments"><span class="token punctuation">(</span><span class="token string">"PrivateLib.Class1&amp;, PrivateLib"</span><span class="token punctuation">)</span></span></span><span class="token punctuation">]</span> <span class="token keyword">ref</span> <span class="token class-name"><span class="token keyword">object</span></span> a<span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token comment">// 6. Invoking a method on a generic type, and returning a closed-generic type</span>
<span class="token comment">// The return type uses a mix of fully qualified (for BCL types) and assembly qualified types</span>
<span class="token comment">// Note that the open generic definition uses !0 to indicate an unspecified type parameter</span>
<span class="token punctuation">[</span><span class="token function">UnsafeAccessor</span><span class="token punctuation">(</span>UnsafeAccessorKind<span class="token punctuation">.</span>Method<span class="token punctuation">,</span> Name <span class="token operator">=</span> <span class="token string">"ClosedGeneric"</span><span class="token punctuation">)</span><span class="token punctuation">]</span>
<span class="token punctuation">[</span><span class="token attribute"><span class="token target keyword">return</span><span class="token punctuation">:</span> <span class="token class-name">UnsafeAccessorType</span><span class="token attribute-arguments"><span class="token punctuation">(</span><span class="token string">"System.Collections.Generic.List`1[[PrivateLib.Class1, PrivateLib]]"</span><span class="token punctuation">)</span></span></span><span class="token punctuation">]</span>
<span class="token keyword">extern</span> <span class="token keyword">static</span> <span class="token return-type class-name"><span class="token keyword">object</span></span> <span class="token function">CallGenericClassClosedGeneric</span><span class="token punctuation">(</span><span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">UnsafeAccessorType</span><span class="token attribute-arguments"><span class="token punctuation">(</span><span class="token string">"PrivateLib.GenericClass`1[[!0]], PrivateLib"</span><span class="token punctuation">)</span></span></span><span class="token punctuation">]</span> <span class="token class-name"><span class="token keyword">object</span></span> a<span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token comment">// 7. Invoking a generic method on a generic type.</span>
<span class="token comment">// This is similar to the above, but we use !!0 to indicate an unspecified generic method type parameter.</span>
<span class="token comment">// Note that the accessor method itself must be generic.</span>
<span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">UnsafeAccessor</span><span class="token attribute-arguments"><span class="token punctuation">(</span>UnsafeAccessorKind<span class="token punctuation">.</span>Method<span class="token punctuation">,</span> Name <span class="token operator">=</span> <span class="token string">"GenericMethod"</span><span class="token punctuation">)</span></span></span><span class="token punctuation">]</span>
<span class="token punctuation">[</span><span class="token attribute"><span class="token target keyword">return</span><span class="token punctuation">:</span> <span class="token class-name">UnsafeAccessorType</span><span class="token attribute-arguments"><span class="token punctuation">(</span><span class="token string">"System.Collections.Generic.List`1[[!!0]]"</span><span class="token punctuation">)</span></span></span><span class="token punctuation">]</span>
<span class="token keyword">extern</span> <span class="token keyword">static</span> <span class="token return-type class-name"><span class="token keyword">object</span></span> <span class="token generic-method"><span class="token function">CallGenericClassGenericMethod</span><span class="token generic class-name"><span class="token punctuation">&lt;</span>U<span class="token punctuation">&gt;</span></span></span><span class="token punctuation">(</span><span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">UnsafeAccessorType</span><span class="token attribute-arguments"><span class="token punctuation">(</span><span class="token string">"PrivateLib.GenericClass`1[[!0]], PrivateLib"</span><span class="token punctuation">)</span></span></span><span class="token punctuation">]</span> <span class="token class-name"><span class="token keyword">object</span></span> a<span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token comment">// 8. Invoking a generic method, on a generic type, with type constraints</span>
<span class="token comment">// This is a more complex version of the above, but additionally specifies</span>
<span class="token comment">// type constraints which match the target method's constraints.</span>
<span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">UnsafeAccessor</span><span class="token attribute-arguments"><span class="token punctuation">(</span>UnsafeAccessorKind<span class="token punctuation">.</span>Method<span class="token punctuation">,</span> Name <span class="token operator">=</span> <span class="token string">"GenericWithConstraints"</span><span class="token punctuation">)</span></span></span><span class="token punctuation">]</span>
<span class="token keyword">public</span> <span class="token keyword">extern</span> <span class="token keyword">static</span> <span class="token return-type class-name"><span class="token keyword">bool</span></span> <span class="token generic-method"><span class="token function">CallGenericClassGenericWithConstraints</span><span class="token generic class-name"><span class="token punctuation">&lt;</span>V<span class="token punctuation">,</span>  W<span class="token punctuation">&gt;</span></span></span><span class="token punctuation">(</span>
    <span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">UnsafeAccessorType</span><span class="token attribute-arguments"><span class="token punctuation">(</span><span class="token string">"PrivateLib.GenericClass`1[[!0]], PrivateLib"</span><span class="token punctuation">)</span></span></span><span class="token punctuation">]</span> <span class="token class-name"><span class="token keyword">object</span></span> tgt<span class="token punctuation">,</span>
    <span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">UnsafeAccessorType</span><span class="token attribute-arguments"><span class="token punctuation">(</span><span class="token string">"System.Collections.Generic.List`1[[!0]]"</span><span class="token punctuation">)</span></span></span><span class="token punctuation">]</span>
    <span class="token class-name"><span class="token keyword">object</span></span> a<span class="token punctuation">,</span>
    <span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">UnsafeAccessorType</span><span class="token attribute-arguments"><span class="token punctuation">(</span><span class="token string">"System.Collections.Generic.List`1[[!!0]]"</span><span class="token punctuation">)</span></span></span><span class="token punctuation">]</span>
    <span class="token class-name"><span class="token keyword">object</span></span> b<span class="token punctuation">,</span>
    <span class="token class-name">List<span class="token punctuation">&lt;</span>W<span class="token punctuation">&gt;</span></span> c<span class="token punctuation">,</span>
    <span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">UnsafeAccessorType</span><span class="token attribute-arguments"><span class="token punctuation">(</span><span class="token string">"System.Collections.Generic.List`1[[PrivateLib.Class1, PrivateLib]]"</span><span class="token punctuation">)</span></span></span><span class="token punctuation">]</span>
    <span class="token class-name"><span class="token keyword">object</span></span> d<span class="token punctuation">)</span> <span class="token keyword">where</span> <span class="token class-name">W</span> <span class="token punctuation">:</span> <span class="token type-list"><span class="token class-name">T</span></span><span class="token punctuation">;</span>
</code></pre></div> <p>These examples show the <em>wide</em> variety of invocations you can make using <code>[UnsafeAccessorType]</code> which makes this a very powerful approach. And what's more, it's <em>fast</em>. Using <code>[UnsafeAccessor]</code> in general is much faster than traditional reflection.</p> <p>Unfortunately, even in .NET 10, there's still a couple of gaps with what we can do.</p> <h3 id="limitations-of-unsafeaccessortype-" class="heading-with-anchor"><a href="https://andrewlock.net/exploring-dotnet-10-preview-features-9-easier-reflection-with-unsafeaccessortype/#limitations-of-unsafeaccessortype-" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Limitations of <code>[UnsafeAccessorType]</code></a></h3> <p>As I showed in the previous section, you can use <code>[UnsafeAccessorType]</code> in conjunction with <code>[UnsafeAccessor]</code> to access private members of types that you can't reference at runtime, but there's still a few gaps where you <em>can't</em> replace the use of traditional reflection. In summary, these are:</p> <ul><li>You can't call an accessor on a generic type if you can't represent the generic type argument.</li> <li>You can't call fields where the field needs to be marked with <code>[UnsafeAccessorType]</code>.</li> <li>You can't call methods that return a <code>ref</code> where the return needs to be marked with <code>[UnsafeAccessorType]</code>.</li></ul> <p>To clarify those cases, I'll provide some examples of types and accessors that <em>don't</em> work.</p> <h4 id="1-unable-to-represent-the-type-argument" class="heading-with-anchor"><a href="https://andrewlock.net/exploring-dotnet-10-preview-features-9-easier-reflection-with-unsafeaccessortype/#1-unable-to-represent-the-type-argument" class="relative text-zinc-800 dark:text-white no-underline hover:underline">1. Unable to represent the type argument</a></h4> <p>The first case is pretty simple. Imagine we have the <code>Generic&lt;T&gt;</code> type, plus a helper class, <code>Class1</code>:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">internal</span> <span class="token keyword">class</span> <span class="token class-name">Generic<span class="token punctuation">&lt;</span>T<span class="token punctuation">&gt;</span></span> <span class="token punctuation">{</span> <span class="token punctuation">}</span>
<span class="token keyword">internal</span> <span class="token keyword">class</span> <span class="token class-name">Class1</span> <span class="token punctuation">{</span> <span class="token punctuation">}</span>
</code></pre></div> <p>We need to create instances of the <code>Generic&lt;T&gt;</code> type even though it's marked internal, so we create an accessor for it:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">static</span> <span class="token keyword">class</span> <span class="token class-name">Accessors<span class="token punctuation">&lt;</span>T<span class="token punctuation">&gt;</span></span>
<span class="token punctuation">{</span>
    <span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">UnsafeAccessor</span><span class="token attribute-arguments"><span class="token punctuation">(</span>UnsafeAccessorKind<span class="token punctuation">.</span>Constructor<span class="token punctuation">)</span></span></span><span class="token punctuation">]</span>
    <span class="token punctuation">[</span><span class="token attribute"><span class="token target keyword">return</span><span class="token punctuation">:</span> <span class="token class-name">UnsafeAccessorType</span><span class="token attribute-arguments"><span class="token punctuation">(</span><span class="token string">"Generic`1[[!0]]"</span><span class="token punctuation">)</span></span></span><span class="token punctuation">]</span>
    <span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token keyword">extern</span> <span class="token return-type class-name"><span class="token keyword">object</span></span> <span class="token function">Create</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>This works fine when the <code>T</code> we need can be referenced:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token class-name"><span class="token keyword">object</span></span> instance <span class="token operator">=</span> Accessors<span class="token operator">&lt;</span><span class="token keyword">int</span><span class="token operator">&gt;</span><span class="token punctuation">.</span><span class="token function">Create</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
Console<span class="token punctuation">.</span><span class="token function">WriteLine</span><span class="token punctuation">(</span>generic<span class="token punctuation">.</span><span class="token function">GetType</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// Generic`1[System.Int32]</span>
</code></pre></div> <p>but what happens if we need to create an instance of <code>Generic&lt;Class1&gt;</code>? The simple answer is we can't. We would need to call <code>Accessors&lt;Class1&gt;.Create()</code>, but that won't compile as we can't reference <code>Class1</code>. So if we have this pattern then we have to fallback to traditional reflection.</p> <p>What's more, even if we created an instance of <code>Generic&lt;Class1&gt;</code> using "traditional" reflection, we <em>wouldn't</em> be able to use <code>[UnsafeAccessor]</code> methods to interact with the object, because we would always need to do so through a method defined on <code>Accessors&lt;Class1&gt;</code>, which is again not possible because we can't reference <code>Class1</code>.</p> <h4 id="2-unable-to-represent-field-return-types" class="heading-with-anchor"><a href="https://andrewlock.net/exploring-dotnet-10-preview-features-9-easier-reflection-with-unsafeaccessortype/#2-unable-to-represent-field-return-types" class="relative text-zinc-800 dark:text-white no-underline hover:underline">2. Unable to represent field return types</a></h4> <p>We'll extend the previous example to add a new type, <code>Class2</code>, which has a couple of fields that reference the <code>Class1</code> type:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">internal</span> <span class="token keyword">class</span> <span class="token class-name">Class1</span> <span class="token punctuation">{</span> <span class="token punctuation">}</span>

<span class="token keyword">internal</span> <span class="token keyword">class</span> <span class="token class-name">Class2</span>
<span class="token punctuation">{</span>
    <span class="token keyword">private</span> <span class="token class-name">Class1</span> _field1 <span class="token operator">=</span> <span class="token keyword">new</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">private</span> <span class="token keyword">readonly</span> <span class="token class-name">Class1</span> _field2 <span class="token operator">=</span> <span class="token keyword">new</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>If <code>_field1</code> and <code>_field2</code> referenced known types, then we could create <code>[UnsafeAccessor]</code>s for them without issue. You might think that accessors like the following would work:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token comment">// Helper for creating a C2 instance</span>
<span class="token punctuation">[</span><span class="token function">UnsafeAccessor</span><span class="token punctuation">(</span>UnsafeAccessorKind<span class="token punctuation">.</span>Constructor<span class="token punctuation">)</span><span class="token punctuation">]</span>
<span class="token punctuation">[</span><span class="token attribute"><span class="token target keyword">return</span><span class="token punctuation">:</span> <span class="token class-name">UnsafeAccessorType</span><span class="token attribute-arguments"><span class="token punctuation">(</span><span class="token string">"Class2"</span><span class="token punctuation">)</span></span></span><span class="token punctuation">]</span>
<span class="token keyword">static</span> <span class="token keyword">extern</span> <span class="token return-type class-name"><span class="token keyword">object</span></span> <span class="token function">Create</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">UnsafeAccessor</span><span class="token attribute-arguments"><span class="token punctuation">(</span>UnsafeAccessorKind<span class="token punctuation">.</span>Field<span class="token punctuation">,</span> Name <span class="token operator">=</span> <span class="token string">"_field1"</span><span class="token punctuation">)</span></span></span><span class="token punctuation">]</span>
<span class="token punctuation">[</span><span class="token attribute"><span class="token target keyword">return</span><span class="token punctuation">:</span> <span class="token class-name">UnsafeAccessorType</span><span class="token attribute-arguments"><span class="token punctuation">(</span><span class="token string">"Class1"</span><span class="token punctuation">)</span></span></span><span class="token punctuation">]</span>
<span class="token keyword">static</span> <span class="token keyword">extern</span> <span class="token keyword">ref</span> <span class="token return-type class-name"><span class="token keyword">object</span></span> <span class="token function">CallField1</span><span class="token punctuation">(</span><span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">UnsafeAccessorType</span><span class="token attribute-arguments"><span class="token punctuation">(</span><span class="token string">"Class2"</span><span class="token punctuation">)</span></span></span><span class="token punctuation">]</span> <span class="token class-name"><span class="token keyword">object</span></span> instance<span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">UnsafeAccessor</span><span class="token attribute-arguments"><span class="token punctuation">(</span>UnsafeAccessorKind<span class="token punctuation">.</span>Field<span class="token punctuation">,</span> Name <span class="token operator">=</span> <span class="token string">"_field2"</span><span class="token punctuation">)</span></span></span><span class="token punctuation">]</span>
<span class="token punctuation">[</span><span class="token attribute"><span class="token target keyword">return</span><span class="token punctuation">:</span> <span class="token class-name">UnsafeAccessorType</span><span class="token attribute-arguments"><span class="token punctuation">(</span><span class="token string">"Class1"</span><span class="token punctuation">)</span></span></span><span class="token punctuation">]</span>
<span class="token keyword">static</span> <span class="token keyword">extern</span> <span class="token keyword">ref</span> <span class="token keyword">readonly</span> <span class="token return-type class-name"><span class="token keyword">object</span></span> <span class="token function">CallField2</span><span class="token punctuation">(</span><span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">UnsafeAccessorType</span><span class="token attribute-arguments"><span class="token punctuation">(</span><span class="token string">"Class2"</span><span class="token punctuation">)</span></span></span><span class="token punctuation">]</span> <span class="token class-name"><span class="token keyword">object</span></span> instance<span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre></div> <p>However, if you try to use <code>CallField1()</code> or <code>CallField2()</code> you'll get an error at runtime:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token class-name"><span class="token keyword">object</span></span> class2 <span class="token operator">=</span> <span class="token function">Create</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token class-name"><span class="token keyword">object</span></span> field1 <span class="token operator">=</span> <span class="token function">CallField1</span><span class="token punctuation">(</span>class2<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// throws System.NotSupportedException: Invalid usage of UnsafeAccessorTypeAttribute</span>
<span class="token class-name"><span class="token keyword">object</span></span> field2 <span class="token operator">=</span> <span class="token function">CallField2</span><span class="token punctuation">(</span>class2<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// throws System.NotSupportedException: Invalid usage of UnsafeAccessorTypeAttribute</span>
</code></pre></div> <p>In both cases, you get a <code>System.NotSupportedException</code> stating <code>Invalid usage of UnsafeAccessorTypeAttribute</code>: you simply can't access fields unless you can represent the types.</p> <blockquote> <p>As an aside, this really confused me when I was first trying out the feature. Accessing a field was the first thing I tried and I assumed I was doing it wrong. 😅</p> </blockquote> <p>Just to repeat, this works fine if the field you're accessing is <em>not</em> using <code>[UnsafeAccessorType]</code>, so if <code>_field1</code> was an <code>int</code> for example. It's only trying to use <code>[UnsafeAccessorType]</code> with the field which fails.</p> <h4 id="3-unable-to-represent-ref-method-returns" class="heading-with-anchor"><a href="https://andrewlock.net/exploring-dotnet-10-preview-features-9-easier-reflection-with-unsafeaccessortype/#3-unable-to-represent-ref-method-returns" class="relative text-zinc-800 dark:text-white no-underline hover:underline">3. Unable to represent <code>ref</code> method returns</a></h4> <p>Similar to the previous issue, if you have a <code>ref</code> returning method, and you can't represent the type, then you can't use <code>[UnsafeAccessor]</code>. For example, let's add a <code>GetField1()</code> method, which returns a <code>ref</code> to <code>_field1</code>:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">internal</span> <span class="token keyword">class</span> <span class="token class-name">Class1</span> <span class="token punctuation">{</span> <span class="token punctuation">}</span>

<span class="token keyword">internal</span> <span class="token keyword">class</span> <span class="token class-name">Class2</span>
<span class="token punctuation">{</span>
    <span class="token keyword">private</span> <span class="token class-name">Class1</span> _field1 <span class="token operator">=</span> <span class="token keyword">new</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">private</span> <span class="token keyword">ref</span> <span class="token return-type class-name">Class1</span> <span class="token function">GetField1</span><span class="token punctuation">(</span><span class="token class-name">Class2</span> a<span class="token punctuation">)</span> <span class="token operator">=&gt;</span> <span class="token keyword">ref</span> _field1<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>An accessor for this might look like the following:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">UnsafeAccessor</span><span class="token attribute-arguments"><span class="token punctuation">(</span>UnsafeAccessorKind<span class="token punctuation">.</span>Method<span class="token punctuation">,</span> Name <span class="token operator">=</span> <span class="token string">"GetField1"</span><span class="token punctuation">)</span></span></span><span class="token punctuation">]</span>
<span class="token punctuation">[</span><span class="token attribute"><span class="token target keyword">return</span><span class="token punctuation">:</span> <span class="token class-name">UnsafeAccessorType</span><span class="token attribute-arguments"><span class="token punctuation">(</span><span class="token string">"Class1&amp;"</span><span class="token punctuation">)</span></span></span><span class="token punctuation">]</span> <span class="token comment">// ref return </span>
<span class="token keyword">static</span> <span class="token keyword">extern</span> <span class="token keyword">ref</span> <span class="token return-type class-name"><span class="token keyword">object</span></span> <span class="token function">CallGetField1</span><span class="token punctuation">(</span><span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">UnsafeAccessorType</span><span class="token attribute-arguments"><span class="token punctuation">(</span><span class="token string">"Class2"</span><span class="token punctuation">)</span></span></span><span class="token punctuation">]</span> <span class="token class-name"><span class="token keyword">object</span></span> instance<span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre></div> <p>But trying to call this accessor fails in the same way at runtime as trying to access the field directly:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token class-name"><span class="token keyword">object</span></span> class2 <span class="token operator">=</span> <span class="token function">Create</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token class-name"><span class="token keyword">object</span></span> field1 <span class="token operator">=</span> <span class="token function">CallGetField1</span><span class="token punctuation">(</span>class2<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// throws System.NotSupportedException: Invalid usage of UnsafeAccessorTypeAttribute</span>
</code></pre></div> <p>Those are all the limitations I found, so as long as what you're trying to do doesn't fall into one of these camps, then you should hopefully be ok!</p> <h2 id="summary" class="heading-with-anchor"><a href="https://andrewlock.net/exploring-dotnet-10-preview-features-9-easier-reflection-with-unsafeaccessortype/#summary" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Summary</a></h2> <p>In this post I described some improvements to the <code>[UnsafeAccessor]</code> mechanism that was introduced in .NET 8. I showed how <code>[UnsafeAccessor]</code> allows you to easily access private fields and invoke private members of types, without needing to use the reflection APIs in .NET 8 and .NET 9. I then described some of the limitations, namely that you need to be able to reference the types used by the members you're accessing.</p> <p>Next I introduced the <code>[UnsafeAccessorType]</code> attribute, and showed how you could use it to invoke methods on types that you <em>can't</em> reference at compile time. I showed how you can use this to invoke methods, constructors, and fields, and how to work with generic types and generic methods. Finally, I described the limitations of <code>[UnsafeAccessorType]</code>, namely that you can't use it to work with instances of generic types where you can't reference the type parameter, and that you can't use <code>[UnsafeAccessorType]</code> with fields or <code>ref</code> returning methods.</p> ]]></content:encoded><category><![CDATA[.NET 10;Performance]]></category></item><item><title><![CDATA[Understanding the worst .NET vulnerability ever: request smuggling and CVE-2025-55315]]></title><description><![CDATA[In this post I discuss request smuggling, the recent vulnerability in ASP.NET Core with a severity score of 9.9, and how attackers could exploit it]]></description><link>https://andrewlock.net/understanding-the-worst-dotnet-vulnerability-request-smuggling-and-cve-2025-55315/</link><guid isPermaLink="true">https://andrewlock.net/understanding-the-worst-dotnet-vulnerability-request-smuggling-and-cve-2025-55315/</guid><pubDate>Tue, 28 Oct 2025 10:00:00 GMT</pubDate><dc:creator><![CDATA[Andrew Lock]]></dc:creator><media:content url="https://andrewlock.net/content/images/2025/request_smuggling_banner.png" medium="image" /><content:encoded><![CDATA[<img src="https://andrewlock.net/content/images/2025/request_smuggling_banner.png" /><p>I admit, that's a very click-baity headline, but Microsoft have given the vulnerability a CVSS score of 9.9, their highest ever. Time to panic, right?</p> <p>In this post I try to provide a bit more context. I explain how request smuggling vulnerabilities work in general, how it works in <em>this</em> case, what attackers could use it for, how the vulnerability was fixed, what you can do to protect yourself.</p> <blockquote> <p>WARNING: I am not a security professional, so do not take anything in this post as gospel or advice. I'm just a developer trying to make sense of things. 😄 All of the details in this post are based on information that was provided or referenced in the original announcement.</p> </blockquote> <h2 id="what-is-the-cve-2025-55315-vulnerability-" class="heading-with-anchor"><a href="https://andrewlock.net/understanding-the-worst-dotnet-vulnerability-request-smuggling-and-cve-2025-55315/#what-is-the-cve-2025-55315-vulnerability-" class="relative text-zinc-800 dark:text-white no-underline hover:underline">What is the CVE-2025-55315 vulnerability?</a></h2> <p>On October 14th 2025, on a standard Microsoft "patch Tuesday", Microsoft released new versions of all their supported versions of .NET, and also published a security advisory: <a href="https://github.com/dotnet/aspnetcore/issues/64033">Microsoft Security Advisory CVE-2025-55315: .NET Security Feature Bypass Vulnerability</a>. The high level summary from that announcement said:</p> <blockquote> <p>Inconsistent interpretation of http requests ('http request/response smuggling') in ASP.NET Core allows an authorized attacker to bypass a security feature over a network.</p> </blockquote> <p>The advice was "patch all of your things", but the real headline was that this vulnerability was given a <a href="https://en.wikipedia.org/wiki/Common_Vulnerability_Scoring_System">CVSS score</a> of 9.9 our of 10, which you know, sounds pretty bad! <a href="https://github.com/blowdart">Barry Dorrans AKA blowdart</a>, .NET security head honcho, gave <a href="https://github.com/dotnet/aspnetcore/issues/64033#issuecomment-3403054914">an explanation of the reasoning behind the score</a> in a comment on the original issue:</p> <blockquote> <p>The bug enables HTTP Request Smuggling, which on its own for ASP.NET Core would be nowhere near that high, but that's not how we rate things...</p> <p>Instead, we score based on how the bug might affect applications built on top of ASP.NET.</p> <p>Request Smuggling allows an attacker to hide an extra request inside an another, and what that hidden request can do is very application specific.</p> <p>The smuggled request could cause your application code to</p> <ul><li>Login as a different user (EOP)</li> <li>Make an internal request (SSRF)</li> <li>Bypass CSRF checks</li> <li>Perform an injection attack</li></ul> <p>But we don't know what's possible because it's dependent on how you've written your app.</p> </blockquote> <p>That <em>does</em> all sound pretty scary! 😱 So you can understand the consternation that the issue has caused, especially given the hesitation to explain exactly what "how you've written your app" <em>means</em>.</p> <p>Out of curiosity, I decided to dig in further to really understand this vulnerability, how it could impact you, and what "how you've written your app" could mean.</p> <h2 id="how-does-request-smuggling-work-" class="heading-with-anchor"><a href="https://andrewlock.net/understanding-the-worst-dotnet-vulnerability-request-smuggling-and-cve-2025-55315/#how-does-request-smuggling-work-" class="relative text-zinc-800 dark:text-white no-underline hover:underline">How does request smuggling work?</a></h2> <p>Before we get to the actual patched vulnerability in ASP.NET Core and how the vulnerability works, I think it's important to have some background about the general <em>class</em> of exploits known as <em>HTTP request smuggling</em>.</p> <p><a href="https://en.wikipedia.org/wiki/HTTP_request_smuggling">HTTP request smuggling</a> is a security exploit that has been known about for a <em>long</em> time (according to Wikipedia, <a href="https://www.cgisecurity.com/lib/HTTP-Request-Smuggling.pdf">it was first documented in 2005</a>). It fundamentally arises when you have two different servers processing an HTTP request (e.g. a server and a proxy server), and where those two servers <em>differ</em> in how they handle "invalid" HTTP requests.</p> <p>In all cases of HTTP request smuggling, the exploit works by creating an invalid HTTP request (or sometimes just an <em>ambiguous</em> request), that looks a bit like two HTTP requests glued together. In summary, the exploit then works a bit like this:</p> <ul><li>The proxy server receives the ambiguous HTTP request</li> <li>The proxy server forwards the request (unmodified) to the destination server</li> <li>The server interprets the ambiguous request as <em>two</em> pipelined HTTP requests sent to the server, and processes them separately.</li></ul> <p>I think it's easiest to understand the problem with an example, so the request below shows an example from <a href="https://www.cgisecurity.com/lib/HTTP-Request-Smuggling.pdf">the original 2005 paper</a>.</p> <blockquote> <p>Note that this is <em>not</em> an example of the request smuggling vulnerability in CVE-2025-55315, it's just a representative example of request smuggling in <em>general</em>.</p> </blockquote> <p>Let's imagine the attacker sends an HTTP request that looks like this:</p> <div class="pre-code-wrapper"><pre class="language-http"><code class="language-http"><span class="token request-line"><span class="token method property">POST</span> <span class="token request-target url">/some_script.jsp</span> <span class="token http-version property">HTTP/1.0</span></span>
<span class="token header"><span class="token header-name keyword">Connection</span><span class="token punctuation">:</span> <span class="token header-value">Keep-Alive</span></span>
<span class="token header"><span class="token header-name keyword">Content-Type</span><span class="token punctuation">:</span> <span class="token header-value">application/x-www-form-urlencoded</span></span>
<span class="token header"><span class="token header-name keyword">Content-Length</span><span class="token punctuation">:</span> <span class="token header-value">9</span></span>
<span class="token header"><span class="token header-name keyword">Content-Length</span><span class="token punctuation">:</span> <span class="token header-value">204</span></span>

this=thatPOST /vuln_page.jsp HTTP/1.0
<span class="token header"><span class="token header-name keyword">Content-Type</span><span class="token punctuation">:</span> <span class="token header-value">application/x-www-form-urlencoded</span></span>
<span class="token header"><span class="token header-name keyword">Content-Length</span><span class="token punctuation">:</span> <span class="token header-value">95</span></span>

param1=value1&amp;data=&lt;script&gt;alert("stealing%20your%20data:"%2bdocument.cookie)&lt;/script&gt;&amp;foobar
</code></pre></div> <p>The important feature of this request is that there are <em>two</em> <code>Content-Length</code> headers in the first request (and one later in the smuggled request), with different values: <code>9</code> or <code>204</code>. <strong>This is the core of the exploit</strong>; the difference between <em>which</em> of the these first two headers the HTTP proxy and HTTP server honour is what causes the vulnerability.</p> <p>Let's walk through how the exploit works, step-by-step:</p> <ul><li>The attacker sends the above HTTP request.</li> <li>The HTTP proxy receives the request, notes the duplicate <code>Content-Length</code> headers, and accepts the <em>second</em> header, the <code>204</code> length. That means the <em>whole</em> rest of the request is treated as the message body, and seems fine as far as the proxy is concerned.</li> <li>The HTTP proxy forwards the request on to the destination server.</li> <li>This server also notes the duplicate <code>Content-Length</code> header, but it takes the <em>first</em> of the headers, with the length of <code>9</code>.</li> <li>The server reads <code>9</code> bytes of the body (i.e. <code>this=that</code>) and <em>treats that as the whole request</em>. As far as the server is concerned, the whole (valid) request has been received, and it sees the rest of the data <em>as a whole new request</em>.</li> <li>That means that the destination server sees an entirely new HTTP request to process, <code>POST /vuln_page.jsp</code>, and treats it as a new request.</li></ul> <p>That's the core of the issue; the proxy saw one request, while the destination server saw <em>two</em>—the second request has been "smuggled" past the proxy to the server.</p> <blockquote> <p>The request smuggling technique shown here, where you have multiple <code>Content-Length</code> headers isn't the "canonical" example you'll generally see referenced, but I used it here because it's simpler to understand in a lot of ways.</p> <p>The canonical request smuggling attack is where you send both a <code>Content-Length</code> header and a <code>Transfer-Encoding: chunked</code> header (which specifies the length of the body as part of the body itself). As before, the request smuggling exploit relies on differences in how proxy and destination servers interpret these <a href="https://datatracker.ietf.org/doc/html/rfc9112#section-6.3-2.3">conflicting headers</a>.</p> </blockquote> <p>So as you've seen, request smuggling enables sending a secret request to a destination server that an intermediate proxy server hasn't seen. In the next section we'll look at why that's a bad thing, and how it can be exploited.</p> <h2 id="how-can-an-attacker-exploit-request-smuggling-" class="heading-with-anchor"><a href="https://andrewlock.net/understanding-the-worst-dotnet-vulnerability-request-smuggling-and-cve-2025-55315/#how-can-an-attacker-exploit-request-smuggling-" class="relative text-zinc-800 dark:text-white no-underline hover:underline">How can an attacker exploit request smuggling?</a></h2> <p>On the face of it, request smuggling might not <em>seem</em> like a big deal. So the server sees two requests, so what? You could always send two requests to the server <em>anyway</em>, right? Well, yes and no.</p> <p>The issue with request smuggling is really all about the <em>mismatch</em> between the proxy and destination servers. Thanks to this mismatch, and depending on what behaviours and expectations the target application has, attackers can use request smuggling to</p> <ul><li>Reflect malicious data to other users on sites that are vulnerable to cross-site scripting.</li> <li>Poison caches with bad data.</li> <li>Exfiltrate authentication credentials or other data from client requests.</li> <li>Invoke endpoints that shouldn't be publicly accessible (because the proxy would block external access to them).</li> <li>Replace/override authentication controls handled by the proxy.</li> <li>Redirect users to malicious sites on sites vulnerable to open-redirect attacks.</li> <li>And more…</li></ul> <p>As you can see, these are all Bad™️, so you can kind of understand why the 9.9 rating was given! 😱</p> <p>That said, it's worth mentioning that not <em>all</em> of these attacks will be fruitful against <em>all</em> applications. Some of the easiest to understand versions of these exploits are where the proxy is not just doing "dumb" forwarding of requests, but rather it's validating or enhancing the request in some way.</p> <p>For example, if you have a proxy sat in front of your server which is responsible for handling TLS termination and client-authentication and identification using certificates, then request smuggling could be used to bypass these checks and insert your <em>own</em> identification.</p> <p>As an example of <a href="https://portswigger.net/web-security/request-smuggling/exploiting#bypassing-client-authentication">that attack</a>, the HTTP request below demonstrates using a <code>Content-Length</code> and <code>Transfer-Encoding</code> request smuggling attack to "hide" the request to <code>/admin</code> from the front-end proxy, and insert a malicious <code>X-SSL-CLIENT-CN</code> header, which would <em>normally</em> be added by the front-end proxy:</p> <div class="pre-code-wrapper"><pre class="language-http"><code class="language-http"><span class="token request-line"><span class="token method property">POST</span> <span class="token request-target url">/example</span> <span class="token http-version property">HTTP/1.1</span></span>
<span class="token header"><span class="token header-name keyword">Host</span><span class="token punctuation">:</span> <span class="token header-value">some-website.com</span></span>
<span class="token header"><span class="token header-name keyword">Content-Type</span><span class="token punctuation">:</span> <span class="token header-value">x-www-form-urlencoded</span></span>
<span class="token header"><span class="token header-name keyword">Content-Length</span><span class="token punctuation">:</span> <span class="token header-value">64</span></span>
<span class="token header"><span class="token header-name keyword">Transfer-Encoding</span><span class="token punctuation">:</span> <span class="token header-value">chunked</span></span>

0

<span class="token request-line"><span class="token method property">GET</span> <span class="token request-target url">/admin</span> <span class="token http-version property">HTTP/1.1</span></span>
<span class="token header"><span class="token header-name keyword">X-SSL-CLIENT-CN</span><span class="token punctuation">:</span> <span class="token header-value">administrator</span></span>
<span class="token header"><span class="token header-name keyword">Foo</span><span class="token punctuation">:</span> <span class="token header-value">x</span></span>
</code></pre></div> <p>In this example, the server assumes that the <code>X-SSL-CLIENT-CN: administrator</code> header was added by the proxy, and so the server assumes that the proxy already did all the necessary authentication and authorization. The attacker is able to perform a request as an entirely different user.</p> <p>Request smuggling is clearly a big problem whenever you have a front-end proxy that does some functionality, but even when it's essentially a dumb proxy, request smuggling can still be used to <a href="https://portswigger.net/web-security/request-smuggling/exploiting#capturing-other-users-requests">steal and exfiltrate data</a> from <em>other</em> user's requests, even if the attacked site is not vulnerable to cross-site scripting or other vulnerabilities.</p> <blockquote> <p>In these attacks, simply having functionality that displays data provided by a user (even sanitised) can be sufficient to <a href="https://portswigger.net/web-security/request-smuggling/exploiting#capturing-other-users-requests">steal the credentials of other users</a>. So something as simple as displaying a user name or a comment could be sufficient.</p> </blockquote> <p>This post is long enough, and there are so many different attacks, that I'm going to leave it there for looking at exploits. If you'd like to learn more about what's possible, along with simple explanations and examples of exploits, I recommend the <a href="https://portswigger.net/">PortSwigger</a> documentation on <a href="https://portswigger.net/web-security/request-smuggling/exploiting">exploiting request smuggling</a>.</p> <h2 id="does-request-smuggling-only-apply-if-i-have-a-proxy-" class="heading-with-anchor"><a href="https://andrewlock.net/understanding-the-worst-dotnet-vulnerability-request-smuggling-and-cve-2025-55315/#does-request-smuggling-only-apply-if-i-have-a-proxy-" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Does request smuggling only apply if I have a proxy?</a></h2> <p>In general, whenever people talk about request smuggling, they normally talk about the case where you have multiple servers: the canonical example is a proxy server and a destination server, as I've discussed so far. But don't be fooled, these issues and vulnerabilities can apply even if you aren't strictly using a proxy.</p> <p>The <em>key</em> feature of the vulnerability is that there's an opportunity for confusion between two "systems", whether they're full "servers" or not. This obviously applies to proxy servers, but could also apply to your application if you're doing anything where you're reading/manipulating/forwarding request streams, or where there's the possibility for confusion <em>inside the same application</em>.</p> <p>For ASP.NET Core applications, if you're working with <code>HttpRequest.Body</code> or <code>HttpRequest.BodyReader</code>, or other similar methods then you <em>may</em> be vulnerable to attacks <em>even if you're not explicitly using a proxy server</em>. Even if you don't think of your application as a proxy or as using a proxy, if you're doing "proxy-like" things, then you could be vulnerable.</p> <blockquote> <p>Put in other words, if you're reading, manipulating, or forwarding request streams directly in ASP.NET Core, as opposed to just relying on the built-in model binding, then you could be at risk to request smuggling attacks. It's very hard to enumerate all the attack vectors, so you should consider any code that does so as a potential avenue of exploitation.</p> </blockquote> <p>We've now covered how request smuggling works and can be exploited in general, so it's time to look at the <em>specific</em> version of request smuggling that is targeted in the .NET CVE-2025-55315 vulnerability.</p> <h2 id="how-does-the-request-smuggling-in-cve-2025-55315-work-" class="heading-with-anchor"><a href="https://andrewlock.net/understanding-the-worst-dotnet-vulnerability-request-smuggling-and-cve-2025-55315/#how-does-the-request-smuggling-in-cve-2025-55315-work-" class="relative text-zinc-800 dark:text-white no-underline hover:underline">How does the request smuggling in CVE-2025-55315 work?</a></h2> <p>As we've seen, HTTP request smuggling is a general technique that relies on differences between proxies and servers in how they parse HTTP requests. I've shown two specific versions of this so far: duplicate <code>Content-Length</code> headers, and <code>Content-Length</code>/<code>Transfer-Encoding</code> confusion, but these are not exhaustive. There are variations on these approaches which also lead to request smuggling.</p> <p>The request smuggling vulnerability in <a href="https://github.com/dotnet/aspnetcore/issues/64033">CVE-2025-55315</a> relies on a variation which (as far as I can tell) was first reported in June 2025 by <a href="https://w4ke.info/2025/06/18/funky-chunks.html">Jeppe Bonde Weikop on their blog</a>. This variation relies on <code>Transfer-Encoding</code> and the <a href="https://datatracker.ietf.org/doc/html/rfc9112#name-chunk-extensions">Chunk Extensions</a> feature.</p> <blockquote> <p>All the details and images in this section are based on the descriptions and examples in <a href="https://w4ke.info/2025/06/18/funky-chunks.html">the original post</a>. That post is excellent, so if you want even more detail and explanation than here, you should definitely read it, and then you can skip the abbreviated version I provide here.</p> </blockquote> <p>To understand the vulnerability, we'll first look at how chunked transfer encoding works and what chunk extensions are. We'll then look at how invalid line-endings can lead to differences in interpretation of a request. Finally, we'll look at how this difference in interpretation can open the way for request smuggling, and how ASP.NET Core fixed the problem.</p> <h3 id="transfer-encoding-chunked-and-chunk-extensions" class="heading-with-anchor"><a href="https://andrewlock.net/understanding-the-worst-dotnet-vulnerability-request-smuggling-and-cve-2025-55315/#transfer-encoding-chunked-and-chunk-extensions" class="relative text-zinc-800 dark:text-white no-underline hover:underline"><code>Transfer-Encoding: chunked</code> and chunk extensions</a></h3> <p>To understand the vulnerability, we first need to understand how <code>Transfer-Encoding: chunked</code> works, and how chunk extensions complicate things.</p> <p>When you're <em>sending</em> a request, you might not always know up-front how big the request is that you're sending. Let's take a practical example of serializing a .NET object to JSON into a request body. The only way to know for sure how big the serialized data is going to be is to actually serialize it. So you <em>could</em> serialize the data to memory <em>before</em> writing the request, but if the data is very big, then that could cause issues with allocating big arrays.</p> <p>Instead, <a href="https://en.wikipedia.org/wiki/Chunked_transfer_encoding"><code>Transfer-Encoding: chunked</code></a> allows sending the request data in multiple "chunks". You need to know the size of each individual chunk, but not the <em>overall</em> size of the data, or how many chunks there are. This works well for serializing to a small buffer, sending that small buffer as a chunk, and then re-using the buffer to serialize the next part, until you have serialized the whole object.</p> <p>In terms of the HTTP request itself, each chunk consists of a header and a body. The header consists of a hexadecimal-formatted number of bytes, followed by a <code>\r\n</code> (<code>CRLF</code>) line ending. The chunk body is then the specified number of bytes, followed by another <code>\r\n</code>. You can have as many chunks as you need, and the request will keep being passed until you send a <code>0</code> length chunk, which indicates the end of the request.</p> <p>As an example, the following HTTP <code>POST</code> shows posting some JSON to an endpoint, but the JSON is sent as three distinct chunks:</p> <p><img src="https://andrewlock.net/content/images/2025/request_smuggling_01.svg" alt="A simple HTTP request using chunked transfer encoding"></p> <ul><li>Chunk 1: The header is <code>9</code> indicating 9 bytes will be sent (followed by <code>\r\n</code>), and then the 9 bytes of the start of the JSON document in the chunk body, again followed by <code>\r\n</code>.</li> <li>Chunk 1: The header is <code>e</code> indicating 14 bytes (14 in hexadecimal is <code>e</code>) will be sent (followed by <code>\r\n</code>), and then the remaining 14 bytes of the end of the JSON document, followed by <code>\r\n</code>.</li> <li>The final chunk is an "empty" chunk, <code>0\r\n\r\n</code>, indicating the end of the request.</li></ul> <p>We're going to see shortly that line endings are very important, so the following diagram shows the same as the above HTTP request, but with the line endings included:</p> <p><img src="https://andrewlock.net/content/images/2025/request_smuggling_02.svg" alt="A simple HTTP request using chunked transfer encoding with the line endings shown"></p> <p>That's "normal" chunked transfer encoding, so now we come to chunk extensions. <a href="https://datatracker.ietf.org/doc/html/rfc9112#name-chunk-extensions">Chunk extensions</a> are part of the HTTP 1.1 protocol which allows for adding key-value pairs of metadata to individual chunks. The following example shows the same request as before, but with a chunk extension, <code>;foo=bar</code> in the second chunk:</p> <p><img src="https://andrewlock.net/content/images/2025/request_smuggling_03.svg" alt="The same HTTP request with a chunk extension in the second chunk"></p> <p>A chunk extension is indicated by a <code>;</code> after the chunk header length, followed by one or more key-value pairs in the form <code>key=value</code>. It's important to understand that chunk extensions are not part of the <em>data</em> that's seen by a request handler; chunk extensions are just metadata about the individual chunk. And tl;dr; they're completely useless 😅</p> <p>To the closest approximation, no-one cares about chunk extensions; client implementations don't send them, and servers just ignore them. If that's the case, how can they be the cause of such a problematic bug in .NET?</p> <p>The problem is <em>how</em> the implementation ignores them…</p> <h3 id="invalid-chunk-extensions-with-incorrect-line-endings" class="heading-with-anchor"><a href="https://andrewlock.net/understanding-the-worst-dotnet-vulnerability-request-smuggling-and-cve-2025-55315/#invalid-chunk-extensions-with-incorrect-line-endings" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Invalid chunk extensions with incorrect line endings</a></h3> <p>In general with HTTP, clients and server implementations often try to follow the <a href="https://en.wikipedia.org/wiki/Robustness_principle"><em>robustness principle</em></a> of "be conservative in what you send, and lenient with what you accept". Unfortunately, it's this very leniency which can sometimes leave us in hot water. After all, it was leniency around requests containing both a <code>Content-Length</code> and <code>Transfer-Encoding</code> header that was the root cause of the original request smuggling exploit.</p> <blockquote> <p>Note that the <a href="https://datatracker.ietf.org/doc/html/rfc9112#section-6.3-2.3">HTTP 1.1 RFC now forbids</a> forwarding both these headers, precisely to avoid request smuggling attacks.</p> </blockquote> <p>For chunk extensions though, leniency is often <em>accidentally</em> built in to the server implementations. Given that no implementations actually <em>do</em> anything with the chunk extensions, the canonical approach to handling them when parsing a chunk header is just to <em>ignore</em> them. When a <code>;</code> is parsed, it's common to just look for the end of the line, and ignore everything in between.</p> <p>For ASP.NET Core (prior to the fix), on finding a <code>;</code> in the chunk header, <a href="https://github.com/dotnet/aspnetcore/blob/4cf89470a7866a963d7118bb1ba90dc35683965c/src/Servers/Kestrel/Core/src/Internal/Http/Http1ChunkedEncodingMessageBody.cs#L348-L356">Kestrel would "parse" the extension</a>, but in practice, it would search for the carriage return <code>\r</code> and then check for the following <code>\n</code>, skipping everything in between, a little bit like this (very simplified compared to <a href="https://github.com/dotnet/aspnetcore/blob/4cf89470a7866a963d7118bb1ba90dc35683965c/src/Servers/Kestrel/Core/src/Internal/Http/Http1ChunkedEncodingMessageBody.cs#L348-L356">original code</a>):</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">private</span> <span class="token return-type class-name"><span class="token keyword">void</span></span> <span class="token function">ParseExtension</span><span class="token punctuation">(</span><span class="token class-name">ReadOnlySequence<span class="token punctuation">&lt;</span><span class="token keyword">byte</span><span class="token punctuation">&gt;</span></span> buffer<span class="token punctuation">)</span>
<span class="token punctuation">{</span>
    <span class="token keyword">while</span><span class="token punctuation">(</span><span class="token boolean">true</span><span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        <span class="token comment">// Chunk-extensions not currently parsed</span>
        <span class="token comment">// Just drain the data</span>
        <span class="token class-name"><span class="token keyword">var</span></span> extensionCursor <span class="token operator">=</span> buffer<span class="token punctuation">.</span><span class="token function">PositionOf</span><span class="token punctuation">(</span>ByteCR<span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token class-name"><span class="token keyword">var</span></span> suffixBuffer <span class="token operator">=</span> buffer<span class="token punctuation">.</span><span class="token function">Slice</span><span class="token punctuation">(</span>extensionCursor<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// skips over extensionCursor bytes</span>

        <span class="token class-name"><span class="token keyword">var</span></span> suffixSpan <span class="token operator">=</span> suffixBuffer<span class="token punctuation">.</span><span class="token function">Slice</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">,</span> <span class="token number">2</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">ToSpan</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

        <span class="token keyword">if</span> <span class="token punctuation">(</span>suffixSpan<span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span> <span class="token operator">==</span> <span class="token char">'\n'</span><span class="token punctuation">)</span>
        <span class="token punctuation">{</span>
            <span class="token comment">// We consumed the \r\n at the end of the extension, so switch modes.</span>
            <span class="token keyword">return</span><span class="token punctuation">;</span>
        <span class="token punctuation">}</span>

        <span class="token comment">// Otherwise, keep reading data until we do find \r\n</span>
        buffer <span class="token operator">=</span> <span class="token function">ReadMoreData</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>The implementation in ASP.NET Core wasn't particularly special; <a href="https://github.com/golang/go/blob/1d45a7ef560a76318ed59dfdb178cecd58caf948/src/net/http/internal/chunked.go#L193-L199">most servers</a> simply skip over the bytes until they find a <code>\r\n</code>. The big question is exactly <em>how</em> the servers search for <code>\r\n</code>. What happens if they see a lone <code>\r</code>, or a lone <code>\n</code>? Do they treat that the same as a <code>\r\n</code>? Do they throw an error if they find an un-paired <code>\r</code> or <code>\n</code>? Or do they ignore it and keep looking for a <code>\r\n</code>?</p> <p>That ambiguity is at the heart of the CVE-2025-55315 request smuggling vulnerability. Differences in how proxy and server implementations treat standalone <code>\r</code> or <code>\n</code> in a chunk header allow for request smuggling exploits that use this ambiguity.</p> <blockquote> <p>Note that according to <a href="https://www.rfc-editor.org/errata/eid7633">the RFC</a>, implementers must <em>not</em> treat <code>\r</code> or <code>\n</code> as "valid" line terminators for a chunk header, and neither <code>\r</code> or <code>\n</code> are allowed elsewhere in chunk headers, so correct implementations must reject requests that include these standalone line endings in chunk headers.</p> </blockquote> <p>For complete clarity, the following example is the same as the previous implementation but with an <em>invalid</em> chunk header in the chunk extension of the second chunk. Instead of ending with <code>\r\n</code>, the chunk extension ends with a single <code>\n</code>:</p> <p><img src="https://andrewlock.net/content/images/2025/request_smuggling_04.svg" alt="An HTTP request with an ambiguous line ending in a chunk extension"></p> <p>That's the root cause of the request smuggling vulnerability, so in the next section we'll look at <em>how</em> this could be used to craft a malicious HTTP request.</p> <h3 id="exploiting-invalid-chunk-extensions-for-request-smuggling" class="heading-with-anchor"><a href="https://andrewlock.net/understanding-the-worst-dotnet-vulnerability-request-smuggling-and-cve-2025-55315/#exploiting-invalid-chunk-extensions-for-request-smuggling" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Exploiting invalid chunk extensions for request smuggling</a></h3> <p>Just as with other examples of request smuggling, the chunk extensions approach relies on differences in how a proxy parses a request compared to a subsequent server. This difference means the proxy sees one request, while the destination request sees two requests, and allows for <a href="https://andrewlock.net/understanding-the-worst-dotnet-vulnerability-request-smuggling-and-cve-2025-55315/#how-can-an-attacker-exploit-request-smuggling-">all the same exploits I discussed earlier</a>.</p> <blockquote> <p>As discussed, these examples come from <a href="https://w4ke.info/2025/06/18/funky-chunks.html">this excellent blog post</a>, so see that post for more details, variations on the attack, and further ways to exploit the vulnerability.</p> </blockquote> <p>The following example shows a malicious HTTP request that exploits a difference in line-ending handling between a proxy and the destination server to smuggle a request to the <code>/admin</code> endpoint. We can imagine that the proxy is configured to automatically reject requests to <code>/admin</code> normally, and the server assumes that the proxy handles that for us.</p> <p><img src="https://andrewlock.net/content/images/2025/request_smuggling_05.svg" alt="A request smuggling attack exploiting differences between a proxy and server implementation"></p> <p>In this example the attacker creates a malformed chunk header with a chunk extension by sending <code>2;\n</code>. The <code>;</code> ensures that both the proxy and and server treat the header as a chunk extension, but using <code>\n</code> instead of <code>\r\n</code> results in differential parsing:</p> <ul><li>The proxy only sees a single request: <ul><li>It treats the <code>\n</code> as a "valid" line-ending for the chunk header</li> <li>It then treats the <code>xx</code> as the chunk body</li> <li><code>47</code> is the next chunk header</li> <li>The next 71 bytes (<code>47</code> is hex, which is 71 in decimal) are treated as the chunk body.</li> <li>Finally there's the empty chunk block</li></ul> </li> <li>The server sees two requests: <ul><li>The server ignores the lone <code>\n</code>, and skips all the way to <code>xx\r\n</code></li> <li>It then treats the <code>47</code> as the chunk body</li> <li>It sees an ending chunk,<code>0\r\n\r\n</code> and thinks the request is over</li> <li>The remaining data is treated as a completely separate request, which contains only an empty chunk in the body.</li></ul> </li></ul> <p>This is pretty much the simplest example, but you can essentially exploit this difference in all the ways I described previously. <em>Exactly</em> what the implications are for <em>your</em> application are hard to say, but given that all sorts of security bypass, credential stealing, and injection attacks are possible, it's easy to understand why the vulnerability received a CVSS rating of 9.9.</p> <blockquote> <p>One very interesting thing I found was looking at the security advisories for the same flaw in other HTTP implementations from other languages. In the python <a href="https://github.com/aio-libs/aiohttp">aiohttp</a> and ruby <a href="https://github.com/puma/puma">puma</a> servers, for example, give <a href="https://github.com/advisories/GHSA-8495-4g3g-x7pr">the vulnerability only a moderate severity</a> rating in <a href="https://github.com/advisories/GHSA-c2f4-cvqm-65w2">both cases</a>. In <a href="https://github.com/netty/netty">netty</a> it's even given <a href="https://github.com/netty/netty/security/advisories/GHSA-fghv-69vj-qj49">a low severity</a>.</p> <p>As far as I can tell, these servers are essentially vulnerable in the same way as ASP.NET Core is, so it's just an interesting data point, and I think reflects how Microsoft really want to make sure this gets the visibility it deserves and that customers patch their apps!</p> </blockquote> <h3 id="how-was-the-vulnerability-fixed-" class="heading-with-anchor"><a href="https://andrewlock.net/understanding-the-worst-dotnet-vulnerability-request-smuggling-and-cve-2025-55315/#how-was-the-vulnerability-fixed-" class="relative text-zinc-800 dark:text-white no-underline hover:underline">How was the vulnerability fixed?</a></h3> <p>As with most fixes for request-smuggling, the solution is to stop being lenient and/or ambiguous about how standalone line-endings are handled in chunk headers.</p> <p>In ASP.NET Core, <a href="https://github.com/dotnet/aspnetcore/pull/64037">the PR that fixes the issue</a> does so by explicitly checking for <em>any</em> line-endings, instead of just looking for <code>\r</code>. If it finds a line ending and it's <em>not</em> strictly <code>\r\n</code>, then Kestrel now throws a <code>KestrelBadHttpRequestException</code> and returns a <code>400</code> response.</p> <p><img src="https://andrewlock.net/content/images/2025/request_smuggling_06.png" alt="A screenshot of the fix from the PR https://github.com/dotnet/aspnetcore/pull/64037"></p> <blockquote> <p>I'll mention here there <em>is</em> an <code>AppContext</code> switch for <a href="https://github.com/dotnet/aspnetcore/blob/33ab51daf86b690432f44749824972c1f5019e83/src/Servers/Kestrel/Core/src/Internal/Http/Http1ChunkedEncodingMessageBody.cs#L31"><em>opting-in</em> to the dangerous/vulnerable parsing behaviour</a> after you have patched your application, but please don't use it, I can't believe there's really a good (or <em>safe</em>) reason to.😅</p> </blockquote> <p>The vulnerability has been patched in ASP.NET Core, so what should you do?</p> <h2 id="what-should-you-do-" class="heading-with-anchor"><a href="https://andrewlock.net/understanding-the-worst-dotnet-vulnerability-request-smuggling-and-cve-2025-55315/#what-should-you-do-" class="relative text-zinc-800 dark:text-white no-underline hover:underline">What should you do?</a></h2> <p>Obviously the <em>good</em> news here is that there is a fix for ASP.NET Core. As described <a href="https://github.com/dotnet/aspnetcore/issues/64033">in the original issue</a>, the important thing is to update to the latest supported version of ASP.NET Core as soon as possible.</p> <blockquote> <p>There's no announced evidence of the request smuggling vulnerability being exploited in the wild, but given the vast number of ways that request smuggling <em>could</em> be used, would we even know? 🤔</p> </blockquote> <p>That means you should update your version of .NET 8, .NET 9, or .NET 10:</p> <table><thead><tr><th></th><th>Vulnerable versions</th><th>Lowest patched version</th></tr></thead><tbody><tr><td>.NET 10</td><td>10.0.0-rc1</td><td>10.0.0-rc2</td></tr><tr><td>.NET 9</td><td>9.0.0 - 9.0.9</td><td>9.0.10</td></tr><tr><td>.NET 8</td><td>8.0.0 - 8.0.20</td><td>8.0.21</td></tr></tbody></table> <p>If you're using ASP.NET Core 2.3 on .NET Framework, then you'll need to update your version of <em>Microsoft.AspNetCore.Server.Kestrel.Core</em>:</p> <table><thead><tr><th></th><th>Vulnerable versions</th><th>Lowest patched version</th></tr></thead><tbody><tr><td>Microsoft.AspNetCore.Server.Kestrel.Core</td><td>2.0.0-2.3.0</td><td>2.3.6</td></tr></tbody></table> <p>If you are doing self-contained deployments of your applications, you'll need to update to the patched versions and then redeploy your applications.</p> <p>And if you're using older versions of .NET Core? Well, then you <em>can't</em> patch… <a href="https://www.herodevs.com/">HeroDevs</a> provide additional support for out-of-support versions of .NET (<a href="https://github.com/dotnet/aspnetcore/issues/64033#issuecomment-3411924593">and have confirmed they'll be patching it in .NET 6</a>), but this vulnerability is present in basically <em>all</em> versions of .NET Core as far as I can tell. I've personally tested down to .NET Core 3.0 and I can confirm that the vulnerability is there <em>and there are no patches coming for you</em>. The best thing to do is to update to a supported version of .NET.</p> <blockquote> <p>⚠️ If you are running ASP.NET Core using &lt;=.NET Core 3.0, .NET Core 3.1, .NET 5, .NET 6 (<a href="https://www.herodevs.com/blog-posts/critical-asp-net-vulnerability-cve-2025-55315-reported-upgrade-now">unless supported by HeroDevs</a>), or .NET 7, then you are vulnerable, and there are no patches. You should update to a supported version of .NET as soon as possible. Ironically, if you're stuck on old .NET Framework Web Forms or MVC applications <a href="https://github.com/dotnet/aspnetcore/issues/64033#issuecomment-3442910860">you are apparently <em>not</em> vulnerable</a>.</p> </blockquote> <p>It's worth noting that if you are stuck on one of these old framework versions and <em>can't</em> upgrade, then probably the best way to protect yourself is to ensure that you have a proxy in front of your application which is confirmed to not be vulnerable (though obviously you are likely vulnerable to <em>other</em> exploits 😅).</p> <p>For example, <a href="https://azure.github.io/AppService/2025/10/20/dotnet-on-windows.html">Azure App Services (AAS) confirmed</a> that applications running in AAS are no longer vulnerable, even if you haven't updated, because the proxy that AAS uses (itself a <a href="https://devblogs.microsoft.com/dotnet/bringing-kestrel-and-yarp-to-azure-app-services/">YARP based ASP.NET Core proxy</a>) has been patched. By blocking the requests at the proxy level, ambiguous requests will never make it to your application, so you are protected.</p> <p>Unfortunately, right now, it's not clear exactly where you stand if you're using a service other than AAS for hosting your applications. Even IIS hasn't been confirmed to be safe or vulnerable at this point, but I did some unofficial testing on my Windows 11 box, and as fat as I can tell, it <em>is</em> vulnerable.</p> <blockquote> <p>Note that various people <a href="https://github.com/dotnet/aspnetcore/issues/64033#issuecomment-3445099135">in the original issue</a> are attempting to test IIS by using the <code>Content-Length</code>/<code>Transfer-Encoding</code> version of request smuggling, which is not applicable here; we're interested in the chunk-extensions based version.</p> </blockquote> <p>Another interesting point is that this is vulnerability in HTTP/1.0 and HTTP/1.1 <em>only</em>; it is not a vulnerability in HTTP/2 or HTTP/3. HTTP/2 and HTTP/3 do not support chunked transfer encoding, and instead uses a different, more efficient, binary framing layer for data streaming. So another way to protect those applications which you <em>can't</em> upgrade may be to enforce that client's can <em>only</em> use HTTP/2 or HTTP/3. Be aware that's liable to break a <em>lot</em> of clients that are still using HTTP/1.1 though!</p> <blockquote> <p>You can configure the HTTP protocols allowed by Kestrel by configuring your Kestrel endpoints. <a href="https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel/endpoints?view=aspnetcore-9.0#configure-http-protocols">The documentation</a> shows various ways to do this.</p> </blockquote> <h2 id="how-to-know-if-you-re-affected-" class="heading-with-anchor"><a href="https://andrewlock.net/understanding-the-worst-dotnet-vulnerability-request-smuggling-and-cve-2025-55315/#how-to-know-if-you-re-affected-" class="relative text-zinc-800 dark:text-white no-underline hover:underline">How to know if you're affected?</a></h2> <p>The "simplest" way to know if you're affected is to check the version of .NET you're using to run your applications, using <code>dotnet --info</code> and verify that you're using one of the patched versions. If you are, you're safe. That's the only "supported" way to know that you're safe, and it's the one way I would recommend. As far as I can tell, there isn't currently a generalised tool to point at an application to find out if it's vulnerable, though it would likely be possible to write one.</p> <p>The folks at HeroDevs <a href="https://github.com/sirredbeard/CVE-2025-55315-repro">re-implemented the functional tests</a> from the original ASP.NET Core fix as a console application compiled against multiple versions of ASP.NET Core. They used this to confirm that unpatched versions of .NET 8-.NET 10 are vulnerable, while <em>patched</em> versions are not. They also used this to verify .NET 6 is vulnerable, and I tweaked it to confirm everything down to at least .NET Core 3.0 is vulnerable.</p> <p>The <a href="https://github.com/sirredbeard/CVE-2025-55315-repro">test in the repro</a> works by sending a chunked transfer encoding request to ASP.NET Core, with an invalid line ending in a chunk extension header. The vulnerability is identified by ASP.NET Core "hanging", waiting for more data, until it eventually times out. The "fixed" version immediately throws the <code>BadRequest</code> exception included in the fix.</p> <blockquote> <p>I <a href="https://www.youtube.com/watch?v=LE758TvUE5c">saw some confusion</a> about this test online; the argument was "if both the fixed and broken versions throw an exception, why does it matter"? However, that's not the point of the test. The fact that Kestrel is paused waiting for more data indicates that a smuggled HTTP request <em>would</em> have been executed. You can see how this can be leveraged to exfiltrate data or attack other users both in <a href="https://w4ke.info/2025/06/18/funky-chunks.html#exploiting-live-users">the chunk extensions blog</a> or <a href="https://portswigger.net/web-security/request-smuggling/exploiting">on PortSwigger's site</a>.</p> </blockquote> <p>I used a similar approach to try to understand whether IIS might be vulnerable by sending the same crafted HTTP request to IIS and seeing if it hung until timing out: it did on my version of IIS (<code>10.0.26100.1882</code>):</p> <div class="pre-code-wrapper"><pre class="language-bash"><code class="language-bash"><span class="token comment"># Send an HTTP request with an invalid chunk extension, and see</span>
<span class="token comment"># if it times out or if it's rejected with a 400... It times out 🙁</span>
<span class="token builtin class-name">echo</span> <span class="token parameter variable">-e</span> <span class="token string">"GET / HTTP/1.1<span class="token entity" title="\r">\r</span><span class="token entity" title="\n">\n</span>Host:<span class="token entity" title="\r">\r</span><span class="token entity" title="\n">\n</span>Transfer-Encoding: chunked<span class="token entity" title="\r">\r</span><span class="token entity" title="\n">\n</span><span class="token entity" title="\r">\r</span><span class="token entity" title="\n">\n</span>1;<span class="token entity" title="\n">\n</span>"</span> <span class="token punctuation">\</span>
  <span class="token operator">|</span> <span class="token function">nc</span> localhost <span class="token number">80</span>
</code></pre></div> <p>So does that definitely mean IIS is vulnerable? No, don't trust me, I'm not a security researcher 😅 But until you hear otherwise, I would play it safe and assume that IIS <em>won't</em> protect you from chunk extension request smuggling attacks. And in general, I would apply the same rules to any other proxies you are relying on in your infrastructure.</p> <p>And as a final reminder, even though request smuggling is typically described and demonstrated using a proxy in front of your server, just <em>not</em> using a proxy does <em>not</em> mean you're automatically safe. If you're reading, manipulating, or forwarding request streams directly in ASP.NET Core, as opposed to just relying on the built-in model binding, then you <em>might</em> be at risk to request smuggling attacks. It's best to play it safe, patch your apps, and wherever possible leave the complexity of manipulating requests to ASP.NET Core.</p> <p>In general, I would make sure to subscribe to <a href="https://github.com/dotnet/aspnetcore/issues/64033">the ASP.NET Core issue on GitHub</a>, as it's likely that any more announcements around the issue will also be reported there.</p> <h2 id="summary" class="heading-with-anchor"><a href="https://andrewlock.net/understanding-the-worst-dotnet-vulnerability-request-smuggling-and-cve-2025-55315/#summary" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Summary</a></h2> <p>In this post I discuss the recent ASP.NET Core vulnerability: <a href="https://github.com/dotnet/aspnetcore/issues/64033">Microsoft Security Advisory CVE-2025-55315: .NET Security Feature Bypass Vulnerability</a>. This advisory warns of a request smuggling vulnerability that affects basically all versions of ASP.NET Core.</p> <p>I described how request smuggling works in general, using a simple example of request smuggling to show how ambiguity in how HTTP is parsed can lead to HTTP proxies and HTTP servers in handling the same HTTP request in different ways. This can lead to the server seeing two requests where the proxy only sees a single request.</p> <p>After walking through a request smuggling example, I discussed some of the ways attackers could exploit a request smuggling vulnerability. That includes reflecting malicious data to other users of your app, exfiltrating authentication credentials or other data from client requests, invoking endpoints that shouldn't be publicly accessible, and various other attacks.</p> <p>Next I walked through the specific request smuggling vulnerability identified in CVE-2025-55315. This uses ambiguities in the parsing of chunk extensions when sending requests that use chunked transfer encoding. Chunk extensions are generally ignored by all servers, but lenient handling can lead to differential handling between proxy and server, providing an avenue for request smuggling.</p> <p>Finally, I walked through the mitigation steps you should take: patching your applications. I described the information we currently have about vulnerable or patched proxy servers, and how old versions of ASP.NET Core are not going to be getting patches, so will remain vulnerable (shout out again <a href="https://www.herodevs.com/blog-posts/critical-asp-net-vulnerability-cve-2025-55315-reported-upgrade-now">to HeroDevs for supporting .NET 6</a>). If you're running in AAS, <a href="https://azure.github.io/AppService/2025/10/20/dotnet-on-windows.html">then you're ok</a>, but otherwise, you need to check with your proxy provider to establish whether you are vulnerable or not.</p> ]]></content:encoded><category><![CDATA[ASP.NET Core;Security]]></category></item><item><title><![CDATA[Adding metadata to fallback endpoints in ASP.NET Core]]></title><description><![CDATA[In this post I discuss fallback endpoints and show how adding metadata to MVC or Razor Page fallback endpoints has some quirks to be aware of]]></description><link>https://andrewlock.net/adding-metadata-to-fallback-endpoints-in-aspnetcore/</link><guid isPermaLink="true">https://andrewlock.net/adding-metadata-to-fallback-endpoints-in-aspnetcore/</guid><pubDate>Wed, 22 Oct 2025 10:00:00 GMT</pubDate><dc:creator><![CDATA[Andrew Lock]]></dc:creator><media:content url="https://andrewlock.net/content/images/2025/fallbackroutes_banner.png" medium="image" /><content:encoded><![CDATA[<img src="https://andrewlock.net/content/images/2025/fallbackroutes_banner.png" /><p>In this post I describe how metadata works for "fallback" endpoints in ASP.NET Core. First I briefly discuss the routing infrastructure of ASP.NET Core, and how you can add metadata to endpoints to drive other functionality. Next I describe what fallback endpoints are and why they are useful. Finally, I describe how adding metadata works for fallback endpoints, and why this might not work as you first expect for MVC and Razor Pages apps.</p> <h2 id="background-the-routing-infrastructure-in-asp-net-core" class="heading-with-anchor"><a href="https://andrewlock.net/adding-metadata-to-fallback-endpoints-in-aspnetcore/#background-the-routing-infrastructure-in-asp-net-core" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Background: the routing infrastructure in ASP.NET Core</a></h2> <p>The routing infrastructure in ASP.NET Core is a fundamental component that sits as part of the middleware pipeline, it is the primary way that incoming URLs are mapped to "handlers" which are responsible for executing code and generating a response. For example, the following hello world app maps a single route <code>/</code> to a handler (the lambda method) which returns a string <code>"Hello World!"</code>:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token class-name"><span class="token keyword">var</span></span> builder <span class="token operator">=</span> WebApplication<span class="token punctuation">.</span><span class="token function">CreateBuilder</span><span class="token punctuation">(</span>args<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token class-name"><span class="token keyword">var</span></span> app <span class="token operator">=</span> builder<span class="token punctuation">.</span><span class="token function">Build</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

app<span class="token punctuation">.</span><span class="token function">MapGet</span><span class="token punctuation">(</span><span class="token string">"/"</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=&gt;</span> <span class="token string">"Hello World!"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

app<span class="token punctuation">.</span><span class="token function">Run</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre></div> <p>It's not apparent from this simple example, but routing in ASP.NET Core is driven by <em>two</em> main pieces of middleware:</p> <ul><li><code>EndpointRoutingMiddleware</code>—This middleware chooses which registered endpoint to execute for a given request at runtime. It's sometimes referred to as <code>RoutingMiddleware</code> (which is the name I use in this post).</li> <li><code>EndpointMiddleware</code>—This middleware is typically placed at the end of the middleware pipeline. The middleware executes the endpoint selected by the <code>RoutingMiddleware</code> for a given request.</li></ul> <blockquote> <p>Prior to .NET 6, you would typically add these middleware to your pipeline explicitly by calling <code>UseRouting()</code> and <code>UseEndpoints()</code>. However <code>WebApplication</code> adds these for your automatically, so the explicit calls generally aren't required. I described in more detail how <code>WebApplication</code> works and how it compares to the "traditional" <code>Startup</code> approach in <a href="https://andrewlock.net/exploring-dotnet-6-part-4-building-a-middleware-pipeline-with-webapplication/#a-hello-world-pipeline">previous posts</a>.</p> </blockquote> <p>You might wonder why ASP.NET Core uses two pieces of middleware instead of just one. Separating the <em>selection</em> of an endpoint from the <em>execution</em> of an endpoint gives a specific advantage—you can execute middleware <em>between</em> these two events. This middleware can change its behaviour based on <em>metadata</em> associated with the endpoint that is going to be executed, <em>before</em> that endpoint executes.</p> <h2 id="adding-metadata-to-endpoints-to-control-functionality" class="heading-with-anchor"><a href="https://andrewlock.net/adding-metadata-to-fallback-endpoints-in-aspnetcore/#adding-metadata-to-endpoints-to-control-functionality" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Adding metadata to endpoints to control functionality</a></h2> <p>As already described, endpoint routing is a core feature of ASP.NET Core. Adding <em>metadata</em> to specific endpoints is important for controlling the behaviour of different endpoints.</p> <p>There are several features that rely on metadata to work correctly. The most common examples are the <code>AuthorizationMiddleware</code> and <code>CorsMiddleware</code>, which must be placed <em>between</em> the <code>RoutingMiddleware</code> and <code>EndpointMiddleware</code> so that they know which policies to apply for the selected endpoint.</p> <p><img src="https://andrewlock.net/content/images/2019/endpoint_routing.svg" alt="Image of routing in ASP.NET Core "></p> <p>For example, you might have a global authorization requirement policy which applies to all endpoints in your application. You would then apply specific "Allow anonymous access" policies to the "login" and "forgotten password" endpoints so they can be accessed when you're not logged in.</p> <p>For this functionality to work, you need to apply metadata to the login and forgot password policies. You typically apply metadata in one of two ways:</p> <ul><li>Adding attributes, e.g. <code>[AllowAnonymous]</code> or <code>[Authorize]</code>, to an MVC action or Razor Page.</li> <li>Using an extension method, e.g. <code>AllowAnonymous()</code> or <code>RequireAuthorization()</code> to a minimal API or other endpoint.</li></ul> <p>When the <code>RoutingMiddleware</code> endpoint executes, it selects the endpoint that will execute. Subsequent middleware can then inspect the endpoint details to see if there's any attached middleware and act accordingly.</p> <p>Let's look at the authorization case again. Calling the <code>AllowAnonymous()</code> method, for example, <a href="https://github.com/dotnet/aspnetcore/blob/d4349298d046f24282a6030d8935e81aa2440141/src/Security/Authorization/Policy/src/AuthorizationEndpointConventionBuilderExtensions.cs#L125">adds an instance of the <code>AllowAnonymousAttribute</code> as metadata to the endpoint</a>:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token keyword">class</span> <span class="token class-name">AuthorizationEndpointConventionBuilderExtensions</span>
<span class="token punctuation">{</span>
    <span class="token keyword">private</span> <span class="token keyword">static</span> <span class="token keyword">readonly</span> <span class="token class-name">IAllowAnonymous</span> _allowAnonymousMetadata <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token constructor-invocation class-name">AllowAnonymousAttribute</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token return-type class-name">TBuilder</span> <span class="token generic-method"><span class="token function">AllowAnonymous</span><span class="token generic class-name"><span class="token punctuation">&lt;</span>TBuilder<span class="token punctuation">&gt;</span></span></span><span class="token punctuation">(</span><span class="token keyword">this</span> <span class="token class-name">TBuilder</span> builder<span class="token punctuation">)</span> <span class="token keyword">where</span> <span class="token class-name">TBuilder</span> <span class="token punctuation">:</span> <span class="token type-list"><span class="token class-name">IEndpointConventionBuilder</span></span>
    <span class="token punctuation">{</span>
        builder<span class="token punctuation">.</span><span class="token function">Add</span><span class="token punctuation">(</span>endpointBuilder <span class="token operator">=&gt;</span>
        <span class="token punctuation">{</span>
            endpointBuilder<span class="token punctuation">.</span>Metadata<span class="token punctuation">.</span><span class="token function">Add</span><span class="token punctuation">(</span>_allowAnonymousMetadata<span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token keyword">return</span> builder<span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>Similarly, calling <code>RequireAuthorization()</code> <a href="https://github.com/dotnet/aspnetcore/blob/d4349298d046f24282a6030d8935e81aa2440141/src/Security/Authorization/Policy/src/AuthorizationEndpointConventionBuilderExtensions.cs#L21">adds an instance of the <code>AuthorizeAttribute</code></a> as metadata:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token keyword">class</span> <span class="token class-name">AuthorizationEndpointConventionBuilderExtensions</span>
<span class="token punctuation">{</span>
    <span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token return-type class-name">TBuilder</span> <span class="token generic-method"><span class="token function">RequireAuthorization</span><span class="token generic class-name"><span class="token punctuation">&lt;</span>TBuilder<span class="token punctuation">&gt;</span></span></span><span class="token punctuation">(</span><span class="token keyword">this</span> <span class="token class-name">TBuilder</span> builder<span class="token punctuation">)</span> <span class="token keyword">where</span> <span class="token class-name">TBuilder</span> <span class="token punctuation">:</span> <span class="token type-list"><span class="token class-name">IEndpointConventionBuilder</span></span>
    <span class="token punctuation">{</span>
        <span class="token keyword">return</span> builder<span class="token punctuation">.</span><span class="token function">RequireAuthorization</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token constructor-invocation class-name">AuthorizeAttribute</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>The <code>AuthorizationMiddleware</code>, which runs after the <code>RoutingMiddleware</code> and before the <code>EndpointMiddleware</code>, can inspect the selected endpoint and read this metadata to decide what authorization policies to apply to the selected endpoint.</p> <h2 id="trying-it-out-in-a-sample-app" class="heading-with-anchor"><a href="https://andrewlock.net/adding-metadata-to-fallback-endpoints-in-aspnetcore/#trying-it-out-in-a-sample-app" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Trying it out in a sample app</a></h2> <p>We can try this all out in a simple minimal API app that just shows the basics:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">using</span> <span class="token namespace">Microsoft<span class="token punctuation">.</span>AspNetCore<span class="token punctuation">.</span>Authorization</span><span class="token punctuation">;</span>

<span class="token class-name"><span class="token keyword">var</span></span> builder <span class="token operator">=</span> WebApplication<span class="token punctuation">.</span><span class="token function">CreateBuilder</span><span class="token punctuation">(</span>args<span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token comment">// Configure basic cookie authentication (so that authorization works)</span>
builder<span class="token punctuation">.</span>Services<span class="token punctuation">.</span><span class="token function">AddAuthentication</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">AddCookie</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
builder<span class="token punctuation">.</span>Services<span class="token punctuation">.</span><span class="token function">AddAuthorization</span><span class="token punctuation">(</span>opt <span class="token operator">=&gt;</span>
<span class="token punctuation">{</span>
    <span class="token comment">// Unless specified, you must be logged in to be authorized</span>
    opt<span class="token punctuation">.</span>FallbackPolicy <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token constructor-invocation class-name">AuthorizationPolicyBuilder</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
                            <span class="token punctuation">.</span><span class="token function">RequireAuthenticatedUser</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
                            <span class="token punctuation">.</span><span class="token function">Build</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token class-name"><span class="token keyword">var</span></span> app <span class="token operator">=</span> builder<span class="token punctuation">.</span><span class="token function">Build</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

app<span class="token punctuation">.</span><span class="token function">MapGet</span><span class="token punctuation">(</span><span class="token string">"/"</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=&gt;</span> <span class="token string">"Hello World!"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// You can't view this unless logged in</span>
app<span class="token punctuation">.</span><span class="token function">MapGet</span><span class="token punctuation">(</span><span class="token string">"/Account/Login"</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=&gt;</span> <span class="token string">"Login page"</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">AllowAnonymous</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// You can always view this</span>
app<span class="token punctuation">.</span><span class="token function">Run</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre></div> <p>This is a simple minimal API that has two endpoints:</p> <ul><li><code>/</code> the home page</li> <li><code>/Account/Login</code> which is the login page</li></ul> <p>For this app I added rudimentary authentication and authorization. I've not added any way to actually sign in or anything, literally I've just configured the minimum requirements. We then configure a global policy that says "unless otherwise specified, you must be logged in to be authorized to view the page".</p> <p>The result is that if we try to run the app, and navigate to <code>/</code>, We're automatically redirected to the <code>/Account/Login</code> page, because we're not (and can't be) logged in. However, we <em>can</em> view the <code>/Account/Login</code> page, because it's decorated with <code>AllowAnonymous()</code> metadata.</p> <p><img src="https://andrewlock.net/content/images/2025/fallbackroutes_01.png" alt="The authorization metadata prevents directly accessing /, but allows accessing the /Acount/Login route"></p> <p>Many cross-cutting features which are implemented in middleware but which must behave differently for specific endpoints use this metadata approach. Authorization is the canonical example, but <a href="https://learn.microsoft.com/en-us/aspnet/core/security/cors?view=aspnetcore-9.0">CORS policies</a>, <a href="https://learn.microsoft.com/en-us/aspnet/core/fundamentals/openapi/aspnetcore-openapi?view=aspnetcore-9.0">OpenAPI documentation</a>, <a href="https://github.com/andrewlock/NetEscapades.AspNetCore.SecurityHeaders">or my security headers library</a> work in similar ways.</p> <h2 id="fallback-routing-in-asp-net-core" class="heading-with-anchor"><a href="https://andrewlock.net/adding-metadata-to-fallback-endpoints-in-aspnetcore/#fallback-routing-in-asp-net-core" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Fallback routing in ASP.NET Core</a></h2> <p>Routing is a core component of ASP.NET Core, and consists of many distinct concepts. For example:</p> <ul><li><strong>Route patterns</strong>—This is the URL path pattern that should be matched to an incoming request.</li> <li><strong>Handlers</strong>—Every route pattern has an associated handler which is is the code that runs to generate a response when the pattern matches an incoming request.</li> <li><strong>Route parameters</strong>—These are variable sections in a route pattern that can be extracted and automatically converted to types for use in your handlers.</li> <li><strong>Binding</strong>—You can automatically extract details from an incoming request for use in your handlers</li></ul> <p>and many more! These features manifest to greater or lesser extent whichever part of ASP.NET Core you're using, whether it's MVC, Razor Pages, Blazor, or minimal APIs.</p> <p>The fundamental first step of routing is deciding <em>which</em> route pattern the incoming URL matches. This is done by building a graph of the registered endpoints and then finding the correct match for the incoming URL:</p> <p><img src="https://andrewlock.net/content/images/2020/graphs_2_01.png" alt="A ValuesController endpoint routing application"></p> <blockquote> <p>I discussed how ASP.NET Core creates an endpoint graph and how you can visualize that graph in my series <a href="https://andrewlock.net/series/visualizing-asp-net-core-3-endpoints-using-graphvizonline/">Visualizing ASP.NET Core 3.0 endpoints using GraphvizOnline</a>.</p> </blockquote> <p>Each route is associated with a handler, but there's also the concept of a <em>fallback</em> route. This route matches <em>any</em> incoming request, as long as the request is <em>not</em> matched by any other route, and it invokes the provided handler.</p> <p>There are many different ways to add a fallback route to your app, and in general it will depend which part of ASP.NET Core you're using; minimal APIs, MVC, Razor Pages etc. The simplest approach is to call the <code>MapFallback()</code> method, and provide a handler to execute directly. For example, we could add a fallback endpoint to my previous sample:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token class-name"><span class="token keyword">var</span></span> app <span class="token operator">=</span> builder<span class="token punctuation">.</span><span class="token function">Build</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

app<span class="token punctuation">.</span><span class="token function">MapGet</span><span class="token punctuation">(</span><span class="token string">"/"</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=&gt;</span> <span class="token string">"Hello World!"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
app<span class="token punctuation">.</span><span class="token function">MapGet</span><span class="token punctuation">(</span><span class="token string">"/Account/Login"</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=&gt;</span> <span class="token string">"Login page"</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">AllowAnonymous</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
app<span class="token punctuation">.</span><span class="token function">MapFallback</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=&gt;</span> <span class="token string">"Fallback"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// 👈 Add this</span>
app<span class="token punctuation">.</span><span class="token function">Run</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre></div> <p>Now, if we run the app and hit any random URL, we're redirected to the <code>/Account/Login</code> page. That's because our fallback "you must be logged in" authorization policy kicks in, and redirects us. In the image below you can see that <code>/random-url</code> was matched, but redirected automatically to our login page:</p> <p><img src="https://andrewlock.net/content/images/2025/fallbackroutes_02.png" alt="The fallback route /random-url was automatically redirected"></p> <p>It's probably more common to have your fallback route redirect to an <em>existing</em> endpoint, whether that's a file, an MVC controller, or a Razor Page. The most common reason for using a fallback route like this is for handling SPA applications. Many SPA applications <a href="https://weblog.west-wind.com/posts/2020/Jul/12/Handling-SPA-Fallback-Paths-in-a-Generic-ASPNET-Core-Server#client-side-navigation">handle "routing" on the client-side</a> and generate nice, normal looking routes. However, if the client refreshes the page then the "incorrect" path is sent as a request to ASP.NET Core.</p> <p>For example, the client side SPA app might send a request to <code>/something/customers/123</code>, but that doesn't necessarily <em>mean</em> anything to your application. Instead, in that scenario, you often need to return your "home page", have the SPA app run its boot up code and then do the routing on the client-side.</p> <p>Exactly what "return your 'home page'" means will depend on your app, but there's probably a <code>MapFallback*</code> overload for you: For example:</p> <ul><li><code>MapFallbackToFile(string filepath)</code> returns a file e.g. <code>Index.html</code> when there's no match for a route.</li> <li><code>MapFallbackToPath(string page)</code> executes the given Razor Page as the fallback.</li> <li><code>MapFallbackToController(string action, string controller)</code> executes the indicated MVC controller and action as the fallback.</li></ul> <p>All these <code>MapFallback()</code> overloads seem similar, but they actually behave somewhat differently when it comes to metadata, as we'll look at in the next section.</p> <h2 id="fallback-routing-and-metadata-for-simple-endpoints" class="heading-with-anchor"><a href="https://andrewlock.net/adding-metadata-to-fallback-endpoints-in-aspnetcore/#fallback-routing-and-metadata-for-simple-endpoints" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Fallback routing and metadata for simple endpoints</a></h2> <p>We can explore the differences in metadata handling by thinking about the same simple authorization app we've been looking at so far. To test how metadata works, we can simply add an <code>AllowAnonymous()</code> call to our <code>MapFallback()</code> methods. For example, taking our initial <code>MapFallback()</code> example:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">using</span> <span class="token namespace">Microsoft<span class="token punctuation">.</span>AspNetCore<span class="token punctuation">.</span>Authorization</span><span class="token punctuation">;</span>

<span class="token class-name"><span class="token keyword">var</span></span> builder <span class="token operator">=</span> WebApplication<span class="token punctuation">.</span><span class="token function">CreateBuilder</span><span class="token punctuation">(</span>args<span class="token punctuation">)</span><span class="token punctuation">;</span>

builder<span class="token punctuation">.</span>Services<span class="token punctuation">.</span><span class="token function">AddAuthentication</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">AddCookie</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
builder<span class="token punctuation">.</span>Services<span class="token punctuation">.</span><span class="token function">AddAuthorization</span><span class="token punctuation">(</span>opt <span class="token operator">=&gt;</span>
<span class="token punctuation">{</span>
    opt<span class="token punctuation">.</span>FallbackPolicy <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token constructor-invocation class-name">AuthorizationPolicyBuilder</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
                            <span class="token punctuation">.</span><span class="token function">RequireAuthenticatedUser</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
                            <span class="token punctuation">.</span><span class="token function">Build</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token class-name"><span class="token keyword">var</span></span> app <span class="token operator">=</span> builder<span class="token punctuation">.</span><span class="token function">Build</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

app<span class="token punctuation">.</span><span class="token function">MapGet</span><span class="token punctuation">(</span><span class="token string">"/"</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=&gt;</span> <span class="token string">"Hello World!"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> 
app<span class="token punctuation">.</span><span class="token function">MapGet</span><span class="token punctuation">(</span><span class="token string">"/Account/Login"</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=&gt;</span> <span class="token string">"Login page"</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">AllowAnonymous</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
app<span class="token punctuation">.</span><span class="token function">MapFallback</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=&gt;</span> <span class="token string">"Fallback"</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">AllowAnonymous</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
                                 <span class="token comment">// 👆 Add this</span>
app<span class="token punctuation">.</span><span class="token function">Run</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre></div> <p>All I've added in this case is the <code>AllowAnonymous()</code> call to the <code>MapFallback()</code> configuration. Prior to adding the <code>AllowAnonymous()</code>, call, hitting a random URL would result in an unauthorized request, and we would be redirected to the <code>/Account/Login</code> endpoint. However, by adding <code>AllowAnonymous()</code>, we've added metadata to the fallback endpoint which means that the endpoint <em>is</em> authorized, and executes for a random URL:</p> <p><img src="https://andrewlock.net/content/images/2025/fallbackroutes_03.png" alt="The fallback endpoint is allowed to execute"></p> <p>Similarly, with the <code>MapFallbackToFile()</code> method, adding <code>AllowAnonymous()</code> attaches metadata to this fallback endpoint. Changing the above <code>MapFallback()</code> call to <code>MapFallbackToFile("index.html")</code> (and adding an <em>index.html</em> file to the application in the <em>wwwroot</em> folder) gives the same result; hitting any unknown URL returns the <em>index.html</em> file:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp">app<span class="token punctuation">.</span><span class="token function">MapFallbackToFile</span><span class="token punctuation">(</span><span class="token string">"index.html"</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">AllowAnonymous</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre></div> <p><img src="https://andrewlock.net/content/images/2025/fallbackroutes_04.png" alt="The fallback file is also allowed to execute"></p> <p>You would be forgiven for thinking that the Razor Page and MVC based fallback methods behaved in a similar way, but somewhat surprisingly, they don't!</p> <h2 id="fallback-routing-and-metadata-for-razor-pages-and-mvc" class="heading-with-anchor"><a href="https://andrewlock.net/adding-metadata-to-fallback-endpoints-in-aspnetcore/#fallback-routing-and-metadata-for-razor-pages-and-mvc" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Fallback routing and metadata for Razor Pages and MVC</a></h2> <p>To show this in action, I created a tiny Razor Pages app, and added three very simple Razor Pages, which are somewhat equivalent to the minimal API version above:</p> <p><em>/Index.chstml</em>:</p> <div class="pre-code-wrapper"><pre class="language-cshtml"><code class="language-cshtml"><span class="token directive"><span class="token keyword">@page</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>h1</span><span class="token punctuation">&gt;</span></span>Index<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>h1</span><span class="token punctuation">&gt;</span></span>
</code></pre></div> <p><em>/Account/Login.chstml</em> - note the <code>[AllowAnonymous]</code> attribute here to allow anonymous access</p> <div class="pre-code-wrapper"><pre class="language-cshtml"><code class="language-cshtml"><span class="token directive"><span class="token keyword">@page</span></span>
<span class="token directive"><span class="token keyword">@attribute</span> <span class="token csharp language-csharp"><span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">Microsoft<span class="token punctuation">.</span>AspNetCore<span class="token punctuation">.</span>Authorization<span class="token punctuation">.</span>AllowAnonymous</span></span><span class="token punctuation">]</span></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>h1</span><span class="token punctuation">&gt;</span></span>Login<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>h1</span><span class="token punctuation">&gt;</span></span>
</code></pre></div> <p>And finally <em>/Fallback.cshtml</em>:</p> <div class="pre-code-wrapper"><pre class="language-cshtml"><code class="language-cshtml"><span class="token directive"><span class="token keyword">@page</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>h1</span><span class="token punctuation">&gt;</span></span>Fallback<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>h1</span><span class="token punctuation">&gt;</span></span>
</code></pre></div> <p>I then added the same authentication and authorization services as before to the Razor pages app, and added a fallback mapping using <code>MapFallbackToPage("/Fallback")</code>, and marked that fallback route with <code>AllowAnonymous()</code>:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">using</span> <span class="token namespace">Microsoft<span class="token punctuation">.</span>AspNetCore<span class="token punctuation">.</span>Authorization</span><span class="token punctuation">;</span>

<span class="token class-name"><span class="token keyword">var</span></span> builder <span class="token operator">=</span> WebApplication<span class="token punctuation">.</span><span class="token function">CreateBuilder</span><span class="token punctuation">(</span>args<span class="token punctuation">)</span><span class="token punctuation">;</span>

builder<span class="token punctuation">.</span>Services<span class="token punctuation">.</span><span class="token function">AddRazorPages</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token comment">// same authentication and authorization services as before</span>
builder<span class="token punctuation">.</span>Services<span class="token punctuation">.</span><span class="token function">AddAuthentication</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">AddCookie</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
builder<span class="token punctuation">.</span>Services<span class="token punctuation">.</span><span class="token function">AddAuthorization</span><span class="token punctuation">(</span>opt <span class="token operator">=&gt;</span>
<span class="token punctuation">{</span>
    opt<span class="token punctuation">.</span>FallbackPolicy <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token constructor-invocation class-name">AuthorizationPolicyBuilder</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">RequireAuthenticatedUser</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">Build</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>


<span class="token class-name"><span class="token keyword">var</span></span> app <span class="token operator">=</span> builder<span class="token punctuation">.</span><span class="token function">Build</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token comment">// Standard Razor Pages stuff</span>
app<span class="token punctuation">.</span><span class="token function">UseRouting</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
app<span class="token punctuation">.</span><span class="token function">UseAuthorization</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
app<span class="token punctuation">.</span><span class="token function">MapStaticAssets</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
app<span class="token punctuation">.</span><span class="token function">MapRazorPages</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">WithStaticAssets</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token comment">// Add a fallback page and try to mark it as allow anonymous</span>
app<span class="token punctuation">.</span><span class="token function">MapFallbackToPage</span><span class="token punctuation">(</span><span class="token string">"/Fallback"</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">AllowAnonymous</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
app<span class="token punctuation">.</span><span class="token function">Run</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre></div> <p>Overall this is essentially the Razor Pages equivalent of the minimal API app from before. Consequently, if we navigate to <code>/Index</code>, the authorization policy means we get redirected to the <code>/Account/Login</code> page. That page has the <code>[AllowAnonymous]</code> attribute, so we can view that page:</p> <p><img src="https://andrewlock.net/content/images/2025/fallbackroutes_05.png" alt="The /Index page redirects to /Account/Login"></p> <p>Now let's try hitting the fallback route by trying a random URL. Seeing as we marked the fallback route as <code>AllowAnonymous()</code> then we should see the <code>/Fallback</code> page, right?</p> <p><img src="https://andrewlock.net/content/images/2025/fallbackroutes_06.png" alt="The fallback route still redirects to /Account/Login too"></p> <p>Hmmm… that's not right 🤔 It seems like the <code>AllowAnonymous()</code> call on the <code>MapFallbackToPage()</code> definition isn't working?!</p> <p>The explanation is a little more nuanced…</p> <h2 id="why-doesn-t-allowanonymous-work-on-mapfallbacktopage-" class="heading-with-anchor"><a href="https://andrewlock.net/adding-metadata-to-fallback-endpoints-in-aspnetcore/#why-doesn-t-allowanonymous-work-on-mapfallbacktopage-" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Why doesn't <code>AllowAnonymous()</code> work on <code>MapFallbackToPage()</code>?</a></h2> <p><a href="https://github.com/dotnet/aspnetcore/blob/main/src/Http/Routing/src/Builder/EndpointRouteBuilderExtensions.cs#L390">The <code>MapFallback()</code></a> and <code>MapFallbackToFile()</code> calls <a href="https://github.com/dotnet/aspnetcore/blob/main/src/Middleware/StaticFiles/src/StaticFilesEndpointRouteBuilderExtensions.cs#L54-L56">are actually registering <em>new</em> endpoints</a>; they have a catch-all route pattern and a handler, and the metadata gets associated to this new endpoint.</p> <p><code>MapFallbackToPage()</code> and <code>MapFallbackToController()</code> work <a href="https://github.com/dotnet/aspnetcore/blob/cf40577071ca80f7396e702a75212ed0f7ff8f54/src/Mvc/Mvc.RazorPages/src/Builder/RazorPagesEndpointRouteBuilderExtensions.cs#L139-L147">slightly differently</a>. These do add an extra endpoint, but the endpoint is added with additional <code>DynamicPageMetadata</code> metadata (for Razor Pages) or <code>DynamicControllerMetadata</code> metadata (for MVC). The Razor Pages/MVC infrastructure then finds this metadata and uses it to select a <em>different</em> endpoint to execute. <em>That's</em> the endpoint selected by the routing infrastructure, not the "original" fallback endpoint.</p> <p>This all means that <a href="https://github.com/dotnet/aspnetcore/issues/14679#issuecomment-537715318">the "fallback" endpoint is essentially <em>replaced</em> by the real page it points to</a>. Which <em>also</em> means that any metadata you add to that fallback endpoint is <em>lost</em> when it's actually invoked, which includes the <code>AllowAnonymous()</code> call! In other words, calling <code>AllowAnonymous()</code> (or adding any <em>other</em> metadata) on a <code>MapFallbackToPage()</code> or <code>MapFallbackToController()</code> call does nothing.</p> <p>To make our fallback page behave like we want it to, we have to add the <code>[AllowAnonymous]</code> attribute to the <em>destination</em> page instead, to the <code>/Fallback</code> page:</p> <div class="pre-code-wrapper"><pre class="language-cshtml"><code class="language-cshtml"><span class="token directive"><span class="token keyword">@page</span></span>
<span class="token directive"><span class="token keyword">@attribute</span> <span class="token csharp language-csharp"><span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">Microsoft<span class="token punctuation">.</span>AspNetCore<span class="token punctuation">.</span>Authorization<span class="token punctuation">.</span>AllowAnonymous</span></span><span class="token punctuation">]</span></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>h1</span><span class="token punctuation">&gt;</span></span>Fallback<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>h1</span><span class="token punctuation">&gt;</span></span>
</code></pre></div> <p>After making that change, now if we hit a random URL we <em>can</em> access the page, because the <em>destination</em> has the required metadata:</p> <p><img src="https://andrewlock.net/content/images/2025/fallbackroutes_07.png" alt="After adding the allow anonymous to the destination, it works"></p> <p>And that pretty much covers it. Just remember that if you need to add metadata to a fallback Razor Page or MVC controller, then you must add it to the <em>destination</em> endpoint, not the "fallback" endpoint itself.</p> <h2 id="summary" class="heading-with-anchor"><a href="https://andrewlock.net/adding-metadata-to-fallback-endpoints-in-aspnetcore/#summary" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Summary</a></h2> <p>In this post I briefly described the routing infrastructure of ASP.NET Core, and how you can add metadata to endpoints to drive other functionality. Next I describe what fallback endpoints are and why they are useful. Finally, I showed how adding metadata works differently when creating fallback endpoints using <code>MapFallbackToPage()</code> and <code>MapFallbackToController()</code>.</p> <p>For these cases, the fallback endpoint is replaced by the real <em>destination</em> endpoint. Consequently, if you want to add metadata to these endpoints, you must add it to the destination endpoint <em>not</em> the fallback endpoint.</p> ]]></content:encoded><category><![CDATA[.NET Core;ASP.NET Core;Razor Pages;MVC]]></category></item><item><title><![CDATA[Publishing NuGet packages from GitHub actions the easy way with Trusted Publishing]]></title><description><![CDATA[In this post I describe how you can use nuget.org's new Trusted Publishing feature to publish NuGet packages from a GitHub Actions workflow]]></description><link>https://andrewlock.net/easily-publishing-nuget-packages-from-github-actions-with-trusted-publishing/</link><guid isPermaLink="true">https://andrewlock.net/easily-publishing-nuget-packages-from-github-actions-with-trusted-publishing/</guid><pubDate>Tue, 30 Sep 2025 10:00:00 GMT</pubDate><dc:creator><![CDATA[Andrew Lock]]></dc:creator><media:content url="https://andrewlock.net/content/images/2025/trusted_publishing_banner.png" medium="image" /><content:encoded><![CDATA[<img src="https://andrewlock.net/content/images/2025/trusted_publishing_banner.png" /><p>In this post I describe how you can use <a href="https://www.nuget.org/">nuget.org</a>'s <a href="https://devblogs.microsoft.com/dotnet/enhanced-security-is-here-with-the-new-trust-publishing-on-nuget-org/">new Trusted Publishing feature</a> to publish NuGet packages from a GitHub Actions workflow, <em>without</em> having to generate and store API keys, while simultaneously benefiting from improved security.</p> <h2 id="how-do-you-push-nuget-packages-today-" class="heading-with-anchor"><a href="https://andrewlock.net/easily-publishing-nuget-packages-from-github-actions-with-trusted-publishing/#how-do-you-push-nuget-packages-today-" class="relative text-zinc-800 dark:text-white no-underline hover:underline">How do you push NuGet packages today?</a></h2> <p>If you're someone that creates NuGet packages and pushes them to <a href="https://www.nuget.org/">nuget.org</a> today, then you're probably doing so in one of several ways:</p> <ul><li>Building the packages locally and manually uploading them to <a href="https://www.nuget.org">nuget.org</a>.</li> <li>Building the packages in CI, downloading them, and manually uploading them to <a href="https://www.nuget.org">nuget.org</a>.</li> <li>Building the packages in CI, and pushing them directly to <a href="https://www.nuget.org">nuget.org</a> from CI.</li></ul> <p>Each of these approaches is progressively "better" in that they generally improve the consistency of your builds and reduce the number of manual steps you require. However, pushing your <em>.nupkg</em> files to NuGet from CI is also somewhat "harder" than just uploading them locally, via the <a href="https://www.nuget.org/packages/manage/upload">nuget.org website</a>.</p> <p>Instead of just dragging-and-dropping your package onto the web page, you now need to:</p> <ul><li>Generate an API key</li> <li>Store it <em>securely</em> in GitHub Actions (e.g. as a Secret)</li> <li>Pass the secret in your GitHub Actions workflow</li> <li>Rotate the secret whenever it changes</li> <li>Make sure others in your organisation can do the same (if you're publishing packages for an org)</li></ul> <p>None of that is insurmountable, but managing the lifecycle of long-lived secrets <a href="https://www.bleepingcomputer.com/news/security/over-12-million-auth-secrets-and-keys-leaked-on-github-in-2023/">is notoriously difficult</a>. And that's where Trusted Publishing steps in.</p> <h2 id="what-is-trusted-publishing-" class="heading-with-anchor"><a href="https://andrewlock.net/easily-publishing-nuget-packages-from-github-actions-with-trusted-publishing/#what-is-trusted-publishing-" class="relative text-zinc-800 dark:text-white no-underline hover:underline">What is Trusted Publishing?</a></h2> <p>Trusted Publishing is an initiative that's been in place for many ecosystems for a while. For example Python has it for PyPI, Ruby has it for RubyGems.org, and JavaScript has it for npm. And as of last week, .NET has Trusted Publishing for <a href="https://www.nuget.org">nuget.org</a> 🎉</p> <p><a href="https://repos.openssf.org/trusted-publishers-for-all-package-repositories">Trusted Publishing</a> is an approach that uses existing authentication standards (<a href="https://openid.net/developers/how-connect-works/">OpenID Connect</a>) to connect CI infrastructure provides (such as GitHub Actions or GitLab Pipelines) with public package repositories (like PyPI and <a href="https://www.nuget.org">nuget.org</a>). Instead of needing to store and manage API keys so that the two systems can talk to each other, you use OpenID Connect to retrieve short-lived authentication tokens, which you can then use to push packages to the package repository.</p> <p>Exactly how this works will vary by provider and package repository, but the overall flow is the same. For GitHub actions and <a href="https://www.nuget.org">nuget.org</a>, it looks something like this:</p> <ul><li>The user configures a "trust policy" on <a href="https://www.nuget.org">nuget.org</a>.</li> <li>In a CI workflow, you retrieve an OpenID Connect token for the job from GitHub. <ul><li>In this example, GitHub is the Identity Provider (IdP).</li> <li>The token is a signed JSON Web Token (JWT), which includes details about the repository and the workflow that's running.</li></ul> </li> <li>You send the token to <a href="https://www.nuget.org">nuget.org</a>, exchanging the token for an API key. <ul><li>NuGet.org verifies the contents of the JWT against the configured trust policy.</li> <li>If everything matches up, <a href="https://www.nuget.org">nuget.org</a> issues a short-lived API token that can be used to publish packages.</li></ul> </li> <li>The CI workflow uses the API token to push the <em>.nupkg</em> packages to <a href="https://www.nuget.org">nuget.org</a>.</li></ul> <figure> <picture> <img src="https://andrewlock.net/content/images/2025/trusted_publishing_01.png"> </picture><figcaption>Diagram of Trusted Publishers flow. From <a href="https://repos.openssf.org/trusted-publishers-for-all-package-repositories">Trusted Publishers for All Package Repositories</a>.</figcaption></figure> <p>Because the trust-policy is configured ahead of time, and you're <em>already</em> authenticated with GitHub (due to running in their infrastructure) it's pretty easy to get up and running.</p> <h2 id="configuring-trusted-publishing-on-nuget-org" class="heading-with-anchor"><a href="https://andrewlock.net/easily-publishing-nuget-packages-from-github-actions-with-trusted-publishing/#configuring-trusted-publishing-on-nuget-org" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Configuring Trusted Publishing on NuGet.org</a></h2> <p>Once I saw the blog post, I decided to give it a try for <a href="https://andrewlock.net/sleep-pc-a-dotnet-tool-to-make-windows-sleep-after-a-timeout/">my new sleep-pc tool</a>, seeing as I hadn't set up CI for it yet. I'll start by showing the workflow <em>without</em> NuGet publishing, to provide the baseline, and then show the steps I took to add publishing via Trusted Publishing.</p> <h3 id="the-workflow-starting-point" class="heading-with-anchor"><a href="https://andrewlock.net/easily-publishing-nuget-packages-from-github-actions-with-trusted-publishing/#the-workflow-starting-point" class="relative text-zinc-800 dark:text-white no-underline hover:underline">The workflow starting point</a></h3> <p>The initial workflow, without publishing, is shown below. This is stored in the file <code>.github/workflows/BuildAndPack.yml</code>:</p> <div class="pre-code-wrapper"><pre class="language-yml"><code class="language-yml"><span class="token key atrule">name</span><span class="token punctuation">:</span> BuildAndPack
<span class="token key atrule">on</span><span class="token punctuation">:</span>
  <span class="token key atrule">push</span><span class="token punctuation">:</span>
    <span class="token key atrule">branches</span><span class="token punctuation">:</span> <span class="token punctuation">[</span>main<span class="token punctuation">]</span>
    <span class="token key atrule">tags</span><span class="token punctuation">:</span> <span class="token punctuation">[</span><span class="token string">'*'</span><span class="token punctuation">]</span>
  <span class="token key atrule">pull_request</span><span class="token punctuation">:</span>

<span class="token key atrule">jobs</span><span class="token punctuation">:</span>
  <span class="token key atrule">build-and-publish</span><span class="token punctuation">:</span>
    <span class="token key atrule">permissions</span><span class="token punctuation">:</span>
      <span class="token key atrule">contents</span><span class="token punctuation">:</span> read
    <span class="token key atrule">runs-on</span><span class="token punctuation">:</span> windows<span class="token punctuation">-</span>latest
    <span class="token key atrule">steps</span><span class="token punctuation">:</span>
      <span class="token punctuation">-</span> <span class="token key atrule">uses</span><span class="token punctuation">:</span> actions/checkout@v4
      <span class="token punctuation">-</span> <span class="token key atrule">uses</span><span class="token punctuation">:</span> actions/setup<span class="token punctuation">-</span>dotnet@v4
        <span class="token key atrule">with</span><span class="token punctuation">:</span>
          <span class="token key atrule">dotnet-version</span><span class="token punctuation">:</span> 10.0.x

      <span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> dotnet pack
        <span class="token key atrule">run</span><span class="token punctuation">:</span> <span class="token punctuation">|</span><span class="token scalar string">
          dotnet pack
          dotnet pack -r win-x64</span>
</code></pre></div> <p>This just does 3 things:</p> <ul><li>Checkout the repo</li> <li>Install .NET 10</li> <li>Run <code>dotnet pack</code></li></ul> <p>It's worth nothing that I added the <code>permissions</code> entry to limit the permissions to reading from the repository <a href="https://docs.github.com/en/actions/tutorials/authenticate-with-github_token#modifying-the-permissions-for-the-github_token">as a security best practice</a>. This isn't required, but I added it mostly to show what needs to change when we use trusted publishing.</p> <h3 id="updating-the-workflow-to-add-trusted-publishing-support" class="heading-with-anchor"><a href="https://andrewlock.net/easily-publishing-nuget-packages-from-github-actions-with-trusted-publishing/#updating-the-workflow-to-add-trusted-publishing-support" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Updating the workflow to add Trusted Publishing support</a></h3> <p>To add trusted publishing, we need to do three things:</p> <ul><li>Add the <code>id-token: write</code> permission.</li> <li>Use <code>NuGet/login@v1</code> to exchange an OIDC token for a NuGet API key.</li> <li>Use the generated API key to push the packages to NuGet</li></ul> <p>The following shows the updated workflow, with comments highlighting the differences:</p> <div class="pre-code-wrapper"><pre class="language-yml"><code class="language-yml"><span class="token key atrule">name</span><span class="token punctuation">:</span> BuildAndPack
<span class="token key atrule">on</span><span class="token punctuation">:</span>
  <span class="token key atrule">push</span><span class="token punctuation">:</span>
    <span class="token key atrule">branches</span><span class="token punctuation">:</span> <span class="token punctuation">[</span>main<span class="token punctuation">]</span>
    <span class="token key atrule">tags</span><span class="token punctuation">:</span> <span class="token punctuation">[</span><span class="token string">'*'</span><span class="token punctuation">]</span>
  <span class="token key atrule">pull_request</span><span class="token punctuation">:</span>

<span class="token key atrule">jobs</span><span class="token punctuation">:</span>
  <span class="token key atrule">build-and-publish</span><span class="token punctuation">:</span>
    <span class="token key atrule">permissions</span><span class="token punctuation">:</span>
      <span class="token key atrule">contents</span><span class="token punctuation">:</span> read
      <span class="token key atrule">id-token</span><span class="token punctuation">:</span> write  <span class="token comment"># enable GitHub OIDC token issuance for this job</span>

    <span class="token key atrule">runs-on</span><span class="token punctuation">:</span> windows<span class="token punctuation">-</span>latest
    <span class="token key atrule">steps</span><span class="token punctuation">:</span>
      <span class="token punctuation">-</span> <span class="token key atrule">uses</span><span class="token punctuation">:</span> actions/checkout@v4
      <span class="token punctuation">-</span> <span class="token key atrule">uses</span><span class="token punctuation">:</span> actions/setup<span class="token punctuation">-</span>dotnet@v4
        <span class="token key atrule">with</span><span class="token punctuation">:</span>
          <span class="token key atrule">dotnet-version</span><span class="token punctuation">:</span> 10.0.x

      <span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> dotnet pack
        <span class="token key atrule">run</span><span class="token punctuation">:</span> <span class="token punctuation">|</span><span class="token scalar string">
          dotnet pack
          dotnet pack -r win-x64</span>

      <span class="token comment"># Use the ambient GitHub token to login to NuGet and retrieve an API key</span>
      <span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> NuGet login (OIDC → temp API key)
        <span class="token key atrule">uses</span><span class="token punctuation">:</span> NuGet/login@v1
        <span class="token key atrule">id</span><span class="token punctuation">:</span> login
        <span class="token key atrule">with</span><span class="token punctuation">:</span>
          <span class="token comment"># Secret is your NuGet username, e.g. andrewlock</span>
          <span class="token key atrule">user</span><span class="token punctuation">:</span> $<span class="token punctuation">{</span><span class="token punctuation">{</span> secrets.NUGET_USER <span class="token punctuation">}</span><span class="token punctuation">}</span>

      <span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> push to NuGet
        <span class="token comment"># Only push to NuGet if we're building a tag (optional)</span>
        <span class="token key atrule">if</span><span class="token punctuation">:</span> startsWith(github.ref<span class="token punctuation">,</span> 'refs/tags/')
        <span class="token key atrule">shell</span><span class="token punctuation">:</span> pwsh
        <span class="token comment"># Loop through all the packages in the output folder and push them to</span>
        <span class="token comment"># nuget.org, using the NUGET_API_KEY generated by the previous login step</span>
        <span class="token key atrule">run</span><span class="token punctuation">:</span> <span class="token punctuation">|</span><span class="token scalar string">
          Get-ChildItem artifacts/package/release -Filter *.nupkg | ForEach-Object {
            dotnet nuget push $_.FullName `
              --api-key "${{ steps.login.outputs.NUGET_API_KEY }}" `
              --source https://api.nuget.org/v3/index.json
          }</span>
</code></pre></div> <p>In this workflow, the only secret you need to configure is the <code>NUGET_USER</code> secret, which should be set to your <a href="https://www.nuget.org">nuget.org</a> <em>username</em> (not your email address). Given that it's public information anyway, storing it in a secret seems a tad overkill, but why not 😄</p> <p><img src="https://andrewlock.net/content/images/2025/trusted_publishing.png" alt="Add the NUGET_USER name to your repo"></p> <p>If you run this workflow now, the <code>NuGet login</code> step will fail with an error like the following:</p> <div class="pre-code-wrapper"><pre><code>Requesting GitHub OIDC token from: https://run-actions-1-azure-eastus.actions.githubusercontent.com/100//idtoken/aae150a5-757c-411a-b2b0-f82a9b26401f/1943e299-9d47-50e7-8d56-90681c654f24?api-version=2.0&amp;audience=https%3A%2F%2Fwww.nuget.org
Error: Token exchange failed (401): No matching trust policy owned by user '***' was found.
</code></pre></div> <p>As you can see from the message, the stage failed because we haven't yet configured a trust policy on <a href="https://www.nuget.org">nuget.org</a>, so that's our next job.</p> <h3 id="configuring-a-trust-policy-on-nuget-org" class="heading-with-anchor"><a href="https://andrewlock.net/easily-publishing-nuget-packages-from-github-actions-with-trusted-publishing/#configuring-a-trust-policy-on-nuget-org" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Configuring a trust policy on NuGet.org</a></h3> <p>Configuring a trust policy is very easy. We start by signing in at <a href="https://www.nuget.org/">nuget.org</a> and navigating to <a href="https://www.nuget.org/account/trustedpublishing">the Trusted Publishing page</a>:</p> <p><img src="https://andrewlock.net/content/images/2025/trusted_publishing_02.png" alt="The Trusted Publishing page is available in the account menu"></p> <p>Click <strong>Create</strong> to create a new policy, and fill in all the required fields:</p> <ul><li>A name for the policy. I think it makes sense to be the name of the package/github repo.</li> <li>The package owner (whether it's an individual account or an organisation)</li> <li>The GitHub repository details (owner and repository)</li> <li>The workflow file that will be pushing to Nuget</li></ul> <p>You can see how I completed this for my sleep-pc tool below:</p> <p><img src="https://andrewlock.net/content/images/2025/trusted_publishing_03.png" alt="The configured details for the sleep-pc tool"></p> <p>Once you've created the trust policy, it exists in a "partially" active state until you <em>use</em> the policy</p> <p><img src="https://andrewlock.net/content/images/2025/trusted_publishing_04.png" alt="The partially active policy"></p> <p>Now if you run the workflow again the login step is successful:</p> <div class="pre-code-wrapper"><pre><code>Requesting GitHub OIDC token from: https://run-actions-1-azure-eastus.actions.githubusercontent.com/73//idtoken/21d84c5d-b6a5-49e0-bc04-e46694e083df/b3fac26f-8847-5e96-b7ae-47d656a98f51?api-version=2.0&amp;audience=https%3A%2F%2Fwww.nuget.org
Successfully exchanged OIDC token for NuGet API key.
</code></pre></div> <p>and the policy has changed to be fully active:</p> <p><img src="https://andrewlock.net/content/images/2025/trusted_publishing_05.png" alt="The fully active policy"></p> <p>Assuming everything else goes to plan, in the next step your package will be published to NuGet, no long-lived API keys required 🎉</p> <h2 id="wrapping-up" class="heading-with-anchor"><a href="https://andrewlock.net/easily-publishing-nuget-packages-from-github-actions-with-trusted-publishing/#wrapping-up" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Wrapping up</a></h2> <p>As far as I can tell, as of today, there's no additional benefits from using trusted publishing on <a href="https://www.nuget.org">nuget.org</a>, aside from the ease of publishing, and the lack of long-lived credentials. I can certainly foresee additional benefits in the future, with packages pushed via trusted publishing having additional verification marks. For example, maybe there will be some <a href="https://andrewlock.net/creating-provenance-attestations-for-nuget-packages-in-github-actions/">tie-in to provenance attestations</a> in the future? I guess we'll wait and see!</p> <h2 id="summary" class="heading-with-anchor"><a href="https://andrewlock.net/easily-publishing-nuget-packages-from-github-actions-with-trusted-publishing/#summary" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Summary</a></h2> <p>In this post I discussed <a href="https://repos.openssf.org/trusted-publishers-for-all-package-repositories">Trusted Publishing</a> and how it can help by avoiding the need to store long-lived credentials in your GitHub repositories. I then showed how I configured publishing of my sleep-pc package from my <a href="https://github.com/andrewlock/sleep-pc">GitHub repository</a> to <a href="https://www.nuget.org/packages/sleep-pc">nuget.org</a> using Trusted Publishing.</p> ]]></content:encoded><category><![CDATA[GitHub;DevOps;NuGet;Security]]></category></item><item><title><![CDATA[sleep-pc: a .NET Native AOT tool to make Windows sleep after a timeout]]></title><description><![CDATA[In this post I describe a small native AOT .NET tool that I built to force a Windows PC to go to sleep after a timer expires]]></description><link>https://andrewlock.net/sleep-pc-a-dotnet-tool-to-make-windows-sleep-after-a-timeout/</link><guid isPermaLink="true">https://andrewlock.net/sleep-pc-a-dotnet-tool-to-make-windows-sleep-after-a-timeout/</guid><pubDate>Tue, 23 Sep 2025 10:00:00 GMT</pubDate><dc:creator><![CDATA[Andrew Lock]]></dc:creator><media:content url="https://andrewlock.net/content/images/2025/sleep-pc_banner.png" medium="image" /><content:encoded><![CDATA[<img src="https://andrewlock.net/content/images/2025/sleep-pc_banner.png" /><p>In this post I describe a small .NET tool that I built to force a Windows PC to go to sleep after a timer expires. I describe the Win32 API I used to send my laptop to sleep, and how I expanded the proof of concept to be a Native AOT-compiled .NET tool that you can <a href="https://github.com/andrewlock/sleep-pc">view on GitHub</a> or install <a href="https://www.nuget.org/packages/sleep-pc">from Nuget</a>.</p> <h2 id="background-go-to-sleep-" class="heading-with-anchor"><a href="https://andrewlock.net/sleep-pc-a-dotnet-tool-to-make-windows-sleep-after-a-timeout/#background-go-to-sleep-" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Background: go to sleep!</a></h2> <p>Getting a laptop to go to sleep seems to be one of those things that's just way harder than it should be. Whether it's a MacBook that decides to wake up and heat my backpack over night, or a Windows laptop that just <em>won't</em> go to sleep, I always seem to have issues.</p> <p>One weekend, I was wrestling with exactly the latter issue: I just couldn't get my Windows laptop to sleep. <em>Specifically</em>, I couldn't get it to sleep when <a href="https://support.microsoft.com/en-us/windows/windows-media-player-legacy-e8f84f54-cd64-865c-2e83-1d8ec121b5b8">Windows Media Player Legacy</a> finished playing a playlist. As someone that goes to sleep with videos playing in the background, it's <em>very</em> annoying…</p> <blockquote> <p>Yes, I know it's old. Yes, I use <a href="https://www.videolan.org/">VLC</a> too. I still prefer the old WMP for some things, namely adding a few videos to a playlist from my library, seeing the last played time etc</p> </blockquote> <p>What's particularly annoying is that I've tried every troubleshooting approach under the sun. The power plans are correct. I've run and explored <code>powercfg</code>. I did it all, man. And eventually I just got bored and wrote a tiny app that <em>forces</em> the laptop to sleep after a given time limit has expired.</p> <h2 id="sending-a-pc-to-sleep-with-setsuspendstate" class="heading-with-anchor"><a href="https://andrewlock.net/sleep-pc-a-dotnet-tool-to-make-windows-sleep-after-a-timeout/#sending-a-pc-to-sleep-with-setsuspendstate" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Sending a PC to sleep with <code>SetSuspendState</code></a></h2> <p>The first version I knocked out in just a few minutes, and it looked something like this:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">using</span> <span class="token namespace">System<span class="token punctuation">.</span>Runtime<span class="token punctuation">.</span>InteropServices</span><span class="token punctuation">;</span>

<span class="token class-name"><span class="token keyword">var</span></span> wait <span class="token operator">=</span> TimeSpan<span class="token punctuation">.</span><span class="token function">FromSeconds</span><span class="token punctuation">(</span><span class="token number">60</span> <span class="token operator">*</span> <span class="token number">60</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// 1 hour</span>
Console<span class="token punctuation">.</span><span class="token function">WriteLine</span><span class="token punctuation">(</span><span class="token string">"Waiting for {wait}"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
Thread<span class="token punctuation">.</span><span class="token function">Sleep</span><span class="token punctuation">(</span>wait<span class="token punctuation">)</span><span class="token punctuation">;</span>

Console<span class="token punctuation">.</span><span class="token function">WriteLine</span><span class="token punctuation">(</span><span class="token string">"Sleeping!"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token function">SetSuspendState</span><span class="token punctuation">(</span><span class="token boolean">false</span><span class="token punctuation">,</span> <span class="token boolean">false</span><span class="token punctuation">,</span> <span class="token boolean">false</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">DllImport</span><span class="token attribute-arguments"><span class="token punctuation">(</span><span class="token string">"PowrProf.dll"</span><span class="token punctuation">,</span> SetLastError <span class="token operator">=</span> <span class="token boolean">true</span><span class="token punctuation">)</span></span></span><span class="token punctuation">]</span>
<span class="token keyword">static</span> <span class="token keyword">extern</span> <span class="token return-type class-name"><span class="token keyword">bool</span></span> <span class="token function">SetSuspendState</span><span class="token punctuation">(</span><span class="token class-name"><span class="token keyword">bool</span></span> hibernate<span class="token punctuation">,</span> <span class="token class-name"><span class="token keyword">bool</span></span> forceCritical<span class="token punctuation">,</span> <span class="token class-name"><span class="token keyword">bool</span></span> disableWakeEvent<span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre></div> <p>This tiny program simply sleeps for a hardcoded period of time (1 hour for my purposes) and then makes a P/Invoke call to the <a href="https://learn.microsoft.com/en-us/windows/win32/api/powrprof/nf-powrprof-setsuspendstate"><code>SetSuspendState</code> method in <code>PowrProf.dll</code></a>, a Win32 API (i.e. Windows only) which sends the laptop to sleep.</p> <blockquote> <p>I set <code>hibernate=false</code> here, however it appears that Windows often still hibernates regardless of this value, depending on your system settings. This can apparently happen particularly when Hybrid Sleep is enabled. In my case it does hibernate, which is fine for me, as that's what I <em>really</em> wanted anyway.</p> </blockquote> <p>And that's pretty much all you need to implement the functionality I was after! Of course, I couldn't <em>quite</em> leave it at simple as that.</p> <h2 id="adding-some-pizzazz" class="heading-with-anchor"><a href="https://andrewlock.net/sleep-pc-a-dotnet-tool-to-make-windows-sleep-after-a-timeout/#adding-some-pizzazz" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Adding some pizzazz</a></h2> <p>Given this was such a little tool, my first thoughts were:</p> <ul><li>Add some proper command-line arg parsing</li> <li>Package it as a Native AOT tool using <a href="https://andrewlock.net/exploring-dotnet-10-preview-features-7-packaging-self-contained-and-native-aot-dotnet-tools-for-nuget/">the new .NET 10 tools support</a></li> <li>Jazz up the console somewhat</li> <li>Allow a "dry run" option</li></ul> <p>I first set about adding command-line parsing and help generation as a quick way for testing various options.</p> <h3 id="using-consoleappframework-to-create-console-apps" class="heading-with-anchor"><a href="https://andrewlock.net/sleep-pc-a-dotnet-tool-to-make-windows-sleep-after-a-timeout/#using-consoleappframework-to-create-console-apps" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Using ConsoleAppFramework to create console apps</a></h3> <p>There's lots of different options for this, for example:</p> <ul><li><a href="https://www.nuget.org/packages/System.CommandLine">System.CommandLine</a></li> <li><a href="https://www.nuget.org/packages/Spectre.Console.Cli">Spectre.Console.Cli</a></li> <li><a href="https://www.nuget.org/packages/ConsoleAppFramework">ConsoleAppFramework</a></li></ul> <p><em>ConsoleAppFramework</em> is a project I haven't seen mentioned very much, but it's one I've used in the past that I've had a good experience with. As per <a href="https://github.com/Cysharp/ConsoleAppFramework">the project description</a>:</p> <blockquote> <p><strong>ConsoleAppFramework</strong> v5 is Zero Dependency, Zero Overhead, Zero Reflection, Zero Allocation, AOT Safe CLI Framework powered by C# Source Generator; achieves exceptionally high performance, fastest start-up time(with NativeAOT) and minimal binary size. Leveraging the latest features of .NET 8 and C# 13 (<a href="https://github.com/dotnet/roslyn/blob/main/docs/features/incremental-generators.md">IncrementalGenerator</a>, <a href="https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-9.0/function-pointers#function-pointers-1">managed function pointer</a>, <a href="https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/lambda-expressions#input-parameters-of-a-lambda-expression">params arrays and default values lambda expression</a>, <a href="https://learn.microsoft.com/en-us/dotnet/api/system.ispanparsable-1"><code>ISpanParsable&lt;T&gt;</code></a>, <a href="https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.posixsignalregistration"><code>PosixSignalRegistration</code></a>, etc.), this library ensures maximum performance while maintaining flexibility and extensibility.</p> </blockquote> <p>Which you know, is all pretty cool😄 From my point of view, what I like the most is the simplicity, but it also ticks a lot of boxes for performance. It does require a modern version of .NET (.NET 8+) but that's fine with me in this case.</p> <p>Add the package to your project:</p> <div class="pre-code-wrapper"><pre class="language-bash"><code class="language-bash">dotnet <span class="token function">add</span> package ConsoleAppFramework
</code></pre></div> <p>Converting my little proof of concept to a "real" console app, with help text, argument parsing and validation was as simple as this:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">using</span> <span class="token namespace">System<span class="token punctuation">.</span>ComponentModel<span class="token punctuation">.</span>DataAnnotations</span><span class="token punctuation">;</span>
<span class="token keyword">using</span> <span class="token namespace">ConsoleAppFramework</span><span class="token punctuation">;</span>
<span class="token keyword">using</span> <span class="token namespace">System<span class="token punctuation">.</span>Runtime<span class="token punctuation">.</span>InteropServices</span><span class="token punctuation">;</span>

<span class="token keyword">await</span> ConsoleApp<span class="token punctuation">.</span><span class="token function">RunAsync</span><span class="token punctuation">(</span>args<span class="token punctuation">,</span> App<span class="token punctuation">.</span>Countdown<span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token keyword">static</span> <span class="token keyword">partial</span> <span class="token keyword">class</span> <span class="token class-name">App</span>
<span class="token punctuation">{</span>
    <span class="token comment">/// &lt;summary&gt;</span>
    <span class="token comment">/// Waits for the provided number of seconds before sending the computer to sleep</span>
    <span class="token comment">/// &lt;/summary&gt;</span>
    <span class="token comment">/// &lt;param name="sleepDelaySeconds"&gt;-s|--seconds, The time in seconds before the computer is put to sleep. Defaults to 1 hour&lt;/param&gt;</span>
    <span class="token comment">/// &lt;param name="dryRun"&gt;If true, prints a message instead of sleeping&lt;/param&gt;</span>
    <span class="token comment">/// &lt;param name="ct"&gt;Used to cancel execution&lt;/param&gt;</span>
    <span class="token comment">/// &lt;returns&gt;&lt;/returns&gt;</span>
    <span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token keyword">async</span> <span class="token return-type class-name">Task</span> <span class="token function">Countdown</span><span class="token punctuation">(</span>
        <span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">Range</span><span class="token attribute-arguments"><span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">,</span> <span class="token number">99</span> <span class="token operator">*</span> <span class="token number">60</span> <span class="token operator">*</span> <span class="token number">60</span><span class="token punctuation">)</span></span></span><span class="token punctuation">]</span><span class="token class-name"><span class="token keyword">uint</span></span> sleepDelaySeconds <span class="token operator">=</span> <span class="token number">60</span> <span class="token operator">*</span> <span class="token number">60</span><span class="token punctuation">,</span>
        <span class="token class-name"><span class="token keyword">bool</span></span> dryRun <span class="token operator">=</span> <span class="token boolean">false</span><span class="token punctuation">,</span>
        <span class="token class-name">CancellationToken</span> ct <span class="token operator">=</span> <span class="token keyword">default</span><span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        <span class="token class-name"><span class="token keyword">var</span></span> wait <span class="token operator">=</span> TimeSpan<span class="token punctuation">.</span><span class="token function">FromSeconds</span><span class="token punctuation">(</span>sleepDelaySeconds<span class="token punctuation">)</span><span class="token punctuation">;</span>
        Console<span class="token punctuation">.</span><span class="token function">WriteLine</span><span class="token punctuation">(</span><span class="token string">"Waiting for {wait}"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token keyword">await</span> Task<span class="token punctuation">.</span><span class="token function">Delay</span><span class="token punctuation">(</span>wait<span class="token punctuation">,</span> ct<span class="token punctuation">)</span><span class="token punctuation">;</span>

        Console<span class="token punctuation">.</span><span class="token function">WriteLine</span><span class="token punctuation">(</span><span class="token string">"Sleeping!"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>dryRun<span class="token punctuation">)</span>
        <span class="token punctuation">{</span>
            <span class="token function">SetSuspendState</span><span class="token punctuation">(</span><span class="token boolean">false</span><span class="token punctuation">,</span> <span class="token boolean">false</span><span class="token punctuation">,</span> <span class="token boolean">false</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token punctuation">}</span>
    <span class="token punctuation">}</span>

    <span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">DllImport</span><span class="token attribute-arguments"><span class="token punctuation">(</span><span class="token string">"PowrProf.dll"</span><span class="token punctuation">,</span> SetLastError <span class="token operator">=</span> <span class="token boolean">true</span><span class="token punctuation">)</span></span></span><span class="token punctuation">]</span>
    <span class="token keyword">static</span> <span class="token keyword">extern</span> <span class="token return-type class-name"><span class="token keyword">bool</span></span> <span class="token function">SetSuspendState</span><span class="token punctuation">(</span><span class="token class-name"><span class="token keyword">bool</span></span> hibernate<span class="token punctuation">,</span> <span class="token class-name"><span class="token keyword">bool</span></span> forceCritical<span class="token punctuation">,</span> <span class="token class-name"><span class="token keyword">bool</span></span> disableWakeEvent<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>To add <code>ConsoleAppFramework</code> support I just had to:</p> <ul><li>Call the source-generated <code>ConsoleApp.RunAsync()</code> method</li> <li>Decorate my target method with XML comments describing the command, the arguments, and validation requirements</li></ul> <p>This generates all the help text you would expect:</p> <div class="pre-code-wrapper"><pre class="language-bash"><code class="language-bash"><span class="token operator">&gt;</span> dotnet run -- <span class="token parameter variable">--help</span>
Usage: <span class="token punctuation">[</span>options<span class="token punctuation">..</span>.<span class="token punctuation">]</span> <span class="token punctuation">[</span>-h<span class="token operator">|</span>--help<span class="token punctuation">]</span> <span class="token punctuation">[</span>--version<span class="token punctuation">]</span>

Waits <span class="token keyword">for</span> the provided number of seconds before sending the computer to <span class="token function">sleep</span>

Options:
  -s<span class="token operator">|</span>--seconds<span class="token operator">|</span>--sleep-delay-seconds <span class="token operator">&lt;</span>uint<span class="token operator">&gt;</span>    The <span class="token function">time</span> <span class="token keyword">in</span> seconds before the computer is put to sleep. Defaults to <span class="token number">1</span> hour <span class="token punctuation">(</span>Default: <span class="token number">3600</span><span class="token punctuation">)</span>
  --dry-run                                    If true, prints a message instead of sleeping <span class="token punctuation">(</span>Optional<span class="token punctuation">)</span>
</code></pre></div> <p>Pretty neat!</p> <p>Being source generated, you can even F12 the implementation and see exactly what it's doing, but I'm not going to dig into that here. You can see I also added the "dry run" option, so that I could easily test without my laptop going to sleep repeatedly!</p> <h3 id="adding-native-aot-support" class="heading-with-anchor"><a href="https://andrewlock.net/sleep-pc-a-dotnet-tool-to-make-windows-sleep-after-a-timeout/#adding-native-aot-support" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Adding Native AOT support</a></h3> <p>Adding Native AOT support was pretty simple, as ConsoleAppFramework already supports Native AOT, and the app isn't doing much else. I added the <code>&lt;PublishAot&gt;true&lt;/PublishAot&gt;</code> setting to my project file, and there were no build warnings.</p> <p>I was somewhat surprised to find that <code>[DllImport]</code> wasn't flagged, as I thought you had to use the new <code>[LibraryImport]</code> source-generator-based attribute, but apparently that's not always necessary. The <a href="https://learn.microsoft.com/en-us/dotnet/standard/native-interop/best-practices">docs still recommend using it</a> though, so I switched to the new attribute, specifying all the marshalling value as required:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">LibraryImport</span><span class="token attribute-arguments"><span class="token punctuation">(</span><span class="token string">"PowrProf.dll"</span><span class="token punctuation">,</span> SetLastError <span class="token operator">=</span> <span class="token boolean">true</span><span class="token punctuation">)</span></span></span><span class="token punctuation">]</span>
<span class="token punctuation">[</span><span class="token attribute"><span class="token target keyword">return</span><span class="token punctuation">:</span> <span class="token class-name">MarshalAs</span><span class="token attribute-arguments"><span class="token punctuation">(</span>UnmanagedType<span class="token punctuation">.</span>Bool<span class="token punctuation">)</span></span></span><span class="token punctuation">]</span> <span class="token comment">// return is actually BOOL (int)</span>
<span class="token keyword">private</span> <span class="token keyword">static</span> <span class="token keyword">partial</span> <span class="token return-type class-name"><span class="token keyword">bool</span></span> <span class="token function">SetSuspendState</span><span class="token punctuation">(</span>
    <span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">MarshalAs</span><span class="token attribute-arguments"><span class="token punctuation">(</span>UnmanagedType<span class="token punctuation">.</span>U1<span class="token punctuation">)</span></span></span><span class="token punctuation">]</span> <span class="token class-name"><span class="token keyword">bool</span></span> hibernate<span class="token punctuation">,</span> 
    <span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">MarshalAs</span><span class="token attribute-arguments"><span class="token punctuation">(</span>UnmanagedType<span class="token punctuation">.</span>U1<span class="token punctuation">)</span></span></span><span class="token punctuation">]</span> <span class="token class-name"><span class="token keyword">bool</span></span> forceCritical<span class="token punctuation">,</span> 
    <span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">MarshalAs</span><span class="token attribute-arguments"><span class="token punctuation">(</span>UnmanagedType<span class="token punctuation">.</span>U1<span class="token punctuation">)</span></span></span><span class="token punctuation">]</span> <span class="token class-name"><span class="token keyword">bool</span></span> disableWakeEvent<span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre></div> <p>After making this change I tested publishing a Native AOT version of the app using</p> <div class="pre-code-wrapper"><pre class="language-bash"><code class="language-bash">dotnet publish <span class="token parameter variable">-r</span> win-x64 <span class="token parameter variable">-c</span> Release
</code></pre></div> <p>And the resulting <em>sleep-pc.exe</em> file was 3.3MB—not bad for a .NET app including all the runtime bits it needs! But I wondered if I could push that further…</p> <p>After looking through <a href="https://learn.microsoft.com/en-us/dotnet/core/deploying/trimming/trimming-options">the trimming options documentation</a>, I found a bunch of knobs and levers to pull and ultimately managed to shave off ~0.3MB. Not a massive additional improvement, and maybe I could have gone smaller, but meh, good enough! I show the final <em>.csproj</em> I ended up with in the next section.</p> <blockquote> <p><a href="https://github.com/MichalStrehovsky/sizoscope">Michal Strehovský's Sizoscope project</a> is a neat way to "peak inside" your Native AOT binaries to see where all that space is going, and to give you ideas for things you could remove.</p> </blockquote> <p>With the tool now publishing as Native AOT, I worked on publishing it as a NuGet package.</p> <h3 id="packaging-as-a-native-aot-tool" class="heading-with-anchor"><a href="https://andrewlock.net/sleep-pc-a-dotnet-tool-to-make-windows-sleep-after-a-timeout/#packaging-as-a-native-aot-tool" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Packaging as a Native AOT tool</a></h3> <p>I've <a href="https://andrewlock.net/using-and-authoring-dotnet-tools/">written</a> <a href="https://andrewlock.net/exploring-dotnet-10-preview-features-7-packaging-self-contained-and-native-aot-dotnet-tools-for-nuget/">a lot</a> <a href="https://andrewlock.net/exploring-dotnet-10-preview-features-8-supporting-platform-specific-dotnet-tools-on-old-sdks/">recently</a> about .NET tools, and particularly the .NET 10 feature for creating Native AOT .NET tools. Somewhat as a proof of concept, I tried applying this to my new <em>sleep-pc</em> tool.</p> <p>I opted for the <a href="https://andrewlock.net/exploring-dotnet-10-preview-features-8-supporting-platform-specific-dotnet-tools-on-old-sdks/#the-best-of-both-worlds-framework-dependent-and-platform-specific-tools-in-the-same-package">"compromise package"</a> approach that I described in my previous post. With this appoach:</p> <ul><li>The "root" <em>sleep-pc.nupkg</em> contains a .NET 8 framework-dependent build of the tool, with a pointer to the platform-specific version of the tool for .NET 10 SDK users.</li> <li>The packaged application has <code>&lt;RollForward&gt;Major&lt;/RollForward&gt;</code> so that it will run on .NET 9 if the user has that installed and no .NET 8 runtime.</li> <li>The platform-specific package contains the Native AOT asset only, which is used when the user has .NET 10+ installed.</li></ul> <p>I discussed this compromise package style a lot in the last post, so here I just show my final <em>.csproj</em> file:</p> <div class="pre-code-wrapper"><pre class="language-xml"><code class="language-xml"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>Project</span> <span class="token attr-name">Sdk</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>Microsoft.NET.Sdk<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>

  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PropertyGroup</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>OutputType</span><span class="token punctuation">&gt;</span></span>Exe<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>OutputType</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>RootNamespace</span><span class="token punctuation">&gt;</span></span>SleepPc<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>RootNamespace</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>ImplicitUsings</span><span class="token punctuation">&gt;</span></span>enable<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>ImplicitUsings</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>Nullable</span><span class="token punctuation">&gt;</span></span>enable<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>Nullable</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>LangVersion</span><span class="token punctuation">&gt;</span></span>latest<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>LangVersion</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>AllowUnsafeBlocks</span><span class="token punctuation">&gt;</span></span>true<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>AllowUnsafeBlocks</span><span class="token punctuation">&gt;</span></span>

    <span class="token comment">&lt;!--  NuGet/Tool settings --&gt;</span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PackAsTool</span><span class="token punctuation">&gt;</span></span>true<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>PackAsTool</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>ToolCommandName</span><span class="token punctuation">&gt;</span></span>sleep-pc<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>ToolCommandName</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PackageId</span><span class="token punctuation">&gt;</span></span>sleep-pc<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>PackageId</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PackageVersion</span><span class="token punctuation">&gt;</span></span>0.1.0<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>PackageVersion</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>Authors</span><span class="token punctuation">&gt;</span></span>Andrew Lock<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>Authors</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>Description</span><span class="token punctuation">&gt;</span></span>A tool for sending your windows laptop to sleep  after a given time<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>Description</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PackageTags</span><span class="token punctuation">&gt;</span></span>sleep;timer;windows;tool<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>PackageTags</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PackageLicenseExpression</span><span class="token punctuation">&gt;</span></span>MIT<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>PackageLicenseExpression</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>RollForward</span><span class="token punctuation">&gt;</span></span>Major<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>RollForward</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>CopyOutputSymbolsToPublishDirectory</span><span class="token punctuation">&gt;</span></span>false<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>CopyOutputSymbolsToPublishDirectory</span><span class="token punctuation">&gt;</span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>PropertyGroup</span><span class="token punctuation">&gt;</span></span>

  <span class="token comment">&lt;!-- Conditional framework version to support NuGet tool --&gt;</span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PropertyGroup</span> <span class="token attr-name">Condition</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>$(RuntimeIdentifier) != <span class="token punctuation">'</span><span class="token punctuation">'</span><span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>TargetFramework</span><span class="token punctuation">&gt;</span></span>net10.0<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>TargetFramework</span><span class="token punctuation">&gt;</span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>PropertyGroup</span><span class="token punctuation">&gt;</span></span>

  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PropertyGroup</span> <span class="token attr-name">Condition</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>$(RuntimeIdentifier) == <span class="token punctuation">'</span><span class="token punctuation">'</span><span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>TargetFrameworks</span><span class="token punctuation">&gt;</span></span>net8.0;net10.0<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>TargetFrameworks</span><span class="token punctuation">&gt;</span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>PropertyGroup</span><span class="token punctuation">&gt;</span></span>

  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PropertyGroup</span> <span class="token attr-name">Condition</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>$(TargetFramework) == <span class="token punctuation">'</span>net10.0<span class="token punctuation">'</span><span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>RuntimeIdentifiers</span><span class="token punctuation">&gt;</span></span>win-x64<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>RuntimeIdentifiers</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PublishAot</span><span class="token punctuation">&gt;</span></span>true<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>PublishAot</span><span class="token punctuation">&gt;</span></span>
    
    <span class="token comment">&lt;!--  Size optimisation bits for Native AOT --&gt;</span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>DebuggerSupport</span><span class="token punctuation">&gt;</span></span>false<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>DebuggerSupport</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>EventSourceSupport</span><span class="token punctuation">&gt;</span></span>false<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>EventSourceSupport</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>HttpActivityPropagationSupport</span><span class="token punctuation">&gt;</span></span>false<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>HttpActivityPropagationSupport</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>MetricsSupport</span><span class="token punctuation">&gt;</span></span>false<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>MetricsSupport</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>TrimmerRemoveSymbols</span><span class="token punctuation">&gt;</span></span>true<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>TrimmerRemoveSymbols</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>StripSymbols</span><span class="token punctuation">&gt;</span></span>true<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>StripSymbols</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>InvariantGlobalization</span><span class="token punctuation">&gt;</span></span>true<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>InvariantGlobalization</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>IlcDisableReflection</span><span class="token punctuation">&gt;</span></span>true<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>IlcDisableReflection</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>IlcTrimMetadata</span><span class="token punctuation">&gt;</span></span>true<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>IlcTrimMetadata</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>IlcOptimizationPreference</span><span class="token punctuation">&gt;</span></span>Size<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>IlcOptimizationPreference</span><span class="token punctuation">&gt;</span></span>

    <span class="token comment">&lt;!-- For analysis by Sizoscope  --&gt;</span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>IlcGenerateMstatFile</span><span class="token punctuation">&gt;</span></span>true<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>IlcGenerateMstatFile</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>IlcGenerateDgmlFile</span><span class="token punctuation">&gt;</span></span>true<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>IlcGenerateDgmlFile</span><span class="token punctuation">&gt;</span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>PropertyGroup</span><span class="token punctuation">&gt;</span></span>

  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>ItemGroup</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PackageReference</span> <span class="token attr-name">Include</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>ConsoleAppFramework<span class="token punctuation">"</span></span> <span class="token attr-name">Version</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>5.5.0<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>IncludeAssets</span><span class="token punctuation">&gt;</span></span>runtime; build; native; contentfiles; analyzers; buildtransitive<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>IncludeAssets</span><span class="token punctuation">&gt;</span></span>
      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PrivateAssets</span><span class="token punctuation">&gt;</span></span>all<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>PrivateAssets</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>PackageReference</span><span class="token punctuation">&gt;</span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>ItemGroup</span><span class="token punctuation">&gt;</span></span>

<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>Project</span><span class="token punctuation">&gt;</span></span>
</code></pre></div> <p>With this project file we can now package the tool as two NuGet packages by running</p> <div class="pre-code-wrapper"><pre class="language-bash"><code class="language-bash">dotnet pack
dotnet pack <span class="token parameter variable">-r</span> win-x64
</code></pre></div> <p>and the resulting packages are pretty decent sizes:</p> <p><img src="https://andrewlock.net/content/images/2025/sleep-pc.png" alt="The two sleep-pc NuGet packages"></p> <p>That covers most things, so all that's left is spicing up the console logs!</p> <h3 id="improving-the-console-output" class="heading-with-anchor"><a href="https://andrewlock.net/sleep-pc-a-dotnet-tool-to-make-windows-sleep-after-a-timeout/#improving-the-console-output" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Improving the console output</a></h3> <p>Your go-to option whenever you want to have nice looking console output is to use <a href="https://www.nuget.org/packages/Spectre.Console">Spectre.Console</a> but, perhaps controversially, I decided to forgo it in this case. My reasoning being that Spectre.Console technically isn't AOT compatible, and there was really only a tiny amount of dynamism that I wanted.</p> <p>Basically, all I wanted to add was a countdown timer, instead of the app just sitting in a <code>Thread.Sleep()</code> or <code>Task.Wait()</code>. To achieve that I used a <a href="https://stackoverflow.com/questions/888533/how-can-i-update-the-current-line-in-a-c-sharp-windows-console-app">trick</a> that I've used in the past to "replace" a line in the console, by sending backspaces in a <code>Console.WriteLine()</code>:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token class-name"><span class="token keyword">var</span></span> wait <span class="token operator">=</span> TimeSpan<span class="token punctuation">.</span><span class="token function">FromSeconds</span><span class="token punctuation">(</span>sleepDelaySeconds<span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token comment">// Write the initial text</span>
Console<span class="token punctuation">.</span><span class="token function">Write</span><span class="token punctuation">(</span><span class="token string">"⏰ Time remaining:         "</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token comment">// Send a bunch of backspaces followed by the formatted text</span>
Console<span class="token punctuation">.</span><span class="token function">Write</span><span class="token punctuation">(</span><span class="token string">"\b\b\b\b\b\b\b\b{0:hh\\:mm\\:ss}"</span><span class="token punctuation">,</span> wait<span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre></div> <p>When this is running, it looks like the console is updating in place. It's not perfect, but it does the job well enough for me:</p> <p><img src="https://andrewlock.net/content/images/2025/sleep-pc.gif" alt="The animated console loop"></p> <p>To make the countdown run properly, we have to switch to a loop to make sure we're updating the console regularly. That adds a bit of complexity, particularly given we don't want to "drift" from the deadline, but it's relatively self explanatory:</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token comment">// Calculate when we should end, to avoid drift</span>
<span class="token class-name"><span class="token keyword">var</span></span> wait <span class="token operator">=</span> TimeSpan<span class="token punctuation">.</span><span class="token function">FromSeconds</span><span class="token punctuation">(</span>sleepDelaySeconds<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token class-name"><span class="token keyword">var</span></span> deadline <span class="token operator">=</span> DateTimeOffset<span class="token punctuation">.</span>UtcNow<span class="token punctuation">.</span><span class="token function">Add</span><span class="token punctuation">(</span>wait<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token class-name"><span class="token keyword">var</span></span> dryRunText <span class="token operator">=</span> <span class="token punctuation">(</span>dryRun <span class="token punctuation">?</span> <span class="token string">" (dry run)"</span> <span class="token punctuation">:</span> <span class="token string">""</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

Console<span class="token punctuation">.</span>OutputEncoding <span class="token operator">=</span> System<span class="token punctuation">.</span>Text<span class="token punctuation">.</span>Encoding<span class="token punctuation">.</span>UTF8<span class="token punctuation">;</span>
Console<span class="token punctuation">.</span><span class="token function">WriteLine</span><span class="token punctuation">(</span><span class="token interpolation-string"><span class="token string">$@"Will sleep after, </span><span class="token interpolation"><span class="token punctuation">{</span><span class="token expression language-csharp">wait</span><span class="token format-string"><span class="token punctuation">:</span>hh\:mm\:ss</span><span class="token punctuation">}</span></span><span class="token string"> at </span><span class="token interpolation"><span class="token punctuation">{</span><span class="token expression language-csharp">deadline</span><span class="token format-string"><span class="token punctuation">:</span>dd MMM yy HH:mm:ss</span><span class="token punctuation">}</span></span><span class="token interpolation"><span class="token punctuation">{</span><span class="token expression language-csharp">dryRunText</span><span class="token punctuation">}</span></span><span class="token string">"</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span>
Console<span class="token punctuation">.</span><span class="token function">WriteLine</span><span class="token punctuation">(</span><span class="token string">"Press ctrl-c to cancel"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
Console<span class="token punctuation">.</span><span class="token function">WriteLine</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
Console<span class="token punctuation">.</span><span class="token function">Write</span><span class="token punctuation">(</span><span class="token string">"⏰ Time remaining:         "</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token keyword">while</span> <span class="token punctuation">(</span><span class="token operator">!</span>ct<span class="token punctuation">.</span>IsCancellationRequested<span class="token punctuation">)</span>
<span class="token punctuation">{</span>
    <span class="token comment">// Update the clock</span>
    Console<span class="token punctuation">.</span><span class="token function">Write</span><span class="token punctuation">(</span><span class="token string">"\b\b\b\b\b\b\b\b{0:hh\\:mm\\:ss}"</span><span class="token punctuation">,</span> wait<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">try</span>
    <span class="token punctuation">{</span>
        <span class="token keyword">await</span> Task<span class="token punctuation">.</span><span class="token function">Delay</span><span class="token punctuation">(</span><span class="token number">1_000</span><span class="token punctuation">,</span> ct<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
    <span class="token keyword">catch</span>
    <span class="token punctuation">{</span>
        <span class="token comment">// Canceled by the user (Ctrl+C)</span>
        <span class="token keyword">goto</span> canceled<span class="token punctuation">;</span>
    <span class="token punctuation">}</span>

    wait <span class="token operator">=</span> deadline <span class="token operator">-</span> DateTimeOffset<span class="token punctuation">.</span>UtcNow<span class="token punctuation">;</span> 

    <span class="token keyword">if</span> <span class="token punctuation">(</span>wait <span class="token operator">&lt;=</span> TimeSpan<span class="token punctuation">.</span>Zero<span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        Console<span class="token punctuation">.</span><span class="token function">WriteLine</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        Console<span class="token punctuation">.</span><span class="token function">WriteLine</span><span class="token punctuation">(</span><span class="token interpolation-string"><span class="token string">$"💤 Deadline reached, sleeping</span><span class="token interpolation"><span class="token punctuation">{</span><span class="token expression language-csharp">dryRunText</span><span class="token punctuation">}</span></span><span class="token string">"</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>dryRun<span class="token punctuation">)</span>
        <span class="token punctuation">{</span>
            <span class="token keyword">try</span>
            <span class="token punctuation">{</span>
                <span class="token function">SetSuspendState</span><span class="token punctuation">(</span><span class="token boolean">false</span><span class="token punctuation">,</span> <span class="token boolean">false</span><span class="token punctuation">,</span> <span class="token boolean">false</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
            <span class="token punctuation">}</span>
            <span class="token keyword">catch</span> <span class="token punctuation">(</span><span class="token class-name">Exception</span> ex<span class="token punctuation">)</span>
            <span class="token punctuation">{</span>
                Console<span class="token punctuation">.</span><span class="token function">WriteLine</span><span class="token punctuation">(</span><span class="token interpolation-string"><span class="token string">$"⚠️ Error triggering sleep: </span><span class="token interpolation"><span class="token punctuation">{</span><span class="token expression language-csharp">ex</span><span class="token punctuation">}</span></span><span class="token string">"</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span>
            <span class="token punctuation">}</span> 
        <span class="token punctuation">}</span>

        <span class="token keyword">return</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>

canceled<span class="token punctuation">:</span>
Console<span class="token punctuation">.</span><span class="token function">WriteLine</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
Console<span class="token punctuation">.</span><span class="token function">WriteLine</span><span class="token punctuation">(</span><span class="token string">"❌ Sleep cancelled"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre></div> <p>And that's it!</p> <p>You can find the <a href="https://github.com/andrewlock/sleep-pc">full code for the tool on GitHub </a> and the tool <a href="https://www.nuget.org/packages/sleep-pc">on NuGet</a>:</p> <p><img src="https://andrewlock.net/content/images/2025/sleep-pc_2.png" alt="The sleep-pc tool on NuGet"></p> <p>Install it using <code>dotnet tool install -g sleep-pc</code>, and no more failed sleeping!</p> <h2 id="summary" class="heading-with-anchor"><a href="https://andrewlock.net/sleep-pc-a-dotnet-tool-to-make-windows-sleep-after-a-timeout/#summary" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Summary</a></h2> <p>In this post I described a small .NET tool that I built to force a Windows PC to go to sleep after a timer expires. I started by showing the the Win32 API I used to send my laptop to sleep, and the small proof of concept app. I then expanded this implementation to Native AOT compile the app, pack it as a .NET tool, and add some improved console output. You can <a href="https://github.com/andrewlock/sleep-pc">view the code on GitHub</a> or install the tool <a href="https://www.nuget.org/packages/sleep-pc">from Nuget</a>.</p> ]]></content:encoded><category><![CDATA[.NET Core;.NET 10;AOT;NuGet]]></category></item><item><title><![CDATA[Supporting platform-specific .NET tools on old .NET SDKs: Exploring the .NET 10 preview - Part 8]]></title><description><![CDATA[In this post I look at the advantages, trade-offs, and implications of the new platform-specific .NET tool feature added in .NET 10, and how to support old SDKs]]></description><link>https://andrewlock.net/exploring-dotnet-10-preview-features-8-supporting-platform-specific-dotnet-tools-on-old-sdks/</link><guid isPermaLink="true">https://andrewlock.net/exploring-dotnet-10-preview-features-8-supporting-platform-specific-dotnet-tools-on-old-sdks/</guid><pubDate>Tue, 16 Sep 2025 10:00:00 GMT</pubDate><dc:creator><![CDATA[Andrew Lock]]></dc:creator><media:content url="https://andrewlock.net/content/images/2025/tools.webp" medium="image" /><content:encoded><![CDATA[<img src="https://andrewlock.net/content/images/2025/tools.webp" /><nav><p>This is the eighth post in the series: <a href="https://andrewlock.net/series/exploring-the-dotnet-10-preview/">Exploring the .NET 10 preview</a>. </p> <ol class="list-none"><li><a href="https://andrewlock.net/exploring-dotnet-10-preview-features-1-exploring-the-dotnet-run-app.cs/">Part 1 - Exploring the features of dotnet run app.cs</a></li><li><a href="https://andrewlock.net/exploring-dotnet-10-preview-features-2-behind-the-scenes-of-dotnet-run-app.cs/">Part 2 - Behind the scenes of dotnet run app.cs</a></li><li><a href="https://andrewlock.net/exploring-dotnet-10-preview-features-3-csharp-14-extensions-members/">Part 3 - C# 14 extension members; AKA extension everything</a></li><li><a href="https://andrewlock.net/exploring-dotnet-10-preview-features-4-solving-the-source-generator-marker-attribute-problem-in-dotnet-10/">Part 4 - Solving the source generator 'marker attribute' problem in .NET 10</a></li><li><a href="https://andrewlock.net/exploring-dotnet-10-preview-features-5-running-one-off-dotnet-tools-with-dnx/">Part 5 - Running one-off .NET tools with dnx</a></li><li><a href="https://andrewlock.net/exploring-dotnet-10-preview-features-6-passkey-support-for-aspnetcore-identity/">Part 6 - Passkey support for ASP.NET Core identity</a></li><li><a href="https://andrewlock.net/exploring-dotnet-10-preview-features-7-packaging-self-contained-and-native-aot-dotnet-tools-for-nuget/">Part 7 - Packaging self-contained and native AOT .NET tools for NuGet</a></li><li>Part 8 - Supporting platform-specific .NET tools on old .NET SDKs (this post) </li><li><a href="https://andrewlock.net/exploring-dotnet-10-preview-features-9-easier-reflection-with-unsafeaccessortype/">Part 9 - Easier reflection with [UnsafeAccessorType] in .NET 10</a></li></ol></nav><p>In this post I look in more depth at the <a href="https://andrewlock.net/exploring-dotnet-10-preview-features-7-packaging-self-contained-and-native-aot-dotnet-tools-for-nuget/">platform-specific (self-contained or Native AOT) NuGet package support</a> added in .NET 10, and in particular look at how you can continue to support users working with older versions of the .NET SDK.</p> <p>I start by discussing what the new platform-specific features mean to .NET tool authors, in terms of advantages, trade-offs, and implications. I then discuss some possible approaches that attempt to give the best of both worlds: improvements for .NET 10 SDK users, but continued support for .NET 9 SDK and earlier users.</p> <h2 id="the-evolution-of-net-tools-in-net-10" class="heading-with-anchor"><a href="https://andrewlock.net/exploring-dotnet-10-preview-features-8-supporting-platform-specific-dotnet-tools-on-old-sdks/#the-evolution-of-net-tools-in-net-10" class="relative text-zinc-800 dark:text-white no-underline hover:underline">The evolution of .NET tools in .NET 10</a></h2> <p>I have talked about .NET tools in my <a href="https://andrewlock.net/using-and-authoring-dotnet-tools/">last couple</a> of <a href="https://andrewlock.net/exploring-dotnet-10-preview-features-7-packaging-self-contained-and-native-aot-dotnet-tools-for-nuget/">posts</a>, but the reality is that nothing much changed about authoring or consuming .NET tools for a long time. .NET Core 3.0 introduced "local" tools back in 2019, and they pretty much stayed the same since then.</p> <p>However in .NET 10, we suddenly have new features 🎉</p> <ul><li><code>dotnet tool exec</code>/<code>dotnet dnx</code>/<code>dnx</code> allows <a href="https://andrewlock.net/exploring-dotnet-10-preview-features-5-running-one-off-dotnet-tools-with-dnx/">running a .NET tool without explicitly installing it first</a>.</li> <li><a href="https://andrewlock.net/exploring-dotnet-10-preview-features-7-packaging-self-contained-and-native-aot-dotnet-tools-for-nuget/">Platform-specific tools</a> allow packaging tools as self-contained or even Native AOT compiled tools.</li></ul> <p>In this post I discuss how these two features interact and their implications when thinking about the maintenance and support of a .NET tool, as a tool author.</p> <h2 id="what-do-these-new-features-mean-for-tool-authors-" class="heading-with-anchor"><a href="https://andrewlock.net/exploring-dotnet-10-preview-features-8-supporting-platform-specific-dotnet-tools-on-old-sdks/#what-do-these-new-features-mean-for-tool-authors-" class="relative text-zinc-800 dark:text-white no-underline hover:underline">What do these new features mean for tool authors?</a></h2> <p>The new features in .NET 10 are cool for consumers of packages, but what do they mean for tool authors?</p> <h3 id="one-shot-tool-running-with-dnx" class="heading-with-anchor"><a href="https://andrewlock.net/exploring-dotnet-10-preview-features-8-supporting-platform-specific-dotnet-tools-on-old-sdks/#one-shot-tool-running-with-dnx" class="relative text-zinc-800 dark:text-white no-underline hover:underline">One-shot tool running with <code>dnx</code></a></h3> <p>We'll start by thinking about the <code>dnx</code> "one-off" tool running scenario. The primary benefit of this feature is to the <em>consumers</em> of packages, as they don't need to run two commands just to run a tool. It also means they don't "pollute" their path, among other minor things, for something that they only want to run once.</p> <p>As a package author, I don't think there's <em>much</em> for you to think about here. Regardless of how you've written your package, the consumer-side feature works pretty much the same. Customers need to be using the .NET 10 SDK, but you can pack your tool using any SDK, so there's no limitations there. And the runtime requirements for your package are the same regardless of whether they are run using <code>dnx</code> or <code>dotnet tool install</code>.</p> <p>I think the one aspect you could tweak to improve the experience for consumers of your package is to keep your packages as small as possible. The first time a consumer runs <code>dnx</code>, .NET must download your package, and the smaller the package is, the quicker this will be. This applies to the "normal" <code>dotnet tool install</code> path too, but the "one-off" nature of <code>dnx</code> adds additional weight to the size aspect I think.</p> <h3 id="platform-specific-tools" class="heading-with-anchor"><a href="https://andrewlock.net/exploring-dotnet-10-preview-features-8-supporting-platform-specific-dotnet-tools-on-old-sdks/#platform-specific-tools" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Platform-specific tools</a></h3> <p>The other feature, platform-specific tools, has more nuance to it, as it requires changes to the <em>.nupkg</em> packages themselves, and isn't solely an SDK feature. It puts requirements on the author-side (you need to use the .NET 10 SDK to produce platform-specific tools), but it <em>also</em> puts requirements on the consumers of the package.</p> <p>As far as I can tell, there are three main advantages to platform-specific tools:</p> <ol><li>Simpler support matrix</li> <li>Reduced package size</li> <li>Faster startup for Native AOT tools</li></ol> <p>I'm thinking primarily about self-contained or nativeAOT packages for these advantages, but I'll explain each advantage in more detail below.</p> <h4 id="1-simpler-support-matrix-with-self-contained-tools" class="heading-with-anchor"><a href="https://andrewlock.net/exploring-dotnet-10-preview-features-8-supporting-platform-specific-dotnet-tools-on-old-sdks/#1-simpler-support-matrix-with-self-contained-tools" class="relative text-zinc-800 dark:text-white no-underline hover:underline">1. Simpler support matrix with self-contained tools</a></h4> <p>As I explained in <a href="https://andrewlock.net/using-and-authoring-dotnet-tools/">a previous post</a>, if you really want to support as many consumers of your package as possible, you need to compile your tool for <em>all</em> the .NET runtime versions you support. For example if you want to support .NET 6+ that means you need to build and ideally <em>test</em> your tool against .NET 6, .NET 7, .NET 8, .NET 9, and soon .NET 10. That's easy enough in practice, but it's also a bit of a pain.</p> <blockquote> <p>As a reminder, this is necessary because you don't know what .NET runtime or SDK version consumers may have installed when they try to use your tool.</p> </blockquote> <p>You can potentially <em>partially</em> work around this issue by <em>only</em> building for .NET 6 (given the example above), and relying <a href="https://andrewlock.net/using-and-authoring-dotnet-tools/#configuring-your-tools-to-roll-forward">on setting <code>RollForward=Major</code></a> to ensure consumers can still run the tool if they only have .NET 8 installed (for example). That reduces the "build" burden, but you still need to test your tool on newer runtimes, as there can potentially be breaking changes between major versions. If there are breaking changes that affect you, then you might not be able to rely on the rollforward setting at all.</p> <p>With self-contained or NativeAOT tools, the whole support matrix issue goes away. You only need to support a single target framework, the one you pack into the package. This takes away a bunch of complexity, avoids potential issues related to rollforward and simplifies your life overall as a package author by removing this variable. And on the consumer side, you hopefully get a more reliable experience too.</p> <h4 id="2-reduced-package-size" class="heading-with-anchor"><a href="https://andrewlock.net/exploring-dotnet-10-preview-features-8-supporting-platform-specific-dotnet-tools-on-old-sdks/#2-reduced-package-size" class="relative text-zinc-800 dark:text-white no-underline hover:underline">2. Reduced package size</a></h4> <p>Given you're only packing your tool for a <em>single</em> platform and framework with self-contained/NativeAOT tools, you <em>may</em> be able to significantly reduce the size of the resulting package. This will inevitably be very tool-specific:</p> <ul><li>Some tools can't be NativeAOT compiled, and so may need to bundle more of the underlying runtime in the package, especially if your tool isn't trim-safe.</li> <li>If a tool has native platform-specific dependencies, the platform-specific tool can significantly reduce the number of dependencies required, by only packing a single file, instead of one per platform.</li> <li>If a tool supports many target frameworks (for maximum compatibility with consumer environments) you may be able to save a lot of space by switching to only including the single self-contained runtime.</li></ul> <p>The <a href="https://andrewlock.net/exploring-dotnet-10-preview-features-7-packaging-self-contained-and-native-aot-dotnet-tools-for-nuget/">sample app in my previous post</a> was the perfect example of how the platform-specific tools <em>can</em> significantly reduce the size of the package, due to a large number of supported frameworks, and a native dependency that supports many target platforms:</p> <p><img src="https://andrewlock.net/content/images/2025/multirid_15.png" alt="Comparing framework-dependent with Native AOT packages"></p> <p>All those files mean a difference of 92MB to 2.6MB!</p> <blockquote> <p>Bear in mind, this is pretty much a worst/base case example, given the large number of supported frameworks and the large number of native dependencies. Your mileage may vary.</p> </blockquote> <p>Of course, disk space is cheap, so should you really care? The answer, as always, is it depends. Depending on how your tool is used, the size difference may or may not make a big difference.</p> <p>That said, a smaller package is clearly better for the <code>dnx</code> feature of .NET 10, as the assumption is that consumers often <em>won't</em> have the tool downloaded. The bigger the package, the slower it will be to download, and the longer the user has to wait for your tool to execute. Smaller packages are always going to be better in these cases.</p> <h4 id="3-faster-startup" class="heading-with-anchor"><a href="https://andrewlock.net/exploring-dotnet-10-preview-features-8-supporting-platform-specific-dotnet-tools-on-old-sdks/#3-faster-startup" class="relative text-zinc-800 dark:text-white no-underline hover:underline">3. Faster startup</a></h4> <p>Having a smaller package should make <code>dnx</code> executions faster the first time, but once the tool is downloaded, the size of the package doesn't <em>really</em> matter that much. However, if you have native AOT compiled your application, then you'll consistently benefit from the faster startup characteristics.</p> <blockquote> <p>Fast startup times are one of <a href="https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/?tabs=windows%2Cnet8">the key benefits</a> of native AOT compiling your .NET application. They also often have a smaller memory footprint when running, as there's no JIT compiler running (among other reasons).</p> </blockquote> <p>Even if you're not native AOT compiling your application, then another <em>theoretical</em> benefit of a self-contained application is that you may be able to benefit from the performance improvements of a newer runtime. <a href="https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-10/">Every version of .NET gets a bit faster</a>; so if consumers of your package use the included .NET 10 runtime instead of an earlier runtime, then your tool could see performance improvements.</p> <p>However, as self-contained tools <em>require</em> the .NET 10 SDK, that means customers will <em>definitely</em> have the .NET 10 runtime available, so you likely <em>won't</em> see performance improvements today compared to a framework-dependent package. For that reason, I've not considered this as a benefit today, though in the future, as more .NET SDKs are released this calculation may change.</p> <p>So we have three <em>potential</em> benefits of platform-specific tools: a simpler support matrix, reduced package size, and Native AOT startup time improvements. Unfortunately, it's not <em>all</em> roses. There are downsides to adopting platform-specific tools too.</p> <h3 id="downsides-to-platform-specific-tools-" class="heading-with-anchor"><a href="https://andrewlock.net/exploring-dotnet-10-preview-features-8-supporting-platform-specific-dotnet-tools-on-old-sdks/#downsides-to-platform-specific-tools-" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Downsides to platform-specific tools.</a></h3> <p>In my <a href="https://andrewlock.net/exploring-dotnet-10-preview-features-7-packaging-self-contained-and-native-aot-dotnet-tools-for-nuget/#limitations-and-recommendations">previous post</a> I discussed some of the implications of using the platform-specific tools feature, and I also mentioned it above. The crux of the problem is that by default, using platform-specific tools means consumers <em>must</em> be using the .NET 10 SDK. If they try to install a platform-specific tool when using an earlier SDK, they'll get an error like this:</p> <div class="pre-code-wrapper"><pre class="language-bash"><code class="language-bash"><span class="token operator">&gt;</span> dotnet tool <span class="token function">install</span> sayhello

Tool <span class="token string">'sayhello'</span> failed to update due to the following:
The settings <span class="token function">file</span> <span class="token keyword">in</span> the tool<span class="token string">'s NuGet package is invalid: Command '</span>sayhello<span class="token string">' uses unsupported runner '</span>'."
Tool <span class="token string">'sayhello'</span> failed to install. Contact the tool author <span class="token keyword">for</span> assistance.
</code></pre></div> <p>or like this:</p> <div class="pre-code-wrapper"><pre class="language-bash"><code class="language-bash"><span class="token operator">&gt;</span> dotnet tool <span class="token function">install</span> sayhello.win-x64

Tool <span class="token string">'sayhello.win-x64'</span> failed to update due to the following:
The settings <span class="token function">file</span> <span class="token keyword">in</span> the tool<span class="token string">'s NuGet package is invalid: Command '</span>sayhello<span class="token string">' uses unsupported runner '</span>executable<span class="token string">'."
Tool '</span>sayhello.win-x64' failed to install. Contact the tool author <span class="token keyword">for</span> assistance.
</code></pre></div> <p>This all stems from the <em>DotnetToolSettings.xml</em> that's included in the <em>.nupkg</em> package and tells the .NET SDK <em>how</em> to run your app. For .NET 9 and below (and for framework-dependent, platform-agnostic tools in .NET 10) the file looks something like this:</p> <div class="pre-code-wrapper"><pre class="language-xml"><code class="language-xml"><span class="token prolog">&lt;?xml version="1.0" encoding="utf-8"?&gt;</span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>DotNetCliTool</span> <span class="token attr-name">Version</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>1<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>Commands</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>Command</span> <span class="token attr-name">Name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>sayhello<span class="token punctuation">"</span></span> <span class="token attr-name">EntryPoint</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>MultiRid.dll<span class="token punctuation">"</span></span> <span class="token attr-name">Runner</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>dotnet<span class="token punctuation">"</span></span> <span class="token punctuation">/&gt;</span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>Commands</span><span class="token punctuation">&gt;</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>DotNetCliTool</span><span class="token punctuation">&gt;</span></span>
</code></pre></div> <p>As you can see, this uses <code>Version=1</code> for the schema, and has <code>Runner=dotnet</code> for the <code>Command</code> entry. In contrast, if we look at the <em>DotnetToolSettings.xml</em> files for your .NET 10 platform-specific tools, these use schema <code>Version=2</code>, and a different (or missing) <code>Runner</code>.</p> <p>The <em>DotnetToolSettings.xml</em> file in the platform-specific package looks like this, for example:</p> <div class="pre-code-wrapper"><pre class="language-xml"><code class="language-xml"><span class="token prolog">&lt;?xml version="1.0" encoding="utf-8"?&gt;</span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>DotNetCliTool</span> <span class="token attr-name">Version</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>2<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>Commands</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>Command</span> <span class="token attr-name">Name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>sayhello<span class="token punctuation">"</span></span> <span class="token attr-name">EntryPoint</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>MultiRid<span class="token punctuation">"</span></span> <span class="token attr-name">Runner</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>executable<span class="token punctuation">"</span></span> <span class="token punctuation">/&gt;</span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>Commands</span><span class="token punctuation">&gt;</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>DotNetCliTool</span><span class="token punctuation">&gt;</span></span>
</code></pre></div> <p>Unfortunately, the <code>Runner=executable</code> is only understood by the .NET 10+ SDK, which means you can't install these packages on earlier SDKs.</p> <h2 id="the-conundrum-should-tool-authors-use-platform-specific-packages-" class="heading-with-anchor"><a href="https://andrewlock.net/exploring-dotnet-10-preview-features-8-supporting-platform-specific-dotnet-tools-on-old-sdks/#the-conundrum-should-tool-authors-use-platform-specific-packages-" class="relative text-zinc-800 dark:text-white no-underline hover:underline">The conundrum: should tool authors use platform-specific packages?</a></h2> <p>We've established that platform-specific packages may have many benefits for some .NET tools, but they can only be used by users with the .NET 10 SDK. The two questions you need to answer as a tool author are:</p> <ol><li>Would my tool benefit from platform-specific packages?</li> <li>Is it a problem to require that my users use the .NET 10 SDK?</li></ol> <p>As I've already described, the answer to the first question will depend on your tool. Platform-specific packages give 3 main advantages in general, but these advantages are biggest if you have a large range of supported target frameworks, if your tool uses platform-specific dependencies, and if you can NativeAOT your package. If those don't apply to you, then the trade off in installation support may not be worth it.</p> <p>However, if we assume your tool <em>would</em> benefit from being platform-specific packages, you have to consider whether the requirement that users use the .NET 10 SDK is a problem.</p> <p>Depending on the tool that you're creating, this might be fine. If you're just creating an OSS tool, or a tool for your own consumption, for example, then requiring that users have the .NET 10 SDK installed might not be a problem for you. After all, <em>in general</em>, people should always be using the newest .NET SDK as it can still build for earlier .NET runtimes.</p> <p>However, if you're building a tool where adoption and ease-of-use is your primary concern, then this may not be acceptable. This might be the case if your tool is produced by a company, if it's a part of your product, or if it's intended to run in places where .NET 10 won't.</p> <blockquote> <p><a href="https://github.com/dotnet/runtime/issues/109939">.NET 10 doesn't support as many Linux distributions as some previous versions</a>, dropping support for older Alpine and Ubuntu versions, for example. If you need your tool to run in those scenarios, and you want to distribute it via NuGet, then you <em>must</em> support earlier versions of the SDK.</p> </blockquote> <p>This conflict made me wonder if there was some sort of middle-ground possible, a kind of "compromise" package that gives you the best of all worlds.</p> <h2 id="the-best-of-both-worlds-framework-dependent-and-platform-specific-tools-in-the-same-package" class="heading-with-anchor"><a href="https://andrewlock.net/exploring-dotnet-10-preview-features-8-supporting-platform-specific-dotnet-tools-on-old-sdks/#the-best-of-both-worlds-framework-dependent-and-platform-specific-tools-in-the-same-package" class="relative text-zinc-800 dark:text-white no-underline hover:underline">The best of both worlds? Framework-dependent and platform-specific tools in the same package</a></h2> <p>The compromise solution I had in mind looks something like this:</p> <p><img src="https://andrewlock.net/content/images/2025/multirid_12.png" alt="A workaround for the .NET 10 SDK compatibility problem"></p> <p>I'll show how to implement this sort of package shortly, but before we get into the technical details, I think it's worth discussing the pros and cons of this approach, what scenarios it supports, and the implications for both tool authors and consumers.</p> <p>With this setup, you pack the framework-dependent, platform-agnostic version of your tool inside the lowest support target-framework <em>tools</em> folder, i.e. <code>tools/netcoreapp3.1</code> in this case. In addition, in the <code>net10.0</code> folder, we pack the <code>Version=2</code> <em>DotnetToolSettings.xml</em> file that points to all our other platform-specific packages.</p> <blockquote> <p>As a reminder, when you produce platform-specific tools, you typically produce <code>N+1</code> packages, where <code>N</code> is the number of platforms. e.g. you produce the root package, <code>sayhello.&lt;version&gt;.nupkg</code>, <code>sayhello.win-x64.&lt;version&gt;.nupkg</code>, <code>sayhello.linux-x64.&lt;version&gt;.nupkg</code>, etc.</p> </blockquote> <p>If you install or run this tool with the .NET 10 SDK, the SDK will download the package, read the <em>xml</em> file in the <code>net10.0</code> folder, and then immediately fetch and run the appropriate platform-specific package, e.g. <code>sayhello.win-x64</code>. So if you have the .NET 10 SDK installed you get the optimised Native AOT version of the package.</p> <p>For users that are using earlier versions of the SDK, anything from <code>netcoreapp3.1</code> to <code>net9.0</code>, the SDK looks for the highest <em>compatible</em> tools folder (<code>tools/netcoreapp3.1</code>) and reads the <em>xml</em> file it contains, which is a standard <code>Version=1</code> file:</p> <div class="pre-code-wrapper"><pre class="language-xml"><code class="language-xml"><span class="token prolog">&lt;?xml version="1.0" encoding="utf-8"?&gt;</span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>DotNetCliTool</span> <span class="token attr-name">Version</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>1<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>Commands</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>Command</span> <span class="token attr-name">Name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>sayhello<span class="token punctuation">"</span></span> <span class="token attr-name">EntryPoint</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>MultiRid.dll<span class="token punctuation">"</span></span> <span class="token attr-name">Runner</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>dotnet<span class="token punctuation">"</span></span> <span class="token punctuation">/&gt;</span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>Commands</span><span class="token punctuation">&gt;</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>DotNetCliTool</span><span class="token punctuation">&gt;</span></span>
</code></pre></div> <p>The older SDKs just see a normal, "traditional", framework-dependent platform-agnostic .NET tool and can run it without issue.</p> <p>On the face of it, this looks like you have the best of both worlds:</p> <ul><li>.NET 10 SDK users can use a NativeAOT version of the tool distributed in platform-specific packages.</li> <li>Earlier SDK users can still use the .NET tool, just as they would before.</li></ul> <p>However, it's not quite as simple as that. Let's consider the three advantages of platform-specific tools I highlighted earlier:</p> <ol><li><strong>Simpler support matrix</strong>. ❌ With this model you're <em>not</em> simplifying your matrix. Instead of replacing your support matrix with a single self-contained tool, your existing support matrix stays the same, assuming you were also always going to add support for .NET 10 (likely self-contained or Native AOT compiled).</li> <li><strong>Reduced package size</strong>. ❌ This also likely doesn't apply. Your "root" package contains the same target framework files as in the previous (non-platform specific) version of the package. So users on old SDKs have the exact same package size, and users on the .NET 10 SDK have the combination of the "root" package <em>and</em> the platform-specific package to download!</li> <li><strong>Faster startup for Native AOT tools</strong>. ✅ This one still applies. For .NET 10 SDK users, once you've downloaded the platform-specific Native AOT package, they'll benefit from the Native AOT advantages.</li></ol> <p>So on the face of it, this approach currently only seems to make sense if you are producing Native AOT platform-specific packages, which will see the associated startup benefits.</p> <p>It's worth pointing out that this cost-benefit calculation applies <em>today</em>, but that it will change over time. In the future, when .NET 11, .NET 12, or .NET 13 SDKs are released, your package doesn't need to change. Those newer users can still install your .NET 10 tool, and your package size and support matrix doesn't need to change, because you can support those users with the existing NativeAOT tool.</p> <p>In addition, as time goes on, you may well want to reduce your support for earlier versions of the .NET SDK. That will likely involve removing support for earlier runtimes from your root package, or relying on <code>RollForward=Major</code> for support instead. All of which means this approach may become <em>more</em> useful with time than it is today.</p> <p>So unfortunately there's still not a "correct" answer here. It really depends on your requirements for supporting older .NET SDKs and how much you're willing to "penalise" users who <em>are</em> on the .NET 10 SDK with larger package versions.</p> <h2 id="case-study-1-the-datadog-dd-trace-net-tool" class="heading-with-anchor"><a href="https://andrewlock.net/exploring-dotnet-10-preview-features-8-supporting-platform-specific-dotnet-tools-on-old-sdks/#case-study-1-the-datadog-dd-trace-net-tool" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Case-study 1: the Datadog <code>dd-trace</code> .NET tool</a></h2> <p>There's so many pros and cons at play here, it's hard to grasp onto something concrete, so I thought it would make sense to consider some concrete examples, starting with the Datadog <code>dd-trace</code> .NET Tool.</p> <p><a href="https://www.nuget.org/packages/dd-trace#supportedframeworks-body-tab">The Datadog <code>dd-trace</code> tool</a> provides an easy way to add automatic instrumentation to your application. For example, after installing the tool you can instrument your app something like this:</p> <div class="pre-code-wrapper"><pre class="language-bash"><code class="language-bash">dd-trace run -- MyApp.exe
</code></pre></div> <p>The exact details of how to use the tool or how it works aren't important for this discussion, the more important point is that we support basically <em>all</em> of the frameworks that we can instrument:</p> <p><img src="https://andrewlock.net/content/images/2025/dd_trace.png" alt="The contents of the dd-trace tool showing support for .NET Core 2.1"></p> <p>In addition, we also ship <a href="https://docs.datadoghq.com/tests/setup/dotnet/?tab=ciproviderwithautoinstrumentationsupport#installing-the-net-tracer-cli">self-contained versions</a> of the tool for some platforms which we make available as direct download links rather than using the .NET SDK.</p> <p>The question is whether platform-specific packages make sense for the <code>dd-trace</code> .NET tool 🤔</p> <p>The obvious first point is that we <em>can't</em> require our users to use the .NET 10 SDK. We need to support users with whatever tools they're currently using, which means if we do anything with platform-specific packages, we would <em>need</em> to ship the "combination" package I described in the previous section (and which I'll show how to create in the next section).</p> <p>But would that actually be useful? We already create self-contained (trimmed) builds of our tool, but we <em>don't</em> produce Native AOT builds, and I don't think we will be able to. Bearing that in mind, and considering the theoretical benefits:</p> <ol><li><strong>Simpler support matrix</strong>. ❌ We are shipping for all TFMs from .NET Core 2.1-.NET 10 whichever approach we take.</li> <li><strong>Reduced package size</strong>. ❌ The size is actually much <em>worse</em> with platform-specific packages, as .NET 10 SDK users pay the cost of the existing tool (~135MB), and <em>then</em> the cost of a self-contained (trimmed) package (~50MB). In contrast, simply adding .NET 10 to the existing package would only add ~5MB to the package size</li> <li><strong>Faster startup for Native AOT tools</strong>. ❌ We don't ship the tool as a NativeAOT tool, so there will likely not be any performance benefits.</li></ol> <p>So it seems like shipping a platform-specific package for <code>dd-trace</code> simply doesn't make sense. That said, it's possible we could redesign some of the tool internals so that it <em>does</em> make more sense, but without the potential benefits of Native AOT it's hard to justify.</p> <h2 id="case-study-2-the-sleep-pc-net-tool" class="heading-with-anchor"><a href="https://andrewlock.net/exploring-dotnet-10-preview-features-8-supporting-platform-specific-dotnet-tools-on-old-sdks/#case-study-2-the-sleep-pc-net-tool" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Case study 2: the <code>sleep-pc</code> .NET tool</a></h2> <p>As a contrast to <code>dd-trace</code> is a small tool I wrote the other week to solve a simple problem called <code>sleep-pc</code>. <code>sleep-pc</code> simply sends a Windows PC to sleep after a specified duration.</p> <blockquote> <p>A short post on this tool is coming shortly!</p> </blockquote> <p>In general this is a tool for me, which I don't expect (or particularly <em>want</em>) to be widely used, so I could simply require the .NET 10 SDK. However as an exercise, I decided I would support .NET 8+. What's more, I could easily NativeAOT compile this tool.</p> <p>In this case, the compromise-package approach works pretty well. If we look inside the <code>sleep-pc</code> package, we can see the framework-dependent platform-agnostic tool is in the <code>net8.0</code> folder, so the tool supports the .NET 8 SDK+, and we have a link to the Native AOT compiled <code>sleep-pc.win-x64</code> package in the <code>net10.0</code> folder:</p> <p><img src="https://andrewlock.net/content/images/2025/multirid_16.png" alt="The sleep-pc tool root package contents"></p> <p>What's more, this "root" package is only 17KB, so you <em>really</em> aren't paying much size cost for having to download it first on .NET 10 (in addition to the 1.5MB Native AOT package). So in this case I think the compromise-package approach works well 🎉.</p> <h2 id="implementing-the-compromise-package" class="heading-with-anchor"><a href="https://andrewlock.net/exploring-dotnet-10-preview-features-8-supporting-platform-specific-dotnet-tools-on-old-sdks/#implementing-the-compromise-package" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Implementing the compromise package</a></h2> <p>Hopefully we've established that the compromise-package approach, in which we embed one or more framework-dependent platform-agnostic versions of a .NET tool into a "root" package, <em>may</em> be useful. In this section I'll (finally) show how to implement it.</p> <p>The good news is that it's actually relatively simple to do. The basic steps are:</p> <ul><li>Target all the runtimes you need. You must target at least .NET 10 plus one or more older runtimes.</li> <li>Add conditions to your <em>.csproj</em> file so that you apply the self-contained/Native AOT properties to the <code>net10.0</code> target <em>only</em>.</li> <li>Conditionally set the <code>&lt;TargetFramework&gt;</code>/<code>&lt;TargetFrameworks&gt;</code> based on the value of <code>RuntimeIdentifier</code> (which is optionally specified when you're calling <code>dotnet pack</code>)</li> <li>Run <code>dotnet pack</code> to produce the root package, and then <code>dotnet pack -r &lt;runtime&gt;</code> for each of your supported runtimes.</li></ul> <p>The <em>.csproj</em> below shows a concrete example based on the "sayhello" example from <a href="https://andrewlock.net/exploring-dotnet-10-preview-features-7-packaging-self-contained-and-native-aot-dotnet-tools-for-nuget/">my previous post</a>.</p> <div class="pre-code-wrapper"><pre class="language-xml"><code class="language-xml"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>Project</span> <span class="token attr-name">Sdk</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>Microsoft.NET.Sdk<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>

  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PropertyGroup</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>OutputType</span><span class="token punctuation">&gt;</span></span>Exe<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>OutputType</span><span class="token punctuation">&gt;</span></span>
    <span class="token comment">&lt;!-- We specify these later, conditionally --&gt;</span>
    <span class="token comment">&lt;!-- &lt;TargetFrameworks&gt;netcoreapp3.1;net10.0&lt;/TargetFrameworks&gt; --&gt;</span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>LangVersion</span><span class="token punctuation">&gt;</span></span>latest<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>LangVersion</span><span class="token punctuation">&gt;</span></span>

    <span class="token comment">&lt;!-- Standard tool settings --&gt;</span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PackAsTool</span><span class="token punctuation">&gt;</span></span>true<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>PackAsTool</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>ToolCommandName</span><span class="token punctuation">&gt;</span></span>sayhello<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>ToolCommandName</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PackageId</span><span class="token punctuation">&gt;</span></span>sayhello<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>PackageId</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PackageVersion</span><span class="token punctuation">&gt;</span></span>1.0.0<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>PackageVersion</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>Authors</span><span class="token punctuation">&gt;</span></span>Andrew Lock<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>Authors</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>Description</span><span class="token punctuation">&gt;</span></span>A tool that says hello<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>Description</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PackageTags</span><span class="token punctuation">&gt;</span></span>ascii;art;figlet;console;tool<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>PackageTags</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PackageLicenseExpression</span><span class="token punctuation">&gt;</span></span>MIT<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>PackageLicenseExpression</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>RollForward</span><span class="token punctuation">&gt;</span></span>Major<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>RollForward</span><span class="token punctuation">&gt;</span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>PropertyGroup</span><span class="token punctuation">&gt;</span></span>
  
  <span class="token comment">&lt;!-- Package dependencies  --&gt;</span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>ItemGroup</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PackageReference</span> <span class="token attr-name">Include</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>Spectre.Console<span class="token punctuation">"</span></span> <span class="token attr-name">Version</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>0.50.0<span class="token punctuation">"</span></span> <span class="token punctuation">/&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PackageReference</span> <span class="token attr-name">Include</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>Microsoft.Data.Sqlite<span class="token punctuation">"</span></span> <span class="token attr-name">Version</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>9.0.8<span class="token punctuation">"</span></span> <span class="token punctuation">/&gt;</span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>ItemGroup</span><span class="token punctuation">&gt;</span></span>

  <span class="token comment">&lt;!-- If we're running 'dotnet pack -r &lt;runtime&gt;' then             --&gt;</span>
  <span class="token comment">&lt;!-- RuntimeIdentifier will have a value, and we explicitly        --&gt;</span>
  <span class="token comment">&lt;!-- target .NET 10. This produces the platform-specific packages. --&gt;</span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PropertyGroup</span> <span class="token attr-name">Condition</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>$(RuntimeIdentifier) != <span class="token punctuation">'</span><span class="token punctuation">'</span><span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>TargetFramework</span><span class="token punctuation">&gt;</span></span>net10.0<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>TargetFramework</span><span class="token punctuation">&gt;</span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>PropertyGroup</span><span class="token punctuation">&gt;</span></span>

  <span class="token comment">&lt;!-- By default, 'dotnet pack' does not have a RuntimeIdentifier  --&gt;</span>
  <span class="token comment">&lt;!-- so the 'root' package will know about both of these runtimes --&gt;</span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PropertyGroup</span> <span class="token attr-name">Condition</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>$(RuntimeIdentifier) == <span class="token punctuation">'</span><span class="token punctuation">'</span><span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>TargetFrameworks</span><span class="token punctuation">&gt;</span></span>netcoreapp3.1;net10.0<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>TargetFrameworks</span><span class="token punctuation">&gt;</span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>PropertyGroup</span><span class="token punctuation">&gt;</span></span>
  
  <span class="token comment">&lt;!-- For .NET 10, dotnet pack will treat the app as a Native AOT  --&gt;</span>
  <span class="token comment">&lt;!-- package so it _won't_ pack it in the root package, and will  --&gt;</span>
  <span class="token comment">&lt;!-- create a Version=2 DotnetToolSettings.xml file in the root.  --&gt;</span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PropertyGroup</span> <span class="token attr-name">Condition</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>$(TargetFramework) == <span class="token punctuation">'</span>net10.0<span class="token punctuation">'</span><span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>RuntimeIdentifiers</span><span class="token punctuation">&gt;</span></span>linux-x64;linux-arm64;win-x64;win-arm64;osx-arm64;<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>RuntimeIdentifiers</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PublishSelfContained</span><span class="token punctuation">&gt;</span></span>true<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>PublishSelfContained</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PublishAot</span><span class="token punctuation">&gt;</span></span>true<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>PublishAot</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PublishTrimmed</span><span class="token punctuation">&gt;</span></span>true<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>PublishTrimmed</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>StripSymbols</span><span class="token punctuation">&gt;</span></span>true<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>StripSymbols</span><span class="token punctuation">&gt;</span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>PropertyGroup</span><span class="token punctuation">&gt;</span></span>
  
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>Project</span><span class="token punctuation">&gt;</span></span>
</code></pre></div> <p>The 'trick' to creating this compromise package is the conditional setting of frameworks when you're creating the 'root' package with <code>dotnet pack</code> versus the platform-specific packages with <code>dotnet pack -r &lt;runtime&gt;</code>.</p> <p>When you run <code>dotnet pack</code> as-is, <code>$(RuntimeIdentifier)</code> is empty, so the pack tool sees both <code>netcoreapp3.1</code> and <code>net10.0</code> target frameworks. Additionally, it sees that the <code>net10.0</code> target is going to be Native AOT compiled, so it <em>shouldn't</em> include it in the root package. For the <code>netcoreapp3.1</code> target it just sees a normal framework-dependent, platform agnostic build, so it packs that using the <code>Version=1</code> <em>xml</em> file.</p> <p>In contrast, when you run <code>dotnet pack -r &lt;runtime&gt;</code>, <code>$(RuntimeIdentifier)</code> has a value, so the pack tool <em>only</em> sees the <code>net10.0</code> target framework. This is important because .NET Core 3.1 doesn't support Native AOT so it would fail to build, but even if this was <code>net8.0</code> or something which <em>does</em> support Native AOT, we don't want (or need) to include it in the platform-specific package; it would just be dead weight.</p> <p>When you build this package you get a root package that supports .NET Core 3.1+, as I showed earlier:</p> <p><img src="https://andrewlock.net/content/images/2025/multirid_12.png" alt="A workaround for the .NET 10 SDK compatibility problem"></p> <p>And additionally, you'll have a Native AOT platform-specific package:</p> <p><img src="https://andrewlock.net/content/images/2025/multirid_17.png" alt="The platform-specific package contents"></p> <p>So if the compromise-package works for your use-case: win-win!</p> <h2 id="alternative-compromises" class="heading-with-anchor"><a href="https://andrewlock.net/exploring-dotnet-10-preview-features-8-supporting-platform-specific-dotnet-tools-on-old-sdks/#alternative-compromises" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Alternative compromises</a></h2> <p>When I was initially discussing the possibility of <a href="https://github.com/dotnet/sdk/issues/50517">a compromise package on the .NET SDK repo</a>, <a href="https://github.com/baronfel">Chet Husk</a> described a slightly different approach that <a href="https://github.com/dotnet/aspire/commit/55e0bdc9796f3ea3da4c54402969d07610179f16">he had taken with the Aspire CLI</a>. This approach is by necessity pretty hacky, as it has to dive into the internals of the pack command and tweak things. Overall, the result is quite different to my previous compromise package proposal.</p> <p>This alternative takes the following approach:</p> <ul><li>The tool targets a single target framework, anything .NET 9 or below.</li> <li>A framework-dependent platform agnostic version of the tool is embedded in the "root" package</li> <li>The <em>DotnetToolSettings.xml</em> file embedded in the root package is tweaked to look a bit like a cross between a <code>Version=1</code> file and a <code>Version=2</code> file</li></ul> <p>That last point is a surprising one, in that it really doesn't feel like it should work 😅 The resulting root package looks something like this:</p> <p><img src="https://andrewlock.net/content/images/2025/multirid_18.png" alt="The alternative compromise package"></p> <p>The <em>xml</em> file is marked as a <code>Version=1</code> file, and has <code>Runner=dotnet</code> for the command so it can be run by older .NET SDKs without issue. The <em>interesting</em> point is that apparently the .NET 10 SDK still reads the <code>RuntimeIdentifierPackage</code> elements, even though the file <em>isn't</em> <code>Version=2</code>.</p> <blockquote> <p>Which makes me wonder… why was a breaking <code>Version=2</code> needed if a purely additive solution to <code>Version=1</code> would have worked? 🤷</p> </blockquote> <p>Overall, this alternative package has many of the same pros and cons as my compromise package. There are a few notable differences:</p> <ul><li>My compromise package requires that you multi-target, with at least one framework being .NET 10, the alternative package makes most sense with a single non-.NET 10 framework.</li> <li>My compromise package generally requires that your platform-specific package is .NET 10, whereas the alternative package uses the same framework for everything.</li></ul> <p>I think that the differences between the approaches mean one isn't necessarily always better than the other, but you might find it suits some scenarios better than others.</p> <p>For completeness, the following is a complete <em>.csproj</em> file for the tool generated above. Just be aware that it's somewhat messing with the internals of the <code>Microsoft.NET.PackTool</code> targets:</p> <div class="pre-code-wrapper"><pre class="language-xml"><code class="language-xml"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>Project</span> <span class="token attr-name">Sdk</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>Microsoft.NET.Sdk<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>

  <span class="token comment">&lt;!-- Standard tool settings --&gt;</span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PropertyGroup</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>OutputType</span><span class="token punctuation">&gt;</span></span>Exe<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>OutputType</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>LangVersion</span><span class="token punctuation">&gt;</span></span>latest<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>LangVersion</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PackAsTool</span><span class="token punctuation">&gt;</span></span>true<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>PackAsTool</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>ToolCommandName</span><span class="token punctuation">&gt;</span></span>sayhello<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>ToolCommandName</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PackageId</span><span class="token punctuation">&gt;</span></span>sayhello<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>PackageId</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PackageVersion</span><span class="token punctuation">&gt;</span></span>1.0.0<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>PackageVersion</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>Authors</span><span class="token punctuation">&gt;</span></span>Andrew Lock<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>Authors</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>Description</span><span class="token punctuation">&gt;</span></span>A tool that says hello<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>Description</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PackageTags</span><span class="token punctuation">&gt;</span></span>ascii;art;figlet;console;tool<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>PackageTags</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PackageLicenseExpression</span><span class="token punctuation">&gt;</span></span>MIT<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>PackageLicenseExpression</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>RollForward</span><span class="token punctuation">&gt;</span></span>Major<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>RollForward</span><span class="token punctuation">&gt;</span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>PropertyGroup</span><span class="token punctuation">&gt;</span></span>
  
  <span class="token comment">&lt;!-- Package dependencies  --&gt;</span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>ItemGroup</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PackageReference</span> <span class="token attr-name">Include</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>Spectre.Console<span class="token punctuation">"</span></span> <span class="token attr-name">Version</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>0.50.0<span class="token punctuation">"</span></span> <span class="token punctuation">/&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PackageReference</span> <span class="token attr-name">Include</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>Microsoft.Data.Sqlite<span class="token punctuation">"</span></span> <span class="token attr-name">Version</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>9.0.8<span class="token punctuation">"</span></span> <span class="token punctuation">/&gt;</span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>ItemGroup</span><span class="token punctuation">&gt;</span></span>

  <span class="token comment">&lt;!-- target .NET 10 and configure AOT etc--&gt;</span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PropertyGroup</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>TargetFramework</span><span class="token punctuation">&gt;</span></span>net10.0<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>TargetFramework</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>RuntimeIdentifiers</span><span class="token punctuation">&gt;</span></span>linux-x64;linux-arm64;win-x64;win-arm64;osx-arm64;<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>RuntimeIdentifiers</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PublishSelfContained</span><span class="token punctuation">&gt;</span></span>true<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>PublishSelfContained</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PublishAot</span><span class="token punctuation">&gt;</span></span>true<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>PublishAot</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PublishTrimmed</span><span class="token punctuation">&gt;</span></span>true<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>PublishTrimmed</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>StripSymbols</span><span class="token punctuation">&gt;</span></span>true<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>StripSymbols</span><span class="token punctuation">&gt;</span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>PropertyGroup</span><span class="token punctuation">&gt;</span></span>
  
  <span class="token comment">&lt;!-- A bunch of hackery to embed the FDD build in the root package --&gt;</span>
  <span class="token comment">&lt;!-- and to fix the xml files to look like V1 --&gt;</span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PropertyGroup</span> <span class="token attr-name">Condition</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>$(RuntimeIdentifier) == <span class="token punctuation">'</span><span class="token punctuation">'</span><span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>_ToolPackageShouldIncludeImplementation</span><span class="token punctuation">&gt;</span></span>true<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>_ToolPackageShouldIncludeImplementation</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>GenerateNuspecDependsOn</span><span class="token punctuation">&gt;</span></span>$(GenerateNuspecDependsOn);_MakeV2ConfigLookLikeV1<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>GenerateNuspecDependsOn</span><span class="token punctuation">&gt;</span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>PropertyGroup</span><span class="token punctuation">&gt;</span></span>

  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>Target</span> <span class="token attr-name">Name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>_MakeV2ConfigLookLikeV1<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
    <span class="token comment">&lt;!-- Use the XmlPoke Task to make the v2 config also look like a v1 config --&gt;</span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>XmlPoke</span>
            <span class="token attr-name">XmlInputPath</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>$(_ToolsSettingsFilePath)<span class="token punctuation">"</span></span>
            <span class="token attr-name">Value</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>1<span class="token punctuation">"</span></span>
            <span class="token attr-name">Query</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>/DotNetCliTool/@Version<span class="token punctuation">"</span></span>
            <span class="token attr-name">Namespaces</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>$(Namespace)<span class="token punctuation">"</span></span><span class="token punctuation">/&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>XmlPoke</span>
            <span class="token attr-name">XmlInputPath</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>$(_ToolsSettingsFilePath)<span class="token punctuation">"</span></span>
            <span class="token attr-name">Value</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span><span class="token entity named-entity" title="<">&amp;lt;</span>Command Name=<span class="token entity named-entity" title="&quot;">&amp;quot;</span>$(ToolCommandName)<span class="token entity named-entity" title="&quot;">&amp;quot;</span> EntryPoint=<span class="token entity named-entity" title="&quot;">&amp;quot;</span>$(ToolEntryPoint)<span class="token entity named-entity" title="&quot;">&amp;quot;</span> Runner=<span class="token entity named-entity" title="&quot;">&amp;quot;</span>$(ToolCommandRunner)<span class="token entity named-entity" title="&quot;">&amp;quot;</span> /<span class="token entity named-entity" title=">">&amp;gt;</span><span class="token punctuation">"</span></span>
            <span class="token attr-name">Query</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>/DotNetCliTool/Commands<span class="token punctuation">"</span></span>
            <span class="token attr-name">Namespaces</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>$(Namespace)<span class="token punctuation">"</span></span><span class="token punctuation">/&gt;</span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>Target</span><span class="token punctuation">&gt;</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>Project</span><span class="token punctuation">&gt;</span></span>
</code></pre></div> <p>This post is already plenty long enough, so I'm not going to go into any more detail about this approach, but it should at least give you an alternative for creating packages that can use the newest features of the .NET 10 SDK while remaining compatible with earlier .NET SDKs</p> <h2 id="summary" class="heading-with-anchor"><a href="https://andrewlock.net/exploring-dotnet-10-preview-features-8-supporting-platform-specific-dotnet-tools-on-old-sdks/#summary" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Summary</a></h2> <p>In this post I discussed what the new platform-specific (self-contained or Native AOT compiled) NuGet package support means for package authors, and how to leverage them while still supporting users on pre-.NET 10 SDK versions. I started by describing the problem: users on older .NET SDKs can't use your tool at all by default if you use the new .NET 10 platform-specific tools feature.</p> <p>For the remainder of the post I described a potential compromise in which .NET 10 SDK users can use platform-specific tools, while pre-.NET SDK users continue to use a framework-dependent platform-agnostic version of the tool. This has some trade offs, in that the "root" package needs to be large, but this may still be worth it in some cases. I described two different variants of the package, each with different requirements and trade-offs.</p> ]]></content:encoded><category><![CDATA[.NET 10;AOT;NuGet;Datadog]]></category></item><item><title><![CDATA[Packaging self-contained and native AOT .NET tools for NuGet: Exploring the .NET 10 preview - Part 7]]></title><description><![CDATA[In this post we look at the new support for platform-specific .NET tools, so that you can pack your tools as self-contained or Native AOT packages]]></description><link>https://andrewlock.net/exploring-dotnet-10-preview-features-7-packaging-self-contained-and-native-aot-dotnet-tools-for-nuget/</link><guid isPermaLink="true">https://andrewlock.net/exploring-dotnet-10-preview-features-7-packaging-self-contained-and-native-aot-dotnet-tools-for-nuget/</guid><pubDate>Tue, 09 Sep 2025 10:00:00 GMT</pubDate><dc:creator><![CDATA[Andrew Lock]]></dc:creator><media:content url="https://andrewlock.net/content/images/2018/03/tinyapi_banner.jpg" medium="image" /><content:encoded><![CDATA[<img src="https://andrewlock.net/content/images/2018/03/tinyapi_banner.jpg" /><nav><p>This is the seventh post in the series: <a href="https://andrewlock.net/series/exploring-the-dotnet-10-preview/">Exploring the .NET 10 preview</a>. </p> <ol class="list-none"><li><a href="https://andrewlock.net/exploring-dotnet-10-preview-features-1-exploring-the-dotnet-run-app.cs/">Part 1 - Exploring the features of dotnet run app.cs</a></li><li><a href="https://andrewlock.net/exploring-dotnet-10-preview-features-2-behind-the-scenes-of-dotnet-run-app.cs/">Part 2 - Behind the scenes of dotnet run app.cs</a></li><li><a href="https://andrewlock.net/exploring-dotnet-10-preview-features-3-csharp-14-extensions-members/">Part 3 - C# 14 extension members; AKA extension everything</a></li><li><a href="https://andrewlock.net/exploring-dotnet-10-preview-features-4-solving-the-source-generator-marker-attribute-problem-in-dotnet-10/">Part 4 - Solving the source generator 'marker attribute' problem in .NET 10</a></li><li><a href="https://andrewlock.net/exploring-dotnet-10-preview-features-5-running-one-off-dotnet-tools-with-dnx/">Part 5 - Running one-off .NET tools with dnx</a></li><li><a href="https://andrewlock.net/exploring-dotnet-10-preview-features-6-passkey-support-for-aspnetcore-identity/">Part 6 - Passkey support for ASP.NET Core identity</a></li><li>Part 7 - Packaging self-contained and native AOT .NET tools for NuGet (this post) </li><li><a href="https://andrewlock.net/exploring-dotnet-10-preview-features-8-supporting-platform-specific-dotnet-tools-on-old-sdks/">Part 8 - Supporting platform-specific .NET tools on old .NET SDKs</a></li><li><a href="https://andrewlock.net/exploring-dotnet-10-preview-features-9-easier-reflection-with-unsafeaccessortype/">Part 9 - Easier reflection with [UnsafeAccessorType] in .NET 10</a></li></ol></nav><p>In this post we'll look at the support for platform-specific .NET tools feature added in .NET 10,. This new feature allows you to pack tools in a variety of different ways—including self-contained, trimmed, and using native AOT—mirroring the many different ways you can publish .NET apps today.</p> <p>In this post we'll use a sample app to look at each of those package types in turn to see the impact the package type has on the package size and the contents of the packages. I'll highlight some of the bugs I found during testing, and also when the various package types might be most useful.</p> <h2 id="-net-tools" class="heading-with-anchor"><a href="https://andrewlock.net/exploring-dotnet-10-preview-features-7-packaging-self-contained-and-native-aot-dotnet-tools-for-nuget/#-net-tools" class="relative text-zinc-800 dark:text-white no-underline hover:underline">.NET tools</a></h2> <p>I discussed .NET tools at length in <a href="https://andrewlock.net/using-and-authoring-dotnet-tools/">my previous post</a>, so for this post I'm going to assume you're already familiar with the general .NET tools feature. There's not been much change to them since .NET Core 3.0, so that's a pretty safe bet!</p> <p>That said, the previous post covers one important aspect that's particularly relevant to this post: <a href="https://andrewlock.net/using-and-authoring-dotnet-tools/#ensuring-compatibility-by-multi-targeting">ensuring compatibility for consumers of your package by multi-targeting your tool against multiple frameworks</a>. I strongly recommend reading that section of the post at least, if you haven't already.</p> <h2 id="the-many-ways-to-deploy-net-applications-" class="heading-with-anchor"><a href="https://andrewlock.net/exploring-dotnet-10-preview-features-7-packaging-self-contained-and-native-aot-dotnet-tools-for-nuget/#the-many-ways-to-deploy-net-applications-" class="relative text-zinc-800 dark:text-white no-underline hover:underline">The many ways to deploy .NET applications.</a></h2> <p>So I'm assuming you already know a bit about .NET tools, but before we get to the new .NET tool features in .NET 10, we first need to look at some of the ways you can <a href="https://learn.microsoft.com/en-us/dotnet/core/deploying/">publish your .NET applications</a> today:</p> <ul><li><strong>Framework-dependent executable</strong>. The .NET runtime must be installed on the target machine. The published app is small because it does not include any of the runtime binaries.</li> <li><strong>Self-contained executable</strong>. The published app includes all the .NET runtime binaries it needs, so is very large, but the benefit is that you don't need a .NET runtime installed on the target machine.</li> <li><strong>Trimmed self-contained executable</strong>. As with the previous example, but the binaries are <a href="https://learn.microsoft.com/en-us/dotnet/core/deploying/trimming/trim-self-contained">trimmed</a> to remove unused code, significantly reducing the size of the published app.</li> <li><strong>Native AOT executable</strong>. <a href="https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/?tabs=windows%2Cnet8">Native Ahead of Time (AOT)</a> compiled applications have fast startup and smaller memory footprints, and don't need a .NET runtime installed on the host machine. They are platform-specific and some features (like some reflection APIs) are not available.</li></ul> <p>Another dimension to consider is whether deployments are "platform agnostic" or "platform specific". In simple terms:</p> <ul><li><strong>Platform agnostic deployments</strong> can run on <em>any</em> platform, e.g. Linux ARM64 or Windows x64, typically expressed as a runtime ID e.g. <code>linux-arm64</code>, <code>win-x64</code>.</li> <li><strong>Platform specific deployments</strong> can <em>only</em> run on a specific platform.</li></ul> <p>This becomes particularly important when you have <em>native</em> dependencies, so you need a different version of the library for each platform. In these cases, platform agnostic deployments include the native libraries for <em>all</em> supported platforms, whereas platform specific deployments need only include the libraries for a single platform.</p> <p>The rough summary is that the .NET 10 SDK now supports creating and consuming .NET tools using all these new deployment models.</p> <h2 id="-net-tools-support-more-deployment-models-in-net-10" class="heading-with-anchor"><a href="https://andrewlock.net/exploring-dotnet-10-preview-features-7-packaging-self-contained-and-native-aot-dotnet-tools-for-nuget/#-net-tools-support-more-deployment-models-in-net-10" class="relative text-zinc-800 dark:text-white no-underline hover:underline">.NET tools support more deployment models in .NET 10</a></h2> <p>Prior to .NET 10, it was only possible to publish .NET tools using the "framework dependent" deployment model. .NET 10 preview 6 <a href="https://github.com/dotnet/core/blob/main/release-notes/10.0/preview/preview6/sdk.md#platform-specific-net-tools">announced support</a> for publishing .NET tools using more deployment models:</p> <ul><li>Framework-dependent, platform-agnostic (the way it works today)</li> <li>Framework-dependent, platform-specific</li> <li>Self-contained, platform-specific</li> <li>Trimmed, platform-specific</li> <li>Native AOT-compiled, platform-specific</li></ul> <p>The "Framework-dependent, platform-specific" option is useful if you have native dependencies in your app, as it effectively splits these across multiple NuGet packages, making each individual package smaller.</p> <p>The self-contained, trimmed, and NativeAOT options are particularly useful, as they mean you're no longer dependent on the consumer having the correct .NET runtime already installed on the target machine. Instead of having to <a href="https://andrewlock.net/using-and-authoring-dotnet-tools/#ensuring-compatibility-by-multi-targeting">support multiple runtimes</a> or <a href="https://andrewlock.net/using-and-authoring-dotnet-tools/#configuring-your-tools-to-roll-forward">configure <code>RollForward</code></a> for your application, you can just pack the whole runtime (ideally, trimmed or AOT compiled) into your package, and don't worry about the target machine.</p> <blockquote> <p>There's some caveats to this, which I discuss <a href="https://andrewlock.net/exploring-dotnet-10-preview-features-7-packaging-self-contained-and-native-aot-dotnet-tools-for-nuget/#limitations-and-recommendations">at the end of this post</a>.</p> </blockquote> <p>That's the crux of the feature, so for the remainder of the post we'll look at how to generate each of the different packages, what the NuGet packages look like, and what they contain.</p> <h3 id="the-sample-app" class="heading-with-anchor"><a href="https://andrewlock.net/exploring-dotnet-10-preview-features-7-packaging-self-contained-and-native-aot-dotnet-tools-for-nuget/#the-sample-app" class="relative text-zinc-800 dark:text-white no-underline hover:underline">The sample app</a></h3> <p>For the test app, I created a sample based on Chet Husk's <a href="https://github.com/baronfel/multi-rid-tool">multi-rid-tool</a> which looked a bit like this:</p> <div class="pre-code-wrapper"><pre class="language-xml"><code class="language-xml"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>Project</span> <span class="token attr-name">Sdk</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>Microsoft.NET.Sdk<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>

  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PropertyGroup</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>OutputType</span><span class="token punctuation">&gt;</span></span>Exe<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>OutputType</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>TargetFrameworks</span><span class="token punctuation">&gt;</span></span>netcoreapp3.1;net5.0;net6.0;net7.0;net8.0;net9.0<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>TargetFrameworks</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>EmbedUntrackedSources</span><span class="token punctuation">&gt;</span></span>true<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>EmbedUntrackedSources</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>CopyOutputSymbolsToPublishDirectory</span><span class="token punctuation">&gt;</span></span>false<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>CopyOutputSymbolsToPublishDirectory</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PackAsTool</span><span class="token punctuation">&gt;</span></span>true<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>PackAsTool</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>ToolCommandName</span><span class="token punctuation">&gt;</span></span>sayhello<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>ToolCommandName</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PackageId</span><span class="token punctuation">&gt;</span></span>sayhello<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>PackageId</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PackageVersion</span><span class="token punctuation">&gt;</span></span>1.0.0<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>PackageVersion</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>Authors</span><span class="token punctuation">&gt;</span></span>Andrew Lock<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>Authors</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>Description</span><span class="token punctuation">&gt;</span></span>A tool that says hello<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>Description</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PackageLicenseExpression</span><span class="token punctuation">&gt;</span></span>MIT<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>PackageLicenseExpression</span><span class="token punctuation">&gt;</span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>PropertyGroup</span><span class="token punctuation">&gt;</span></span>
  
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>ItemGroup</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PackageReference</span> <span class="token attr-name">Include</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>Spectre.Console<span class="token punctuation">"</span></span> <span class="token attr-name">Version</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>0.50.0<span class="token punctuation">"</span></span> <span class="token punctuation">/&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PackageReference</span> <span class="token attr-name">Include</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>Microsoft.Data.Sqlite<span class="token punctuation">"</span></span> <span class="token attr-name">Version</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>9.0.8<span class="token punctuation">"</span></span> <span class="token punctuation">/&gt;</span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>ItemGroup</span><span class="token punctuation">&gt;</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>Project</span><span class="token punctuation">&gt;</span></span>
</code></pre></div> <p>There's nothing particularly interesting there, it's a simple exe called <code>sayhello</code> that has a <em>bunch</em> of target frameworks, is packed as a .NET tool, and has a couple of dependencies. The <em>Microsoft.Data.Sqlite</em> dependency is there to ensure we have a native dependency (SQLite) while Spectre.Console is there because it's cool.</p> <p>In <em>Program.cs</em> we have some simple code that ensures we use the dependencies (to make sure they're not completely trimmed out for example):</p> <div class="pre-code-wrapper"><pre class="language-csharp"><code class="language-csharp"><span class="token keyword">using</span> <span class="token namespace">Microsoft<span class="token punctuation">.</span>Data<span class="token punctuation">.</span>Sqlite</span><span class="token punctuation">;</span>
<span class="token keyword">using</span> <span class="token namespace">Spectre<span class="token punctuation">.</span>Console</span><span class="token punctuation">;</span>

<span class="token keyword">using</span> <span class="token class-name"><span class="token keyword">var</span></span> connection <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token constructor-invocation class-name">SqliteConnection</span><span class="token punctuation">(</span><span class="token string">"Data Source=:memory:"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
connection<span class="token punctuation">.</span><span class="token function">Open</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token class-name"><span class="token keyword">var</span></span> command <span class="token operator">=</span> connection<span class="token punctuation">.</span><span class="token function">CreateCommand</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
command<span class="token punctuation">.</span>CommandText <span class="token operator">=</span> <span class="token string">"SELECT 'world'"</span><span class="token punctuation">;</span>

<span class="token keyword">using</span> <span class="token class-name"><span class="token keyword">var</span></span> reader <span class="token operator">=</span> command<span class="token punctuation">.</span><span class="token function">ExecuteReader</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">while</span> <span class="token punctuation">(</span>reader<span class="token punctuation">.</span><span class="token function">Read</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span>
<span class="token punctuation">{</span>
    <span class="token class-name"><span class="token keyword">var</span></span> name <span class="token operator">=</span> reader<span class="token punctuation">.</span><span class="token function">GetString</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token class-name"><span class="token keyword">var</span></span> figlet <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token constructor-invocation class-name">FigletText</span><span class="token punctuation">(</span><span class="token interpolation-string"><span class="token string">$"Hello </span><span class="token interpolation"><span class="token punctuation">{</span><span class="token expression language-csharp">name</span><span class="token punctuation">}</span></span><span class="token string">!"</span></span><span class="token punctuation">)</span>
        <span class="token punctuation">.</span><span class="token function">Centered</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
        <span class="token punctuation">.</span><span class="token function">Color</span><span class="token punctuation">(</span>Color<span class="token punctuation">.</span>Green<span class="token punctuation">)</span><span class="token punctuation">;</span>

    AnsiConsole<span class="token punctuation">.</span><span class="token function">Write</span><span class="token punctuation">(</span>figlet<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>This program just prints <code>Hello world!</code> when you run it:</p> <div class="pre-code-wrapper"><pre class="language-powershell"><code class="language-powershell">    _   _          _   _                                       _       _   _ 
   <span class="token punctuation">|</span> <span class="token punctuation">|</span> <span class="token punctuation">|</span> <span class="token punctuation">|</span>   ___  <span class="token punctuation">|</span> <span class="token punctuation">|</span> <span class="token punctuation">|</span> <span class="token punctuation">|</span>   ___     __      __   ___    _ __  <span class="token punctuation">|</span> <span class="token punctuation">|</span>   __<span class="token punctuation">|</span> <span class="token punctuation">|</span> <span class="token punctuation">|</span> <span class="token punctuation">|</span>
   <span class="token punctuation">|</span> <span class="token punctuation">|</span>_<span class="token punctuation">|</span> <span class="token punctuation">|</span>  <span class="token operator">/</span> _ \ <span class="token punctuation">|</span> <span class="token punctuation">|</span> <span class="token punctuation">|</span> <span class="token punctuation">|</span>  <span class="token operator">/</span> _ \    \ \ <span class="token operator">/</span>\ <span class="token operator">/</span> <span class="token operator">/</span>  <span class="token operator">/</span> _ \  <span class="token punctuation">|</span> '__<span class="token punctuation">|</span> <span class="token punctuation">|</span> <span class="token punctuation">|</span>  <span class="token operator">/</span> _` <span class="token punctuation">|</span> <span class="token punctuation">|</span> <span class="token punctuation">|</span>
   <span class="token punctuation">|</span>  _  <span class="token punctuation">|</span> <span class="token punctuation">|</span>  __/ <span class="token punctuation">|</span> <span class="token punctuation">|</span> <span class="token punctuation">|</span> <span class="token punctuation">|</span> <span class="token punctuation">|</span> <span class="token punctuation">(</span>_<span class="token punctuation">)</span> <span class="token punctuation">|</span>    \ V  V <span class="token operator">/</span>  <span class="token punctuation">|</span> <span class="token punctuation">(</span>_<span class="token punctuation">)</span> <span class="token punctuation">|</span> <span class="token punctuation">|</span> <span class="token punctuation">|</span>    <span class="token punctuation">|</span> <span class="token punctuation">|</span> <span class="token punctuation">|</span> <span class="token punctuation">(</span>_<span class="token punctuation">|</span> <span class="token punctuation">|</span> <span class="token punctuation">|</span>_<span class="token punctuation">|</span>
   <span class="token punctuation">|</span>_<span class="token punctuation">|</span> <span class="token punctuation">|</span>_<span class="token punctuation">|</span>  \___<span class="token punctuation">|</span> <span class="token punctuation">|</span>_<span class="token punctuation">|</span> <span class="token punctuation">|</span>_<span class="token punctuation">|</span>  \___/      \_/\_/    \___/  <span class="token punctuation">|</span>_<span class="token punctuation">|</span>    <span class="token punctuation">|</span>_<span class="token punctuation">|</span>  \__<span class="token punctuation">,</span>_<span class="token punctuation">|</span> <span class="token punctuation">(</span>_<span class="token punctuation">)</span>
</code></pre></div> <p>Next we're going to publish it using each of the five different approaches. In each case we <em>could</em> control the publish output by passing values to the <code>dotnet pack</code> commands. However, in practice you'll likely want to embed the options inside the project file, so that's the approach I take in this post.</p> <h3 id="framework-dependent-platform-agnostic" class="heading-with-anchor"><a href="https://andrewlock.net/exploring-dotnet-10-preview-features-7-packaging-self-contained-and-native-aot-dotnet-tools-for-nuget/#framework-dependent-platform-agnostic" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Framework-dependent, platform agnostic</a></h3> <p>For the application above, by default, when you run <code>dotnet pack</code> you'll get a framework-dependent, platform agnostic package. The tool can be installed on any of the supported platforms, but you need one of the supported .NET runtimes installed on the target machine:</p> <div class="pre-code-wrapper"><pre class="language-bash"><code class="language-bash">dotnet pack <span class="token parameter variable">-o</span> ./artifacts/packages/agnostic
</code></pre></div> <p>This produces the package in the <em>./artifacts/packages/agnostic</em> folder, and results in a single, chonky, package:</p> <p><img src="https://andrewlock.net/content/images/2025/multirid_01.png" alt="The sayhello package is large, at 91MB"></p> <p>If you open the package using <a href="https://github.com/nugetpackageexplorer">NuGet Package Explorer</a>, you can see why the package is so large: it contains duplicate files for every target framework, and each target framework contains the native files for <em>all</em> the supported platforms</p> <p><img src="https://andrewlock.net/content/images/2025/multirid_02.png" alt="The package contains a folder for each target framework, each of which contains all the runtimes"></p> <p>Finally, if we look at the <em>DotnetToolSettings.xml</em> file, we see something like this:</p> <div class="pre-code-wrapper"><pre class="language-xml"><code class="language-xml"><span class="token prolog">&lt;?xml version="1.0" encoding="utf-8"?&gt;</span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>DotNetCliTool</span> <span class="token attr-name">Version</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>1<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>Commands</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>Command</span> <span class="token attr-name">Name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>sayhello<span class="token punctuation">"</span></span> <span class="token attr-name">EntryPoint</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>MultiRid.dll<span class="token punctuation">"</span></span> <span class="token attr-name">Runner</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>dotnet<span class="token punctuation">"</span></span> <span class="token punctuation">/&gt;</span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>Commands</span><span class="token punctuation">&gt;</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>DotNetCliTool</span><span class="token punctuation">&gt;</span></span>
</code></pre></div> <p>This package is effectively the same as you get today if you're packing your .NET tools with the .NET 9 SDK or earlier.</p> <p>There are two main reasons the package is large:</p> <ul><li>We target multiple frameworks to support multiple .NET runtime environments</li> <li>We support all platforms in a single package.</li></ul> <p>For the next scenario, we address the second of those points.</p> <h3 id="framework-dependent-platform-specific" class="heading-with-anchor"><a href="https://andrewlock.net/exploring-dotnet-10-preview-features-7-packaging-self-contained-and-native-aot-dotnet-tools-for-nuget/#framework-dependent-platform-specific" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Framework-dependent, platform specific</a></h3> <p>In the next scenario, instead of supporting <em>all</em> platforms (<code>linux-x64</code>/<code>win-x64</code> etc) in a single package, we split each of those into a separate package. This is handled automatically by the .NET 10 SDK when you specify runtime IDs in your project. To produce platform-specific NuGet packages, we simply add the following property group to our project:</p> <div class="pre-code-wrapper"><pre class="language-xml"><code class="language-xml"><span class="token comment">&lt;!-- specific --&gt;</span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PropertyGroup</span><span class="token punctuation">&gt;</span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>RuntimeIdentifiers</span><span class="token punctuation">&gt;</span></span>linux-x64;linux-arm64;win-x64;win-arm64;any<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>RuntimeIdentifiers</span><span class="token punctuation">&gt;</span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PublishSelfContained</span><span class="token punctuation">&gt;</span></span>false<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>PublishSelfContained</span><span class="token punctuation">&gt;</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>PropertyGroup</span><span class="token punctuation">&gt;</span></span>
</code></pre></div> <p>There's a couple of interesting points here:</p> <ul><li><strong>We explicitly set <code>PublishSelfContained=false</code></strong>. This is because on earlier versions of .NET Core, setting a runtime ID would automatically set <code>PublishSelfContained=true</code>, but that's a different scenario we're going to get to later!</li> <li><strong>We have an additional <code>any</code> runtime ID</strong>. The <code>any</code> option was <a href="https://github.com/dotnet/core/blob/main/release-notes/10.0/preview/preview7/sdk.md#any-rid-in-multi-rid-tools">added in .NET 10 preview 7</a> and is meant to ensure you still produce a "platform agnostic" package <em>in addition</em> to the platform-specific packages, so that you have a "fallback".</li></ul> <p>If we run .NET pack again:</p> <div class="pre-code-wrapper"><pre class="language-bash"><code class="language-bash">dotnet pack <span class="token parameter variable">-o</span> ./artifacts/packages/specific
</code></pre></div> <p>This time we end up with 6 different packages:</p> <p><img src="https://andrewlock.net/content/images/2025/multirid_03.png" alt="The dotnet pack command produces 6 different packages"></p> <p>We now have</p> <ul><li>A "top level" NuGet package with the "correct" name for our tool.</li> <li>A package for each of the 4 specific runtime IDs we specified in our project file.</li> <li>An "any" package, which should be used on platforms that don't have a dedicated package.</li></ul> <p>We'll look at each of these packages in turn.</p> <p>The top-level package is tiny, and contains just a single file for each target framework:</p> <p><img src="https://andrewlock.net/content/images/2025/multirid_04.png" alt="The DotnetToolSettings.xml file for each framework "></p> <p>The <em>DotnetToolSettings.xml</em> file in this case looks like this:</p> <div class="pre-code-wrapper"><pre class="language-xml"><code class="language-xml"><span class="token prolog">&lt;?xml version="1.0" encoding="utf-8"?&gt;</span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>DotNetCliTool</span> <span class="token attr-name">Version</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>2<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>Commands</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>Command</span> <span class="token attr-name">Name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>sayhello<span class="token punctuation">"</span></span> <span class="token punctuation">/&gt;</span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>Commands</span><span class="token punctuation">&gt;</span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>RuntimeIdentifierPackages</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>RuntimeIdentifierPackage</span> <span class="token attr-name">RuntimeIdentifier</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>linux-x64<span class="token punctuation">"</span></span> <span class="token attr-name">Id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>sayhello.linux-x64<span class="token punctuation">"</span></span> <span class="token punctuation">/&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>RuntimeIdentifierPackage</span> <span class="token attr-name">RuntimeIdentifier</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>linux-arm64<span class="token punctuation">"</span></span> <span class="token attr-name">Id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>sayhello.linux-arm64<span class="token punctuation">"</span></span> <span class="token punctuation">/&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>RuntimeIdentifierPackage</span> <span class="token attr-name">RuntimeIdentifier</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>win-x64<span class="token punctuation">"</span></span> <span class="token attr-name">Id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>sayhello.win-x64<span class="token punctuation">"</span></span> <span class="token punctuation">/&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>RuntimeIdentifierPackage</span> <span class="token attr-name">RuntimeIdentifier</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>win-arm64<span class="token punctuation">"</span></span> <span class="token attr-name">Id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>sayhello.win-arm64<span class="token punctuation">"</span></span> <span class="token punctuation">/&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>RuntimeIdentifierPackage</span> <span class="token attr-name">RuntimeIdentifier</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>any<span class="token punctuation">"</span></span> <span class="token attr-name">Id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>sayhello.any<span class="token punctuation">"</span></span> <span class="token punctuation">/&gt;</span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>RuntimeIdentifierPackages</span><span class="token punctuation">&gt;</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>DotNetCliTool</span><span class="token punctuation">&gt;</span></span>
</code></pre></div> <p>There's some interesting aspects to this file compared to the platform agnostic version:</p> <ul><li>The <code>&lt;DotNetCliTool&gt;</code> element has <code>Version=2</code> for the platform-specific version, and <code>Version=1</code> for the platform-agnostic version.</li> <li>There's a collection of <code>&lt;RuntimeIdentifierPackage&gt;</code> which match a runtime ID to a different package.</li></ul> <p>If we look at one of those platform-specific packages, e.g. <em>sayhello.linux-x64.1.0.0</em>, and compare it to the platform agnostic version, we can see why it's so much smaller:</p> <p><img src="https://andrewlock.net/content/images/2025/multirid_05.png" alt="The sayhello.linux-x64 package contents"></p> <p>There's still a folder for every target framework, but now instead of a <em>runtimes</em> folder with all the different platform-specific binaries, there's only the single platform, <code>linux-x64</code> in this case.</p> <p><img src="https://andrewlock.net/content/images/2025/multirid_14.png" alt="Comparing the platform agnostic package to the platform specific package"></p> <p>It's also worth looking at the <em>DotnetToolSettings.xml</em> file in these packages:</p> <div class="pre-code-wrapper"><pre class="language-xml"><code class="language-xml"><span class="token prolog">&lt;?xml version="1.0" encoding="utf-8"?&gt;</span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>DotNetCliTool</span> <span class="token attr-name">Version</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>2<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>Commands</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>Command</span> <span class="token attr-name">Name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>sayhello<span class="token punctuation">"</span></span> <span class="token attr-name">EntryPoint</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>MultiRid<span class="token punctuation">"</span></span> <span class="token attr-name">Runner</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>executable<span class="token punctuation">"</span></span> <span class="token punctuation">/&gt;</span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>Commands</span><span class="token punctuation">&gt;</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>DotNetCliTool</span><span class="token punctuation">&gt;</span></span>
</code></pre></div> <p>As you can see, this is another <code>Version=2</code> manifest and interestingly this is where we see a new "runner" type, <code>executable</code>, with a defined <code>EntryPoint</code> which is the executable that will run.</p> <p>So the platform-specific packages can be much smaller as they only need a single runtime, but the <code>any</code> package <em>does</em> still need all those runtimes, so that it will work on any platform. Unfortunately, <a href="https://github.com/dotnet/sdk/issues/50312">there's currently a bug</a> which means that <em>none</em> of the runtimes are currently included, so actually the <em>any</em> package <em>won't</em> work on any platform if your app has native dependencies 😅 That should be fixed <a href="https://github.com/dotnet/sdk/pull/50376">in .NET 10 RC2</a>.</p> <h3 id="self-contained-platform-specific" class="heading-with-anchor"><a href="https://andrewlock.net/exploring-dotnet-10-preview-features-7-packaging-self-contained-and-native-aot-dotnet-tools-for-nuget/#self-contained-platform-specific" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Self-contained, platform specific</a></h3> <p>For the next permutation, we build a self-contained application, so we bundle the .NET runtime as part of the package. The other change in this case is that there's no point in targeting multiple frameworks for increased compatibility; you only need to target one, because it's going to be bundled in the package anyway:</p> <div class="pre-code-wrapper"><pre class="language-xml"><code class="language-xml"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PropertyGroup</span><span class="token punctuation">&gt;</span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>TargetFramework</span><span class="token punctuation">&gt;</span></span>net9.0<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>TargetFramework</span><span class="token punctuation">&gt;</span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>RuntimeIdentifiers</span><span class="token punctuation">&gt;</span></span>linux-x64;linux-arm64;win-x64;win-arm64<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>RuntimeIdentifiers</span><span class="token punctuation">&gt;</span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PublishSelfContained</span><span class="token punctuation">&gt;</span></span>true<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>PublishSelfContained</span><span class="token punctuation">&gt;</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>PropertyGroup</span><span class="token punctuation">&gt;</span></span>
</code></pre></div> <p>The other thing to note is that there's no <code>any</code> runtime ID here. That's because there's currently a bug that will cause errors if you try to add it:</p> <div class="pre-code-wrapper"><pre class="language-bash"><code class="language-bash"> C:<span class="token punctuation">\</span>Program Files<span class="token punctuation">\</span>dotnet<span class="token punctuation">\</span>sdk<span class="token punctuation">\</span><span class="token number">10.0</span>.100-preview.7.25380.108<span class="token punctuation">\</span>Sdks<span class="token punctuation">\</span>Microsoft.NET.Sdk<span class="token punctuation">\</span>targets<span class="token punctuation">\</span>Microsoft.NET.Sdk.FrameworkReferenceResolution.targets<span class="token punctuation">(</span><span class="token number">528,5</span><span class="token punctuation">)</span>:
error NETSDK1082: There was no runtime pack <span class="token keyword">for</span> Microsoft.NETCore.App available <span class="token keyword">for</span> the specified RuntimeIdentifier <span class="token string">'any'</span><span class="token builtin class-name">.</span>
</code></pre></div> <p>This <a href="https://github.com/dotnet/sdk/pull/50421">should also be fixed</a> for .NET 10 RC 2, and will mean you can have a true fallback package, just as we saw for the framework-dependent, platform-specific case. For now though, we'll have to do without the <code>any</code> package.</p> <p>Running <code>dotnet pack</code> once again:</p> <div class="pre-code-wrapper"><pre class="language-bash"><code class="language-bash">dotnet pack <span class="token parameter variable">-o</span> ./artifacts/packages/specific
</code></pre></div> <p>If we look at the packages this scenario produces, they're very similar to the previous framework-dependent platform-specific case, but much larger, because they ship .NET in the package too:</p> <p><img src="https://andrewlock.net/content/images/2025/multirid_06.png" alt="The self-contained, platform-specific packages"></p> <p>The root <code>sayhello</code> package is essentially the same as the root package for the platform-specific version, the only difference being that the self-contained package only contains a single target framework.</p> <p><img src="https://andrewlock.net/content/images/2025/multirid_08.png" alt="The sayhello root package only contains a sing any"></p> <p>However, If we look inside the platform-specific packages like <code>sayhello.linux-arm64</code> you'll see that there's a <em>lot</em> of files in the package, everything you need to run the .NET app, even when there's no .NET runtime installed on the target application.</p> <p><img src="https://andrewlock.net/content/images/2025/multirid_07.png" alt="Inside the sayhello.linux-arm64 package there are a lot files, that are part of the .NET runtime"></p> <p>If we look at the <em>DotnetToolSettings.xml</em> file in these self-contained package you can see that it's still the same as the framework-dependent platform-specific package, pointing towards the executable shim:</p> <div class="pre-code-wrapper"><pre class="language-xml"><code class="language-xml"><span class="token prolog">&lt;?xml version="1.0" encoding="utf-8"?&gt;</span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>DotNetCliTool</span> <span class="token attr-name">Version</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>2<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>Commands</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>Command</span> <span class="token attr-name">Name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>sayhello<span class="token punctuation">"</span></span> <span class="token attr-name">EntryPoint</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>MultiRid.exe<span class="token punctuation">"</span></span> <span class="token attr-name">Runner</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>executable<span class="token punctuation">"</span></span> <span class="token punctuation">/&gt;</span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>Commands</span><span class="token punctuation">&gt;</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>DotNetCliTool</span><span class="token punctuation">&gt;</span></span>
</code></pre></div> <p>Even though these packages are large, if you think back to the first framework-dependent, platform agnostic package we're much smaller than the 90MB we started with.</p> <h3 id="self-contained-trimmed" class="heading-with-anchor"><a href="https://andrewlock.net/exploring-dotnet-10-preview-features-7-packaging-self-contained-and-native-aot-dotnet-tools-for-nuget/#self-contained-trimmed" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Self-contained, trimmed</a></h3> <p>If you're going to go to the effort of building self-contained, platform-specific packages, then it <em>likely</em> makes sense to go to the next step and enable trimming too. <a href="https://learn.microsoft.com/en-us/dotnet/core/deploying/trimming/trim-self-contained">Enabling an application for trimming</a> <em>can</em> be somewhat risky depending on the libraries and methods your application uses. However, if your application <em>is</em> amenable to trimming, it can significantly reduce the resulting package sizes.</p> <p>To create trimmed NuGet packages, you just need the <code>&lt;PublishTrimmed&gt;</code> property on top of our previous configuration:</p> <div class="pre-code-wrapper"><pre class="language-xml"><code class="language-xml"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PropertyGroup</span><span class="token punctuation">&gt;</span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>TargetFramework</span><span class="token punctuation">&gt;</span></span>net9.0<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>TargetFramework</span><span class="token punctuation">&gt;</span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>RuntimeIdentifiers</span><span class="token punctuation">&gt;</span></span>linux-x64;linux-arm64;win-x64;win-arm64<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>RuntimeIdentifiers</span><span class="token punctuation">&gt;</span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PublishSelfContained</span><span class="token punctuation">&gt;</span></span>true<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>PublishSelfContained</span><span class="token punctuation">&gt;</span></span>
  <span class="token comment">&lt;!-- 👇 Add this --&gt;</span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PublishTrimmed</span><span class="token punctuation">&gt;</span></span>true<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>PublishTrimmed</span><span class="token punctuation">&gt;</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>PropertyGroup</span><span class="token punctuation">&gt;</span></span>
</code></pre></div> <p>Again we publish with</p> <div class="pre-code-wrapper"><pre class="language-bash"><code class="language-bash">dotnet pack <span class="token parameter variable">-o</span> ./artifacts/packages/trimmed
</code></pre></div> <p>And this time the packages are a third of the size, around 10MB each, much better!</p> <p><img src="https://andrewlock.net/content/images/2025/multirid_09.png" alt="The packages when building self-contained trimmed packages"></p> <p>We're almost done, the last case we have to look at is native AOT.</p> <h3 id="native-aot" class="heading-with-anchor"><a href="https://andrewlock.net/exploring-dotnet-10-preview-features-7-packaging-self-contained-and-native-aot-dotnet-tools-for-nuget/#native-aot" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Native AOT</a></h3> <p>As per <a href="https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/">the documentation</a>:</p> <blockquote> <p>Publishing your app as <em>Native AOT</em> produces an app that's self-contained and that has been ahead-of-time (AOT) compiled to native code. Native AOT apps have faster startup time and smaller memory footprints. These apps can run on machines that don't have the .NET runtime installed.</p> </blockquote> <p>If you're <em>already</em> packaging your application as both self-contained and trimmed, it's very possible that your application could be Native AOT compatible too. And given that you're building tools, it's very possible that you could get real benefits from compiling your application as native AOT.</p> <p>We'll update the tool to build as Native AOT, and to reduce the size of the package, we'll also strip the symbols</p> <div class="pre-code-wrapper"><pre class="language-xml"><code class="language-xml"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PropertyGroup</span><span class="token punctuation">&gt;</span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>RuntimeIdentifiers</span><span class="token punctuation">&gt;</span></span>linux-x64;linux-arm64;win-x64;win-arm64;<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>RuntimeIdentifiers</span><span class="token punctuation">&gt;</span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PublishSelfContained</span><span class="token punctuation">&gt;</span></span>true<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>PublishSelfContained</span><span class="token punctuation">&gt;</span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PublishTrimmed</span><span class="token punctuation">&gt;</span></span>true<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>PublishTrimmed</span><span class="token punctuation">&gt;</span></span>
  <span class="token comment">&lt;!-- 👇 Add these --&gt;</span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PublishAot</span><span class="token punctuation">&gt;</span></span>true<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>PublishAot</span><span class="token punctuation">&gt;</span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>StripSymbols</span><span class="token punctuation">&gt;</span></span>true<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>StripSymbols</span><span class="token punctuation">&gt;</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>PropertyGroup</span><span class="token punctuation">&gt;</span></span>
</code></pre></div> <p>Publishing for native AOT is a little trickier than for the other packages, because you can <em>only</em> native AOT compile on the same platform as you're running on. In total, you need to run <code>dotnet pack</code> once for each runtime, and once for the "root" package.</p> <p>Running the simple <code>dotnet pack</code> produces the "root" package:</p> <div class="pre-code-wrapper"><pre class="language-bash"><code class="language-bash">dotnet pack <span class="token parameter variable">-o</span> ./artifacts/packages/nativeoat
</code></pre></div> <p>and then you need to run <code>dotnet pack</code> for each runtime ID, for example:</p> <div class="pre-code-wrapper"><pre class="language-bash"><code class="language-bash">dotnet pack <span class="token parameter variable">-o</span> ./artifacts/packages/nativeaot --runtime-id win-x64
</code></pre></div> <p>This produces the <code>sayhello.win-x64.1.0.0</code> package only. You then need to run it again for each of the supported runtimes.</p> <blockquote> <p><a href="https://bsky.app/profile/chethusk.bsky.social">Chet Husk</a> has an example of how you can do this in GitHub actions and later aggregate the packages together. Note that he uses <code>--current-runtime</code> instead of listing each runtime ID specifically. Be aware that <a href="https://github.com/dotnet/sdk/issues/50313">this <em>doesn't</em> work</a> if you are using <code>&lt;TargetFrameworks&gt;</code> in your project; you must use <code>&lt;TargetFramework&gt;</code> instead.</p> </blockquote> <p>If we look at those packages, you can see that the native AOT packages are even smaller than the trimmed packages!</p> <p><img src="https://andrewlock.net/content/images/2025/multirid_10.png" alt="The native AOT packages are only 2.5MB"></p> <p>And if we look inside the package, you can see that there's only 2 application files left: the native AOT'd .NET tool, and the native library:</p> <p><img src="https://andrewlock.net/content/images/2025/multirid_11.png" alt="The contents of the native AOT package"></p> <p>In many ways this represents the "pinnacle" of .NET tool usability; on supported platforms, consumers of your tool will benefit from the fastest start up times and the smallest download sizes (which further improves <a href="https://andrewlock.net/exploring-dotnet-10-preview-features-5-running-one-off-dotnet-tools-with-dnx/">the dnx one-off-tool experience</a>). In the cases where you're on an unsupported platform, you should be able to fallback to the same framework dependent, platform-agnostic packages that are available today, so there's nothing much lost there.</p> <blockquote> <p>Note that as mentioned earlier, the <code>any</code> fallback for self-contained, trimmed, and Native AOT packages does not work as of .NET 10 preview 7, but <a href="https://github.com/dotnet/sdk/pull/50421">it should be working</a> for RC2.</p> </blockquote> <p>So the last remaining question is: as a tool author, should you actually use these new platform-specific packages?</p> <h2 id="limitations-and-recommendations" class="heading-with-anchor"><a href="https://andrewlock.net/exploring-dotnet-10-preview-features-7-packaging-self-contained-and-native-aot-dotnet-tools-for-nuget/#limitations-and-recommendations" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Limitations and recommendations</a></h2> <p>The main limitation with <em>all</em> of the new platform-specific package types is that they only work when you're using the .NET 10 SDK. If you try to install any of the packages that use version 2 of the <code>&lt;DotNetCliTool&gt;</code> element then you'll get an error similar to the following:</p> <div class="pre-code-wrapper"><pre class="language-bash"><code class="language-bash"><span class="token operator">&gt;</span> dotnet tool <span class="token function">install</span> sayhello --tool-path .<span class="token punctuation">\</span>specific <span class="token parameter variable">--source</span> D:<span class="token punctuation">\</span>repos<span class="token punctuation">\</span>blog-examples<span class="token punctuation">\</span>MultiRid<span class="token punctuation">\</span>artifacts<span class="token punctuation">\</span>packages<span class="token punctuation">\</span>specific

Tool <span class="token string">'sayhello'</span> failed to update due to the following:
The settings <span class="token function">file</span> <span class="token keyword">in</span> the tool<span class="token string">'s NuGet package is invalid: Command '</span>sayhello<span class="token string">' uses unsupported runner '</span>'."
Tool <span class="token string">'sayhello'</span> failed to install. Contact the tool author <span class="token keyword">for</span> assistance.
</code></pre></div> <p>Given this requirement, depending on the specifics of the .NET tool you're creating, the benefits of creating a platform-specific tool may be limited.</p> <p>First of all, if your tool don't have any native dependencies, then there's fundamentally no difference between the framework-dependent platform-<em>specific</em> packages and the framework-dependent platform <em>agnostic</em> packages, other than the fact that you can only run the platform-specific tool if you're using the .NET 10 SDK.</p> <p>Where things get interesting is if you're currently targeting <em>multiple</em> frameworks with your tool. In these cases you end up with multiple instances of the app, compiled for each target framework, all packaged in the same <em>.nupkg</em>. If you want to support the widest range of installed frameworks, then theoretically you need a new copy for every framework. That can both increase complexity in the tool and increase the size of the package that needs to be downloaded.</p> <blockquote> <p>You can avoid that somewhat by configuring <a href="https://andrewlock.net/using-and-authoring-dotnet-tools/#configuring-your-tools-to-roll-forward">the <code>rollForward</code> rules</a> for your tool, but there's always a certain amount of risk in relying solely on that approach.</p> </blockquote> <p>The neat thing about the self-contained/trimmed/Native AOT packages is that you <em>don't</em> need to multi-target your application for multiple frameworks, because you include the framework in the package! That reduces the complexity in your tool (no need to multi-target) and the size of your package (no need for multiple copies of the compiled app).</p> <p>At least, that's the theory. <em>Today</em>, it's not really true, because these packages <em>only</em> support .NET 10, because they require the .NET 10 SDK to install the tool. So ease of support and compatibility is actually a reason <em>not</em> to use the platform-specific packages today. This will become less of a problem as time goes on and more people are using the latest .NET SDKs, but today it's hard to recommend as your sole approach.</p> <p>That said, there are <em>some</em> "escape route" avenues I explored to try to get the best of both worlds. These may work for you if you want to support both the enhanced experience of the .NET 10 SDK while still supporting users stuck on older SDKs.</p> <h2 id="summary" class="heading-with-anchor"><a href="https://andrewlock.net/exploring-dotnet-10-preview-features-7-packaging-self-contained-and-native-aot-dotnet-tools-for-nuget/#summary" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Summary</a></h2> <p>In this post I described the new platform-specific .NET tool packages that are supported in .NET 10 as of preview 6 and 7. The .NET 10 SDK allows you to easily create "root" meta-packages that delegate to platform-specific versions of your .NET tool. This is particularly useful if your app has native dependencies, as it can significantly reduce the size of each package.</p> <p>In addition, you can create self-contained, trimmed, or native AOT compiled versions of your app. These packages mean you're no longer beholden to the consumer having the correct version of the runtime installed for your tool (though currently it does mean they <em>must</em> have the .NET 10 SDK installed).</p> <p>In this post we looked at the results of using each of the new package types, the impact it has on the package size and the contents of the packages. I highlighted some of the bugs I found during testing (but which will hopefully be resolved by the time .NET 10 goes GA in November). Finally, I discussed some of the limitations of the new packages, the most important being that you must have the .NET 10 SDK installed to install tools that use the new package types.</p> ]]></content:encoded><category><![CDATA[.NET 10;AOT;NuGet]]></category></item><item><title><![CDATA[Using and authoring .NET tools]]></title><description><![CDATA[In this post I describe some of the complexities around authoring .NET tools, specifically around supporting multiple .NET runtimes and testing in CI]]></description><link>https://andrewlock.net/using-and-authoring-dotnet-tools/</link><guid isPermaLink="true">https://andrewlock.net/using-and-authoring-dotnet-tools/</guid><pubDate>Tue, 02 Sep 2025 10:00:00 GMT</pubDate><dc:creator><![CDATA[Andrew Lock]]></dc:creator><media:content url="https://andrewlock.net/content/images/2019/tools.jpg" medium="image" /><content:encoded><![CDATA[<img src="https://andrewlock.net/content/images/2019/tools.jpg" /><p>In this post I describe some of the complexities around authoring .NET tools, particularly where you don't know which version of the .NET runtime customers will have installed. Finally, I provide some tips for working with and testing .NET tools in a continuous integration (CI) environment.</p> <h2 id="what-are-net-tools-" class="heading-with-anchor"><a href="https://andrewlock.net/using-and-authoring-dotnet-tools/#what-are-net-tools-" class="relative text-zinc-800 dark:text-white no-underline hover:underline">What are .NET tools?</a></h2> <p><a href="https://learn.microsoft.com/en-us/dotnet/core/tools/global-tools">.NET tools</a> are programs that are distributed via NuGet and can be installed using the .NET SDK. They can either be installed globally on a machine or locally to a specific folder.</p> <p>There are a number of first-party global tools from Microsoft, like the <a href="https://learn.microsoft.com/en-us/ef/core/cli/dotnet">EF Core tool</a>, but you can also write your own. In the past I've described creating <a href="https://andrewlock.net/creating-a-net-core-global-cli-tool-for-squashing-images-with-the-tinypng-api/">a tool that uses the TinyPNG API to squash images</a>, and <a href="https://andrewlock.net/converting-web-config-files-to-appsettings-json-with-a-net-core-global-tool/">a tool for converting web.config files to appsettings.json format</a>. The majority of .NET tools are command-line tools, but there's no reason they <em>need</em> to be. MonoGame's Content Builder tools for example <a href="https://andrewlock.net/creating-your-first-sample-game-with-monogame/#exploring-the-default-monogame-template">include GUI tools</a> as well.</p> <h2 id="working-with-local-tools" class="heading-with-anchor"><a href="https://andrewlock.net/using-and-authoring-dotnet-tools/#working-with-local-tools" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Working with local tools</a></h2> <p>Some tools make the most sense as global tools. If they're broadly applicable to multiple applications, and you generally don't need a specific version of the tool for use in different projects (<a href="https://www.nuget.org/packages/DiffEngineTray"><code>DiffEngineTray</code> is a good example</a>) then global tools make sense. However, that's <em>not</em> always the case. Sometimes the version of the tool <em>does</em> matter, and you want different versions for different applications.</p> <p>In these cases, <em>local tools</em> are a better option. You can define the tools that are required for a specific project by creating a <em>dotnet-tools manifest</em>. This is a JSON file which lives in your repository and is checked-in to source control. You can create a new tool-manifest by running the following in the root of your repository:</p> <div class="pre-code-wrapper"><pre class="language-bash"><code class="language-bash">dotnet new tool-manifest
</code></pre></div> <p>By default, this creates the following manifest JSON file <em>dotnet-tools.json</em> inside the .config folder of your repository:</p> <div class="pre-code-wrapper"><pre class="language-json"><code class="language-json"><span class="token punctuation">{</span>
  <span class="token property">"version"</span><span class="token operator">:</span> <span class="token number">1</span><span class="token punctuation">,</span>
  <span class="token property">"isRoot"</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span>
  <span class="token property">"tools"</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>The initial manifest doesn't include any tools, but you can install new ones by running <code>dotnet tool install</code> (i.e. without the <code>-g</code> or <code>--tool-path</code> flag). So you can, for example, install the Cake tool for your project by running:</p> <div class="pre-code-wrapper"><pre class="language-bash"><code class="language-bash"><span class="token operator">&gt;</span> dotnet tool <span class="token function">install</span> Cake.Tool

You can invoke the tool from this directory using the following commands: <span class="token string">'dotnet tool run dotnet-cake'</span> or <span class="token string">'dotnet dotnet-cake'</span><span class="token builtin class-name">.</span>
Tool <span class="token string">'cake.tool'</span> <span class="token punctuation">(</span>version <span class="token string">'0.35.0'</span><span class="token punctuation">)</span> was successfully installed. Entry is added to the manifest <span class="token function">file</span> C:<span class="token punctuation">\</span>repos<span class="token punctuation">\</span>test<span class="token punctuation">\</span>.config<span class="token punctuation">\</span>dotnet-tools.json.
</code></pre></div> <p>This updates the manifest by adding the <code>cake.tool</code> reference to the <code>tools</code> section, including the version required (the current latest version - you can update the version manually as required), and the command you need to run to execute the tool (<code>dotnet-cake</code>):</p> <div class="pre-code-wrapper"><pre class="language-json"><code class="language-json"><span class="token punctuation">{</span>
  <span class="token property">"version"</span><span class="token operator">:</span> <span class="token number">1</span><span class="token punctuation">,</span>
  <span class="token property">"isRoot"</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span>
  <span class="token property">"tools"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
    <span class="token property">"cake.tool"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
      <span class="token property">"version"</span><span class="token operator">:</span> <span class="token string">"5.0.0"</span><span class="token punctuation">,</span>
      <span class="token property">"commands"</span><span class="token operator">:</span> <span class="token punctuation">[</span>
        <span class="token string">"dotnet-cake"</span>
      <span class="token punctuation">]</span>
    <span class="token punctuation">}</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>When a colleague clones the repository and wants to run the Cake tool, they can run the following commands to first restore the tool, and then run it:</p> <div class="pre-code-wrapper"><pre class="language-bash"><code class="language-bash"><span class="token comment"># Restore the NuGet packages specifed in the manifest</span>
dotnet tool restore

<span class="token comment"># Run the tool using one of the following forms:</span>
dotnet tool run dotnet-cake
<span class="token comment"># or you can use:</span>
dotnet dotnet-cake
<span class="token comment"># or even shorter:</span>
dotnet cake
</code></pre></div> <p>Alternatively, as of .NET 10 preview 6, you can use the even simpler <code>dnx</code> or <code>dotnet dnx</code> tools to one-shot run the tools. When the tool is specified in the tool manifest, you can just use <code>dnx</code> and it will automatically use the version specified in the manifest:</p> <div class="pre-code-wrapper"><pre class="language-bash"><code class="language-bash"><span class="token comment"># One-shot run the tool using either of the following:</span>
dnx Cake.Tool
dotnet dnx Cake.Tool
</code></pre></div> <p>.NET tools are basically just a .NET application packed into a NuGet package, so they are subject to all the same requirements. One of the most important aspects is the fact that .NET applications are compiled against a specific runtime. And that's also where things can get a bit tricky.</p> <h2 id="ensuring-compatibility-by-multi-targeting" class="heading-with-anchor"><a href="https://andrewlock.net/using-and-authoring-dotnet-tools/#ensuring-compatibility-by-multi-targeting" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Ensuring compatibility by multi-targeting</a></h2> <p>There are a couple of slightly annoying difficulties working with, and authoring, .NET tools. .NET tools are just normal <a href="https://learn.microsoft.com/en-us/dotnet/core/deploying/?pivots=visualstudio#framework-dependent-deployment">framework-dependent</a> .NET apps, so they are dependent on the correct .NET runtime being available on your machine. As a concrete example, if you build a .NET tool, and it targets <code>net8.0</code>, then you <em>must</em> have the .NET 8 runtime installed on the target machine, regardless of which version of the SDK you <em>install</em> the tool with.</p> <p>As a consequence, if you want to support <em>any</em> the of the runtimes a customer <em>might</em> have installed on their machine, then you need to build and pack your tool for <em>multiple</em> target frameworks.</p> <p>That's easy enough to do in principal, as you can "just" add all the target frameworks you need to support in your project's <code>&lt;TargetFrameworks&gt;</code> element in the <em>.csproj</em> file</p> <div class="pre-code-wrapper"><pre class="language-xml"><code class="language-xml"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>Project</span> <span class="token attr-name">Sdk</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>Microsoft.NET.Sdk<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>

  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PropertyGroup</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>OutputType</span><span class="token punctuation">&gt;</span></span>Exe<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>OutputType</span><span class="token punctuation">&gt;</span></span>
    <span class="token comment">&lt;!-- 👇 Targeting ALL the frameworks!--&gt;</span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>TargetFrameworks</span><span class="token punctuation">&gt;</span></span>netcoreapp2.1;netcoreapp3.0;netcoreapp3.1;net5.0;net6.0;net7.0;net8.0;net9.0<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>TargetFrameworks</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>LangVersion</span><span class="token punctuation">&gt;</span></span>latest<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>LangVersion</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PackAsTool</span><span class="token punctuation">&gt;</span></span>true<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>PackAsTool</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>ToolCommandName</span><span class="token punctuation">&gt;</span></span>sayhello<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>ToolCommandName</span><span class="token punctuation">&gt;</span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>PropertyGroup</span><span class="token punctuation">&gt;</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>Project</span><span class="token punctuation">&gt;</span></span>
</code></pre></div> <p>As you can see from the above list, if you <em>really</em> want to support everything, then that's a <em>lot</em> of target frameworks to add. And it's not without its downsides.</p> <p>For a start, when you create multi-targeted apps like this, you'll <em>generally</em> be limited to <em>only</em> APIs present in the <em>lowest</em> target framework. In the example above, that means .NET Core 2.1 APIs😬</p> <p>What's more, each target framework you add here increases the size of the NuGet package. When you pack your app, you'll build it for each of the target frameworks, and pack everything into the same NuGet package:</p> <p><img src="https://andrewlock.net/content/images/2025/multirid_13.png" alt="The contents of the package with multiple runtimes packed into the same package"></p> <p>This can significantly increase the size of the package, which isn't <em>generally</em> a problem, except that it makes all restore and <code>dnx</code> operations (for example) slower.</p> <p>Building and packaging for all the target runtimes that you support is the "safest" approach to supporting the widest range of customers that you can. However, if you're willing to take a little risk there's an alternative approach.</p> <h2 id="configuring-your-tools-to-roll-forward" class="heading-with-anchor"><a href="https://andrewlock.net/using-and-authoring-dotnet-tools/#configuring-your-tools-to-roll-forward" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Configuring your tools to roll forward</a></h2> <p>In the previous section I said that explicitly targeting all the .NET runtimes that you support in a .NET tool is the "best" way to make sure your tool can run on a customer's environment, regardless of which runtime they use.</p> <p>However, an alternative (and in many ways, complementary) approach, is to <em>not</em> target all these runtime versions. Instead, you allow your application to run with a <em>newer</em> version of the runtime than it was built for, by using the <code>&lt;RollForward&gt;</code> element.</p> <p>For example, let's say you have a tool that works on .NET 6 and you don't want to have to multi-target it for .NET 7, .NET 8, .NET 9 etc as well. Given that each version of .NET has very high compatibility with the previous version, you could instead <em>only</em> build your tool for .NET 6, and then tell the <code>dotnet</code> host to allow using <em>any</em> runtime that's available for .NET 6 or above. You can do this by setting <code>RollForward=Major</code> in your project file:</p> <div class="pre-code-wrapper"><pre class="language-xml"><code class="language-xml"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>PropertyGroup</span><span class="token punctuation">&gt;</span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>TargetFramework</span><span class="token punctuation">&gt;</span></span>net6.0<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>TargetFramework</span><span class="token punctuation">&gt;</span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>RollForward</span><span class="token punctuation">&gt;</span></span>Major<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>RollForward</span><span class="token punctuation">&gt;</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>PropertyGroup</span><span class="token punctuation">&gt;</span></span>
</code></pre></div> <p>Setting <code>&lt;RollForward&gt;</code> in your project ensures that this property is copied to the <a href="https://learn.microsoft.com/en-us/dotnet/core/versions/selection#control-roll-forward-behavior"><em>runtimeonfig.json</em> file</a> that is deployed with your tool. You can find this inside your NuGet packge; note the <code>rollFoward</code> property below that mirrors the value you set in your project:</p> <div class="pre-code-wrapper"><pre class="language-json"><code class="language-json"><span class="token punctuation">{</span>
  <span class="token property">"runtimeOptions"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
    <span class="token property">"tfm"</span><span class="token operator">:</span> <span class="token string">"net6.0"</span><span class="token punctuation">,</span>
    <span class="token property">"rollForward"</span><span class="token operator">:</span> <span class="token string">"Major"</span><span class="token punctuation">,</span>
    <span class="token property">"framework"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
      <span class="token property">"name"</span><span class="token operator">:</span> <span class="token string">"Microsoft.NETCore.App"</span><span class="token punctuation">,</span>
      <span class="token property">"version"</span><span class="token operator">:</span> <span class="token string">"6.0.0"</span>
    <span class="token punctuation">}</span><span class="token punctuation">,</span>
    <span class="token property">"configProperties"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
      <span class="token property">"System.Reflection.Metadata.MetadataUpdater.IsSupported"</span><span class="token operator">:</span> <span class="token boolean">false</span>
    <span class="token punctuation">}</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div> <p>With the <code>rollForward</code> configured for your tool, as long as someone is able to install your .NET tool (which they can do as long as they have the .NET 6+ SDK installed) then they will be able to run the app, even though you <em>only</em> built and packaged your app for .NET 6.</p> <blockquote> <p>Note that this isn't <em>completely</em> safe, as the .NET runtime isn't <em>guaranteed</em> to be compatible across major versions. Nevertheless, in practice it's relatively safe, and is generally recommended.</p> </blockquote> <p>One of the best reasons to set <code>RollForward=Major</code> in your project even if you <em>do</em> pack for multiple target frameworks is to support <em>currently unreleased</em> .NET versions that come out in the future. For example, let's say you have a .NET tool published that supports .NET 9. By default, when .NET 10 comes out, people won't be able to run your tool unless you go back and explicitly add a <code>net10.0</code> target. By setting <code>RollForward=Major</code> you can ensure there's <em>some</em> support immediately.</p> <h2 id="handy-dotnet-tool-tips" class="heading-with-anchor"><a href="https://andrewlock.net/using-and-authoring-dotnet-tools/#handy-dotnet-tool-tips" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Handy <code>dotnet tool</code> tips</a></h2> <p>The final section in this post is a bit of a grab-bag of handy options available when working with .NET tools, particularly when you're doing things in continuous integration (CI) systems. These are generally things I have run into when working with them myself, and they aren't always obvious.</p> <h3 id="testing-locally-built-packages-with-source-and-tool-path" class="heading-with-anchor"><a href="https://andrewlock.net/using-and-authoring-dotnet-tools/#testing-locally-built-packages-with-source-and-tool-path" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Testing locally built packages with <code>--source</code> and <code>--tool-path</code></a></h3> <p>As mentioned previously, .NET tools are basically just .NET apps, so for the most part you can test them the way you would test any other apps. However, you may also want to explicitly test the final artifact that you're producing, i.e. the <em>.nupkg</em> file.</p> <p>When you're testing a tool you've produced locally, I recommend using both the <code>--source</code> and <code>--tool-path</code> settings:</p> <ul><li><code>--source</code> Specifies where to install the tool <em>from</em>. Point it to a folder containing <em>.nupkg</em> files to install from those packages only, instead of other NuGet sources.</li> <li><code>--tool-path</code> Specifies where to install the tool <em>to</em>. The .NET tool will be installed and unpacked to this directory and can then be run from this directory.</li></ul> <p>For example:</p> <div class="pre-code-wrapper"><pre class="language-bash"><code class="language-bash"><span class="token comment"># Install version 1.2.3 of the dd-trace package </span>
<span class="token comment"># that is found in the /app/install/ folder</span>
<span class="token comment"># and install it into the /tool path</span>
dotnet tool <span class="token function">install</span> dd-trace <span class="token punctuation">\</span>
    <span class="token parameter variable">--source</span> /app/install/. <span class="token punctuation">\</span>
    --tool-path /tool <span class="token punctuation">\</span>
    <span class="token parameter variable">--version</span> <span class="token number">1.2</span>.3
</code></pre></div> <p>Using both of these settings when you're testing locally-built packages ensures that you are both <em>actually</em> installing the tool that you think you are (instead of accidentally installing from a remote source), and that you're not "polluting" your local NuGet cache with these test files.</p> <h3 id="installing-pre-release-versions-with-prerelease" class="heading-with-anchor"><a href="https://andrewlock.net/using-and-authoring-dotnet-tools/#installing-pre-release-versions-with-prerelease" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Installing pre-release versions with <code>--prerelease</code></a></h3> <p>If you're producing a package that has a pre-release suffix (i.e. it has a version like <code>1.0.0-beta</code> or <code>0.0.1-preview</code> instead of just <code>1.0.0</code> or <code>0.0.1</code>) then you may be surprised to find you <em>can't</em> easily test it locally. This is because you must pass the <code>--prerelease</code> flag when installing a pre-release version:</p> <div class="pre-code-wrapper"><pre class="language-bash"><code class="language-bash"><span class="token comment"># The --prerelease flag is required when installing pre-release versions</span>
dotnet tool <span class="token function">install</span> dd-trace <span class="token punctuation">\</span>
    <span class="token parameter variable">--source</span> /app/install/. <span class="token punctuation">\</span>
    --tool-path /tool <span class="token punctuation">\</span>
    <span class="token parameter variable">--version</span> <span class="token number">1.2</span>.3-preview <span class="token punctuation">\</span>
    <span class="token parameter variable">--prerelease</span>
</code></pre></div> <p>Note that this flag is only available from .NET 5+ of the .NET SDK</p> <h3 id="provide-robustness-with-allow-downgrade" class="heading-with-anchor"><a href="https://andrewlock.net/using-and-authoring-dotnet-tools/#provide-robustness-with-allow-downgrade" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Provide robustness with <code>--allow-downgrade</code></a></h3> <p>If you're installing a .NET tool in CI you should generally specify a version, to make sure that your CI is repeatable. But what happens if the tool is already installed?</p> <div class="pre-code-wrapper"><pre class="language-bash"><code class="language-bash"><span class="token operator">&gt;</span> dotnet tool <span class="token function">install</span> <span class="token parameter variable">-g</span> dotnet-serve <span class="token parameter variable">--version</span> <span class="token number">1.10</span>.175
The requested version <span class="token number">1.10</span>.175 is lower than existing version <span class="token number">1.10</span>.190.
</code></pre></div> <p>As shown in the above example, if you try to install a version of a tool that is <em>lower</em> than the currently installed version, this will fail.</p> <blockquote> <p>I <em>think</em> that historically you actually couldn't install <em>any</em> new version of a tool if it was already installed on the machine, and instead you would have to use <code>dotnet tool update</code>, but as of at least .NET 9 it seems you can technically update to a <em>newer</em> version of a package using the above <code>dotnet tool install</code> command.</p> </blockquote> <p>The <code>dotnet tool update</code> command works by uninstalling the tool and then installing a new version, so you might think that you can use that instead, but no:</p> <div class="pre-code-wrapper"><pre class="language-bash"><code class="language-bash"><span class="token operator">&gt;</span> dotnet tool update <span class="token parameter variable">-g</span> dotnet-serve <span class="token parameter variable">--version</span> <span class="token number">1.10</span>.175
The requested version <span class="token number">1.10</span>.175 is lower than existing version <span class="token number">1.10</span>.190.
</code></pre></div> <p>You get the exact same error message. The key here is that you need to include the <code>--allow-downgrade</code> option when running <code>dotnet tool update</code></p> <div class="pre-code-wrapper"><pre class="language-bash"><code class="language-bash"><span class="token operator">&gt;</span> dotnet tool update <span class="token parameter variable">-g</span> dotnet-serve <span class="token parameter variable">--version</span> <span class="token number">1.10</span>.175 --allow-downgrade
Tool <span class="token string">'dotnet-serve'</span> was successfully updated from version <span class="token string">'1.10.190'</span> to version <span class="token string">'1.10.175'</span><span class="token builtin class-name">.</span>
</code></pre></div> <blockquote> <p>Note that <code>dotnet tool install --allow-downgrade</code> <em>also</em> works. It seems like the two commands do exactly the same thing these days, so I don't know why update hasn't been deprecated to be honest 😅</p> </blockquote> <p>That's the last of my tips for now. In the next post we'll look at some new features coming to .NET tool packages in .NET 10!</p> <h2 id="summary" class="heading-with-anchor"><a href="https://andrewlock.net/using-and-authoring-dotnet-tools/#summary" class="relative text-zinc-800 dark:text-white no-underline hover:underline">Summary</a></h2> <p>In this post I discussed how to work .NET tools. I described how to install local tools using a tools manifest, and some of the considerations when you're authoring tools. In particular, I discussed the considerations about multi-targeting to ensure maximum compatibility with customer environments and using <code>RollForward=Major</code> to ensure <em>future</em> compatibility. Finally I provided some general tips about using .NET tools, particularly for when you're building and testing .NET tools in CI. In the next post we'll look at some of the new features coming to .NET tools in .NET 10!</p> ]]></content:encoded><category><![CDATA[.NET 10;NuGet;DevOps]]></category></item></channel></rss>