<?xml version="1.0" encoding="UTF-8"?><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" version="2.0"><channel><title><![CDATA[Asylkhan Azat]]></title><description><![CDATA[Programming notes: C# code snippets, experiments, solved problems, and ideas from building and breaking things. Practical posts]]></description><link>https://asylkhan-azat.hashnode.dev</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1593680282896/kNC7E8IR4.png</url><title>Asylkhan Azat</title><link>https://asylkhan-azat.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Tue, 13 Oct 2026 16:12:02 GMT</lastBuildDate><atom:link href="https://asylkhan-azat.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Too early, too late: your C# method doesn't run in the order you wrote it]]></title><description><![CDATA[There's a method you've probably written many times: check the arguments, return early if there's nothing to do, and hand the real work to LINQ with a lambda that uses one of the parameters.
We read a]]></description><link>https://asylkhan-azat.hashnode.dev/too-early-too-late-csharp</link><guid isPermaLink="true">https://asylkhan-azat.hashnode.dev/too-early-too-late-csharp</guid><category><![CDATA[Programming Blogs]]></category><category><![CDATA[C#]]></category><category><![CDATA[dotnet]]></category><category><![CDATA[software development]]></category><category><![CDATA[compiler]]></category><dc:creator><![CDATA[Azat Asylkhan]]></dc:creator><pubDate>Wed, 07 Oct 2026 20:04:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6ac2c427cb7c09e8586c956f/5a1a829b-a0fb-4a11-9a64-23f49d1cfa13.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>There's a method you've probably written many times: check the arguments, return early if there's nothing to do, and hand the real work to LINQ with a lambda that uses one of the parameters.</p>
<p>We read a method like that from top to bottom and assume it runs in the same order: the checks first, the work last, and if the method returns early, nothing below the <code>return</code> has happened yet. That's usually true, but lambdas, <code>yield</code> and <code>await</code> make the compiler rewrite the code around them.</p>
<p>A lambda that captures a variable makes some work run too early. <code>yield</code> and <code>await</code> turn the method into a state machine, and the checks at its top fail too late. This post is mostly about that timing, but the rewrites also allocate, so we'll count those bytes too.</p>
<p>If you've read the .NET source, you've probably seen the BCL's fix for this: a public method <code>Foo</code> that does a couple of checks and then calls a private <code>FooCore</code>. Let's put one small method through both rewrites and see why that second method is there.</p>
<h2>Too early: the closure</h2>
<p>Say we look up a user by name, with the usual checks and an early exit for an empty collection. Every static method in this post lives in this <code>Users</code> class, even when a sample leaves the class out:</p>
<pre><code class="language-csharp">public sealed record User(string Name, bool IsActive);

public static class Users
{
    public static User? FindByName(IReadOnlyCollection&lt;User&gt; users, string name)
    {
        ArgumentNullException.ThrowIfNull(users);
        ArgumentException.ThrowIfNullOrEmpty(name);

        if (users.Count == 0)
            return null;

        return users.FirstOrDefault(u =&gt; u.Name == name);
    }
}
</code></pre>
<p>The lambda <code>u =&gt; u.Name == name</code> reads <code>name</code>, which is a parameter of <code>FindByName</code>. But the compiler turns the lambda into a separate method, and one method can't read another method's parameters. So the compiler creates a hidden class, called a closure, moves <code>name</code> into a field of that class and makes the lambda one of its methods. After that, every read of <code>name</code> in <code>FindByName</code> goes through that field, including the one in the validation.</p>
<p>The compiler creates the closure object when execution enters the scope of the captured variable. A parameter's scope is the whole method, so the closure is created as soon as the method starts, before any of the checks run.</p>
<p>You can see that in SharpLab, or in ILSpy with lambda decompilation turned off. The generated names aren't valid C# on purpose, so they can't clash with yours. The closure is <code>&lt;&gt;c__DisplayClass0_0</code>, which is why closures are also called display classes, and the lambda is <code>&lt;FindByName&gt;b__0</code>. Here's the same code with readable names:</p>
<pre><code class="language-csharp">// What the compiler generates
public static class Users
{
    private sealed class FindByNameClosure
    {
        public string? name;

        public bool Lambda(User u) =&gt; u.Name == name;
    }

    public static User? FindByName(IReadOnlyCollection&lt;User&gt; users, string name)
    {
        var closure = new FindByNameClosure();
        closure.name = name;

        ArgumentNullException.ThrowIfNull(users);
        ArgumentException.ThrowIfNullOrEmpty(closure.name);

        if (users.Count == 0)
            return null;

        return users.FirstOrDefault(closure.Lambda);
    }
}
</code></pre>
<p>The first two lines of <code>FindByName</code> create the closure and copy <code>name</code> into it, before any of the checks. Only the <code>Func&lt;User, bool&gt;</code> delegate is still created where the lambda was written, on the last line.</p>
<p>On .NET 10.0.12 (x64, Release), BenchmarkDotNet's <code>[MemoryDiagnoser]</code> shows 24 bytes per call to <code>FindByName</code> with an empty collection. That's the closure, allocated for a lambda that never runs, and every call that fails validation allocates it too.</p>
<blockquote>
<p><strong>Side note:</strong> when the collection isn't empty, the number depends on the runtime. On .NET 8, the call also allocates the 64-byte delegate and the enumerator that <code>FirstOrDefault</code> gets from the collection: 120 bytes in total for a <code>User[]</code> and 128 for a <code>List&lt;User&gt;</code>. On .NET 10, <code>FirstOrDefault</code> reads arrays and lists as spans and the JIT keeps the delegate on the stack, so only the closure's 24 bytes are left.</p>
</blockquote>
<h3>Fix: copy the parameter inside a block</h3>
<p>A local declared inside an inner <code>{ }</code> block has that block as its scope, so if a lambda captures it, the compiler creates the closure only when execution enters the block. That means we can copy <code>name</code> into such a local after the early return and let the lambda capture the copy instead of the parameter:</p>
<pre><code class="language-csharp">public static User? FindByName(IReadOnlyCollection&lt;User&gt; users, string name)
{
    ArgumentNullException.ThrowIfNull(users);
    ArgumentException.ThrowIfNullOrEmpty(name);

    if (users.Count == 0)
        return null;

    {
        string localName = name;
        return users.FirstOrDefault(u =&gt; u.Name == localName);
    }
}
</code></pre>
<p>The captured variable now belongs to the block, so the compiler creates the closure there, below the checks and the early return. The closure class is the same as before, except that its field is called <code>localName</code>:</p>
<pre><code class="language-csharp">// What the compiler generates
public static User? FindByName(IReadOnlyCollection&lt;User&gt; users, string name)
{
    ArgumentNullException.ThrowIfNull(users);
    ArgumentException.ThrowIfNullOrEmpty(name);

    if (users.Count == 0)
        return null;

    {
        var closure = new FindByNameClosure();
        closure.localName = name;
        return users.FirstOrDefault(closure.Lambda);
    }
}
</code></pre>
<p>On both .NET 8 and .NET 10, the empty path now allocates nothing, and the non-empty path allocates the same as before.</p>
<p>This fix is fragile, because it all depends on the braces. Without them, the scope of <code>localName</code> is the whole method again. The closure goes back to the first line, even though the copy is written after the <code>return</code>, and the empty path is back to 24 bytes. The method also allocates on entry again if the lambda reads anything else from the outer scope, like another parameter, or if another lambda in the method captures <code>name</code>. To a reader, the copy looks redundant, and nothing warns when someone inlines it or removes the braces.</p>
<h3>Fix: split the method</h3>
<p>The sturdier fix is to move the lambda out of the method. The public method keeps the checks and the early exit, and everything that captures a variable goes into a private method with the same name and a <code>Core</code> suffix:</p>
<pre><code class="language-csharp">public static User? FindByName(IReadOnlyCollection&lt;User&gt; users, string name)
{
    ArgumentNullException.ThrowIfNull(users);
    ArgumentException.ThrowIfNullOrEmpty(name);

    if (users.Count == 0)
        return null;

    return FindByNameCore(users, name);
}

private static User? FindByNameCore(IReadOnlyCollection&lt;User&gt; users, string name) =&gt;
    users.FirstOrDefault(u =&gt; u.Name == name);
</code></pre>
<p><code>FindByName</code> no longer contains a lambda, so it has nothing to capture, and the compiler leaves it as written. The closure moved into <code>FindByNameCore</code>:</p>
<pre><code class="language-csharp">// What the compiler generates
public static User? FindByName(IReadOnlyCollection&lt;User&gt; users, string name)
{
    ArgumentNullException.ThrowIfNull(users);
    ArgumentException.ThrowIfNullOrEmpty(name);

    if (users.Count == 0)
        return null;

    return FindByNameCore(users, name);
}

private static User? FindByNameCore(IReadOnlyCollection&lt;User&gt; users, string name)
{
    var closure = new FindByNameCoreClosure();
    closure.name = name;
    return users.FirstOrDefault(closure.Lambda);
}
</code></pre>
<p>The closure is still created on the first line of a method, but that method is now <code>FindByNameCore</code>, which only runs when the checks pass and the collection isn't empty. On both .NET 8 and .NET 10, the empty path went from 24 bytes to zero, and the non-empty path allocates the same as before.</p>
<div>
<div>💡</div>
<div><strong>Note on </strong><code>static</code><strong> lambdas.</strong> Some APIs let you skip the second method. <code>ConcurrentDictionary&lt;TKey, TValue&gt;.GetOrAdd</code> has an overload that takes an extra argument and passes it on to the factory, so the lambda doesn't need to capture anything and can be <code>static</code>: <code>cache.GetOrAdd(id, static (key, prefix) =&gt; prefix + key, prefix)</code>. With <code>static</code>, the compiler rejects any capture, and a lambda that captures nothing needs no closure. The compiler also caches the delegate of such a lambda in a static field, so when the key is already in the dictionary, the call allocates nothing. <code>FirstOrDefault</code> has no such overload, so it still needs the split.</div>
</div>

<p><code>FirstOrDefault</code> runs the lambda during the call. <code>Where</code> doesn't call the lambda until someone enumerates the result, and a method you write with <code>yield return</code> doesn't run any of its code until then, not even the checks. That's where the timing goes wrong in the other direction.</p>
<h2>Too late: the state machine</h2>
<p>When a method contains <code>yield return</code> or <code>await</code>, the compiler rewrites the whole method. It generates a type, the state machine, and moves the body into its <code>MoveNext</code> method. The method you call becomes a stub that creates the state machine. Your checks move into <code>MoveNext</code> with everything else, so they run when <code>MoveNext</code> runs. Iterators and async methods run <code>MoveNext</code> at different times, so they go wrong in two different ways.</p>
<h3>Iterators: the checks wait for the first <code>MoveNext</code></h3>
<p>Here's a method that returns every user whose name starts with a prefix, with the same kind of checks at the top:</p>
<pre><code class="language-csharp">public static IEnumerable&lt;User&gt; FindByPrefix(IReadOnlyCollection&lt;User&gt; users, string prefix)
{
    ArgumentNullException.ThrowIfNull(users);
    ArgumentException.ThrowIfNullOrEmpty(prefix);

    foreach (User user in users)
    {
        if (user.Name.StartsWith(prefix))
            yield return user;
    }
}
</code></pre>
<p>For an iterator, the state machine is a class that implements <code>IEnumerable&lt;User&gt;</code>. Calling <code>FindByPrefix</code> only creates an instance of that class and returns it. None of the code you wrote runs until someone starts enumerating. Here's the class, written by hand like the closure. The real one has more states and bookkeeping, and the <code>IEnumerable&lt;User&gt;</code> plumbing at the end is left out:</p>
<pre><code class="language-csharp">// What the compiler generates
public static IEnumerable&lt;User&gt; FindByPrefix(IReadOnlyCollection&lt;User&gt; users, string prefix)
{
    return new FindByPrefixIterator(users, prefix);
}

private sealed class FindByPrefixIterator(IReadOnlyCollection&lt;User&gt; users, string prefix)
    : IEnumerable&lt;User&gt;, IEnumerator&lt;User&gt;
{
    private enum State { NotStarted, PausedAtYield, Finished }

    private State state = State.NotStarted;
    private IEnumerator&lt;User&gt; usersEnumerator = null!;

    public User Current { get; private set; } = null!;

    public bool MoveNext()
    {
        switch (state)
        {
            case State.NotStarted:
                ArgumentNullException.ThrowIfNull(users);
                ArgumentException.ThrowIfNullOrEmpty(prefix);
                usersEnumerator = users.GetEnumerator();
                break;
            case State.PausedAtYield:
                break;
            case State.Finished:
                return false;
        }

        while (usersEnumerator.MoveNext())
        {
            User user = usersEnumerator.Current;
            if (user.Name.StartsWith(prefix))
            {
                Current = user;
                state = State.PausedAtYield;
                return true;
            }
        }

        state = State.Finished;
        usersEnumerator.Dispose();
        return false;
    }

    // GetEnumerator, Dispose and the rest of the IEnumerable&lt;User&gt; plumbing
}
</code></pre>
<p>The constructor only stores the arguments. The <code>foreach</code> became a <code>while</code> over <code>usersEnumerator</code>, and the <code>yield return</code> became three lines that store the user in <code>Current</code>, set the state to <code>PausedAtYield</code> and return <code>true</code>. The next <code>MoveNext</code> takes the <code>PausedAtYield</code> case and continues the loop. The checks ended up in the <code>NotStarted</code> case, so they run on the first <code>MoveNext</code>, not during the call.</p>
<p>So <code>FindByPrefix(users, "")</code> returns without complaint. The <code>ArgumentException</code> only shows up on the first <code>MoveNext</code>, maybe in a <code>foreach</code> three layers away, long after the code that passed the empty prefix has returned. And with an empty collection, the call still allocates a 72-byte iterator that has nothing to iterate.</p>
<h3>Fix: split the iterator</h3>
<p>There's no lambda this time, but the same split helps. The public method keeps the checks and gets an early exit for an empty collection, and the iterator moves into <code>FindByPrefixCore</code>:</p>
<pre><code class="language-csharp">public static IEnumerable&lt;User&gt; FindByPrefix(IReadOnlyCollection&lt;User&gt; users, string prefix)
{
    ArgumentNullException.ThrowIfNull(users);
    ArgumentException.ThrowIfNullOrEmpty(prefix);

    if (users.Count == 0)
        return [];

    return FindByPrefixCore(users, prefix);
}

private static IEnumerable&lt;User&gt; FindByPrefixCore(IReadOnlyCollection&lt;User&gt; users, string prefix)
{
    foreach (User user in users)
    {
        if (user.Name.StartsWith(prefix))
            yield return user;
    }
}
</code></pre>
<p><code>FindByPrefix</code> has no <code>yield</code>, so the compiler leaves it almost as written. Only <code>FindByPrefixCore</code> turns into a stub, and its iterator class is the one from before, minus the two checks:</p>
<pre><code class="language-csharp">// What the compiler generates
public static IEnumerable&lt;User&gt; FindByPrefix(IReadOnlyCollection&lt;User&gt; users, string prefix)
{
    ArgumentNullException.ThrowIfNull(users);
    ArgumentException.ThrowIfNullOrEmpty(prefix);

    if (users.Count == 0)
        return Array.Empty&lt;User&gt;();

    return FindByPrefixCore(users, prefix);
}

private static IEnumerable&lt;User&gt; FindByPrefixCore(IReadOnlyCollection&lt;User&gt; users, string prefix)
{
    return new FindByPrefixCoreIterator(users, prefix);
}
</code></pre>
<p>The checks now run in a plain method, so <code>FindByPrefix(users, "")</code> throws on the spot. The <code>[]</code> became <code>Array.Empty&lt;User&gt;()</code>, a cached empty array, so the empty case allocates nothing. The iterator is only created when there's something to iterate.</p>
<p>An iterator's <code>MoveNext</code> waits for someone to enumerate. An async method's <code>MoveNext</code> starts during the call, so its checks run on time, but their exceptions still arrive late.</p>
<h3>Async methods: the exception waits in the task</h3>
<p>Here's <code>FindByName</code> again, but this time the users come from somewhere asynchronous:</p>
<pre><code class="language-csharp">public static async Task&lt;User?&gt; FindByNameAsync(IUserSource source, string name)
{
    ArgumentNullException.ThrowIfNull(source);
    ArgumentException.ThrowIfNullOrEmpty(name);

    IReadOnlyList&lt;User&gt; users = await source.LoadAsync();
    return users.FirstOrDefault(u =&gt; u.Name == name);
}

public interface IUserSource
{
    Task&lt;IReadOnlyList&lt;User&gt;&gt; LoadAsync();
}
</code></pre>
<p>Rewritten the same way, <code>FindByNameAsync</code> becomes a stub that sets up the state machine and starts it:</p>
<pre><code class="language-csharp">// What the compiler generates
public static Task&lt;User?&gt; FindByNameAsync(IUserSource source, string name)
{
    var stateMachine = new FindByNameAsyncStateMachine
    {
        builder = AsyncTaskMethodBuilder&lt;User?&gt;.Create(),
        source = source,
        name = name,
    };
    stateMachine.builder.Start(ref stateMachine);
    return stateMachine.builder.Task;
}
</code></pre>
<p><code>Start</code> calls the state machine's <code>MoveNext</code> right away, on the caller's thread, and the stub returns the task only after that call comes back. <code>MoveNext</code> holds the body of your method, and everything it has to keep between calls, like the parameters, the closure and the awaiter, is a field of the state machine. Here's <code>MoveNext</code>, again with some bookkeeping left out:</p>
<pre><code class="language-csharp">// What the compiler generates
public void MoveNext()
{
    User? result;
    try
    {
        if (state == State.NotStarted)
        {
            closure = new FindByNameAsyncClosure();
            closure.name = name;

            ArgumentNullException.ThrowIfNull(source);
            ArgumentException.ThrowIfNullOrEmpty(closure.name);

            loadAwaiter = source.LoadAsync().GetAwaiter();
            if (!loadAwaiter.IsCompleted)
            {
                state = State.WaitingForLoad;
                builder.AwaitUnsafeOnCompleted(ref loadAwaiter, ref this);
                return;
            }
        }

        IReadOnlyList&lt;User&gt; users = loadAwaiter.GetResult();
        result = users.FirstOrDefault(closure.Lambda);
    }
    catch (Exception exception)
    {
        state = State.Finished;
        builder.SetException(exception);
        return;
    }

    state = State.Finished;
    builder.SetResult(result);
}
</code></pre>
<p><code>MoveNext</code> runs until an <code>await</code> actually has to wait. If the task from <code>LoadAsync</code> isn't finished yet, <code>MoveNext</code> sets the state to <code>WaitingForLoad</code>, asks <code>AwaitUnsafeOnCompleted</code> to call it again when the task completes, and returns. The second call skips the <code>NotStarted</code> block and picks up the result. The state machine itself doesn't allocate too early: in Release builds it's a struct, and it only moves to the heap at that <code>await</code>.</p>
<p>What makes it late is the <code>try</code> around the whole body of <code>MoveNext</code>. It catches every exception, even one thrown before the first <code>await</code>, and passes it to <code>SetException</code>, which stores it in the task that the stub returns. So <code>FindByNameAsync(source, "")</code> returns normally, with a task that has already faulted.</p>
<p>If you <code>await FindByNameAsync(source, name)</code> directly, you won't notice, because the exception comes out on that same line anyway. The difference shows when the task isn't awaited right away: a task stored and awaited later, a fire-and-forget call, or several calls awaited together with <code>Task.WhenAll</code>. In that last case, the <code>ArgumentException</code> comes out of <code>await Task.WhenAll(...)</code>, away from the call that passed the empty name.</p>
<p>The first section's problem is in there too: the first two lines of the <code>NotStarted</code> block create a closure and copy <code>name</code> into it, before the checks. That comes from the lambda, not from <code>async</code>, and an iterator with the same lambda would do the same at the top of its <code>MoveNext</code>. This method has both a lambda and an <code>await</code>, so it gets both problems.</p>
<h3>Fix: split the async method</h3>
<p>The split fixes both problems, as long as the public method itself isn't <code>async</code>:</p>
<pre><code class="language-csharp">public static Task&lt;User?&gt; FindByNameAsync(IUserSource source, string name)
{
    ArgumentNullException.ThrowIfNull(source);
    ArgumentException.ThrowIfNullOrEmpty(name);

    return FindByNameAsyncCore(source, name);
}

private static async Task&lt;User?&gt; FindByNameAsyncCore(IUserSource source, string name)
{
    IReadOnlyList&lt;User&gt; users = await source.LoadAsync();
    return users.FirstOrDefault(u =&gt; u.Name == name);
}
</code></pre>
<p>Now only <code>FindByNameAsyncCore</code> gets rewritten. <code>FindByNameAsync</code> stays a plain method that runs its checks and then calls the stub:</p>
<pre><code class="language-csharp">// What the compiler generates
public static Task&lt;User?&gt; FindByNameAsync(IUserSource source, string name)
{
    ArgumentNullException.ThrowIfNull(source);
    ArgumentException.ThrowIfNullOrEmpty(name);

    return FindByNameAsyncCore(source, name);
}

private static Task&lt;User?&gt; FindByNameAsyncCore(IUserSource source, string name)
{
    var stateMachine = new FindByNameAsyncCoreStateMachine
    {
        builder = AsyncTaskMethodBuilder&lt;User?&gt;.Create(),
        source = source,
        name = name,
    };
    stateMachine.builder.Start(ref stateMachine);
    return stateMachine.builder.Task;
}
</code></pre>
<p>The checks in <code>FindByNameAsync</code> are ordinary code now, so <code>FindByNameAsync(source, "")</code> throws at the call. If the arguments are valid, it returns the task from <code>FindByNameAsyncCore</code>. The <code>MoveNext</code> of that state machine is the one from before without the two checks. It still creates the closure first, but by then the checks have already passed.</p>
<div>
<div>💡</div>
<div><strong>Note on .NET 11.</strong> .NET 11 isn't released at the time of writing. It brings <a target="_self" rel="noopener noreferrer nofollow" class="text-primary underline underline-offset-2 hover:text-primary/80 cursor-pointer" href="https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-11/runtime" style="pointer-events:none">runtime async</a>, where the runtime handles async methods itself instead of running a compiler-generated state machine, so some of the allocation numbers in this post may not hold there. .NET 10 already has runtime async as an experimental feature. Turning it on didn't change the timing described here, but that doesn't guarantee anything for .NET 11. All measurements in this post are from .NET 8 and .NET 10.</div>
</div>

<h2>Back to <code>FooCore</code></h2>
<p>Put the fixes next to each other and they all do the same thing: the checks and the cheap exits stay in a plain public method, and everything the compiler rewrites (the capturing lambda, the <code>yield</code>, the <code>await</code>) goes into a private one. The public method runs entirely during the call, so its checks throw right there and its early exits don't allocate.</p>
<p>That's the <code>Foo</code> and <code>FooCore</code> pair from the beginning, and it already has a name. Jon Skeet calls it the "split method" pattern in his <a href="https://codeblog.jonskeet.uk/2010/12/24/reimplementing-linq-to-objects-part-6-repeat/">Edulinq series</a>, where he reimplements LINQ to Objects. The <a href="https://learn.microsoft.com/en-us/dotnet/csharp/iterators#enumeration-sources-with-iterator-methods">Microsoft Learn article on iterators</a> recommends the same split: in some cases, you "split an iterator method into two different methods", one that uses <code>return</code> and one that uses <code>yield return</code>. In a few places, the .NET source explains in a comment why the pair exists.</p>
<p><a href="https://github.com/dotnet/runtime/blob/main/src/libraries/System.Linq/src/System/Linq/Chunk.cs"><code>Chunk</code></a> checks its arguments, returns <code>[]</code> for an empty array, and only then calls the <code>yield</code>-based <code>ArrayChunkIterator</code> or <code>EnumerableChunkIterator</code>. A comment there says that one reason for this order is to avoid allocating an iterator for an empty array. The <a href="https://github.com/dotnet/runtime/blob/main/src/libraries/System.Linq/src/System/Linq/Where.cs"><code>Where</code> overload</a> that passes an index to the predicate is built the same way. The plain <code>Where</code> doesn't use <code>yield</code> at all: after the same checks, it returns an iterator class like <code>ArrayWhereIterator</code>, so it's the same split with the second half written by hand.</p>
<p>On the async side, <a href="https://github.com/dotnet/runtime/blob/main/src/libraries/System.Net.Http/src/System/Net/Http/HttpClient.cs"><code>HttpClient.GetStringAsync</code></a> creates and checks the request, then returns <code>GetStringAsyncCore(...)</code>. The comment above the check explains that it runs outside the async state machine so that certain exceptions propagate even when nobody awaits the returned task. <a href="https://github.com/dotnet/runtime/blob/main/src/libraries/System.Net.ServerSentEvents/src/System/Net/ServerSentEvents/SseFormatter.cs"><code>SseFormatter.WriteAsync</code></a> is built the same way.</p>
<p>Not every second half ends in <code>Core</code>. When it uses <code>yield</code>, the BCL often calls it <code>...Iterator</code>, like <code>ArrayChunkIterator</code> and <code>WhereIterator</code>, and Jon Skeet's Edulinq uses <code>...Impl</code>. Some code doesn't make it a separate method at all and keeps it inside the public one as a local function. The <code>AsyncEnumerable</code> LINQ operators that come with .NET 10 do that in methods like <a href="https://github.com/dotnet/runtime/blob/main/src/libraries/System.Linq.AsyncEnumerable/src/System/Linq/AnyAsync.cs"><code>AnyAsync</code></a>: after the null check, they return <code>Impl(...)</code> and declare <code>Impl</code> right below as a <code>static async</code> local function. The same shape works for an iterator. Here's the <code>FindByPrefix</code> split with its second half as a local function:</p>
<pre><code class="language-csharp">public static IEnumerable&lt;User&gt; FindByPrefix(IReadOnlyCollection&lt;User&gt; users, string prefix)
{
    ArgumentNullException.ThrowIfNull(users);
    ArgumentException.ThrowIfNullOrEmpty(prefix);

    if (users.Count == 0)
        return [];

    return Core(users, prefix);

    static IEnumerable&lt;User&gt; Core(IReadOnlyCollection&lt;User&gt; users, string prefix)
    {
        foreach (User user in users)
        {
            if (user.Name.StartsWith(prefix))
                yield return user;
        }
    }
}
</code></pre>
<p><code>Core</code> is <code>static</code> so that it can't capture anything. Without <code>static</code>, it could drop its parameters and read <code>users</code> and <code>prefix</code> straight from <code>FindByPrefix</code>. Because <code>Core</code> is an iterator, that capture would bring the closure class back, created on the first line of <code>FindByPrefix</code>, and the empty path would allocate 32 bytes instead of nothing. With <code>static</code>, any capture is <code>error CS8421</code>, so the closure can't come back by accident.</p>
<h2>When to split</h2>
<p>Not every method needs to be split. The strongest reason to split one is timing: in an iterator or an async method, the checks don't fail when the caller expects them to. A bad argument should cause an exception at the call that passed it, not in a <code>foreach</code> or a <code>Task.WhenAll</code> somewhere else, and that's reason enough for a second method.</p>
<p>Allocations are a weaker reason. In most code, 24 or 72 bytes is nothing. A split made only to save them pays off on a hot path where the early exit is the common case, like cache hits, empty inputs or operations that have already completed. If the early exit happens once a day, the allocation doesn't matter.</p>
<div>
<div>⚠️</div>
<div><strong>Note on guarantees.</strong> The language doesn't specify when the closure is created. The C# standard (ECMA-334) only says that a local variable is instantiated when execution enters its scope. The section of the standard that shows lambdas compiled into hidden classes is marked informative and calls itself "by no means a mandated implementation". The cached delegate of the <code>static</code> lambda in the <code>GetOrAdd</code> note is the same story: the standard permits reusing the delegate but doesn't require it. So the byte counts in this post describe what today's compiler emits, and a future compiler could create the closure later or optimize it away entirely. The BCL splits methods anyway, because it has to be as fast and as efficient as possible with the compiler that exists now. In your own code, treat the saved bytes as an optimization, not as behavior to depend on. Iterators and async methods are different: the standard itself says that an iterator's body waits for the first <code>MoveNext</code> and that an async method's exceptions go into the returned task.</div>
</div>

<p>So reading a method from top to bottom doesn't always tell you when each line runs. Once you know which code the compiler moves, a <code>Foo</code> that does little more than call <code>FooCore</code> no longer looks pointless. It's a split method: <code>Foo</code> keeps the checks that have to happen at the call, and <code>FooCore</code> gets everything the compiler rewrites.</p>
]]></content:encoded></item><item><title><![CDATA[Let the C# compiler remember it for you]]></title><description><![CDATA[I've been programming for seven years, and the bugs that made me angriest were the ones I'd have spotted in a second on a better day: two Guids passed in the wrong order, a value read from a result th]]></description><link>https://asylkhan-azat.hashnode.dev/let-the-csharp-compiler-remember-it-for-you</link><guid isPermaLink="true">https://asylkhan-azat.hashnode.dev/let-the-csharp-compiler-remember-it-for-you</guid><category><![CDATA[Programming Tips]]></category><category><![CDATA[dotnet]]></category><category><![CDATA[software development]]></category><category><![CDATA[C#]]></category><category><![CDATA[clean code]]></category><dc:creator><![CDATA[Azat Asylkhan]]></dc:creator><pubDate>Tue, 06 Oct 2026 20:26:58 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6ac2c427cb7c09e8586c956f/c74cedf7-e8f9-4782-bfc0-484edc40c7e4.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I've been programming for seven years, and the bugs that made me angriest were the ones I'd have spotted in a second on a better day: two <code>Guid</code>s passed in the wrong order, a value read from a result that was actually an error, a status check forgotten in the one method that needed it. Sometimes I was tired when I wrote them, and sometimes I was completely fresh and they slipped through anyway.</p>
<p>I don't think there's a deeper reason behind them than human nature. Attention runs out, and the boring lines are usually the first to lose it. Even the sharpest engineer you know writes bugs like these.</p>
<p>The longer I work, the more I value the compiler. It reads every line before anything runs and never gets tired, yet most of us only use it to catch typos.</p>
<p>I also like how much intent code can carry. Programming is mostly explaining an idea to whoever reads the code next, and in C#, types and names do a lot of that explaining. The person who wrote <code>Find(Guid, Guid)</code> knew which <code>Guid</code> goes first, but the person calling it had to guess. A <code>UserId</code> parameter would have told both the compiler and the colleague. Types are one of the first things we learn, and judging by how much code is built from nothing but <code>Guid</code>, <code>string</code> and <code>bool</code>, a lot of us stop learning about them right there.</p>
<p>Many junior developers try to get better by becoming a kind of Iron Man who sees every detail and knows every API by heart. I think that effort is better spent on writing code that doesn't need a superhero to get right.</p>
<p>Look at those three bugs again. You don't need to be clever to avoid any of them, you just need to remember which <code>Guid</code> goes first, that a result might be an error, and that the status needs checking. People are bad at remembering things and compilers are good at it, so when code depends on someone's memory, I'd rather give that job to the compiler. And if the compiler can't reject the wrong code outright, it can often at least warn on the exact line where the mistake is.</p>
<p>That said, I'm not telling you to go and rewrite your project my way. Over the years I've seen the same few problems come up again and again, and I've found a few tools that help with them. This post is me sharing those. Take the ones that fit your code, and if something in your project looks like one of my examples, that alone isn't a reason to refactor it.</p>
<p>It's easier to show all this with code than to argue about it, so let's start with the dumbest of the three bugs: the swapped IDs.</p>
<h2>An ID you can't swap</h2>
<p>It's the most boring bug there is:</p>
<pre><code class="language-csharp">public sealed class Orders
{
    public OrderSummary? Find(Guid userId, Guid orderId) =&gt; null; // the lookup itself is not the point
}

// var order = orders.Find(orderId, userId);
</code></pre>
<p>This compiles, runs, and returns <code>null</code> for the rest of your career. Both parameters are <code>Guid</code>, so the compiler has no way to tell them apart, and since the parameter names aren't visible at the call site, the line looks fine in review too. You find out when someone reports a missing order, and even then you stare at the line for a while.</p>
<p>You knew which ID was which, but nothing in the code passed that on to the compiler. A small wrapper type per ID fixes that:</p>
<pre><code class="language-csharp">public readonly record struct UserId(Guid Value);
public readonly record struct OrderId(Guid Value);

public sealed record OrderSummary(OrderId Id, UserId Owner);

public sealed class Orders
{
    public OrderSummary? Find(UserId userId, OrderId orderId) =&gt; null;
}

// orders.Find(orderId, userId);
// error CS1503: Argument 1: cannot convert from 'OrderId' to 'UserId'
// error CS1503: Argument 2: cannot convert from 'UserId' to 'OrderId'
</code></pre>
<p>Now the same swap gives two compile errors, one per argument. You don't have to rely on someone catching it in review anymore, and as a bonus, <code>Find(UserId, OrderId)</code> is easier to read.</p>
<h2>An email you validate once</h2>
<p>I bet you've seen this one. An email address travels from a controller through a service to a repository, and every layer checks it:</p>
<pre><code class="language-csharp">public sealed class SignupController(SignupService service)
{
    public string Register(string email)
    {
        if (string.IsNullOrWhiteSpace(email) || !email.Contains('@'))
            return "Invalid email";

        service.Register(email);
        return "OK";
    }
}

public sealed class SignupService(UserRepository users)
{
    public void Register(string email)
    {
        if (string.IsNullOrWhiteSpace(email) || !email.Contains('@'))
            throw new ArgumentException("Invalid email", nameof(email));

        users.Add(email.Trim());
    }
}

public sealed class UserRepository
{
    public void Add(string email)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(email);
        if (!email.Contains('@'))
            throw new ArgumentException("Invalid email", nameof(email));

        // insert
    }
}
</code></pre>
<p>There are three copies of the same check, and they already disagree, because the service trims the input and the controller doesn't. A CSV import job that calls the service directly skips the controller's check entirely. And when someone needs the domain part, they write <code>email.Split('@')[1]</code>, which then gets copied to three more places.</p>
<p>Every layer checks because it doesn't trust the layer before it, and with a <code>string</code> that's fair. A <code>string</code> could have been validated a microsecond ago, or it could have come straight from a form, and there's no way to tell. This is also exactly the kind of code AI agents like to generate, since by default they defend every parameter against everything.</p>
<p>The fix is a type that can only hold a valid address:</p>
<pre><code class="language-csharp">public sealed record EmailAddress
{
    public string Value { get; }
    public string Domain =&gt; Value[(Value.IndexOf('@') + 1)..];

    private EmailAddress(string value) =&gt; Value = value;

    public static EmailAddress Parse(string input)
    {
        var value = input.Trim();
        var at = value.IndexOf('@');
        if (at &lt; 1 || at == value.Length - 1 || value.IndexOf('@', at + 1) &gt;= 0)
            throw new FormatException($"'{input}' is not an email address.");
        return new EmailAddress(value);
    }

    public override string ToString() =&gt; Value;
}

// var email = EmailAddress.Parse(form.Email);   // the one place this can fail
//
// new EmailAddress("nope");
// error CS0122: 'EmailAddress.EmailAddress(string)' is inaccessible due to its protection level
//
// var copy = email with { Value = "nope" };
// error CS0200: Property or indexer 'EmailAddress.Value' cannot be assigned to -- it is read only
</code></pre>
<blockquote>
<p><strong>Side note:</strong> the check itself is a stand-in. Real email validation is a rabbit hole and not the point here.</p>
</blockquote>
<p>The constructor is private, so the only way to create an <code>EmailAddress</code> is through <code>Parse</code>, and <code>Parse</code> throws on anything invalid. <code>Value</code> has no setter, not even <code>init</code>, so a <code>with</code> expression can't get around the check either. That means every <code>EmailAddress</code> in the program has passed the check at some point. With that type in place, the layers look like this:</p>
<pre><code class="language-csharp">public sealed class SignupService(UserRepository users)
{
    public void Register(EmailAddress email) =&gt; users.Add(email);   // nothing to check
}
</code></pre>
<p>The controller calls <code>Parse</code> once, where the text enters the system, and nothing after that has to check again. The CSV import can't skip the check, because it has no other way to get an <code>EmailAddress</code>. And <code>Domain</code> is now a property that's always correct, because the address was validated when it was created, instead of a <code>Split('@')[1]</code> copied into four places.</p>
<p>This is what DDD calls a value object: a small type defined by its value, with its rules built in. People often use the term for any wrapper around a <code>string</code>, but the wrapper alone isn't the useful part. An <code>EmailAddress</code> refuses almost every value a <code>string</code> would accept, and that's why it's worth having. Being a <code>record</code> also gives it value equality, which is where the "value" in the name comes from: two addresses with the same text are the same address.</p>
<h2>A payload you can't misread</h2>
<p><code>EmailAddress</code> takes care of what goes into the system. The second bug from the beginning, reading a value from a result that was actually an error, is about what a method gives back. Here's a result type that holds either a number or an error:</p>
<pre><code class="language-csharp">public sealed class UnsafeResult
{
    public bool IsOk { get; init; }
    public int Value { get; init; }      // meaningful only when IsOk
    public string? Error { get; init; }  // meaningful only when !IsOk
}

// var r = Compute();
// return r.Value;   // compiles fine when r is an error
</code></pre>
<p>Look at the comments. <code>Value</code> only means something when <code>IsOk</code> is <code>true</code>, but the type shows every field to everyone and trusts you to check first. If you forget once, you get a <code>0</code> instead of an exception, and since <code>0</code> is a perfectly believable number, it travels a few methods further before anything looks wrong.</p>
<p>A safer design gives out the payload only through a method that makes you handle every case:</p>
<pre><code class="language-csharp">public abstract class Result
{
    private Result() { }

    public abstract T Match&lt;T&gt;(Func&lt;int, T&gt; onOk, Func&lt;string, T&gt; onError);

    public static Result Ok(int value) =&gt; new OkResult(value);
    public static Result Error(string message) =&gt; new ErrorResult(message);

    private sealed class OkResult(int value) : Result
    {
        public override T Match&lt;T&gt;(Func&lt;int, T&gt; onOk, Func&lt;string, T&gt; onError) =&gt; onOk(value);
    }

    private sealed class ErrorResult(string message) : Result
    {
        public override T Match&lt;T&gt;(Func&lt;int, T&gt; onOk, Func&lt;string, T&gt; onError) =&gt; onError(message);
    }
}

// var label = r.Match(onOk: v =&gt; $"got {v}", onError: e =&gt; $"failed: {e}");
// There is no .Value to grab.
</code></pre>
<p><code>Match</code> takes one delegate per case and calls the one that applies, so each branch gets its own payload and there's no property to misuse. Because the constructor is private, only the nested classes can derive from <code>Result</code>, and nobody outside can add a third case. The case classes are private as well, so outside code can't even name them, which leaves <code>Match</code> as the only way to get at the value.</p>
<p>You might ask why I don't just make the cases public and use a <code>switch</code> expression. The reason is that, as of C# 14, a <code>switch</code> can't tell when you've covered every subclass:</p>
<pre><code class="language-csharp">public abstract record ChargeOutcome;
public sealed record ApprovedCharge(decimal Amount) : ChargeOutcome;
public sealed record DeclinedCharge(string Reason) : ChargeOutcome;

public static class ChargeLog
{
    public static string Describe(ChargeOutcome outcome) =&gt; outcome switch
    {
        ApprovedCharge a =&gt; $"charged {a.Amount}",
        DeclinedCharge d =&gt; $"declined: {d.Reason}",
    };
    // warning CS8509: The switch expression does not handle all possible values of its input type (it is not exhaustive).
    // For example, the pattern '_' is not covered.
}
</code></pre>
<p>Both cases are handled and you still get the warning. The compiler doesn't keep track of which classes derive from <code>ChargeOutcome</code>, so as far as it knows there could always be another one. Locking the hierarchy down with a private constructor, like <code>Result</code> does, doesn't change that. The usual fix is a <code>_ =&gt; throw</code> arm, which turns a new case into a runtime exception that shows up somewhere, at some point. With <code>Match</code>, adding a case means adding a parameter, and every call site stops compiling until it handles the new case.</p>
<div>
<div>💡</div>
<div><strong>Note on C# 15.</strong> Before someone points it out in the comments: C# 15, which ships with .NET 11, adds proper union types. A declaration like <code>public union ChargeOutcome(ApprovedCharge, DeclinedCharge);</code> gives you a closed set of cases, and a <code>switch</code> over it is checked for exhaustiveness without a <code>_</code> arm. Once you're on it, both <code>Result</code> and <code>ChargeOutcome</code> can be written with a fraction of the boilerplate above. I'm aware of that. Until then, <code>Match</code> works on the C# version you have today.</div>
</div>

<h2>A state you can't forget</h2>
<p>The same problem shows up in domain state. Say an order starts as a draft, gets paid, then gets shipped. The usual model is a status field:</p>
<pre><code class="language-csharp">public readonly record struct PaymentId(Guid Value);

public enum OrderStatus { Draft, Paid, Shipped }

public sealed class Order
{
    public OrderId Id { get; init; }
    public OrderStatus Status { get; private set; }
    public decimal Total { get; private set; }
    public PaymentId? Payment { get; private set; }      // meaningful only when Paid or Shipped
    public string? TrackingNumber { get; private set; }  // meaningful only when Shipped

    public void AddLine(decimal price)
    {
        if (Status != OrderStatus.Draft) throw new InvalidOperationException("Order is no longer editable.");
        Total += price;
    }

    public void Pay(PaymentId payment)
    {
        if (Status != OrderStatus.Draft) throw new InvalidOperationException("Order is already paid.");
        Payment = payment;
        Status = OrderStatus.Paid;
    }

    public void Ship(string trackingNumber)
    {
        if (Status != OrderStatus.Paid) throw new InvalidOperationException("Order is not paid.");
        TrackingNumber = trackingNumber;
        Status = OrderStatus.Shipped;
    }

    public void ApplyDiscount(decimal amount)
    {
        Total -= amount;   // added six months later
    }
}
</code></pre>
<p>Look familiar? It's <code>UnsafeResult</code> again in a different hat: a status, plus data that only means something for certain statuses. Each method is supposed to start with a status check, and someone has to remember to write it. <code>AddLine</code>, <code>Pay</code> and <code>Ship</code> have theirs. <code>ApplyDiscount</code> was added six months later by someone in a hurry and doesn't, so now a paid order can get cheaper after the money has already changed hands, even though the status field was correct the whole time.</p>
<div>
<div>💡</div>
<div><strong>Note on enums.</strong> Like the <code>ChargeOutcome</code> records, the <code>enum</code> doesn't give you a closed set either. <code>(OrderStatus)42</code> compiles fine, so a <code>switch</code> expression that covers all three named values still gets <code>warning CS8524</code>, and it throws <code>SwitchExpressionException</code> at run time if a 42 ever shows up.</div>
</div>

<p>Here's the same order with each state as its own type:</p>
<pre><code class="language-csharp">public sealed record DraftOrder(OrderId Id, decimal Total)
{
    public DraftOrder AddLine(decimal price) =&gt; this with { Total = Total + price };
    public PaidOrder Pay(PaymentId payment) =&gt; new(Id, Total, payment);
}

public sealed record PaidOrder(OrderId Id, decimal Total, PaymentId Payment)
{
    public ShippedOrder Ship(string trackingNumber) =&gt; new(Id, Total, Payment, trackingNumber);
}

public sealed record ShippedOrder(OrderId Id, decimal Total, PaymentId Payment, string TrackingNumber);

public sealed class Invoices
{
    public void Issue(PaidOrder order) { }   // no status check, because there is nothing to check
}

// var paid = draft.AddLine(9.99m).Pay(paymentId);
// var shipped = paid.Ship("1Z999");
//
// draft.Ship("1Z999");
// error CS1061: 'DraftOrder' does not contain a definition for 'Ship'
//
// invoices.Issue(draft);
// error CS1503: Argument 1: cannot convert from 'DraftOrder' to 'PaidOrder'
</code></pre>
<p>Paying an order now means turning one type into another: you call <code>Pay</code> on a <code>DraftOrder</code>, get back a <code>PaidOrder</code>, and nothing gets mutated. With a status field, checking that an order is <code>Paid</code> only tells you about that moment, since any later line could change the status. A <code>PaidOrder</code> has no status to change, so it stays paid for as long as you hold it. It can still go out of date compared to the database, like any snapshot, but it can never be half draft and half paid.</p>
<p>The checks are still there, they've just moved into the types. A draft has no <code>Ship</code> method, so instead of throwing at run time, <code>draft.Ship(...)</code> doesn't compile. <code>Invoices.Issue</code> asks for a <code>PaidOrder</code>, so its check is part of the signature. A discount method would go on <code>DraftOrder</code>, where a paid order can't reach it. And <code>TrackingNumber</code> exists only on <code>ShippedOrder</code>, so there's no need for "meaningful only when" comments anymore.</p>
<p>There's one place where you don't know the state: an order loaded from the database could be any of the three. There you need a type that can hold any of them and makes you ask which one it is. A small struct with a <code>TryGet</code> method per state does the job:</p>
<pre><code class="language-csharp">using System.Diagnostics.CodeAnalysis;

public readonly struct StoredOrder
{
    private readonly object _state;

    public StoredOrder(DraftOrder draft) =&gt; _state = draft;
    public StoredOrder(PaidOrder paid) =&gt; _state = paid;
    public StoredOrder(ShippedOrder shipped) =&gt; _state = shipped;

    public bool TryGetDraft([NotNullWhen(true)] out DraftOrder? draft)
        =&gt; (draft = _state as DraftOrder) is not null;

    public bool TryGetPaid([NotNullWhen(true)] out PaidOrder? paid)
        =&gt; (paid = _state as PaidOrder) is not null;

    public bool TryGetShipped([NotNullWhen(true)] out ShippedOrder? shipped)
        =&gt; (shipped = _state as ShippedOrder) is not null;
}

// StoredOrder order = store.Load(orderId);
// if (order.TryGetPaid(out var paid))
//     invoices.Issue(paid);   // fine, the compiler knows paid isn't null here
</code></pre>
<p>When a <code>TryGet</code> method returns <code>false</code>, there's no order to hand out, so the <code>out</code> parameter has to be nullable. <code>[NotNullWhen(true)]</code> tells the compiler what it can't work out by itself: when the method returns <code>true</code>, the value isn't null. If you skip the <code>if</code> or remove the attribute, passing <code>paid</code> on gets <code>warning CS8604: Possible null reference argument for parameter 'order' in 'void Invoices.Issue(PaidOrder order)'.</code></p>
<p>It reads like <code>Dictionary.TryGetValue</code> and lets you ask about one state without caring about the others. The downside is that a chain of <code>TryGet</code> calls won't complain when a fourth state appears, while <code>Match</code> would break every call site. Which one to pick depends on how strict you want the compiler to be at that spot.</p>
<div>
<div>💡</div>
<div><strong>Note on C# 15, again.</strong> The note from earlier applies here too. <code>StoredOrder</code> is a hand-rolled union, and with C# 15 it becomes <code>public union StoredOrder(DraftOrder, PaidOrder, ShippedOrder);</code>. A <code>switch</code> over that is checked for exhaustiveness, so a fourth state would break call sites the way <code>Match</code> does.</div>
</div>

<p>Modeling states as types works in other places too: builders where <code>Build()</code> only exists once the required parts are set, or connections that only have <code>Query()</code> after <code>Open()</code>. Whenever what you're allowed to call depends on what happened before, you can usually express that with types.</p>
<h2>A result you can't drop</h2>
<p>The state records have one weak spot, though, and it comes from habit:</p>
<pre><code class="language-csharp">var draft = new DraftOrder(orderId, 0m);
draft.AddLine(9.99m);              // looks like it adds a line
var paid = draft.Pay(paymentId);   // a paid order with a Total of 0
</code></pre>
<p>With the old <code>Order</code> class, <code>order.AddLine(9.99m);</code> on its own line was exactly right. With records, <code>AddLine</code> returns a new order, and this code throws it away. Nothing fails, and the customer pays nothing. C# lets you ignore a return value, and a type has no way to say "don't ignore me".</p>
<p>An analyzer can catch it, though, if you mark the transitions with <code>[Pure]</code>:</p>
<pre><code class="language-csharp">using System.Diagnostics.Contracts;

public sealed record DraftOrder(OrderId Id, decimal Total)
{
    [Pure] public DraftOrder AddLine(decimal price) =&gt; this with { Total = Total + price };
    [Pure] public PaidOrder Pay(PaymentId payment) =&gt; new(Id, Total, payment);
}

public sealed record PaidOrder(OrderId Id, decimal Total, PaymentId Payment)
{
    [Pure] public ShippedOrder Ship(string trackingNumber) =&gt; new(Id, Total, Payment, trackingNumber);
}
</code></pre>
<p><code>[Pure]</code> (from <code>System.Diagnostics.Contracts</code>) says that a method has no side effects. Nothing checks that this is true, so only put it on methods that really don't have any. Rule <code>CA1806</code>, from the analyzers that ship with the .NET SDK, trusts the attribute: if a method is pure and nobody reads its result, the call was pointless. By default, though, <code>CA1806</code> is only an IDE suggestion and never shows up in the build output, so raise its severity in <code>.editorconfig</code>:</p>
<pre><code class="language-ini">[*.cs]
dotnet_diagnostic.CA1806.severity = warning
</code></pre>
<p>Now the lonely <code>draft.AddLine(9.99m);</code> gets a <code>CA1806</code> warning saying the caller "calls AddLine but does not use the value the method returns". Use <code>error</code> instead of <code>warning</code> if you want it to fail the build.</p>
<h2>Nulls you can't sneak past</h2>
<p><code>[Pure]</code> gives the tools a fact they can't figure out on their own, and nullable reference types depend on hints like that even more. With them turned on, the default flips: a reference can't be null unless its type says so with a <code>?</code>. People usually give up on them at the spots where the compiler can't see something you know. Here's the classic one:</p>
<pre><code class="language-csharp">public sealed class Parser
{
    private List&lt;string&gt; _tokens;

    public Parser()
    {
        Reset();   // warning CS8618 on the constructor
    }

    public void Reset()
    {
        _tokens = new List&lt;string&gt;();
    }
}
</code></pre>
<p>The constructor does set <code>_tokens</code> by calling <code>Reset</code>, but the compiler analyzes one method at a time, so it doesn't see that. The constructor gets <code>warning CS8618: Non-nullable field '_tokens' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable.</code> The usual workaround is <code>private List&lt;string&gt; _tokens = null!;</code>, and that <code>!</code> is a lie the compiler will believe forever. Later someone changes <code>Reset</code> so it skips the assignment on some path, <code>_tokens</code> ends up <code>null</code> at run time, and the compiler says nothing because you told it not to worry.</p>
<p>A better option is to let <code>Reset</code> promise what it sets:</p>
<pre><code class="language-csharp">using System.Diagnostics.CodeAnalysis;

public sealed class Parser
{
    private List&lt;string&gt; _tokens;

    public Parser()
    {
        Reset();   // no warning
    }

    [MemberNotNull(nameof(_tokens))]
    public void Reset()
    {
        _tokens = new List&lt;string&gt;();
    }
}
</code></pre>
<p>The constructor no longer warns, and the compiler now holds <code>Reset</code> to its promise: break the assignment there and you get <code>warning CS8774: Member '_tokens' must have a non-null value when exiting.</code> <code>NotNullWhen</code> from the <code>StoredOrder</code> example works the same way. There are a few more for the less common cases, like <code>MaybeNullWhen</code> (the opposite of <code>NotNullWhen</code>), <code>NotNullIfNotNull</code> (the return value isn't null if a given argument isn't) and <code>DoesNotReturn</code> (the method never returns normally). They all give the compiler information it can't get from the code alone, so the warning shows up on the line that's actually wrong, not scattered across the code that uses it.</p>
<p>Settings objects run into the same warning, except here it's the caller who has to set the value, not the class itself:</p>
<pre><code class="language-csharp">public sealed class SmtpSettings
{
    public required string Host { get; init; }
    public int Port { get; init; } = 25;
}

// var settings = new SmtpSettings { Port = 587 };
// error CS9035: Required member 'SmtpSettings.Host' must be set in the object initializer or attribute constructor.
</code></pre>
<p>Without <code>required</code>, <code>Host</code> gets the same CS8618 warning, someone silences it with <code>= ""</code> or <code>= null!</code>, and the missing host shows up as a connection error three calls later. With <code>required</code>, forgetting to set it is a compile error.</p>
<p>All of this only helps if people pay attention to the warnings, and warnings tend to get ignored. So it's worth turning them into errors:</p>
<pre><code class="language-xml">&lt;PropertyGroup&gt;
  &lt;Nullable&gt;enable&lt;/Nullable&gt;
  &lt;WarningsAsErrors&gt;nullable&lt;/WarningsAsErrors&gt;
&lt;/PropertyGroup&gt;
</code></pre>
<p>Now every nullable warning is an error, and everything else in the build stays as it was. A complaint at compile time, on the line where the assumption broke, is a lot cheaper than a user finding the same bug in production.</p>
<h2>Values nobody changes behind your back</h2>
<p>So far, every bug came from calling something the wrong way. Another kind comes from a value changing while someone else still relies on it:</p>
<pre><code class="language-csharp">public sealed class Money
{
    public decimal Amount { get; set; }
    public string Currency { get; set; } = "EUR";
}

// var lines = new List&lt;Money&gt;();
// var price = new Money { Amount = 10m };
// lines.Add(price);
//
// price.Amount = 9m;   // reuse it for the next line...
// lines.Add(price);    // ...and now both lines cost 9
</code></pre>
<p>Whoever wrote the last two lines thought they were preparing the next price, but they were also changing the first one, since both list entries point to the same object. When you hold a mutable object, you're trusting that nobody else changes it, and nothing in the code enforces that trust.</p>
<p>If a value can't change after it's created, you only need to check it once, and after that you can rely on it:</p>
<pre><code class="language-csharp">public sealed record Money(decimal Amount, string Currency);

// var price = new Money(10m, "EUR");
// var discounted = price with { Amount = 9m };   // a new value; price is untouched
</code></pre>
<p>Positional record classes and <code>readonly record struct</code>s get <code>init</code>-only properties, value equality and <code>with</code> for free. The immutability is shallow, though: a record with a <code>List&lt;T&gt;</code> property still hands out a list anyone can add to.</p>
<p>Functions can cause the same kind of trouble when they quietly change variables that don't belong to them. Here's a local function that looks harmless:</p>
<pre><code class="language-csharp">public static class Prices
{
    public static decimal Total(decimal[] prices, decimal discount)
    {
        decimal total = 0;

        foreach (var price in prices)
            total += Discounted(price);

        return total;

        decimal Discounted(decimal price)
        {
            if (price &gt; 100m) discount = 0;   // big items are never discounted
            return price * (1 - discount);
        }
    }
}
</code></pre>
<p>Big items are never discounted, but neither is anything that comes after one, because <code>Discounted</code> quietly overwrites the outer method's <code>discount</code> parameter. Nothing in the loop hints at that, so to understand one line in the <code>foreach</code>, you have to keep the whole method in your head.</p>
<p>If you mark the local function <code>static</code>, that version stops compiling with <code>error CS8421: A static local function cannot contain a reference to 'discount'.</code> Anything it needs now has to be passed in:</p>
<pre><code class="language-csharp">public static class Prices
{
    public static decimal Total(decimal[] prices, decimal discount)
    {
        decimal total = 0;

        foreach (var price in prices)
            total += Discounted(price, discount);

        return total;

        static decimal Discounted(decimal price, decimal discount)
            =&gt; price &gt; 100m ? price : price * (1 - discount);
    }
}
</code></pre>
<p>A <code>static</code> local function can't touch the enclosing method's locals, parameters or <code>this</code>. It gets everything through its arguments and gives back only its return value. It can still use static fields, so it isn't pure in the mathematical sense, but the compiler does guarantee that it doesn't quietly read or change the surrounding method's variables.</p>
<h2>Don't overdo it</h2>
<p>Please don't take this as a rule to create a new type every time something looks like it could be a discriminated union, meaning a type that is exactly one of a fixed set of cases, like <code>Result</code> or the order states. It depends on the case. A flag that one method checks once doesn't need a family of records and a <code>Match</code> method. The order states were worth it because the same check kept showing up in method after method, and forgetting it cost money.</p>
<p>Types also aren't completely free. They're close to it: a large modern application can have tens or even hundreds of thousands of them without anyone noticing, and in business software, code size is a number almost nobody tracks. The people who really care about it are mostly the .NET runtime developers.</p>
<p>Still, there is a cost, and it's mostly code size. A positional record generates equality, hashing, <code>ToString</code>, <code>Deconstruct</code> and operators you never wrote. And a <code>readonly record struct</code> used as a generic argument, like the key in <code>Dictionary&lt;OrderId, PaidOrder&gt;</code>, gets its own JIT-compiled copy of that generic code, because the runtime shares one copy across all reference types but compiles a separate one for each value type. More code means more pressure on the CPU's instruction cache.</p>
<div>
<div>⚠️</div>
<div><strong>Note on lost fast paths.</strong> There's also a less obvious cost. The runtime and BCL teams put a lot of work into fast paths for specific types, and <code>string</code> gets the most attention. <code>Dictionary&lt;string, TValue&gt;</code> starts out with a cheaper, non-randomized string hash, and <code>FrozenDictionary&lt;string, TValue&gt;</code> looks at the actual keys to pick a lookup strategy, sometimes hashing only part of each string. Primitives get similar treatment: <code>IndexOf</code> on a span of <code>int</code>s is vectorized. Wrap the <code>string</code> in an <code>EmailAddress</code> or the <code>int</code> in a record struct, and you get the general-purpose version instead, because those fast paths only recognize the types they were written for. A struct with a single field usually compiles down to code that's just as good, so this rarely matters, but on a hot path it's worth measuring before you wrap.</div>
</div>

<p>You don't have to use everything in this post, but if you do, that's the price, and in most code it's a small one.</p>
<h2>Say what you mean</h2>
<p>Looking back at the broken versions in this post, they all have something in common. The person writing them knew something important, like which <code>Guid</code> is which or that an order was already paid, and kept it in their head. The fixed versions move that knowledge into a type or a signature, where the compiler can check it and the next person can read it.</p>
<p>There's a speaker I really like listening to, Kevlin Henney. I remember a talk where he brought up the sparring scene from <em>The Matrix</em>, the one where Morpheus tells Neo to stop trying to hit him and hit him. He said developers do the same with naming: instead of just calling a thing what it is, they keep <em>trying</em> to name it. That one stuck with me.</p>
<p>Code is how programmers talk to each other, and nobody can see into your head. So if something matters, put it in a type name or a method signature instead of leaving the reader to guess what you meant.</p>
]]></content:encoded></item><item><title><![CDATA[The GC Doesn't Read Your Flags]]></title><description><![CDATA[In .NET you don't free memory yourself - the garbage collector does it. And its rule is simple: an object stays alive as long as something alive references it (that's a simplification, but let's go wi]]></description><link>https://asylkhan-azat.hashnode.dev/the-gc-doesn-t-read-your-flags</link><guid isPermaLink="true">https://asylkhan-azat.hashnode.dev/the-gc-doesn-t-read-your-flags</guid><category><![CDATA[C#]]></category><category><![CDATA[dotnet]]></category><category><![CDATA[memory]]></category><category><![CDATA[performance]]></category><dc:creator><![CDATA[Azat Asylkhan]]></dc:creator><pubDate>Sun, 04 Oct 2026 23:01:16 GMT</pubDate><content:encoded><![CDATA[<p>In .NET you don't free memory yourself - the garbage collector does it. And its rule is simple: an object stays alive as long as something alive references it (that's a simplification, but let's go with this explanation). Your fields, the objects they point to, the objects those point to, and so on. If the GC can reach an object by following references, the object stays.</p>
<p>The rule works so well that most of the time we don't think about it. But there's a pattern where it quietly works against us, and it shows up in all kinds of code.</p>
<p>Very often, a type keeps track of whether it has data separately from the data itself. A <code>bool</code> that says whether a value is set. A counter that says how many slots of an array are in use. A state field that says whether an object is idle and yada yada yada. The rest of the code trusts that marker: if it says there's nothing there, nothing reads the data.</p>
<p>The GC doesn't know about any of this. It doesn't read your flags or your counters. It only sees fields and the references they hold. So when your code decides some data is gone but a field still points to it, the GC sees a perfectly reachable object and keeps it alive.</p>
<p>Let's walk through it on a small example and fix it step by step.</p>
<h2>A cache for one item</h2>
<p>Say we need a tiny cache that remembers the last key and value it was given:</p>
<pre><code class="language-csharp">public sealed class OneItemCache&lt;TKey, TValue&gt;
{
    private KeyValuePair&lt;TKey, TValue&gt; _item;
    private bool _exists;

    public void Set(TKey key, TValue value)
    {
        _item = new KeyValuePair&lt;TKey, TValue&gt;(key, value);
        _exists = true;
    }

    public bool TryGetValue(TKey key, out TValue value)
    {
        if (_exists &amp;&amp; EqualityComparer&lt;TKey&gt;.Default.Equals(_item.Key, key))
        {
            value = _item.Value;
            return true;
        }

        value = default!;
        return false;
    }
}
</code></pre>
<p>Not the best production-ready type, but you essentially get the idea.</p>
<p>Now, to the problem. The field <code>_exists</code> is the marker here: <code>_item</code> is only read when the flag is set.</p>
<p>Now we need a way to empty the cache.</p>
<h3>Version 1: flip the flag</h3>
<p>Since everything checks <code>_exists</code>, emptying the cache looks trivial:</p>
<pre><code class="language-csharp">public void Clear()
{
    _exists = false;
}
</code></pre>
<p>As far as anyone using the cache can tell, it's empty now. But <code>_item</code> still holds the last key and value. The cache is alive, so <code>_item</code> is reachable, so whatever it references is reachable too.</p>
<p>You can see this for yourself:</p>
<pre><code class="language-csharp">using System.Runtime.CompilerServices;

var cache = new OneItemCache&lt;int, byte[]&gt;();
Fill(cache);
cache.Clear();

long bytes = GC.GetTotalMemory(forceFullCollection: true);
Console.WriteLine($"{bytes / 1_000_000} MB still alive");

GC.KeepAlive(cache);

[MethodImpl(MethodImplOptions.NoInlining)]
static void Fill(OneItemCache&lt;int, byte[]&gt; cache)
{
    cache.Set(1, new byte[100_000_000]);
}
</code></pre>
<p>Run it in Release mode. With this version of <code>Clear</code>, it reports about 100 MB still alive, even though the cache is empty. The <code>Fill</code> method and the <code>GC.KeepAlive</code> call are only there to keep the test honest: the first makes sure no local variable is holding on to the array, and the second makes sure the cache itself is still alive when we measure.</p>
<p>If the cache lives for a few milliseconds, nobody will notice. But caches usually exist precisely because they live a long time, often as long as the application does. And that is the exact problem: the cache holds on to an object, and everything that object references, long after you thought you'd let it go. Oh boy, we like extending objects' lifetimes :D</p>
<h3>Version 2: reset the field</h3>
<p>The fix is to let go of the data itself, not just mark it as gone:</p>
<pre><code class="language-csharp">public void Clear()
{
    _exists = false;
    _item = default;
}
</code></pre>
<p>Now nothing references the old key and value, and the test reports close to 0 MB.</p>
<p>For a single field, that's most of the story. Writing a default value costs next to nothing, and you could reasonably stop here. But the same pattern gets more interesting when a type stores many items instead of one.</p>
<h2>The same pattern with many items</h2>
<p>A typical growable collection stores its items in an array that's usually bigger than what's actually in it, plus a counter of how many slots are in use (keep in mind, it doesn't have to be an array. Almost literally anything you can think of probably works too. Some pooled object nodes with inline array, array chunks work too). If the array has 8 slots and you add a 5th item, it goes into slot 4 and the counter becomes 5. The remaining slots are spare room, so the next few adds don't need a new array.</p>
<pre><code class="language-csharp">public sealed class CustomCollection&lt;T&gt;
{
    private T[] _items = new T[8];
    private int _count;

    public void Add(T item)
    {
        if (_count == _items.Length)
        {
            Array.Resize(ref _items, _items.Length * 2);
        }

        _items[_count++] = item;
    }

    public T this[int index]
    {
        get
        {
            if (index &lt; 0 || index &gt;= _count)
            {
                throw new ArgumentOutOfRangeException(nameof(index));
            }

            return _items[index];
        }
    }

    public void Clear()
    {
        _count = 0;
    }
}
</code></pre>
<p><code>_count</code> plays exactly the role <code>_exists</code> played in the cache. Every method uses it to decide which slots hold real items (the indexer checks against <code>_count</code>, not <code>_items.Length</code>), so this <code>Clear</code> makes the collection behave as empty, and it has exactly the same problem as version 1: the array still references every item that was in it.</p>
<p>The version 2 fix carries over as well:</p>
<pre><code class="language-csharp">public void Clear()
{
    Array.Clear(_items, 0, _count);
    _count = 0;
}
</code></pre>
<p>But this time it's not a single write. Think about what happens with <code>CustomCollection&lt;int&gt;</code>. If it held a million ints, <code>Clear</code> now writes a million zeros, and none of that work is needed. We're wasting CPU time for essentially nothing. The GC doesn't look inside an <code>int[]</code> at all, since there's nothing in it to follow, and the old values would be overwritten by new items anyway. We turned a single assignment into a pass over the whole array, and for types like <code>int</code> it buys us nothing.</p>
<h3>Version 3: clear only when it can matter</h3>
<p>So we only want to clear when <code>T</code> can actually keep something alive. The first idea might be to check <code>typeof(T).IsValueType</code>, but that's not quite right. Remember the cache: it stores a <code>KeyValuePair&lt;TKey, TValue&gt;</code>, which is a struct. Yet a <code>KeyValuePair&lt;int, string&gt;</code> holds a <code>string</code>, and leaving it in a field or an array keeps that string alive just as a class would. The question to ask is whether <code>T</code> is a reference or contains one somewhere inside. And there's a method named exactly that, <code>RuntimeHelpers.IsReferenceOrContainsReferences&lt;T&gt;()</code>:</p>
<pre><code class="language-csharp">public void Clear()
{
    if (RuntimeHelpers.IsReferenceOrContainsReferences&lt;T&gt;())
    {
        Array.Clear(_items, 0, _count);
    }

    _count = 0;
}
</code></pre>
<p>This check costs nothing at run time. The JIT knows the answer for each concrete <code>T</code> when it compiles the method, so the branch disappears. For <code>CustomCollection&lt;int&gt;</code> the clearing code is removed entirely and <code>Clear</code> is back to a single assignment; for <code>CustomCollection&lt;string&gt;</code> the clearing always runs. This is also how <code>List&lt;T&gt;.Clear()</code> is implemented in the .NET runtime. <a href="https://github.com/dotnet/dotnet/blob/d39cc738714af1ed54358ba2d898ba350ad93cce/src/runtime/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/List.cs#L323">Here is how it's used in List</a></p>
<p>The people who wrote <code>List&lt;T&gt;</code> have no idea who you are. They don't know whether your <code>T</code> is an <code>int</code> or a class dragging a 100 MB buffer behind it, and they don't know whether the list lives for a microsecond or sits in a static field for the whole life of the process. Clearing unconditionally would tax every <code>List&lt;int&gt;</code> for nothing, and never clearing would turn every long-lived <code>List&lt;SomeClass&gt;</code> into the cache from the first section. The check lets them avoid picking one. The JIT makes the choice per <code>T</code>, in your program, where the answer is known.</p>
<p>The one-item cache can use the same check. Its field is a <code>KeyValuePair&lt;TKey, TValue&gt;</code>, so that's the type we ask about. It contains a reference exactly when <code>TKey</code> or <code>TValue</code> does, and the JIT folds this one to a constant just like before:</p>
<pre><code class="language-csharp">public void Clear()
{
    _exists = false;

    if (RuntimeHelpers.IsReferenceOrContainsReferences&lt;KeyValuePair&lt;TKey, TValue&gt;&gt;())
    {
        _item = default;
    }
}
</code></pre>
<p>For a single field the savings are small, but the check costs nothing, and it makes the intent explicit: this line exists for the GC.</p>
<h2>It's not about collections</h2>
<p>Make no mistake, my friends. The collection was just the place where clearing got expensive enough to need the check. Underneath, it was the same problem we started with: a marker saying "there's no data here", and a field that still holds the data.</p>
<p>The same thing happens with an object you return to a pool while it still references the last request it handled. Or with a state field set to "idle" while the finished job is still sitting in the field next to it. None of these are collections, and all of them can keep objects alive for as long as their owner lives.</p>
<p>Whenever your code decides a piece of data is no longer needed, the reference to it should go away as well.</p>
<h2>So when should you actually care?</h2>
<p>The GC follows references. A field that still points at something keeps that something alive, whatever your <code>bool</code> says. Whether that's worth a line of code depends on who holds the field and for how long.</p>
<h3>Care when the owner outlives the data</h3>
<p>This covers caches, pools, singletons, static fields and a collection that gets cleared and refilled a thousand times a second. In all of these the owner sticks around, and so does anything it forgot to drop. Reset the field, or clear the used range of the array, in the method that decides the data is gone (<code>Clear</code>, <code>Reset</code>, <code>Return</code>, whatever you call it).</p>
<p>For a single field, just write the default value and move on. The check is optional there: it costs nothing, but neither does the write, so it's mostly documentation.</p>
<p>For a buffer of <code>T</code>, wrap the clearing in <code>RuntimeHelpers.IsReferenceOrContainsReferences&lt;T&gt;()</code>. This method is for generic code. If your field is a <code>byte[]</code> or a <code>Request[]</code>, you already know the answer, so just clear or don't. The JIT folds the check to a constant per instantiation, same as in Version 3.</p>
<h3>Care when it's not your call</h3>
<p>There's a second reason to add the check, and it's the one <code>List&lt;T&gt;</code> had: you don't know the owner. If you write a library, or even just a generic type that other teams will instantiate, you control neither <code>T</code> nor the lifetime. One consumer will use it as a throwaway <code>int</code> buffer. The next will park the <code>Thing</code> version of it in a singleton and keep it there for a year. You can't optimize for either without hurting the other, so do what the BCL does and put the check in. Each consumer gets the code they would have written by hand.</p>
<h3>Don't care when the owner is about to die</h3>
<p>If the object holding the reference is itself about to become unreachable, its fields go with it. A method's locals die when the method returns. An object nobody references anymore takes its fields down with it on the next collection. Writing <code>list = null;</code> as the last line of a method, or nulling out every field in <code>Dispose</code> on something that gets dropped right after, does nothing the GC wasn't about to do anyway. The exception is a disposed object that someone keeps holding, and that's the first rule again.</p>
<p>Same for types without references in them. An <code>int[]</code> or a <code>Guid[]</code> can't keep anything alive, so clearing it for the GC's sake is wasted CPU time. The check skips this for you in generic code. In non-generic code you know the element type, so skip it yourself.</p>
<h3>The one question to ask</h3>
<p>"Who holds this, and how long will they live?" If the answer is "longer than this data should", drop the reference. If the answer is "no idea, somebody else's code is calling mine", add the check and let the JIT figure it out per <code>T</code>. And if the owner dies before the data would, leave the code alone.</p>
]]></content:encoded></item></channel></rss>