<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: VLAD</title>
    <description>The latest articles on DEV Community by VLAD (@vladut02).</description>
    <link>https://dev.to/vladut02</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4103356%2F7cf1ab18-c024-449c-8297-4c1143045611.jpg</url>
      <title>DEV Community: VLAD</title>
      <link>https://dev.to/vladut02</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vladut02"/>
    <language>en</language>
    <item>
      <title>What async/await Actually Does in C# (and Where One .Result Costs You 3 Seconds)</title>
      <dc:creator>VLAD</dc:creator>
      <pubDate>Sat, 31 Oct 2026 22:23:49 +0000</pubDate>
      <link>https://dev.to/vladut02/what-asyncawait-actually-does-in-c-and-where-one-result-costs-you-3-seconds-1o62</link>
      <guid>https://dev.to/vladut02/what-asyncawait-actually-does-in-c-and-where-one-result-costs-you-3-seconds-1o62</guid>
      <description>&lt;p&gt;Microsoft has a tutorial on &lt;a href="https://learn.microsoft.com/en-us/dotnet/core/diagnostics/debug-threadpool-starvation" rel="noopener noreferrer"&gt;debugging thread pool starvation&lt;/a&gt;. Its sample app has an ASP.NET Core endpoint with a fake database call of about half a second. The tutorial hits it with 125 concurrent connections. With &lt;code&gt;.Result&lt;/code&gt; in the code, the average response takes &lt;strong&gt;3.48 seconds&lt;/strong&gt;. With &lt;code&gt;await&lt;/code&gt; instead, it takes &lt;strong&gt;532 ms&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The work and the database are the same. So where do three seconds go? To answer that, you need to know what &lt;code&gt;await&lt;/code&gt; actually does.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/fwFIkwRRQRI" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  The myth: async means another thread
&lt;/h2&gt;

&lt;p&gt;It doesn't. &lt;code&gt;async&lt;/code&gt; doesn't start anything. &lt;code&gt;await&lt;/code&gt; means one thing: &lt;em&gt;if this isn't done yet, give the thread back.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The thread goes off and serves another request. When the answer comes, a thread picks up where your method left off. It may not be the same thread.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who waits for the database?
&lt;/h2&gt;

&lt;p&gt;During that half second, no thread is waiting. The request is out on the network, and the operating system tracks it. When the answer arrives, the OS tells .NET, and a thread pool thread runs the rest of your method for a moment. Then that thread is free again.&lt;/p&gt;

&lt;p&gt;Stephen Cleary's classic post &lt;a href="https://blog.stephencleary.com/2013/11/there-is-no-thread.html" rel="noopener noreferrer"&gt;There Is No Thread&lt;/a&gt; walks through this down to the device driver. The practical consequence is in Microsoft's &lt;a href="https://learn.microsoft.com/en-us/aspnet/core/fundamentals/best-practices" rel="noopener noreferrer"&gt;ASP.NET Core Best Practices&lt;/a&gt;: asynchronous APIs let a small pool of threads handle thousands of concurrent requests, because none of them sit waiting on blocking calls.&lt;/p&gt;

&lt;p&gt;So async doesn't make one request faster. It lets one server handle &lt;strong&gt;more&lt;/strong&gt; of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The compiler rewrites your method
&lt;/h2&gt;

&lt;p&gt;A method can't stop in the middle and come back later. So the C# compiler rewrites it. Take this method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetOrderAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetStringAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"/orders/&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;Parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It becomes a hidden &lt;strong&gt;state machine&lt;/strong&gt;, a type that implements &lt;code&gt;IAsyncStateMachine&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your local variables become &lt;strong&gt;fields&lt;/strong&gt;, so they survive the pause.&lt;/li&gt;
&lt;li&gt;Every &lt;code&gt;await&lt;/code&gt; gets a &lt;strong&gt;number&lt;/strong&gt;, a bookmark.&lt;/li&gt;
&lt;li&gt;One method, &lt;code&gt;MoveNext&lt;/code&gt;, holds your code. It starts by reading the state and jumping to the matching bookmark.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Simplified, it looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;GetOrderAsyncStateMachine&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IAsyncStateMachine&lt;/span&gt;   &lt;span class="c1"&gt;// simplified&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;          &lt;span class="c1"&gt;// -1 = not started · 0, 1 = which await&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;       &lt;span class="c1"&gt;// your local, now a field&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;MoveNext&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;goto&lt;/span&gt; &lt;span class="n"&gt;AfterAwait0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;goto&lt;/span&gt; &lt;span class="n"&gt;AfterAwait1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="c1"&gt;// ...your code, cut at every await&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In release builds it's a struct, and in debug builds it's a class. Stephen Toub's &lt;a href="https://devblogs.microsoft.com/dotnet/how-async-await-really-works/" rel="noopener noreferrer"&gt;How Async/Await Really Works in C#&lt;/a&gt; is the long version, with the real generated code.&lt;/p&gt;

&lt;h2&gt;
  
  
  What one &lt;code&gt;await&lt;/code&gt; turns into
&lt;/h2&gt;

&lt;p&gt;Here's a single &lt;code&gt;await&lt;/code&gt;, simplified:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;awaiter&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetStringAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;GetAwaiter&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;awaiter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsCompleted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                                              &lt;span class="c1"&gt;// the bookmark&lt;/span&gt;
    &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AwaitUnsafeOnCompleted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ref&lt;/span&gt; &lt;span class="n"&gt;awaiter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;ref&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// "call me back"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                                                 &lt;span class="c1"&gt;// thread is free&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;AfterAwait0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="n"&gt;json&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;awaiter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetResult&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Ask the task: are you done?&lt;/li&gt;
&lt;li&gt;If not, save the bookmark, tell the task what to call when it finishes, and &lt;strong&gt;return&lt;/strong&gt;. That &lt;code&gt;return&lt;/code&gt; is the moment your thread goes free.&lt;/li&gt;
&lt;li&gt;When the task finishes, &lt;code&gt;MoveNext&lt;/code&gt; runs again, jumps to the bookmark, and reads the result.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now look at the &lt;code&gt;if&lt;/code&gt; again. If the task is &lt;strong&gt;already&lt;/strong&gt; done, there's no pause at all: no bookmark, no thread switch. The code just keeps going. As the &lt;a href="https://devblogs.microsoft.com/dotnet/configureawait-faq/" rel="noopener noreferrer"&gt;ConfigureAwait FAQ&lt;/a&gt; puts it, awaits on already-completed awaitables keep running past the &lt;code&gt;await&lt;/code&gt; synchronously. So an async method can run from start to finish on the calling thread without ever giving it up. Async doesn't mean it waited.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where does the rest of the method run?
&lt;/h2&gt;

&lt;p&gt;When &lt;code&gt;await&lt;/code&gt; does pause, it captures a context (&lt;code&gt;SynchronizationContext.Current&lt;/code&gt;, or a non-default &lt;code&gt;TaskScheduler&lt;/code&gt;) and resumes there:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;WPF / WinForms&lt;/strong&gt; have a UI context. The rest of your method goes back to the UI thread, so it can touch the screen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ASP.NET Core&lt;/strong&gt; has no &lt;code&gt;SynchronizationContext&lt;/code&gt;. The rest runs on any free thread pool thread.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The &lt;code&gt;.Result&lt;/code&gt; deadlock
&lt;/h2&gt;

&lt;p&gt;This is where &lt;code&gt;.Result&lt;/code&gt; gets dangerous in a desktop app:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A button click calls an async method, then &lt;code&gt;.Result&lt;/code&gt;. The UI thread stops and waits.&lt;/li&gt;
&lt;li&gt;Inside, the &lt;code&gt;await&lt;/code&gt; finishes. The rest of the method needs to run on the UI thread.&lt;/li&gt;
&lt;li&gt;But the UI thread is busy, waiting for that same method.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each one waits for the other, forever. Run the same code in a console app and it works, which is why it passes your tests. Stephen Cleary's &lt;a href="https://blog.stephencleary.com/2012/07/dont-block-on-async-code.html" rel="noopener noreferrer"&gt;Don't Block on Async Code&lt;/a&gt; has the step-by-step version.&lt;/p&gt;

&lt;h2&gt;
  
  
  ASP.NET Core: no deadlock, still slow
&lt;/h2&gt;

&lt;p&gt;ASP.NET Core has no context, so this deadlock doesn't happen there. The problem is still there, though.&lt;/p&gt;

&lt;p&gt;Every &lt;code&gt;.Result&lt;/code&gt; holds a thread pool thread that does nothing for the whole database call. With 125 requests at once, the pool runs out of free threads. New work waits in a queue, and the pool adds threads, but not fast enough. That's &lt;strong&gt;thread pool starvation&lt;/strong&gt;, and that's where the three seconds in the demo went. Microsoft's tutorial calls sync-over-async &lt;em&gt;the most common cause&lt;/em&gt; of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Async all the way.&lt;/strong&gt; Await from the controller down to the database call, with no &lt;code&gt;.Result&lt;/code&gt; or &lt;code&gt;.Wait()&lt;/code&gt; in between. The &lt;a href="https://learn.microsoft.com/en-us/aspnet/core/fundamentals/best-practices" rel="noopener noreferrer"&gt;best practices page&lt;/a&gt; says it directly: don't block asynchronous execution with &lt;code&gt;Task.Wait&lt;/code&gt; or &lt;code&gt;Task.Result&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ConfigureAwait(false)&lt;/code&gt; in shared library code.&lt;/strong&gt; It tells &lt;code&gt;await&lt;/code&gt; not to come back to the captured context.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;  &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetStringAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;ConfigureAwait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In &lt;strong&gt;app code&lt;/strong&gt;, leave it out. That's Toub's advice in the FAQ. It's a safety net in libraries, not the fix for a deadlock.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two more traps
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;async void.&lt;/strong&gt; An &lt;code&gt;async Task&lt;/code&gt; method puts its exception on the task, so you can await it and catch it. &lt;code&gt;async void&lt;/code&gt; has no task, so the exception has nowhere to land, and the caller's &lt;code&gt;try&lt;/code&gt;/&lt;code&gt;catch&lt;/code&gt; &lt;a href="https://learn.microsoft.com/en-us/archive/msdn-magazine/2013/march/async-await-best-practices-in-asynchronous-programming" rel="noopener noreferrer"&gt;never sees it&lt;/a&gt;. In ASP.NET Core, Microsoft calls &lt;code&gt;async void&lt;/code&gt; "ALWAYS a bad practice", and its example crashes the process. Use it only for event handlers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;Task.Run&lt;/code&gt; isn't async.&lt;/strong&gt; It moves the work to another thread pool thread, and that thread still does the work or blocks. On a server you're already on a pool thread, so Microsoft's guidance is to not call &lt;code&gt;Task.Run&lt;/code&gt; and immediately await it. Async helps when you wait, not when you compute.&lt;/p&gt;

&lt;h2&gt;
  
  
  The JavaScript difference
&lt;/h2&gt;

&lt;p&gt;If you also write JavaScript:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JavaScript runs your code on one thread, and a &lt;code&gt;Promise&lt;/code&gt; has no &lt;code&gt;.Result&lt;/code&gt;. You can't block on it, so this deadlock can't happen there. &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await" rel="noopener noreferrer"&gt;&lt;code&gt;await&lt;/code&gt; never blocks the main thread&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;In JavaScript, &lt;code&gt;await&lt;/code&gt; &lt;strong&gt;always&lt;/strong&gt; pauses, even on a promise that's already fulfilled. The rest of the function runs in a later microtask.&lt;/li&gt;
&lt;li&gt;In C#, &lt;code&gt;await&lt;/code&gt; on a finished task doesn't pause at all.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What to look for in your code
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;.Result&lt;/code&gt; and &lt;code&gt;.Wait()&lt;/code&gt; on a task → replace with &lt;code&gt;await&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;async void&lt;/code&gt; outside event handlers → return &lt;code&gt;Task&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;await Task.Run(...)&lt;/code&gt; right away in a controller → just call the method.&lt;/li&gt;
&lt;li&gt;Shared libraries → &lt;code&gt;ConfigureAwait(false)&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;await&lt;/code&gt; doesn't start a thread. It gives one back, and the compiler keeps a bookmark so your method can come back later. The danger isn't &lt;code&gt;await&lt;/code&gt;. It's the code that blocks while it waits.&lt;/p&gt;

&lt;p&gt;Have you ever hit the &lt;code&gt;.Result&lt;/code&gt; deadlock? How long did it take you to find it?&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I make Vlad's Stack: short explainers on how the tools you use every day actually work, for .NET and full-stack developers. More at &lt;a href="https://www.youtube.com/@VladsStack" rel="noopener noreferrer"&gt;https://www.youtube.com/@VladsStack&lt;/a&gt; .&lt;/em&gt;&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>dotnet</category>
      <category>async</category>
      <category>aspnetcore</category>
    </item>
    <item>
      <title>LLM Tokens, Measured in C#: Why AI Can't Count the R's in Strawberry</title>
      <dc:creator>VLAD</dc:creator>
      <pubDate>Sat, 31 Oct 2026 22:16:46 +0000</pubDate>
      <link>https://dev.to/vladut02/llm-tokens-measured-in-c-why-ai-cant-count-the-rs-in-strawberry-1a9l</link>
      <guid>https://dev.to/vladut02/llm-tokens-measured-in-c-why-ai-cant-count-the-rs-in-strawberry-1a9l</guid>
      <description>&lt;p&gt;In August 2024, &lt;a href="https://techcrunch.com/2024/08/27/why-ai-cant-spell-strawberry/" rel="noopener noreferrer"&gt;TechCrunch asked a simple question&lt;/a&gt;: how many R's are in "strawberry"? According to GPT-4o and Claude at the time, two. The answer is three.&lt;/p&gt;

&lt;p&gt;The model isn't bad at counting. It never saw the letters. It saw tokens, and every token is just a number.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/ct2gKcK_jTU" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Every token count in this article was measured by me on October 30, 2026, with Microsoft's own .NET library, &lt;a href="https://learn.microsoft.com/en-us/dotnet/ai/how-to/use-tokenizers" rel="noopener noreferrer"&gt;&lt;code&gt;Microsoft.ML.Tokenizers&lt;/code&gt;&lt;/a&gt; 2.0.1, using the tokenizer for &lt;code&gt;gpt-5&lt;/code&gt; (the same &lt;code&gt;o200k_base&lt;/code&gt; encoding GPT-4o uses). The program is at the end, so you can check them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the model actually gets
&lt;/h2&gt;

&lt;p&gt;Before your prompt reaches the model, a tokenizer cuts it into pieces, called tokens, and turns each piece into an ID from a fixed list.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;How many r's are in strawberry?&lt;/code&gt; becomes 8 tokens:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Token&lt;/th&gt;
&lt;th&gt;&lt;code&gt;How&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;many&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;r&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;'s&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;are&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;in&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;strawberry&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;?&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ID&lt;/td&gt;
&lt;td&gt;5299&lt;/td&gt;
&lt;td&gt;1991&lt;/td&gt;
&lt;td&gt;428&lt;/td&gt;
&lt;td&gt;885&lt;/td&gt;
&lt;td&gt;553&lt;/td&gt;
&lt;td&gt;306&lt;/td&gt;
&lt;td&gt;101830&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Look at &lt;code&gt;strawberry&lt;/code&gt;, with the space in front. It's &lt;strong&gt;one token&lt;/strong&gt;: number 101830. The R's are inside that number, and the model can't open it.&lt;/p&gt;

&lt;p&gt;Spell the word out with spaces, &lt;code&gt;s t r a w b e r r y&lt;/code&gt;, and it becomes 10 tokens. Three of them are &lt;code&gt;r&lt;/code&gt; (ID 428). Now there's something to count.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where tokens come from: byte pair encoding
&lt;/h2&gt;

&lt;p&gt;Before the model is trained, the tokenizer learns from a big pile of text:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Start with single bytes.&lt;/li&gt;
&lt;li&gt;Find the pair of neighbors that shows up most often, and glue it into a new token.&lt;/li&gt;
&lt;li&gt;Repeat, until the list reaches a fixed size. For GPT-4o's encoding, &lt;code&gt;o200k&lt;/code&gt;, that's about 200,000 tokens.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is byte pair encoding. It started as a compression algorithm, and was adapted for words in &lt;a href="https://arxiv.org/abs/1508.07909" rel="noopener noreferrer"&gt;Sennrich, Haddow and Birch (2016)&lt;/a&gt;. &lt;a href="https://github.com/openai/tiktoken" rel="noopener noreferrer"&gt;OpenAI's tiktoken README&lt;/a&gt; lists what you get from it: it's lossless, it works on any text, and on average one token is about 4 bytes.&lt;/p&gt;

&lt;p&gt;The result: common text becomes one token, rare text becomes pieces.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;tokenization&lt;/code&gt; → &lt;code&gt;token&lt;/code&gt; · &lt;code&gt;ization&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Strawberry&lt;/code&gt; at the start of a line → &lt;code&gt;Str&lt;/code&gt; · &lt;code&gt;aw&lt;/code&gt; · &lt;code&gt;berry&lt;/code&gt; (3 tokens)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;strawberry&lt;/code&gt; in the middle of a sentence → 1 token&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Same word, different splits, depending on a capital letter and a space.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same sentence costs more in other languages
&lt;/h2&gt;

&lt;p&gt;I'm Romanian, so I measured this one first:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;English:&lt;/strong&gt; "Your order has shipped. You can track it in your account, and it should arrive within three business days." → &lt;strong&gt;22 tokens&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Romanian:&lt;/strong&gt; "Comanda ta a fost expediată. O poți urmări în contul tău și ar trebui să ajungă în trei zile lucrătoare." → &lt;strong&gt;34 tokens&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's about 55% more for the same meaning. Common English words got their own tokens. Romanian words get cut up: &lt;code&gt;urm·ări&lt;/code&gt;, &lt;code&gt;t·ă·u&lt;/code&gt;, &lt;code&gt;lucr·ăto·are&lt;/code&gt;. The same answer costs more and fills the context window faster.&lt;/p&gt;

&lt;h2&gt;
  
  
  Numbers get cut too
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;2026&lt;/code&gt; → &lt;code&gt;202&lt;/code&gt; · &lt;code&gt;6&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;1234567&lt;/code&gt; → &lt;code&gt;123&lt;/code&gt; · &lt;code&gt;456&lt;/code&gt; · &lt;code&gt;7&lt;/code&gt; (groups of three, from the left)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A 2024 paper, &lt;a href="https://arxiv.org/abs/2402.14903" rel="noopener noreferrer"&gt;Tokenization counts (Singh and Strouse)&lt;/a&gt;, notes that GPT-3.5 and GPT-4 have separate tokens for 1-, 2- and 3-digit numbers, and that forcing the digits to group from the right, just by adding commas, made the models much better at addition.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your code, and the expensive part
&lt;/h2&gt;

&lt;p&gt;This C# method is 234 characters and &lt;strong&gt;59 tokens&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetRecentOrdersAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;customerId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Orders&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CustomerId&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;customerId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OrderByDescending&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreatedAt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Take&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Indentation is cheap: a run of spaces is usually one token. Common identifiers like &lt;code&gt;Where&lt;/code&gt; and &lt;code&gt;Async&lt;/code&gt; are cheap too.&lt;/p&gt;

&lt;p&gt;Now a GUID: &lt;code&gt;3f2b8c1e-9d4a-4e7b-a1c2-5d6e7f809a1b&lt;/code&gt;. 36 characters, &lt;strong&gt;33 tokens&lt;/strong&gt;. Almost one token per character, because random hex has no common pieces to glue together. A log full of GUIDs, hashes or base64 eats your context window much faster than the same length of prose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Price&lt;/strong&gt; is per token, input and output priced separately (&lt;a href="https://platform.claude.com/docs/en/about-claude/pricing" rel="noopener noreferrer"&gt;Claude pricing&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The context window&lt;/strong&gt; is measured in tokens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate limits&lt;/strong&gt; are in tokens per minute (&lt;a href="https://platform.claude.com/docs/en/api/rate-limits" rel="noopener noreferrer"&gt;Claude rate limits&lt;/a&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Characters and words don't matter. Tokens do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Every model has its own tokenizer
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ToListAsync&lt;/code&gt; is one token for GPT-4's tokenizer (&lt;code&gt;cl100k_base&lt;/code&gt;) and three for GPT-4o's (&lt;code&gt;To&lt;/code&gt; · &lt;code&gt;List&lt;/code&gt; · &lt;code&gt;Async&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;It changes inside one family too. &lt;a href="https://platform.claude.com/docs/en/build-with-claude/token-counting" rel="noopener noreferrer"&gt;Anthropic's token counting docs&lt;/a&gt; say Claude models from Opus 4.7 on use a newer tokenizer, and the same text comes out about 30% higher than on earlier models. A count from one model is a guess for another. Count with the model you actually call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Counting tokens in .NET
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package Microsoft.ML.Tokenizers
dotnet add package Microsoft.ML.Tokenizers.Data.O200kBase
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.ML.Tokenizers&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Create once, reuse it.&lt;/span&gt;
&lt;span class="n"&gt;Tokenizer&lt;/span&gt; &lt;span class="n"&gt;tok&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TiktokenTokenizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateForModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"gpt-5"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tok&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CountTokens&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It can also cut text to a budget, without guessing with character counts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tok&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetIndexByTokenCount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;4000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;normalized&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;normalized&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)[..&lt;/span&gt;&lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The library covers Tiktoken (the GPT encodings), Llama, BPE and a few others. For Claude, ask the API: &lt;a href="https://platform.claude.com/docs/en/build-with-claude/token-counting" rel="noopener noreferrer"&gt;&lt;code&gt;POST /v1/messages/count_tokens&lt;/code&gt;&lt;/a&gt; counts with the tokenizer of the model you pass. It's free (with rate limits), and the docs call it an estimate that can differ from the real usage by a small amount.&lt;/p&gt;

&lt;h2&gt;
  
  
  Back to strawberry
&lt;/h2&gt;

&lt;p&gt;Newer models often get the strawberry question right. And you can help any model: ask it to spell the word letter by letter, and each letter becomes its own token. But the tokens didn't go away. Under the hood, the model still reads IDs, not letters.&lt;/p&gt;

&lt;h2&gt;
  
  
  The measurement program
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// dotnet add package Microsoft.ML.Tokenizers&lt;/span&gt;
&lt;span class="c1"&gt;// dotnet add package Microsoft.ML.Tokenizers.Data.O200kBase&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.ML.Tokenizers&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;tok&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TiktokenTokenizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateForModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"gpt-5"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;tok&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EncodeToTokens&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;" strawberry"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"[&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;] #&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// [ strawberry] #101830&lt;/span&gt;

&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tok&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CountTokens&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"3f2b8c1e-9d4a-4e7b-a1c2-5d6e7f809a1b"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// 33&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What's the most expensive thing you paste into a prompt? Logs, JSON, a whole file? Tell me in the comments.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I make Vlad's Stack — short explainers about how the tools you use every day actually work, for people who write code (.NET-flavored). The video version of this article: &lt;a href="https://www.youtube.com/@VladsStack" rel="noopener noreferrer"&gt;https://www.youtube.com/@VladsStack&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>dotnet</category>
      <category>csharp</category>
    </item>
    <item>
      <title>Why TypeScript 7 Is Written in Go, Not C#: 43 Years of Anders Hejlsberg</title>
      <dc:creator>VLAD</dc:creator>
      <pubDate>Sat, 31 Oct 2026 17:21:20 +0000</pubDate>
      <link>https://dev.to/vladut02/why-typescript-7-is-written-in-go-not-c-43-years-of-anders-hejlsberg-ko2</link>
      <guid>https://dev.to/vladut02/why-typescript-7-is-written-in-go-not-c-43-years-of-anders-hejlsberg-ko2</guid>
      <description>&lt;p&gt;On July 8, 2026, &lt;a href="https://devblogs.microsoft.com/typescript/announcing-typescript-7-0/" rel="noopener noreferrer"&gt;TypeScript 7 shipped&lt;/a&gt;: the same compiler, ported to a new language, and about ten times faster. On the VS Code codebase, a full build went from 125.7 seconds to 10.6 seconds.&lt;/p&gt;

&lt;p&gt;The port had been &lt;a href="https://devblogs.microsoft.com/typescript/typescript-native-port/" rel="noopener noreferrer"&gt;announced in March 2025 by Anders Hejlsberg&lt;/a&gt;, the man who designed C#. And the new language wasn't C#. It was Go.&lt;/p&gt;

&lt;p&gt;To see why that makes sense, it helps to go back 43 years. Across four languages, one idea keeps coming back: the time between typing your code and seeing what it does.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/dRuu4sCnEiY" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  A compiler from Denmark
&lt;/h2&gt;

&lt;p&gt;In the early 1980s, Hejlsberg wrote a Pascal compiler for small personal computers in Denmark. It was sold as Compass Pascal, and later as Poly Pascal. A young California company, Borland, licensed it, and Hejlsberg stayed in Denmark: according to &lt;a href="https://jacobfilipp.com/DrDobbs/articles/DDJ/2001/0105/0105a/0105a.htm" rel="noopener noreferrer"&gt;Dr. Dobb's&lt;/a&gt;, Borland sent him 9600-baud modems so he could send each new build to California.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turbo Pascal, 1983
&lt;/h2&gt;

&lt;p&gt;On November 20, 1983, it shipped as &lt;strong&gt;Turbo Pascal&lt;/strong&gt;. The numbers, again from Dr. Dobb's:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;$49.95&lt;/strong&gt;, when its nearest competitor cost nearly $500&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;one floppy disk&lt;/strong&gt;, ten files&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;TURBO.COM&lt;/code&gt;, the main program, was &lt;strong&gt;33,280 bytes&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The editor and the compiler lived in one program. You typed, compiled and ran without leaving it. Hejlsberg later wrote that back in the old Turbo Pascal days, it was possible for one person to write and maintain an entire product.&lt;/p&gt;

&lt;h2&gt;
  
  
  Delphi, 1995
&lt;/h2&gt;

&lt;p&gt;Windows brought windows, buttons and menus, and building them by hand meant pages of code. On February 14, 1995, &lt;a href="https://blogs.embarcadero.com/celebrating-the-30th-anniversary-of-delphi-version-1-0s-launch/" rel="noopener noreferrer"&gt;Borland launched Delphi&lt;/a&gt; in San Francisco, with Hejlsberg as chief architect. You drew a window, dropped a button on it and wrote what should happen on click. Underneath, it was still Pascal, grown out of Turbo Pascal.&lt;/p&gt;

&lt;h2&gt;
  
  
  C
&lt;/h2&gt;

&lt;p&gt;In 1996 he joined Microsoft. In June 2000, Microsoft &lt;a href="https://news.microsoft.com/source/2000/06/26/microsoft-introduces-highly-productive-net-programming-language-c/" rel="noopener noreferrer"&gt;announced C#&lt;/a&gt;, with Hejlsberg as lead designer (the &lt;a href="https://ecma-international.org/wp-content/uploads/ECMA-334_1st_edition_december_2001.pdf" rel="noopener noreferrer"&gt;ECMA-334 standard&lt;/a&gt; names him first among its principal inventors). The goal, as described at the time: the power of C++ with the ease of Visual Basic.&lt;/p&gt;

&lt;p&gt;That Delphi button followed him. In C#, a click calls your method through a delegate, and every event you write works that way. (How delegates ended up in C# is its own story, involving Sun and a court order: &lt;a href="https://dev.toVIDEO_CHANNEL_URL"&gt;Why C# Exists&lt;/a&gt;.)&lt;/p&gt;

&lt;p&gt;He stayed with C# as it grew: &lt;a href="https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-version-history" rel="noopener noreferrer"&gt;generics in 2005, LINQ in 2007, async/await in 2012&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  TypeScript, 2012
&lt;/h2&gt;

&lt;p&gt;By 2012, people were building JavaScript applications of a size the language was never planned for. Some teams inside Microsoft used tools like Script# to write C# and compile it to JavaScript. TypeScript went the other way: don't hide JavaScript, put types on top of it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://learn.microsoft.com/en-us/archive/blogs/somasegar/typescript-javascript-development-at-application-scale" rel="noopener noreferrer"&gt;Microsoft introduced TypeScript on October 1, 2012&lt;/a&gt;. Every JavaScript program was already a TypeScript program, and the types are erased when you compile. So what are they for? For the tools: errors as you type, autocomplete, safe renames. It's the Turbo Pascal idea again. The fastest feedback is the one you get before you run the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The slow compiler
&lt;/h2&gt;

&lt;p&gt;TypeScript took over. By 2025, VS Code alone was about &lt;strong&gt;1.5 million lines&lt;/strong&gt; of it. But the compiler was itself written in TypeScript, running as JavaScript. On VS Code, &lt;code&gt;tsc&lt;/code&gt; took &lt;strong&gt;77.8 seconds&lt;/strong&gt;, and just opening the project in the editor took about &lt;strong&gt;9.6 seconds&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In March 2025, &lt;a href="https://devblogs.microsoft.com/typescript/typescript-native-port/" rel="noopener noreferrer"&gt;Hejlsberg wrote the announcement&lt;/a&gt;: a native port of the compiler. Not a rewrite. A port: the same logic, moved over piece by piece, so it behaves the same.&lt;/p&gt;

&lt;p&gt;One thing worth stating plainly: &lt;strong&gt;your TypeScript code didn't change&lt;/strong&gt;. Only the program that checks it did.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Go, not C
&lt;/h2&gt;

&lt;p&gt;Plenty of C# developers asked. The team answered in public, in a &lt;a href="https://github.com/microsoft/typescript-go/discussions/411" rel="noopener noreferrer"&gt;“Why Go?” FAQ&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It's a port, so the code shape matters most.&lt;/strong&gt; "Idiomatic Go strongly resembles the existing coding patterns of the TypeScript codebase", which is mostly plain functions and data structures rather than classes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory control with a garbage collector.&lt;/strong&gt; Go lets them control how data is laid out in memory, without managing memory by hand.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Batch builds can skip GC.&lt;/strong&gt; "Batch compilations can effectively forego garbage collection entirely, since the process terminates at the end."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In an interview with Michigan TypeScript in March 2025, as &lt;a href="https://devclass.com/2025/03/12/typescript-compiler-ported-to-native-code-c-faithful-ask-why-go-was-used/" rel="noopener noreferrer"&gt;reported by devclass&lt;/a&gt;, Hejlsberg said they did consider C#, but that C# is bytecode-first and its native (AOT) compilation wasn't available on every platform they needed.&lt;/p&gt;

&lt;p&gt;So it wasn't a verdict on C#. It was the tool that fit this one job.&lt;/p&gt;

&lt;h2&gt;
  
  
  TypeScript 7
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://devblogs.microsoft.com/typescript/announcing-typescript-7-0/" rel="noopener noreferrer"&gt;TypeScript 7 shipped on July 8, 2026&lt;/a&gt;, with full builds typically &lt;strong&gt;8 to 12 times faster&lt;/strong&gt;. On VS Code, a full build went from 125.7 s to 10.6 s, and the time until the first error appears in the editor dropped from about 17.5 seconds to under 1.3.&lt;/p&gt;

&lt;h2&gt;
  
  
  One idea, four languages
&lt;/h2&gt;

&lt;p&gt;Turbo Pascal, Delphi, C#, TypeScript. Different companies, different platforms. Each time, the tool around the language mattered as much as the language. And when his own compiler got slow, his team didn't reach for his own language. They reached for the one that fit the job.&lt;/p&gt;

&lt;p&gt;Would you have ported it to C#? Tell me in the comments.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I make Vlad's Stack — short explainers and stories about how the tools you use every day actually work, for people who write code (.NET-flavored). The video version of this story: &lt;a href="https://www.youtube.com/channel/UCUO8Uo5LsEy1b9eRkrH6JNg" rel="noopener noreferrer"&gt;https://www.youtube.com/channel/UCUO8Uo5LsEy1b9eRkrH6JNg&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>csharp</category>
      <category>history</category>
      <category>go</category>
    </item>
    <item>
      <title>Isolation Levels: How the Last Item in Stock Gets Sold Twice</title>
      <dc:creator>VLAD</dc:creator>
      <pubDate>Thu, 29 Oct 2026 21:31:28 +0000</pubDate>
      <link>https://dev.to/vladut02/isolation-levels-how-the-last-item-in-stock-gets-sold-twice-319f</link>
      <guid>https://dev.to/vladut02/isolation-levels-how-the-last-item-in-stock-gets-sold-twice-319f</guid>
      <description>&lt;p&gt;Two customers buy the last item in stock at the same second. Both orders go through. Every line of your code ran inside a transaction, and SQL Server did exactly what its default setting promises.&lt;/p&gt;

&lt;p&gt;That setting is the &lt;strong&gt;isolation level&lt;/strong&gt;. There are four of them. This post walks through each one, the anomaly it stops, and the .NET class that quietly picks the strictest one for you.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/jDygpgmX3Fs" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  A transaction isn't a freeze
&lt;/h2&gt;

&lt;p&gt;A transaction promises &lt;strong&gt;all or nothing&lt;/strong&gt;: your changes land together, or not at all. What it doesn't promise is that the world stops while you work. Other transactions keep reading and writing while yours runs.&lt;/p&gt;

&lt;p&gt;How much of their work you can see is a dial, and that dial is the isolation level. It has four steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Read uncommitted&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Read committed&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Repeatable read&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Serializable&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each step up protects you from one more anomaly. Each step up also makes somebody wait longer.&lt;/p&gt;

&lt;p&gt;The three anomalies are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dirty read&lt;/strong&gt;: you read data another transaction hasn't committed yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Non-repeatable read&lt;/strong&gt;: you read the same row twice and get two values.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phantom read&lt;/strong&gt;: you run the same query twice and get a different &lt;em&gt;set&lt;/em&gt; of rows.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Read uncommitted: a price that never existed
&lt;/h2&gt;

&lt;p&gt;Transaction 1 sets a price to zero and hasn't committed. Transaction 2 reads the price and sees zero. Then transaction 1 rolls back. Transaction 2 just used a price that never existed. That's a dirty read.&lt;/p&gt;

&lt;p&gt;You've probably seen this level in production code without its name on it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;Orders&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NOLOCK&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;People add &lt;code&gt;NOLOCK&lt;/code&gt; so a report stops waiting on writers. But the &lt;a href="https://learn.microsoft.com/en-us/sql/t-sql/statements/set-transaction-isolation-level-transact-sql" rel="noopener noreferrer"&gt;SQL Server docs&lt;/a&gt; are direct about it: read uncommitted has the same effect as &lt;code&gt;NOLOCK&lt;/code&gt; on every table in every &lt;code&gt;SELECT&lt;/code&gt;. Dirty reads are included, and rows can appear or disappear in the data set while you read. &lt;code&gt;NOLOCK&lt;/code&gt; isn't a speed setting. It's agreeing to read data that might never exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read committed: same SELECT, two answers
&lt;/h2&gt;

&lt;p&gt;One step up is &lt;strong&gt;read committed&lt;/strong&gt;, the default on both &lt;a href="https://learn.microsoft.com/en-us/sql/t-sql/statements/set-transaction-isolation-level-transact-sql" rel="noopener noreferrer"&gt;SQL Server&lt;/a&gt; and &lt;a href="https://www.postgresql.org/docs/current/transaction-iso.html" rel="noopener noreferrer"&gt;PostgreSQL&lt;/a&gt;. You only see committed data, so dirty reads are gone.&lt;/p&gt;

&lt;p&gt;But data can still change between two statements of your transaction:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;T1&lt;/th&gt;
&lt;th&gt;T2&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;SELECT balance&lt;/code&gt; → 100&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;UPDATE&lt;/code&gt; → 50, &lt;code&gt;COMMIT&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;SELECT balance&lt;/code&gt; → 50&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Same query, same transaction, two answers. That's a non-repeatable read.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lost update: where the two customers come from
&lt;/h2&gt;

&lt;p&gt;Here's the stock bug in ordinary EF Core code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SingleAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// Stock = 1&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Stock&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Stock&lt;/span&gt; &lt;span class="p"&gt;-=&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveChangesAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;                              &lt;span class="c1"&gt;// SET Stock = 0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it twice at the same time under read committed. Both requests read &lt;code&gt;Stock = 1&lt;/code&gt;. Both pass the check. Both send &lt;code&gt;UPDATE ... SET Stock = 0&lt;/code&gt;. Two orders, one item, and nobody gets an error. That's a &lt;strong&gt;lost update&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You can fix it without touching the isolation level.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 1: one atomic statement.&lt;/strong&gt; Put the check in the &lt;code&gt;WHERE&lt;/code&gt; and let the database do the subtraction with &lt;a href="https://learn.microsoft.com/en-us/ef/core/saving/execute-insert-update-delete" rel="noopener noreferrer"&gt;&lt;code&gt;ExecuteUpdateAsync&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;changed&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Products&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Stock&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ExecuteUpdateAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Stock&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Stock&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;changed&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// sold out&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ExecuteUpdateAsync&lt;/code&gt; returns the number of rows it changed. Zero means somebody else got the last one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 2: a concurrency token.&lt;/strong&gt; Add a &lt;code&gt;rowversion&lt;/code&gt; column with &lt;code&gt;[Timestamp]&lt;/code&gt;. EF Core &lt;a href="https://learn.microsoft.com/en-us/ef/core/saving/concurrency" rel="noopener noreferrer"&gt;adds the original version to the &lt;code&gt;WHERE&lt;/code&gt;&lt;/a&gt; of the update, so the second save matches zero rows and throws &lt;code&gt;DbUpdateConcurrencyException&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Either way, the second buyer sees "sold out", not a free item.&lt;/p&gt;

&lt;h2&gt;
  
  
  Repeatable read: rows you read stay locked
&lt;/h2&gt;

&lt;p&gt;At &lt;strong&gt;repeatable read&lt;/strong&gt;, SQL Server places shared locks on every row you read and &lt;a href="https://learn.microsoft.com/en-us/sql/t-sql/statements/set-transaction-isolation-level-transact-sql" rel="noopener noreferrer"&gt;holds them until the transaction ends&lt;/a&gt;. Nobody can change those rows, so your second &lt;code&gt;SELECT&lt;/code&gt; gets the same answer.&lt;/p&gt;

&lt;p&gt;Run the stock code at this level and the lost update is gone, but something else shows up. Both buyers read the stock and keep their shared locks. Both then try to update, and each one waits for the other. SQL Server detects the deadlock, picks one transaction as the victim, rolls it back, and raises &lt;a href="https://learn.microsoft.com/en-us/sql/relational-databases/errors-events/mssqlserver-1205-database-engine-error" rel="noopener noreferrer"&gt;error 1205&lt;/a&gt;: &lt;em&gt;"chosen as the deadlock victim. Rerun the transaction."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The bug is fixed. The price is that you now need a &lt;strong&gt;retry&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phantoms: 10 seats, 11 people
&lt;/h2&gt;

&lt;p&gt;Repeatable read has a hole. It locks rows that &lt;em&gt;exist&lt;/em&gt;. It can't lock a row that isn't there yet.&lt;/p&gt;

&lt;p&gt;A workshop has 10 seats. Request A counts the sign-ups: 9. There's room, so it inserts one. Request B runs the same count at the same time, also sees 9, and inserts too. Eleven people, ten seats. The new row that appeared under request B's feet is a &lt;strong&gt;phantom&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Serializable: lock the whole range
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Serializable&lt;/strong&gt; places &lt;a href="https://learn.microsoft.com/en-us/sql/t-sql/statements/set-transaction-isolation-level-transact-sql" rel="noopener noreferrer"&gt;range locks&lt;/a&gt; on the key range your query read. "Sign-ups for workshop 7" becomes a range nobody can insert into until you commit. The result behaves as if the transactions ran one at a time.&lt;/p&gt;

&lt;p&gt;The cost: more waiting, more deadlocks, more retries. It's the safest level, and the slowest.&lt;/p&gt;

&lt;h2&gt;
  
  
  The whole dial
&lt;/h2&gt;

&lt;p&gt;This is the SQL standard's version (&lt;a href="https://www.postgresql.org/docs/current/transaction-iso.html" rel="noopener noreferrer"&gt;PostgreSQL's docs&lt;/a&gt; have the clearest table):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Level&lt;/th&gt;
&lt;th&gt;Dirty read&lt;/th&gt;
&lt;th&gt;Non-repeatable read&lt;/th&gt;
&lt;th&gt;Phantom&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Read uncommitted&lt;/td&gt;
&lt;td&gt;possible&lt;/td&gt;
&lt;td&gt;possible&lt;/td&gt;
&lt;td&gt;possible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Read committed&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;possible&lt;/td&gt;
&lt;td&gt;possible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Repeatable read&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;possible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Serializable&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Your database might not follow it exactly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Same name, different behavior
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;SQL Server.&lt;/strong&gt; The default read committed uses &lt;strong&gt;locks&lt;/strong&gt;: a reader waits for a writer to finish. Turn on the &lt;code&gt;READ_COMMITTED_SNAPSHOT&lt;/code&gt; database option and readers get the last committed &lt;strong&gt;version&lt;/strong&gt; of the row instead, with no waiting. Same level name, different behavior. On &lt;a href="https://learn.microsoft.com/en-us/sql/t-sql/statements/set-transaction-isolation-level-transact-sql" rel="noopener noreferrer"&gt;Azure SQL Database that option is on by default&lt;/a&gt;, so the same code can block on your laptop and not in the cloud. You can check yours:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;is_read_committed_snapshot_on&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;databases&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;PostgreSQL.&lt;/strong&gt; &lt;a href="https://www.postgresql.org/docs/current/transaction-iso.html" rel="noopener noreferrer"&gt;Read uncommitted behaves like read committed&lt;/a&gt;, so you can't get a dirty read at all. Its repeatable read doesn't allow phantoms either. But the workshop bug still gets through, because both transactions count from their own snapshot and both insert: that's a serialization anomaly, and only serializable stops it, by failing one transaction with SQLSTATE &lt;code&gt;40001&lt;/code&gt; that you retry.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MySQL (InnoDB).&lt;/strong&gt; The &lt;a href="https://dev.mysql.com/doc/refman/8.4/en/innodb-transaction-isolation-levels.html" rel="noopener noreferrer"&gt;default is repeatable read&lt;/a&gt;, not read committed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What EF Core and .NET pick for you
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;EF Core picks nothing.&lt;/strong&gt; It uses your database's default, and &lt;a href="https://learn.microsoft.com/en-us/ef/core/saving/transactions" rel="noopener noreferrer"&gt;&lt;code&gt;SaveChanges&lt;/code&gt; applies its changes in a transaction&lt;/a&gt; at that level. To choose a level, pass it when you begin the transaction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Database&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BeginTransactionAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IsolationLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Serializable&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;TransactionScope&lt;/code&gt; picks the strictest one.&lt;/strong&gt; Write &lt;code&gt;new TransactionScope()&lt;/code&gt; with no options and, &lt;a href="https://learn.microsoft.com/en-us/dotnet/framework/data/transactions/implementing-an-implicit-transaction-using-transaction-scope" rel="noopener noreferrer"&gt;by default, the transaction runs at Serializable&lt;/a&gt;. That's range locks, more waiting and more deadlocks on every query inside it, and you never asked for any of it. Pass &lt;code&gt;TransactionOptions&lt;/code&gt; and pick the level yourself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;TransactionScope&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TransactionScopeOption&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Required&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;TransactionOptions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;IsolationLevel&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;IsolationLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadCommitted&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;TransactionScopeAsyncFlowOption&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Enabled&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(&lt;code&gt;TransactionScopeAsyncFlowOption.Enabled&lt;/code&gt; is what lets the ambient transaction flow across &lt;code&gt;await&lt;/code&gt;, per the &lt;a href="https://learn.microsoft.com/en-us/ef/core/saving/transactions" rel="noopener noreferrer"&gt;EF Core docs&lt;/a&gt;.)&lt;/p&gt;

&lt;h2&gt;
  
  
  A checklist for your own app
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Know your default.&lt;/strong&gt; One query on &lt;code&gt;sys.databases&lt;/code&gt; tells you whether read committed snapshot is on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treat &lt;code&gt;NOLOCK&lt;/code&gt; as a bug&lt;/strong&gt;, not a speed fix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read, then write?&lt;/strong&gt; Use one &lt;code&gt;UPDATE&lt;/code&gt; with the check in the &lt;code&gt;WHERE&lt;/code&gt;, or a concurrency token.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Find every &lt;code&gt;new TransactionScope()&lt;/code&gt;&lt;/strong&gt; and give it a level.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If you move up the dial, write the retry&lt;/strong&gt; (error 1205 on SQL Server, SQLSTATE 40001 on PostgreSQL).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A transaction doesn't stop others from touching your data. The isolation level decides how much they can, so pick it on purpose, before your users find the default for you.&lt;/p&gt;

&lt;p&gt;Which isolation level does your app run at right now? Do you know it without checking? Tell me in the comments.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I make Vlad's Stack — short explainers on how the tools you use every day actually work, for people who write code (.NET-flavored). The video version of this post is on the channel: &lt;a href="https://www.youtube.com/@VladsStack" rel="noopener noreferrer"&gt;https://www.youtube.com/@VladsStack&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>database</category>
      <category>sql</category>
      <category>dotnet</category>
      <category>csharp</category>
    </item>
    <item>
      <title>EF Core Isn't Slow. These 5 Query Patterns Are.</title>
      <dc:creator>VLAD</dc:creator>
      <pubDate>Thu, 29 Oct 2026 21:25:12 +0000</pubDate>
      <link>https://dev.to/vladut02/ef-core-isnt-slow-these-5-query-patterns-are-1m82</link>
      <guid>https://dev.to/vladut02/ef-core-isnt-slow-these-5-query-patterns-are-1m82</guid>
      <description>&lt;p&gt;Four lines of C# can send 101 queries to SQL Server. One line can pull a whole table into memory just to show ten rows. In both cases EF Core is doing exactly what the code asked for. The problem is the SQL that the code turns into.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/7VbZl8HMH4o" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Here are five patterns behind a lot of slow EF Core code, plus the one habit that catches all of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  First: LINQ isn't run as C
&lt;/h2&gt;

&lt;p&gt;When you write a LINQ query against a &lt;code&gt;DbSet&lt;/code&gt;, EF Core doesn't run your lambdas. It reads the query as an expression tree and translates it into SQL. Nothing hits the database until you ask for results, with &lt;code&gt;ToList&lt;/code&gt;, &lt;code&gt;First&lt;/code&gt;, &lt;code&gt;foreach&lt;/code&gt; and the like.&lt;/p&gt;

&lt;p&gt;So asking "is my C# fast?" doesn't help much. The useful question is: &lt;strong&gt;what SQL did this become?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  See the SQL before you guess
&lt;/h2&gt;

&lt;p&gt;One line in your context options logs every command EF sends (&lt;a href="https://learn.microsoft.com/en-us/ef/core/logging-events-diagnostics/simple-logging" rel="noopener noreferrer"&gt;Simple Logging&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;optionsBuilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LogLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Information&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a single query, &lt;code&gt;ToQueryString()&lt;/code&gt; returns the SQL as a string without running it (available since &lt;a href="https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-5.0/whatsnew" rel="noopener noreferrer"&gt;EF Core 5.0&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;sql&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToQueryString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Count the queries and look at the columns. Each fix below starts there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap 1: N+1
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;     &lt;span class="c1"&gt;// 1 query&lt;/span&gt;
&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;         &lt;span class="c1"&gt;// +1 query per order&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With lazy loading turned on, every &lt;code&gt;o.Customer&lt;/code&gt; access is a separate round trip. 100 orders means 101 queries. Lazy loading is opt-in: you get it from the &lt;code&gt;Microsoft.EntityFrameworkCore.Proxies&lt;/code&gt; package and &lt;code&gt;UseLazyLoadingProxies()&lt;/code&gt;. Once it's on, it hides this cost from you. The docs warn about it directly: lazy loading &lt;a href="https://learn.microsoft.com/en-us/ef/core/performance/efficient-querying#beware-of-lazy-loading" rel="noopener noreferrer"&gt;makes the N+1 problem very easy to trigger&lt;/a&gt;, and they recommend avoiding it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; ask for what you need up front. A projection becomes a single query with a JOIN:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Orders&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CustomerName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;                              &lt;span class="c1"&gt;// 1 query, with a JOIN&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use &lt;code&gt;Include(o =&amp;gt; o.Customer)&lt;/code&gt; if you really need the whole entity. Also watch for the same trap without lazy loading: a query inside a &lt;code&gt;foreach&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap 2: the whole table
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;big&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToList&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Total&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// whole table&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;good&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Total&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;ToList&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;   &lt;span class="c1"&gt;// WHERE in SQL&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ToList()&lt;/code&gt; runs first, so the first line selects every row and filters in memory. In the second line, the filter becomes a &lt;code&gt;WHERE&lt;/code&gt; clause. The lambda is identical, but the query is not.&lt;/p&gt;

&lt;p&gt;There's a sneakier version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Orders&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;big&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Total&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;ToList&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;       &lt;span class="c1"&gt;// whole table, again&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the &lt;code&gt;DbSet&lt;/code&gt; is typed as &lt;code&gt;IEnumerable&amp;lt;T&amp;gt;&lt;/code&gt; (a variable, or a method parameter), &lt;code&gt;Where&lt;/code&gt; resolves to the in-memory LINQ operator, and EF never sees the filter. &lt;strong&gt;Keep it &lt;code&gt;IQueryable&lt;/code&gt; until you're done filtering.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;EF Core does protect you from the accidental version. Since 3.0, an expression it can't translate, anywhere except the final projection, &lt;a href="https://learn.microsoft.com/en-us/ef/core/querying/client-eval" rel="noopener noreferrer"&gt;throws instead of silently running on the client&lt;/a&gt;. It can't protect you here, because here you explicitly asked for the whole table.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap 3: loading more than you show
&lt;/h2&gt;

&lt;p&gt;A list page needs two columns, and &lt;code&gt;db.Orders.ToList()&lt;/code&gt; brings back all of them. EF also tracks every entity it loads. As the docs explain, it keeps &lt;a href="https://learn.microsoft.com/en-us/ef/core/performance/efficient-querying#tracking-no-tracking-and-identity-resolution" rel="noopener noreferrer"&gt;an identity dictionary and a snapshot per entity&lt;/a&gt;, so it can detect changes when you call &lt;code&gt;SaveChanges&lt;/code&gt;. That costs time and memory.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// read-only list: just the columns&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Orders&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;OrderRow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Total&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// read-only, full entities: skip the tracking&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsNoTracking&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A projection with no entity types in the result &lt;a href="https://learn.microsoft.com/en-us/ef/core/querying/tracking#tracking-and-custom-projections" rel="noopener noreferrer"&gt;isn't tracked at all&lt;/a&gt;. If you need full entities but won't save them, add &lt;code&gt;AsNoTracking()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;How much does it matter? In &lt;a href="https://learn.microsoft.com/en-us/ef/core/performance/efficient-querying#tracking-no-tracking-and-identity-resolution" rel="noopener noreferrer"&gt;Microsoft's own benchmark&lt;/a&gt; (10 blogs with 20 posts each), the no-tracking query took about 30% less time (993 µs vs. 1,415 µs) and allocated almost 40% less memory (233 KB vs. 380 KB). That's a small query, and the difference is still real.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap 4: cartesian explosion
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;blogs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Blogs&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Include&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Posts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Include&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Contributors&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two collection &lt;code&gt;Include&lt;/code&gt;s at the same level become one SQL query with two JOINs, and the database returns a cross product. The &lt;a href="https://learn.microsoft.com/en-us/ef/core/querying/single-split-queries#cartesian-explosion" rel="noopener noreferrer"&gt;docs' example&lt;/a&gt;: a blog with 10 posts and 10 contributors comes back as &lt;strong&gt;100 rows&lt;/strong&gt;. EF Core logs a warning when a query loads multiple collections and you haven't configured a splitting mode, so read the log.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; &lt;code&gt;.AsSplitQuery()&lt;/code&gt; sends one query per collection instead of one big JOIN. It comes with &lt;a href="https://learn.microsoft.com/en-us/ef/core/querying/single-split-queries#characteristics-of-split-queries" rel="noopener noreferrer"&gt;trade-offs&lt;/a&gt;: there's an extra round trip per query, and if someone writes in between, the queries don't see one consistent snapshot. Split when the JOIN explodes, not everywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap 5: the index killer
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToLower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;LOWER&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Email&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;EF translates &lt;code&gt;ToLower&lt;/code&gt; faithfully, as a function wrapped around the column. When a query filters on an expression over a column, &lt;a href="https://learn.microsoft.com/en-us/ef/core/performance/efficient-querying#use-indexes-properly" rel="noopener noreferrer"&gt;a simple index can't be used&lt;/a&gt;. On SQL Server you usually didn't need it anyway: the default server collation for an English (US) install, &lt;code&gt;SQL_Latin1_General_CP1_CI_AS&lt;/code&gt;, &lt;a href="https://learn.microsoft.com/en-us/ef/core/miscellaneous/collations-and-case-sensitivity" rel="noopener noreferrer"&gt;is case-insensitive&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The same idea applies to &lt;code&gt;StartsWith&lt;/code&gt; and &lt;code&gt;EndsWith&lt;/code&gt;: on SQL Server, the first can use an index on the column and the second can't.&lt;/p&gt;

&lt;h2&gt;
  
  
  A slow page? Go in this order
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Log&lt;/strong&gt; the SQL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Count&lt;/strong&gt; the queries. More than a few for one page? Look for N+1.&lt;/li&gt;
&lt;li&gt;Check &lt;strong&gt;rows and columns&lt;/strong&gt;. Are you loading the whole table, every column, or a cartesian explosion?&lt;/li&gt;
&lt;li&gt;Open the &lt;strong&gt;query plan&lt;/strong&gt; and look for a scan on a column you indexed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;EF Core doesn't make your query slow. It sends exactly the SQL you described, so read that SQL before you blame the ORM.&lt;/p&gt;

&lt;p&gt;What's the most queries you've ever found behind one page?&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I make Vlad's Stack: short explainers on how the tools you use every day actually work, for people who write code. &lt;a href="https://www.youtube.com/@VladsStack" rel="noopener noreferrer"&gt;VIDEO_CHANNEL_URL&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>efcore</category>
      <category>sql</category>
    </item>
    <item>
      <title>Heartbleed: One Unchecked Length, Two Years, Half a Million Websites (and the C# Version)</title>
      <dc:creator>VLAD</dc:creator>
      <pubDate>Thu, 29 Oct 2026 16:28:13 +0000</pubDate>
      <link>https://dev.to/vladut02/heartbleed-one-unchecked-length-two-years-half-a-million-websites-and-the-c-version-289m</link>
      <guid>https://dev.to/vladut02/heartbleed-one-unchecked-length-two-years-half-a-million-websites-and-the-c-version-289m</guid>
      <description>&lt;p&gt;In April 2014, about half a million trusted websites had the same bug. Ask them the right way, and they'd hand you up to 64 KB of their memory: passwords, session cookies, private messages, sometimes the private key that protects the whole site. The attacker didn't break in. They sent one small message and &lt;strong&gt;lied about one number&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/7z37NHh425Q" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;This post walks through Heartbleed using the primary sources: the OpenSSL advisory, the commit that added the bug, the commit that fixed it, and the RFC it was supposed to implement. At the end there's the part that matters for .NET developers, because the same mistake can still happen in safe C#, just more quietly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The heartbeat
&lt;/h2&gt;

&lt;p&gt;When you see the lock in your browser, your connection is protected by TLS. In 2014, a huge part of the web did that locking with &lt;a href="https://www.openssl.org/" rel="noopener noreferrer"&gt;OpenSSL&lt;/a&gt;, the free, open-source library behind Apache and nginx.&lt;/p&gt;

&lt;p&gt;In early 2012, OpenSSL 1.0.1 added the &lt;strong&gt;heartbeat extension&lt;/strong&gt;, from a new standard, &lt;a href="https://www.rfc-editor.org/rfc/rfc6520" rel="noopener noreferrer"&gt;RFC 6520&lt;/a&gt;. The idea is simple. A secure connection can sit idle for a while, so one side asks "are you still there?", and the other side answers. The request carries a small payload and says how long it is. The response must echo that payload back.&lt;/p&gt;

&lt;p&gt;Say the payload is the word &lt;code&gt;PING&lt;/code&gt;, four bytes. The server copies those four bytes into its answer and sends them back. That's a heartbeat.&lt;/p&gt;

&lt;h2&gt;
  
  
  One number nobody checked
&lt;/h2&gt;

&lt;p&gt;Now, what if the request lies? It sends &lt;code&gt;PING&lt;/code&gt;, four bytes, but claims the length is 64,000.&lt;/p&gt;

&lt;p&gt;Here's the vulnerable function from OpenSSL 1.0.1f (&lt;code&gt;ssl/t1_lib.c&lt;/code&gt;, &lt;code&gt;tls1_process_heartbeat&lt;/code&gt;), trimmed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;hbtype&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;n2s&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="cm"&gt;/* read the length the client claims */&lt;/span&gt;
&lt;span class="n"&gt;pl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;            &lt;span class="cm"&gt;/* pointer to the payload */&lt;/span&gt;
&lt;span class="cm"&gt;/* ... */&lt;/span&gt;
&lt;span class="n"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;OPENSSL_malloc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;bp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;bp&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TLS1_HB_RESPONSE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;s2n&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bp&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;memcpy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="cm"&gt;/* copy `payload` bytes, trusting the client */&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;payload&lt;/code&gt; comes straight from the attacker's message. Nothing compares it with the number of bytes that actually arrived. So &lt;code&gt;memcpy&lt;/code&gt; starts at the four real bytes and keeps going, into whatever sits next in the server's heap: other people's requests, their passwords and cookies, sometimes key material.&lt;/p&gt;

&lt;p&gt;The length field is two bytes, so the largest value is 65,535: about 64 KB per heartbeat. And nothing limited the number of heartbeats. As &lt;a href="https://heartbleed.com/" rel="noopener noreferrer"&gt;heartbleed.com&lt;/a&gt; put it, the 64 KB limit applies to a single heartbeat, not to the attack.&lt;/p&gt;

&lt;p&gt;Worst of all, it was silent. Exploiting it left nothing abnormal in the logs.&lt;/p&gt;

&lt;p&gt;The irony is that the standard already said what to do. RFC 6520: if the &lt;code&gt;payload_length&lt;/code&gt; of a received message is too large, the message &lt;strong&gt;must be discarded silently&lt;/strong&gt;. The code just never did that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two years in plain sight
&lt;/h2&gt;

&lt;p&gt;The heartbeat code was written by one of the authors of the heartbeat RFC. A core OpenSSL developer reviewed it and &lt;a href="https://github.com/openssl/openssl/commit/4817504d069b4c5082161b02a22116ad75f822b1" rel="noopener noreferrer"&gt;committed it&lt;/a&gt; on &lt;strong&gt;December 31, 2011&lt;/strong&gt;, New Year's Eve. The author later said publicly that he missed a length check, the reviewer missed it too, and it wasn't deliberate.&lt;/p&gt;

&lt;p&gt;OpenSSL 1.0.1 shipped on March 14, 2012, with the heartbeat on by default. Then it sat there through 2012 and 2013, on servers all over the world. "Given enough eyeballs, all bugs are shallow" is a nice line, but plenty of people used this code and very few read it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two teams, one week
&lt;/h2&gt;

&lt;p&gt;In early 2014, Neel Mehta of Google Security was auditing OpenSSL. He found the missing check, and it was reported privately to the OpenSSL team on &lt;strong&gt;April 1, 2014&lt;/strong&gt;. On &lt;strong&gt;April 3&lt;/strong&gt;, the Finnish security company Codenomicon found the same bug independently. They gave it a name, a bleeding-heart logo and its own website.&lt;/p&gt;

&lt;p&gt;On &lt;strong&gt;April 7&lt;/strong&gt;, OpenSSL published &lt;a href="https://openssl-library.org/news/secadv/20140407.txt" rel="noopener noreferrer"&gt;the advisory&lt;/a&gt; and version 1.0.1g. The &lt;a href="https://github.com/openssl/openssl/commit/96db9023b881d7cd9f379b0c154650d6c108e9a3" rel="noopener noreferrer"&gt;fix&lt;/a&gt; is exactly what the RFC asked for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;s3&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;rrec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="cm"&gt;/* silently discard per RFC 6520 sec. 4 */&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://netcraft.com/blog/half-a-million-widely-trusted-websites-vulnerable-to-heartbleed-bug" rel="noopener noreferrer"&gt;Netcraft&lt;/a&gt; checked that week: about 17.5% of SSL sites with trusted certificates had the heartbeat extension enabled, around half a million certificates.&lt;/p&gt;

&lt;h2&gt;
  
  
  What leaked
&lt;/h2&gt;

&lt;p&gt;Patching stopped new leaks, but nobody could know what had leaked before, because there was no trace in the logs.&lt;/p&gt;

&lt;p&gt;The scariest question was the private key. Cloudflare first concluded that stealing it was &lt;a href="https://blog.cloudflare.com/answering-the-critical-question-can-you-get-private-ssl-keys-using-heartbleed/" rel="noopener noreferrer"&gt;"at least extremely hard and, likely, impossible"&lt;/a&gt;, and put up a vulnerable server as a public challenge. About nine hours later, two people had extracted its key; &lt;a href="https://blog.cloudflare.com/the-results-of-the-cloudflare-challenge/" rel="noopener noreferrer"&gt;one of them sent at least 2.5 million requests&lt;/a&gt;. Cloudflare updated its post: they had been wrong. So patching wasn't enough. Affected sites had to revoke their certificates and get new keys.&lt;/p&gt;

&lt;p&gt;In Canada, the tax agency took its online services down in the middle of tax season. It still wasn't fast enough: the agency &lt;a href="https://www.ctvnews.ca/canada/cra-says-900-social-insurance-numbers-taken-in-heartbleed-exploit-1.1774708" rel="noopener noreferrer"&gt;reported&lt;/a&gt; that about 900 social insurance numbers were taken through Heartbleed, over a window of about six hours.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who was holding it up
&lt;/h2&gt;

&lt;p&gt;Then came the second question: who pays for OpenSSL?&lt;/p&gt;

&lt;p&gt;Almost nobody. In &lt;a href="https://veridicalsystems.com/blog/of-money-responsibility-and-pride/" rel="noopener noreferrer"&gt;a post from April 12, 2014&lt;/a&gt;, Steve Marquess of the OpenSSL Software Foundation wrote that it typically received about &lt;strong&gt;$2,000 a year&lt;/strong&gt; in donations. Most of its money came from support contracts and consulting, and only one team member worked on it without another full-time job. In the week after the news, about two hundred people donated, mostly $5 or $10, around $9,000 in total.&lt;/p&gt;

&lt;p&gt;On April 24, 2014, the Linux Foundation &lt;a href="https://www.helpnetsecurity.com/2014/04/24/tech-giants-back-initiative-for-funding-crucial-open-source-projects/" rel="noopener noreferrer"&gt;launched the Core Infrastructure Initiative&lt;/a&gt;, with companies including Google, Microsoft, Amazon and Facebook each pledging at least $100,000 a year for three years. OpenSSL was the first project funded.&lt;/p&gt;

&lt;h2&gt;
  
  
  Could this happen in C#?
&lt;/h2&gt;

&lt;p&gt;The classic version, mostly no.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A C# array knows its own length. Read past the end and you get an &lt;a href="https://learn.microsoft.com/en-us/dotnet/api/system.indexoutofrangeexception" rel="noopener noreferrer"&gt;&lt;code&gt;IndexOutOfRangeException&lt;/code&gt;&lt;/a&gt;, not your neighbor's data.&lt;/li&gt;
&lt;li&gt;A new array starts out &lt;a href="https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/arrays" rel="noopener noreferrer"&gt;zero-initialized&lt;/a&gt;, so there's no old secret sitting in it.&lt;/li&gt;
&lt;li&gt;Windows itself wasn't affected out of the box: it uses its own TLS stack, SChannel, not OpenSSL, as &lt;a href="https://learn.microsoft.com/en-us/archive/blogs/erezs_iis_blog/information-about-heartbleed-and-iis" rel="noopener noreferrer"&gt;Microsoft said at the time&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But modern .NET has one place where the old danger comes back: &lt;strong&gt;&lt;code&gt;ArrayPool&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/strong&gt;. To avoid allocations, you rent a buffer and give it back when you're done, so the next caller gets the same array. The &lt;a href="https://learn.microsoft.com/en-us/dotnet/api/system.buffers.arraypool-1.rent" rel="noopener noreferrer"&gt;docs for &lt;code&gt;Rent&lt;/code&gt;&lt;/a&gt; say two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The returned array is &lt;strong&gt;at least&lt;/strong&gt; &lt;code&gt;minimumLength&lt;/code&gt; long. It can be bigger.&lt;/li&gt;
&lt;li&gt;The returned array &lt;strong&gt;may not be zero-initialized&lt;/strong&gt;. It can still hold whatever the previous renter left in it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So this is a small Heartbleed, written entirely in safe code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ArrayPool&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;.&lt;/span&gt;&lt;span class="n"&gt;Shared&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Rent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;256&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;written&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;WriteReply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;      &lt;span class="c1"&gt;// writes, say, 40 bytes&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;       &lt;span class="c1"&gt;// BUG: sends buffer.Length bytes,&lt;/span&gt;
                                           &lt;span class="c1"&gt;// including old data from another request&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;finally&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ArrayPool&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;.&lt;/span&gt;&lt;span class="n"&gt;Shared&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Return&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix is the same idea as OpenSSL's: only send what you actually wrote.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsMemory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;written&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the closer cousin: trusting a length the client sent you, and slicing that much out of a pooled buffer. If the claimed length is smaller than the rented array, nothing throws. You just copy out bytes that were never part of this request. Check the claimed length against the bytes you actually received, and if it doesn't match, drop the message.&lt;/p&gt;

&lt;p&gt;If a pooled buffer ever holds secrets, &lt;code&gt;Return&lt;/code&gt; also takes a &lt;code&gt;clearArray&lt;/code&gt; flag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;ArrayPool&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;.&lt;/span&gt;&lt;span class="n"&gt;Shared&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Return&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;clearArray&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What to take from it
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Every number from outside is a claim, not a fact.&lt;/strong&gt; Lengths, counts, offsets: check them against what you actually received.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No trace in the logs doesn't mean nothing happened.&lt;/strong&gt; With Heartbleed, everyone had to assume the worst and rotate keys.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The code you depend on is maintained by real people.&lt;/strong&gt; Sometimes very few of them.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What's the one open-source library your app can't live without, and do you know who maintains it? Tell me in the comments.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I make Vlad's Stack: short explainers and stories about how the tools you use every day actually work, for .NET and full-stack developers. &lt;a href="https://www.youtube.com/@VladsStack" rel="noopener noreferrer"&gt;The channel is here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>dotnet</category>
      <category>csharp</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Your AI Coding Agent Never Reads Your Whole C# Solution</title>
      <dc:creator>VLAD</dc:creator>
      <pubDate>Thu, 29 Oct 2026 11:55:05 +0000</pubDate>
      <link>https://dev.to/vladut02/your-ai-coding-agent-never-reads-your-whole-c-solution-374</link>
      <guid>https://dev.to/vladut02/your-ai-coding-agent-never-reads-your-whole-c-solution-374</guid>
      <description>&lt;p&gt;You ask an AI agent to fix a bug in your C# solution. It never reads the whole solution. It searches, opens a few files, and decides from those. So when it gets it wrong, the reason is usually a file it &lt;strong&gt;never opened&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/pDmNsQc6aOs" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;This post walks through what the agent reads first, how it hunts for the rest, the three places a .NET solution hides code from it, and the one file that fixes most of it. I use Claude Code as the concrete example because its behavior is documented, but the mechanics apply to most agents.&lt;/p&gt;

&lt;h2&gt;
  
  
  An agent is a model in a loop
&lt;/h2&gt;

&lt;p&gt;An agent works in a loop: &lt;strong&gt;gather context, take an action, check the result&lt;/strong&gt;, then go around again. The &lt;a href="https://code.claude.com/docs/en/how-claude-code-works" rel="noopener noreferrer"&gt;Claude Code docs&lt;/a&gt; describe exactly these three phases, and they're explicit about who does what: the model reasons, and the harness around it provides the tools and manages the context the model sees.&lt;/p&gt;

&lt;p&gt;So the model never touches your disk. It asks for a file, the tool reads it, and the text goes back into the conversation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it doesn't just read everything
&lt;/h2&gt;

&lt;p&gt;Everything the agent reads lands in its &lt;strong&gt;context window&lt;/strong&gt;, which Anthropic's docs call the model's &lt;a href="https://platform.claude.com/docs/en/build-with-claude/context-windows" rel="noopener noreferrer"&gt;working memory&lt;/a&gt;. Current Claude models have a 1M-token window. That sounds like enough for a whole solution.&lt;/p&gt;

&lt;p&gt;But more isn't better. The same docs say that as the token count grows, accuracy and recall degrade, and they give it a name: &lt;strong&gt;context rot&lt;/strong&gt;. So a good agent has to be picky about what it loads.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it reads first
&lt;/h2&gt;

&lt;p&gt;Not your code. An &lt;strong&gt;instruction file&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In Claude Code, that's &lt;code&gt;CLAUDE.md&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Many other agents (Codex, Copilot's coding agent, Cursor, Gemini CLI and others) read &lt;a href="https://agents.md/" rel="noopener noreferrer"&gt;&lt;code&gt;AGENTS.md&lt;/code&gt;&lt;/a&gt;, which describes itself as a README for agents.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That file is loaded at the start of every session, before you type a word. Claude Code also sees your git state: the current branch, uncommitted changes and recent commits. Everything else, the agent has to go and find.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it finds the rest
&lt;/h2&gt;

&lt;p&gt;Pretty much the way you would on a new team:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;File names.&lt;/strong&gt; Show me every project file in this solution.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Text.&lt;/strong&gt; Where does &lt;code&gt;OrderService&lt;/code&gt; appear?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read.&lt;/strong&gt; Open the two or three files that look right.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Glob  &lt;span class="k"&gt;**&lt;/span&gt;/&lt;span class="k"&gt;*&lt;/span&gt;.csproj
Grep  &lt;span class="s2"&gt;"OrderService"&lt;/span&gt;
Read  Orders.Api/OrderService.cs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Anthropic's engineering team calls this &lt;a href="https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents" rel="noopener noreferrer"&gt;just-in-time context&lt;/a&gt;: Claude Code uses glob and grep to pull files in as it needs them, rather than building a big index up front. &lt;code&gt;CLAUDE.md&lt;/code&gt; is the exception: it's put into context right away.&lt;/p&gt;

&lt;h3&gt;
  
  
  Names are most of the clue
&lt;/h3&gt;

&lt;p&gt;The same article points out that folder hierarchies and naming conventions are real signals for an agent. To you, an &lt;code&gt;OrderService&lt;/code&gt; in a tests project and one in the API project are obviously different things. To the agent, the folder and the name are most of what it has.&lt;/p&gt;

&lt;p&gt;In .NET, you get a free map: the &lt;code&gt;.sln&lt;/code&gt; file and the &lt;code&gt;.csproj&lt;/code&gt; files, which say which project references which. Good names make the search work. Vague names make it guess.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it gets lost in a .NET solution
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Too many hits
&lt;/h3&gt;

&lt;p&gt;Search for &lt;code&gt;Order&lt;/code&gt; in a big solution and you get hundreds of lines: entities, DTOs, tests, migrations. Every file the agent opens costs context, so it opens the first few that look right. If the one that matters was result number forty, it never sees it.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Code wired by convention
&lt;/h3&gt;

&lt;p&gt;EF Core lets you move each entity's configuration into its own class and then &lt;a href="https://learn.microsoft.com/en-us/ef/core/modeling/" rel="noopener noreferrer"&gt;apply every configuration in an assembly&lt;/a&gt; with one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderConfiguration&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IEntityTypeConfiguration&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Configure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EntityTypeBuilder&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;HasKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// AppDbContext.OnModelCreating&lt;/span&gt;
&lt;span class="n"&gt;modelBuilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ApplyConfigurationsFromAssembly&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AppDbContext&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;Assembly&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing in that last line says &lt;code&gt;Order&lt;/code&gt;. An agent that reads your &lt;code&gt;DbContext&lt;/code&gt; to understand how &lt;code&gt;Order&lt;/code&gt; is mapped won't see it there, and can decide there's no configuration at all. The link only exists at runtime. The same goes for anything registered by scanning an assembly.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Code that isn't on disk
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://learn.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/" rel="noopener noreferrer"&gt;Source generators&lt;/a&gt; write C# at compile time and add it to the compilation. .NET itself uses them, for example for &lt;a href="https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/source-generation" rel="noopener noreferrer"&gt;System.Text.Json&lt;/a&gt; and &lt;a href="https://learn.microsoft.com/en-us/dotnet/standard/base-types/regular-expression-source-generators" rel="noopener noreferrer"&gt;regular expressions&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;By default, that generated code lives only in memory, inside the compiler. So the agent searches your folders for a method the compiler can see, and it isn't there. Setting &lt;code&gt;EmitCompilerGeneratedFiles&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt; in the project file &lt;a href="https://andrewlock.net/creating-a-source-generator-part-6-saving-source-generator-output-in-source-control/" rel="noopener noreferrer"&gt;writes the generated files out&lt;/a&gt; under the &lt;code&gt;obj&lt;/code&gt; folder, where they can be opened.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;PropertyGroup&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;EmitCompilerGeneratedFiles&amp;gt;&lt;/span&gt;true&lt;span class="nt"&gt;&amp;lt;/EmitCompilerGeneratedFiles&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/PropertyGroup&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The good news: the compiler is on your side
&lt;/h2&gt;

&lt;p&gt;The agent doesn't have to guess for long. It can run &lt;code&gt;dotnet build&lt;/code&gt;, and the compiler answers with the exact file, the exact line, and what's wrong:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OrderService.cs(42,17): error CS1061: 'Order' does not contain a definition for 'Total'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A strong type system turns a wrong guess into an error message, fast. That's the "check the result" step of the loop, and C# gives it a lot to work with.&lt;/p&gt;

&lt;p&gt;You can take it one step further. Claude Code has a &lt;a href="https://code.claude.com/docs/en/plugins/code-intelligence" rel="noopener noreferrer"&gt;C# code intelligence plugin&lt;/a&gt; (&lt;code&gt;csharp-lsp&lt;/code&gt;, backed by &lt;code&gt;csharp-ls&lt;/code&gt;) that connects it to a language server. With it, the agent finds definitions and references by symbol instead of by text, and sees type errors right after each edit. It isn't on by default; you install it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one file that fixes most of it
&lt;/h2&gt;

&lt;p&gt;Write the instruction file the way you'd brief a new teammate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A map&lt;/strong&gt; of the solution: which project does what.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build and test&lt;/strong&gt;, one command each.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The traps&lt;/strong&gt;: what's wired by convention, and where generated code comes from.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Solution map&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Orders.Api: HTTP endpoints, no business logic
&lt;span class="p"&gt;-&lt;/span&gt; Orders.Core: domain and services
&lt;span class="p"&gt;-&lt;/span&gt; Orders.Data: EF Core, configs applied from assembly

&lt;span class="gu"&gt;## Commands&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; build: dotnet build Orders.sln
&lt;span class="p"&gt;-&lt;/span&gt; test:  dotnet test Orders.sln

&lt;span class="gu"&gt;## Watch out&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; JSON contracts come from a source generator
&lt;span class="p"&gt;-&lt;/span&gt; Handlers are registered by assembly scan
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It sits in the context for the whole session, so keep it short.&lt;/p&gt;

&lt;p&gt;There's one more reason it matters. In a long session the context fills up, and Claude Code &lt;a href="https://code.claude.com/docs/en/how-claude-code-works" rel="noopener noreferrer"&gt;compacts the conversation into a summary&lt;/a&gt;. The docs warn that detailed instructions from early in the conversation may be lost in that summary, and their advice is to put persistent rules in &lt;code&gt;CLAUDE.md&lt;/code&gt;. Rules that must stick go in the file, not in the chat.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three rules
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Names that search well.&lt;/li&gt;
&lt;li&gt;A file that maps your solution.&lt;/li&gt;
&lt;li&gt;A build that tells the agent, fast, when it's wrong.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;An AI agent doesn't know your solution. It searches it, like a new hire on day one. Your job is to make that search easy.&lt;/p&gt;

&lt;p&gt;What's in your instruction file right now? Or don't you have one yet? Tell me in the comments.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I make Vlad's Stack: short explainers on how the tools you use every day actually work, for .NET and full-stack developers. &lt;a href="https://www.youtube.com/@VladsStack" rel="noopener noreferrer"&gt;The channel is here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>dotnet</category>
      <category>csharp</category>
      <category>productivity</category>
    </item>
    <item>
      <title>A hash map is just an array that computes its own index</title>
      <dc:creator>VLAD</dc:creator>
      <pubDate>Wed, 28 Oct 2026 11:42:18 +0000</pubDate>
      <link>https://dev.to/vladut02/a-hash-map-is-just-an-array-that-computes-its-own-index-141g</link>
      <guid>https://dev.to/vladut02/a-hash-map-is-just-an-array-that-computes-its-own-index-141g</guid>
      <description>&lt;p&gt;A map with a million keys answers a lookup just as fast as a map with ten. No loop. No searching. Your program jumps &lt;strong&gt;straight&lt;/strong&gt; to the answer.&lt;/p&gt;

&lt;p&gt;That's not magic, and it isn't a tree. It's one very old trick, plus two failure modes that most people never hear about.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Prefer to watch? Full walkthrough with the bucket animation:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/aNPoOn6535o" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Arrays are the fast part
&lt;/h2&gt;

&lt;p&gt;Start with the one thing computers are genuinely fast at: an &lt;strong&gt;array&lt;/strong&gt;. Ask for item number five and nothing is searched. The machine multiplies the index by the element size, adds it to the start address, and lands on the exact spot in memory. Item five and item five million cost the same.&lt;/p&gt;

&lt;p&gt;The catch is that arrays only take &lt;strong&gt;positions&lt;/strong&gt;. You don't think in positions — you want the value stored under &lt;code&gt;"email"&lt;/code&gt;, or under a user ID, or under a whole string of text.&lt;/p&gt;

&lt;p&gt;So the real question is: how do you turn a key like &lt;code&gt;"email"&lt;/code&gt; into a position?&lt;/p&gt;

&lt;h2&gt;
  
  
  The hash function
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;hash function&lt;/strong&gt; takes anything — a word, a sentence, an object — and gives back a number. Two properties matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Deterministic.&lt;/strong&gt; Same key in, same number out, every time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Well spread.&lt;/strong&gt; Change one letter and the result isn't close to the old one, it's unrecognizable. A hash that put similar keys near each other would pile them up in the same place.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That number is usually huge, much bigger than your array. So you do the simplest possible thing: take the remainder.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;hash("email")        = 3,742,909
3,742,909 % 8        = 5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you have a position. Each slot of that array is called a &lt;strong&gt;bucket&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Look at what just happened. You didn't search for the key — you &lt;strong&gt;computed&lt;/strong&gt; where it lives. Writing is hash, remainder, store. Reading is hash, remainder, load.&lt;/p&gt;

&lt;h2&gt;
  
  
  The whole idea, in code
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;buckets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;slot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nx"&gt;buckets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;   &lt;span class="c1"&gt;// "email" → 5&lt;/span&gt;

&lt;span class="nx"&gt;buckets&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;slot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;vlad@stack.dev&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;          &lt;span class="c1"&gt;// write&lt;/span&gt;
&lt;span class="nx"&gt;buckets&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;slot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;                             &lt;span class="c1"&gt;// read&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's a problem hiding in that one line. The hash space is huge, the array is small. So sooner or later, two different keys produce the same slot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Collisions are normal
&lt;/h2&gt;

&lt;p&gt;That's a &lt;strong&gt;collision&lt;/strong&gt;, and it isn't an edge case — with 8 slots it happens almost immediately.&lt;/p&gt;

&lt;p&gt;So a bucket doesn't hold a bare value. It holds &lt;strong&gt;pairs&lt;/strong&gt;: the key &lt;em&gt;and&lt;/em&gt; the value. On a lookup you jump to the bucket, then compare keys inside it. Usually there's one pair. Sometimes two. That's also why a map has to store your key at all — otherwise it could never tell two collided keys apart.&lt;/p&gt;

&lt;p&gt;There are two ways to handle the collision, and languages pick sides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Chaining&lt;/strong&gt; — keep a small list inside the bucket and walk it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open addressing&lt;/strong&gt; — no list; if the slot is taken, step to the next one until you find a free seat. CPython's &lt;code&gt;dict&lt;/code&gt; works this way (see the comment at the top of &lt;a href="https://github.com/python/cpython/blob/main/Objects/dictobject.c" rel="noopener noreferrer"&gt;&lt;code&gt;Objects/dictobject.c&lt;/code&gt;&lt;/a&gt; — an implementation detail, not a language guarantee).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Either way the shape is the same: land close to the answer, then take a step or two.&lt;/p&gt;

&lt;h2&gt;
  
  
  So what does "constant time" actually mean?
&lt;/h2&gt;

&lt;p&gt;One hash. One jump. A tiny compare. It isn't zero work — it just doesn't get &lt;strong&gt;worse&lt;/strong&gt; as the map grows. The &lt;a href="https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/HashMap.html" rel="noopener noreferrer"&gt;Java docs&lt;/a&gt; put a condition on it: constant-time performance for &lt;code&gt;get&lt;/code&gt; and &lt;code&gt;put&lt;/code&gt;, &lt;em&gt;assuming the hash function disperses the elements properly among the buckets&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Hold on to that "assuming". It's where both failure modes live.&lt;/p&gt;

&lt;h2&gt;
  
  
  Load factor: the resize you don't see
&lt;/h2&gt;

&lt;p&gt;Keep adding keys to the same small array and the buckets fill up. Those little lists get long, and comparing one by one stops being cheap. So a map watches how full it is — that ratio is the &lt;strong&gt;load factor&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In Java, a &lt;code&gt;HashMap&lt;/code&gt; starts with &lt;strong&gt;16&lt;/strong&gt; buckets and a load factor of &lt;strong&gt;0.75&lt;/strong&gt;. Cross that line and the table is rebuilt with roughly twice as many buckets, and &lt;strong&gt;every existing key is re-slotted&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Which means one unlucky insert does the work of the entire map. The average stays fast. That one call doesn't. If you know you're about to load a million entries, size the map up front.&lt;/p&gt;

&lt;h2&gt;
  
  
  When O(1) is a lie
&lt;/h2&gt;

&lt;p&gt;Now the interesting failure. Imagine every key you insert lands in the &lt;strong&gt;same&lt;/strong&gt; bucket. One long list; every lookup walks it. Your constant-time map is now a linear scan.&lt;/p&gt;

&lt;p&gt;That can happen by accident with a bad hash — and it can be done to you &lt;strong&gt;on purpose&lt;/strong&gt;. An attacker who knows your hash function can send keys engineered to collide, turning cheap requests into expensive ones. Python's answer is to salt the hash of &lt;code&gt;str&lt;/code&gt; and &lt;code&gt;bytes&lt;/code&gt; with a random value at startup, explicitly as &lt;a href="https://docs.python.org/3/using/cmdline.html" rel="noopener noreferrer"&gt;protection against that denial of service&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The key that disappears
&lt;/h2&gt;

&lt;p&gt;This one is your own fault, and it's a classic.&lt;/p&gt;

&lt;p&gt;You store a value under a key. Then you &lt;strong&gt;mutate&lt;/strong&gt; that key object. Its hash changes with it. The map now computes a different slot, looks there, and finds nothing — while your entry sits in the old bucket, unreachable.&lt;/p&gt;

&lt;p&gt;Use keys that can't change: a string, a number, a frozen/immutable object.&lt;/p&gt;

&lt;h2&gt;
  
  
  And no, it has no order
&lt;/h2&gt;

&lt;p&gt;A hash map scatters keys by their hash, not by when you added them, so iteration order is whatever the buckets happen to hold.&lt;/p&gt;

&lt;p&gt;Python is the exception you've probably been relying on: since 3.7, a &lt;code&gt;dict&lt;/code&gt; &lt;a href="https://docs.python.org/3/library/stdtypes.html#dict" rel="noopener noreferrer"&gt;preserves insertion order&lt;/a&gt; as a language guarantee. Java's &lt;code&gt;HashMap&lt;/code&gt; promises nothing of the sort. If you need order, use the type that promises it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three rules
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Keys that &lt;strong&gt;don't change&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;A hash that &lt;strong&gt;spreads&lt;/strong&gt; keys out.&lt;/li&gt;
&lt;li&gt;Enough room that the map isn't constantly &lt;strong&gt;resizing&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Get those right and it behaves like magic. Get them wrong and it quietly becomes a list.&lt;/p&gt;

&lt;p&gt;A hash map isn't magic and it isn't a tree — it's an array that already knows where you put things.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's the worst hash-map bug you've hit — the key that vanished, or the resize that froze your app?&lt;/strong&gt; Tell me in the comments, I read them.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I make &lt;a href="https://www.youtube.com/channel/UCUO8Uo5LsEy1b9eRkrH6JNg" rel="noopener noreferrer"&gt;Vlad's Stack&lt;/a&gt; — how the tools you use every day actually work, for people who write code. The full video walkthrough is above.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>computerscience</category>
      <category>python</category>
      <category>java</category>
      <category>programming</category>
    </item>
    <item>
      <title>Your database says "saved" before it touches the table. Here's why that's safe.</title>
      <dc:creator>VLAD</dc:creator>
      <pubDate>Tue, 27 Oct 2026 21:31:22 +0000</pubDate>
      <link>https://dev.to/vladut02/your-database-says-saved-before-it-touches-the-table-heres-why-thats-safe-3al4</link>
      <guid>https://dev.to/vladut02/your-database-says-saved-before-it-touches-the-table-heres-why-thats-safe-3al4</guid>
      <description>&lt;p&gt;When your database says your transaction is &lt;strong&gt;saved&lt;/strong&gt;, your row usually isn't in the table file yet. Pull the power cord in the middle of a write, and you still lose zero committed rows.&lt;/p&gt;

&lt;p&gt;That's not luck. It's one file doing one job: the &lt;strong&gt;write-ahead log&lt;/strong&gt; (WAL).&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Prefer to watch? Full walkthrough with the crash-and-replay animation:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/N6kNWC5Tbj4" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The obvious way to save data — and why it fails
&lt;/h2&gt;

&lt;p&gt;The obvious design: you run an &lt;code&gt;UPDATE&lt;/code&gt;, the database finds the right spot in the table file, and overwrites the value there. Simple. It has two problems.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It's slow.&lt;/strong&gt; One small change can touch the table &lt;em&gt;and&lt;/em&gt; two or three indexes — pages scattered all over the disk. Every one of them would have to be forced to disk before the database could say "saved."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's unsafe.&lt;/strong&gt; If the power dies halfway through, the table might have the new value while the index still has the old one. Now the database disagrees with itself, and nothing tells you.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Write it down first
&lt;/h2&gt;

&lt;p&gt;So databases do something that sounds backwards: &lt;strong&gt;before they change the table, they write down what they're about to change.&lt;/strong&gt; One short record, added to the end of a log file — "account 1, balance minus 100."&lt;/p&gt;

&lt;p&gt;That's the &lt;em&gt;write-ahead&lt;/em&gt; part: the log is always written ahead of the data. The &lt;a href="https://www.postgresql.org/docs/current/wal-intro.html" rel="noopener noreferrer"&gt;PostgreSQL docs&lt;/a&gt; put the rule plainly: changes to data files may only be written after the log records describing them have reached permanent storage.&lt;/p&gt;

&lt;p&gt;Think of a waiter with a notepad. They don't run to the kitchen after every word — they write the order down first. If they trip and drop the plates, the order is still on the pad, and the kitchen just cooks it again.&lt;/p&gt;

&lt;h2&gt;
  
  
  One transfer, step by step
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;BEGIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;accounts&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;balance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;balance&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;accounts&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;balance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;balance&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;COMMIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Each &lt;code&gt;UPDATE&lt;/code&gt; changes a page &lt;strong&gt;in memory&lt;/strong&gt; and adds a record to the log.&lt;/li&gt;
&lt;li&gt;On &lt;code&gt;COMMIT&lt;/code&gt;, the database forces &lt;strong&gt;only the log&lt;/strong&gt; to disk.&lt;/li&gt;
&lt;li&gt;As soon as that's safe, you get your "OK."&lt;/li&gt;
&lt;li&gt;The changed table pages are still in memory. They'll reach the disk later, in the background.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Why it's also fast
&lt;/h2&gt;

&lt;p&gt;The log only grows at the end, so writing it is one straight, sequential write. The table pages are scattered across the disk. So you pay for one cheap write at commit time and push the expensive, scattered writes to later. The &lt;a href="https://www.postgresql.org/docs/current/wal-intro.html" rel="noopener noreferrer"&gt;Postgres docs&lt;/a&gt; make the same point: because the WAL is written sequentially, syncing it costs much less than flushing the data pages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pull the plug: crash recovery
&lt;/h2&gt;

&lt;p&gt;Right after a commit, your saved data lives in two places: the log (on disk) and the updated table pages (in memory). Now the power dies.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Everything in memory is gone.&lt;/li&gt;
&lt;li&gt;The table file on disk is old — it still says the money never moved.&lt;/li&gt;
&lt;li&gt;The database starts up again, opens the log, and &lt;strong&gt;replays&lt;/strong&gt; it, record by record: minus 100, plus 100, commit.&lt;/li&gt;
&lt;li&gt;The table catches up. Zero committed rows lost.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Postgres calls this roll-forward recovery, or REDO. Crash recovery is just the database reading its own notes.&lt;/p&gt;

&lt;h3&gt;
  
  
  What about a transaction that never committed?
&lt;/h3&gt;

&lt;p&gt;Say the power dies &lt;em&gt;between&lt;/em&gt; the two updates. There's no commit record in the log for that transaction, so after the restart its changes count as if they never happened. You never get half a transfer — all of it, or none of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checkpoints: why the log doesn't grow forever
&lt;/h2&gt;

&lt;p&gt;If every change goes into the log, doesn't the log grow forever? No. Every so often the database runs a &lt;strong&gt;checkpoint&lt;/strong&gt;: it writes all the changed pages from memory into the table files. Everything before that point is now safely in the table, so the old log can be recycled.&lt;/p&gt;

&lt;p&gt;In Postgres, a checkpoint starts every 5 minutes by default, or sooner if the log is about to pass 1 GB (&lt;a href="https://www.postgresql.org/docs/current/wal-configuration.html" rel="noopener noreferrer"&gt;&lt;code&gt;checkpoint_timeout&lt;/code&gt; and &lt;code&gt;max_wal_size&lt;/code&gt;&lt;/a&gt;). It's also why recovery is fast: it only replays from the last checkpoint, not from the beginning of time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The torn page
&lt;/h2&gt;

&lt;p&gt;One nasty detail almost breaks all of this. A Postgres page is 8 kB. If the power dies while that page is being written, the disk can end up with a &lt;strong&gt;torn page&lt;/strong&gt; — half new data, half old. A small log record describing one row change can't repair a page that's already broken.&lt;/p&gt;

&lt;p&gt;So Postgres does something clever: the &lt;strong&gt;first&lt;/strong&gt; time a page changes after a checkpoint, it writes a full copy of that page into the log (&lt;a href="https://www.postgresql.org/docs/current/runtime-config-wal.html#GUC-FULL-PAGE-WRITES" rel="noopener noreferrer"&gt;&lt;code&gt;full_page_writes&lt;/code&gt;&lt;/a&gt;, on by default). If recovery finds a torn page, it starts from that clean copy and replays the rest on top.&lt;/p&gt;

&lt;h2&gt;
  
  
  One log, many jobs
&lt;/h2&gt;

&lt;p&gt;The log isn't just for crashes.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Replication.&lt;/strong&gt; A replica is just another server replaying the same log. Postgres &lt;a href="https://www.postgresql.org/docs/current/warm-standby.html" rel="noopener noreferrer"&gt;streaming replication&lt;/a&gt; ships these WAL records over the network. A replica that's "behind" is simply at an older position in the log.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Positions.&lt;/strong&gt; Every record has a position called an &lt;strong&gt;LSN&lt;/strong&gt; (log sequence number). You can ask Postgres for the current one:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;pg_current_wal_lsn&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Other databases.&lt;/strong&gt; It's not a Postgres quirk. MySQL's InnoDB has a &lt;a href="https://dev.mysql.com/doc/refman/8.4/en/innodb-redo-log.html" rel="noopener noreferrer"&gt;redo log&lt;/a&gt;, and SQLite has a &lt;a href="https://www.sqlite.org/wal.html" rel="noopener noreferrer"&gt;WAL mode&lt;/a&gt;. Same idea everywhere: write it down first.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What this means for your code: every commit waits for the disk
&lt;/h2&gt;

&lt;p&gt;Even if you never build a database, this changes how you write to one. Every commit waits for the log to reach the disk — once per commit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- autocommit: one commit per row&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'a'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;-- × 10,000&lt;/span&gt;

&lt;span class="c1"&gt;-- one transaction: one commit&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'a'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;-- × 10,000&lt;/span&gt;
&lt;span class="k"&gt;COMMIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                                      &lt;span class="c1"&gt;-- one wait&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Insert 10,000 rows with one commit each and you wait for the disk 10,000 times. Wrap them in one transaction and you wait once. Same rows, same table. If your bulk import is slow, check your commits before you blame the inserts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setting that forgets
&lt;/h2&gt;

&lt;p&gt;Because that wait is the slow part of every commit, Postgres lets you skip it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;synchronous_commit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your commit now returns &lt;strong&gt;before&lt;/strong&gt; the log is on disk. It's faster — but if the server crashes, the most recent "saved" transactions can disappear. According to the &lt;a href="https://www.postgresql.org/docs/current/wal-async-commit.html" rel="noopener noreferrer"&gt;docs&lt;/a&gt;, the risk window is at most three times &lt;code&gt;wal_writer_delay&lt;/code&gt; (200 ms by default, so roughly the last 0.6 seconds).&lt;/p&gt;

&lt;p&gt;Here's the difference that matters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;synchronous_commit = off&lt;/code&gt;&lt;/strong&gt; risks &lt;strong&gt;losing&lt;/strong&gt; the last few transactions, but what's left stays consistent. The docs call it data loss, not data corruption.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;fsync = off&lt;/code&gt;&lt;/strong&gt; means a system crash can &lt;strong&gt;corrupt&lt;/strong&gt; the whole database.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One is a trade-off you can make on purpose, for data you can afford to lose. The other is a trap.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;"Saved" doesn't mean it's in the table. It means it's in the log — and the log is enough to rebuild everything else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's the riskiest durability setting you've ever turned off, just to go faster?&lt;/strong&gt; Tell me in the comments — I read them.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I make &lt;a href="https://www.youtube.com/channel/UCUO8Uo5LsEy1b9eRkrH6JNg" rel="noopener noreferrer"&gt;Vlad's Stack&lt;/a&gt; — how the tools you use every day actually work, for people who write code. The full video walkthrough is above; it's part of a short series on how databases work, next to how an index finds one row in a million.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>database</category>
      <category>postgres</category>
      <category>sql</category>
      <category>backend</category>
    </item>
    <item>
      <title>Where null Came From, and Why C# Needed 17 Years to Push Back</title>
      <dc:creator>VLAD</dc:creator>
      <pubDate>Tue, 27 Oct 2026 12:16:37 +0000</pubDate>
      <link>https://dev.to/vladut02/where-null-came-from-and-why-c-needed-17-years-to-push-back-4kjg</link>
      <guid>https://dev.to/vladut02/where-null-came-from-and-why-c-needed-17-years-to-push-back-4kjg</guid>
      <description>&lt;p&gt;&lt;code&gt;Object reference not set to an instance of an object.&lt;/code&gt; If you write C#, you've read that message more times than you can count. It's a &lt;code&gt;NullReferenceException&lt;/code&gt;, and it goes back to a decision one man made in 1965. In 2009 he gave that decision a name: his billion-dollar mistake.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/I_THb0xbOXQ" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;This is the story of where null came from, why it ended up in almost every language you know, and how C# pushed back without breaking existing code. The strange part comes first: the man who added null was trying to make programs safer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The man who checked everything
&lt;/h2&gt;

&lt;p&gt;Tony Hoare is best known for inventing Quicksort. In his &lt;a href="https://worrydream.com/refs/Hoare_1981_-_The_Emperors_Old_Clothes.pdf" rel="noopener noreferrer"&gt;1980 Turing Award lecture&lt;/a&gt; he tells how he started. In August 1960 he became a programmer at Elliott Brothers, a small British computer manufacturer, and was soon asked to design a subset of ALGOL 60 for the company's next machine.&lt;/p&gt;

&lt;p&gt;He says the first principle of that design was security. Every correct program should produce either a result or an error message you could understand in terms of your own source code. One consequence: every array subscript was checked at run time against the bounds of the array, every time.&lt;/p&gt;

&lt;p&gt;That cost speed. Years later, the company asked its customers whether they wanted an option to switch the checks off for production runs. Hoare says they unanimously asked him not to, because they already knew how often those errors show up in real runs.&lt;/p&gt;

&lt;p&gt;So this was someone who put safety before speed.&lt;/p&gt;

&lt;h2&gt;
  
  
  1965: one value that was easy to add
&lt;/h2&gt;

&lt;p&gt;In 1965, Hoare was working with Niklaus Wirth on a successor to ALGOL 60. The proposal was published in 1966 as &lt;a href="https://ora.ox.ac.uk/objects/uuid:688a382a-8e0f-4729-ae55-48a01155c71f" rel="noopener noreferrer"&gt;"A Contribution to the Development of ALGOL"&lt;/a&gt;, and the language became known as ALGOL W.&lt;/p&gt;

&lt;p&gt;One of its ideas was the record: a bundle of named fields, like a customer with a name and an address. With records came references, variables that point to a record instead of holding it.&lt;/p&gt;

&lt;p&gt;Hoare wanted references to be completely safe. Each reference had a type, so a reference to a customer could only point to a customer, and the compiler would check that before the program ran. In the &lt;a href="https://www.infoq.com/presentations/Null-References-The-Billion-Dollar-Mistake-Tony-Hoare/" rel="noopener noreferrer"&gt;abstract of his 2009 talk&lt;/a&gt; he describes it as the first comprehensive type system for references in an object-oriented language.&lt;/p&gt;

&lt;p&gt;But programs also need a way to say "nothing here". The last item in a list has no next item. So he added one special value that any reference could hold: null. By his own account, he couldn't resist the temptation, because it was so easy to implement.&lt;/p&gt;

&lt;h2&gt;
  
  
  What null breaks
&lt;/h2&gt;

&lt;p&gt;The type says &lt;code&gt;Customer&lt;/code&gt;. The compiler agrees. But the variable may hold null instead.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;Customer&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;FindCustomer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// may return null&lt;/span&gt;
&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;       &lt;span class="c1"&gt;// NullReferenceException at run time&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The type makes a promise the program can't keep. In a safe language, the program stops with an error. In an unsafe one, it reads whatever happens to be in memory. Either way you find out late: not when you write the code, but when a real user hits that one path.&lt;/p&gt;

&lt;h2&gt;
  
  
  It spread everywhere
&lt;/h2&gt;

&lt;p&gt;ALGOL W never became a big language, but the idea traveled. Wirth went on to design Pascal, which had the same thing under the name &lt;code&gt;nil&lt;/code&gt;. C had its null pointer, and so did C++. Java got the famous &lt;code&gt;NullPointerException&lt;/code&gt;. And in 2002, C# 1.0 shipped with the same design: null is the default value of every reference type. Declare a field, forget to set it, and it's null.&lt;/p&gt;

&lt;p&gt;Two things are worth getting right here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Myth: Hoare invented the empty value.&lt;/strong&gt; Lisp already had &lt;code&gt;NIL&lt;/code&gt; around 1960. What Hoare says he invented is the null &lt;em&gt;reference&lt;/em&gt;: null inside a type system that was built to make references safe.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Myth: null has been shown to cost a billion dollars.&lt;/strong&gt; Nobody measured it. It was his own rough estimate for forty years of errors, vulnerabilities and crashes, and he qualified it with "probably".&lt;/p&gt;

&lt;h2&gt;
  
  
  C# looks back
&lt;/h2&gt;

&lt;p&gt;In 2008, Anders Hejlsberg, the lead designer of C#, was &lt;a href="https://www2.computerworld.com.au/article/261958/a-z_programming_languages_c_/?pp=3" rel="noopener noreferrer"&gt;asked by Computerworld&lt;/a&gt; what he would do differently. He talked about null. His guess was that half of the bugs people run into in C#, and in Java too, are probably null reference exceptions. He wished the type system could say that a parameter may never be null, and have the compiler check it.&lt;/p&gt;

&lt;p&gt;The interesting part is that C# had already done this for half of its types. &lt;a href="https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-version-history" rel="noopener noreferrer"&gt;C# 2.0&lt;/a&gt;, in 2005, added nullable value types:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;    &lt;span class="c1"&gt;// compile error&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// fine: the ? is part of the type&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the honest design. Null is allowed, but only where the type says so. It just didn't exist for classes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Other languages went first
&lt;/h2&gt;

&lt;p&gt;Newer languages started from that idea. In Kotlin, a &lt;code&gt;String&lt;/code&gt; can never hold null and a &lt;code&gt;String?&lt;/code&gt; can. Assigning null to the plain one doesn't compile. Kotlin's &lt;a href="https://kotlinlang.org/docs/null-safety.html" rel="noopener noreferrer"&gt;documentation on null safety&lt;/a&gt; names the billion-dollar mistake as the thing it's designed to reduce.&lt;/p&gt;

&lt;p&gt;TypeScript got &lt;a href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-0.html" rel="noopener noreferrer"&gt;&lt;code&gt;--strictNullChecks&lt;/code&gt;&lt;/a&gt; in version 2.0, &lt;a href="https://devblogs.microsoft.com/typescript/announcing-typescript-2-0/" rel="noopener noreferrer"&gt;released in September 2016&lt;/a&gt;. Before that, null and undefined were valid values of every type. The lead designer of TypeScript is the same Anders Hejlsberg.&lt;/p&gt;

&lt;p&gt;So the idea was proven. The hard part was bringing it to a language with 17 years of existing code.&lt;/p&gt;

&lt;h2&gt;
  
  
  C# 8: nullable reference types
&lt;/h2&gt;

&lt;p&gt;In November 2017, Mads Torgersen, the C# lead designer, published the plan in &lt;a href="https://devblogs.microsoft.com/dotnet/nullable-reference-types-in-csharp/" rel="noopener noreferrer"&gt;"Introducing Nullable Reference Types in C#"&lt;/a&gt;, quoting Hoare's billion-dollar mistake. The feature shipped in C# 8.0 in September 2019.&lt;/p&gt;

&lt;p&gt;With the feature on, the meaning of your existing types changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;          &lt;span class="c1"&gt;// warning: plain string means "not null"&lt;/span&gt;
&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;middleName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// fine: this one may be null&lt;/span&gt;

&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;middleName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// warning: possible null dereference&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three design choices made this possible without breaking the world:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Warnings, not errors.&lt;/strong&gt; Old code keeps compiling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Opt-in.&lt;/strong&gt; At first you had to turn it on yourself. Today, &lt;a href="https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references" rel="noopener noreferrer"&gt;recent .NET project templates&lt;/a&gt; set &lt;code&gt;&amp;lt;Nullable&amp;gt;enable&amp;lt;/Nullable&amp;gt;&lt;/code&gt; for you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No runtime change.&lt;/strong&gt; &lt;code&gt;string&lt;/code&gt; and &lt;code&gt;string?&lt;/code&gt; are the same type at run time. Only the compiler sees the difference.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where null still hides
&lt;/h2&gt;

&lt;p&gt;Null isn't gone from C#. The same documentation lists the places where a non-nullable reference can still hold null with no warning:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;names&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;   &lt;span class="c1"&gt;// every element is null, no warning&lt;/span&gt;

&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;Student&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;FirstName&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;Student&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;              &lt;span class="c1"&gt;// s.FirstName is null, no warning&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And there's the null-forgiving operator, &lt;code&gt;!&lt;/code&gt;. It tells the compiler "trust me, this isn't null". Every one you write is a place where the compiler stops protecting you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it took 54 years
&lt;/h2&gt;

&lt;p&gt;In 1965, one special value was the easy way to say "nothing here". And a language can't take back its defaults once millions of programs depend on them.&lt;/p&gt;

&lt;p&gt;The fix was never to delete null. It was to make the type tell the truth about it. From ALGOL W to C# 8, that took 54 years.&lt;/p&gt;

&lt;p&gt;Tony Hoare &lt;a href="https://www.merton.ox.ac.uk/news/professor-sir-charles-anthony-richard-tony-hoare-1934-2026" rel="noopener noreferrer"&gt;died on March 5th, 2026&lt;/a&gt;, at 92. He gave us Quicksort, and he had the honesty to name his own mistake.&lt;/p&gt;

&lt;p&gt;Is nullable turned on in your oldest C# project? And how many warnings did it give you? Tell me in the comments.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I make Vlad's Stack, stories and explainers about how the tools you use every day actually work, for people who write code: &lt;a href="https://www.youtube.com/channel/UCUO8Uo5LsEy1b9eRkrH6JNg" rel="noopener noreferrer"&gt;https://www.youtube.com/channel/UCUO8Uo5LsEy1b9eRkrH6JNg&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>dotnet</category>
      <category>programming</category>
      <category>history</category>
    </item>
    <item>
      <title>Ariane 5, Flight 501: The Exception That Destroyed a Rocket</title>
      <dc:creator>VLAD</dc:creator>
      <pubDate>Sat, 10 Oct 2026 21:57:58 +0000</pubDate>
      <link>https://dev.to/vladut02/ariane-5-flight-501-the-exception-that-destroyed-a-rocket-48i6</link>
      <guid>https://dev.to/vladut02/ariane-5-flight-501-the-exception-that-destroyed-a-rocket-48i6</guid>
      <description>&lt;p&gt;On June 4th, 1996, Europe launched Ariane 5 for the first time. About forty seconds after the flight sequence began, the rocket veered off course, broke up and destroyed itself. The engines were fine. The weather was fine. What failed was a data conversion in code that had no reason to be running.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/KRkJZsA4o9o" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;This story gets retold a lot, usually as "an integer overflow blew up a rocket". The &lt;a href="https://www-users.cse.umn.edu/~arnold/disasters/ariane5rep.html" rel="noopener noreferrer"&gt;inquiry board's report&lt;/a&gt; (chaired by Jacques-Louis Lions, published July 19th, 1996) tells a more useful story. The check actually worked. What nobody had decided was what should happen when it fired.&lt;/p&gt;

&lt;h2&gt;
  
  
  The box that knows which way is up
&lt;/h2&gt;

&lt;p&gt;To fly straight, a rocket has to know which way it's pointing and how it's moving. On Ariane, that's the job of the inertial reference system, called SRI in the report after its French name. It has laser gyros, accelerometers and its own computer. It sends attitude and velocity data to the main on-board computer, and that computer steers by swiveling the engine nozzles.&lt;/p&gt;

&lt;p&gt;Ariane 5 carried two SRIs with identical hardware and identical software. One was active, the other was a hot standby. If the active one failed, the main computer would switch to the backup.&lt;/p&gt;

&lt;p&gt;The report also notes that the SRI design, and especially its software, was practically the same as on Ariane 4. It had flown for years. Nobody saw a reason to change software that worked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code that had no job
&lt;/h2&gt;

&lt;p&gt;Before launch, the SRI aligns itself: it finds the direction of gravity and finds north from the rotation of the Earth. That only makes sense while the rocket stands still on the pad.&lt;/p&gt;

&lt;p&gt;On Ariane 4, the alignment function kept running for a while after the SRI went into flight mode. That was on purpose. If a countdown was held in its last seconds, a full realignment took 45 minutes or more, and the launch window could be lost. Keeping alignment alive a little longer let the team restart the countdown quickly. According to the report, the feature was used once, in 1989.&lt;/p&gt;

&lt;p&gt;Ariane 5 had a different preparation sequence and didn't need this. The requirement was kept anyway, to stay in common with Ariane 4. So on Ariane 5 the alignment code ran for about 40 seconds after lift-off, computing values that no longer meant anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  The number that couldn't get too big
&lt;/h2&gt;

&lt;p&gt;Inside the alignment code there's a value the report calls BH, for horizontal bias. It's related to the horizontal velocity the platform senses. It was held as a 64-bit floating point number, and at one point the Ada code converted it to a 16-bit signed integer. A 16-bit signed integer tops out at 32,767.&lt;/p&gt;

&lt;p&gt;The developers knew conversions like this were a risk. The report says they analysed every operation that could raise an exception and found seven variables at risk. But there was a constraint: a maximum workload target of 80% for the SRI computer. So they protected four of the seven conversions. The other three, including BH, were left unprotected, because the reasoning was that those values were either physically limited or had a large safety margin.&lt;/p&gt;

&lt;p&gt;For Ariane 4 that reasoning was correct. But:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The justification wasn't in the source code. The report says the assumption, although agreed, was in practice hidden from any external review.&lt;/li&gt;
&lt;li&gt;The Ariane 5 trajectory data was not part of the SRI's requirements and specification.&lt;/li&gt;
&lt;li&gt;Ariane 5 builds up horizontal velocity about five times faster than Ariane 4 in the early part of the flight.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What happened in flight
&lt;/h2&gt;

&lt;p&gt;The rocket flew normally until about 36 seconds after the start of the main engine ignition sequence. Then BH exceeded what 16 bits can hold. The conversion raised an Operand Error, and nothing in the code handled it.&lt;/p&gt;

&lt;p&gt;For an unhandled exception, the SRI specification said: indicate the failure on the databus, store the failure context in memory, and shut the processor down. The report explains where that rule came from. The programme was designed around random hardware failures. If a part breaks at random, you switch the unit off and the backup takes over.&lt;/p&gt;

&lt;p&gt;This wasn't random. The backup ran the same software with the same inputs. It had already stopped, one 72-millisecond data cycle earlier, for the same reason. As the report puts it, two critical units that were still healthy got switched off.&lt;/p&gt;

&lt;p&gt;Then it got worse. The failed active SRI put a diagnostic bit pattern on the bus, and the main computer interpreted it as flight data. Based on that, it commanded full nozzle deflections on both solid boosters and then on the main engine. The rocket turned at an angle of attack of more than 20 degrees, the boosters started to separate from the core stage, and that correctly triggered the self-destruct system at about 4 km altitude.&lt;/p&gt;

&lt;p&gt;The debris fell on about 12 square kilometres of swamp and savanna near the launch site. Both SRIs were recovered, and the failure context was read out of their memory. The four Cluster science satellites on board were lost. The loss is widely reported at about $370 million; that figure is not from the inquiry report.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same line in C
&lt;/h2&gt;

&lt;p&gt;Here's the part that matters for those of us who will never write flight software. In C#, the same conversion is one cast:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;bh&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;40000.0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;short&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;short&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;bh&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;            &lt;span class="c1"&gt;// compiles without a warning&lt;/span&gt;
&lt;span class="kt"&gt;short&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;checked&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="kt"&gt;short&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;bh&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// throws OverflowException&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By default, C# evaluates non-constant expressions in an &lt;a href="https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/statements/checked-and-unchecked" rel="noopener noreferrer"&gt;unchecked context&lt;/a&gt;. For a &lt;code&gt;double&lt;/code&gt; to integer conversion that's out of range, the &lt;a href="https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/numeric-conversions" rel="noopener noreferrer"&gt;documentation&lt;/a&gt; says the result is an unspecified value of the destination type. No exception, no warning. Your program keeps going with a number that means nothing. In a &lt;code&gt;checked&lt;/code&gt; context, the same cast throws &lt;code&gt;OverflowException&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So which one is safer? It's tempting to say "checked, obviously". But Ariane's code was the checked one. The conversion raised the error, and the error is what shut both computers down. The value itself wasn't needed any more. The board points out that the SRIs could have kept providing their best estimates of the attitude data instead.&lt;/p&gt;

&lt;p&gt;That doesn't make silent garbage the right answer. It means a check is only half a decision. The other half is what the system does when the check fails: crash, retry, fall back to a safe value, or keep going in a degraded mode. On flight 501, that half had been decided years earlier, for a different kind of failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to take from this
&lt;/h2&gt;

&lt;p&gt;The board made 14 recommendations. Three ideas from the report apply to ordinary code:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Code that isn't needed shouldn't be running.&lt;/strong&gt; Recommendation R1 is exactly this: switch off the alignment function immediately after lift-off, and more generally, run no software function during flight unless it's needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reused code brings its old assumptions with it.&lt;/strong&gt; "This value can't get that big" was true for Ariane 4. It wasn't visible in the code, and nobody tested it against Ariane 5's trajectory. The report notes that a ground test with simulated Ariane 5 flight data would have exposed the failure. That test was never run on the SRI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A backup that runs the same code is no backup against a bug.&lt;/strong&gt; Redundancy protects you from random hardware faults. Two copies of the same software fail the same way at the same moment.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There's also one position in the report that still sounds bold. The project had treated software as correct until shown to be at fault. The board argued for the opposite view: assume software is faulty until accepted best-practice methods can demonstrate that it's correct.&lt;/p&gt;

&lt;p&gt;Ariane 5 recovered. According to &lt;a href="https://www.esa.int/Enabling_Support/Space_Transportation/Launch_vehicles/Ariane_5" rel="noopener noreferrer"&gt;ESA&lt;/a&gt;, it flew 117 times between 1996 and 2023, and it launched the James Webb Space Telescope. The Cluster satellites were rebuilt and &lt;a href="https://www.esa.int/Science_Exploration/Space_Science/Cluster_overview2" rel="noopener noreferrer"&gt;launched in 2000&lt;/a&gt; on Soyuz rockets.&lt;/p&gt;

&lt;p&gt;What's the oldest assumption you've found hiding in code you reused? Tell me in the comments.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I make Vlad's Stack, stories and explainers about how the tools you use every day actually work, for people who write code: &lt;a href="https://www.youtube.com/channel/UCUO8Uo5LsEy1b9eRkrH6JNg" rel="noopener noreferrer"&gt;https://www.youtube.com/channel/UCUO8Uo5LsEy1b9eRkrH6JNg&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>dotnet</category>
      <category>softwareengineering</category>
      <category>history</category>
    </item>
    <item>
      <title>The Court Order Behind C#: How Two Keywords Started a New Language</title>
      <dc:creator>VLAD</dc:creator>
      <pubDate>Tue, 06 Oct 2026 15:09:50 +0000</pubDate>
      <link>https://dev.to/vladut02/the-court-order-behind-c-how-two-keywords-started-a-new-language-29e8</link>
      <guid>https://dev.to/vladut02/the-court-order-behind-c-how-two-keywords-started-a-new-language-29e8</guid>
      <description>&lt;p&gt;On November 17th, 1998, a federal judge in San Jose gave Microsoft ninety days to change its Java tools. The court papers in that case talk about two words that C# developers still type every day: &lt;code&gt;delegate&lt;/code&gt; and &lt;code&gt;multicast&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/8Z8mTPPaI1w" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;This is the story of how a lawsuit over Java ended up producing C#. It's told from the court records, the documents the companies published at the time, and what C#'s lead designer, Anders Hejlsberg, has said in public.&lt;/p&gt;

&lt;h2&gt;
  
  
  The man from Borland
&lt;/h2&gt;

&lt;p&gt;In the early eighties, Anders Hejlsberg wrote a small, very fast Pascal compiler. Borland sold it as Turbo Pascal. Later, at Borland, he led the team that designed Delphi. In Delphi you drew a window, dropped a button on it, and wrote the code that should run on click.&lt;/p&gt;

&lt;p&gt;Keep that button in mind. The whole story turns on one question: how do you connect a button to a piece of your code?&lt;/p&gt;

&lt;p&gt;In 1996, Hejlsberg left Borland and joined Microsoft (&lt;a href="https://www.artima.com/articles/the-c-design-process" rel="noopener noreferrer"&gt;Artima interview bio&lt;/a&gt;). He wasn't the only one to leave. In May 1997, Borland sued Microsoft, saying it had hired away at least 34 of its people in 30 months (&lt;a href="https://www.eetimes.com/borland-sues-microsoft-for-unfair-competition/" rel="noopener noreferrer"&gt;EE Times&lt;/a&gt;). The two companies &lt;a href="https://news.microsoft.com/source/1997/09/19/borland-and-microsoft-announce-settlement/" rel="noopener noreferrer"&gt;settled in September 1997&lt;/a&gt;, and the terms were never made public.&lt;/p&gt;

&lt;h2&gt;
  
  
  Microsoft's Java
&lt;/h2&gt;

&lt;p&gt;In the mid-nineties, Java promised "write once, run anywhere". In March 1996, Microsoft signed a license with Sun Microsystems, the company that owned Java, and built its own Java for Windows: its own virtual machine and a developer tool called &lt;strong&gt;Visual J++&lt;/strong&gt;. Hejlsberg became an architect on Visual J++ and on a Windows library for it, the Windows Foundation Classes (WFC).&lt;/p&gt;

&lt;p&gt;Microsoft's Java could call Windows directly. Code that did that only ran on Windows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sun sues
&lt;/h2&gt;

&lt;p&gt;On October 7th, 1997, Sun sued Microsoft in federal court in San Jose. The charges included breach of contract, trademark infringement, false advertising and unfair competition (&lt;a href="https://www.mactech.com/1997/10/07/md1-sun-sues-microsoft-over-java/" rel="noopener noreferrer"&gt;Sun's announcement, via MacTech&lt;/a&gt;). Sun said Internet Explorer 4.0 and Microsoft's Java SDK failed Sun's compatibility tests while still carrying the "Java Compatible" logo. It also said pieces were missing: &lt;strong&gt;JNI&lt;/strong&gt;, the standard way for Java to call native code, and &lt;strong&gt;RMI&lt;/strong&gt;, for calling objects on other machines.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two new words
&lt;/h2&gt;

&lt;p&gt;While the case moved through the court, Microsoft shipped Visual J++ 6.0. Its compiler understood two words that weren't in Java: &lt;code&gt;delegate&lt;/code&gt; and &lt;code&gt;multicast&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here's the problem they solve. A button is written long before your code exists. When someone clicks it, it has to call one of &lt;em&gt;your&lt;/em&gt; methods. So you need a way to hand the button a method and say: call this, later.&lt;/p&gt;

&lt;p&gt;Java's answer, then, was an object. You wrote a small class that implements a listener interface and passed an instance to the button:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addActionListener&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ActionListener&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;actionPerformed&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ActionEvent&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Hello, world!"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Microsoft's answer was the &lt;strong&gt;delegate&lt;/strong&gt;: a reference that points straight at a method. You hand over the method itself, like a value. A &lt;strong&gt;multicast&lt;/strong&gt; delegate holds a list of methods, so one click runs all of them.&lt;/p&gt;

&lt;p&gt;If you write C#, you use this every day, even if you don't think about it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Click&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;OnClick&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// add a method to the delegate's list&lt;/span&gt;
&lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Click&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;LogClick&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;     &lt;span class="c1"&gt;// one click, both methods run&lt;/span&gt;

&lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OnClick&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;object&lt;/span&gt; &lt;span class="n"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;EventArgs&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Hello, world!"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;LogClick&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;object&lt;/span&gt; &lt;span class="n"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;EventArgs&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"clicked"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The court described the J++ version precisely. When Microsoft's compiler met &lt;code&gt;delegate&lt;/code&gt; and &lt;code&gt;multicast&lt;/code&gt;, it generated classes that extend &lt;code&gt;com.ms.lang.Delegate&lt;/code&gt; and &lt;code&gt;com.ms.lang.MulticastDelegate&lt;/code&gt;, classes that only Microsoft's virtual machine had. The bytecode was valid, but it looked for classes no other JVM shipped (&lt;a href="https://www.courtlistener.com/opinion/2577830/sun-microsystems-inc-v-microsoft-corp/" rel="noopener noreferrer"&gt;Sun v. Microsoft, 21 F. Supp. 2d 1109&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Sun says no, for the second time
&lt;/h2&gt;

&lt;p&gt;Sun's Java team answered with a public white paper, &lt;a href="https://web.archive.org/web/19990210053040/http://www.java.sun.com/docs/white/delegates.html" rel="noopener noreferrer"&gt;"About Microsoft's 'Delegates'"&lt;/a&gt;. It argued that bound method references were unnecessary and bad for the language, and that inner classes could do the same job.&lt;/p&gt;

&lt;p&gt;Then it said something surprising. Sun had considered this exact idea in 1996. It had built working prototypes and thrown them away. And it made that decision in consultation with Borland, which already had bound method references in Delphi.&lt;/p&gt;

&lt;p&gt;Delphi, the language Hejlsberg had led at Borland. The idea Sun turned down in 1996 was back, inside Microsoft's Java.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ninety days
&lt;/h2&gt;

&lt;p&gt;On November 17th, 1998, Judge Ronald Whyte granted Sun a preliminary injunction. Microsoft had ninety days to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ship a Java runtime that supports JNI and passes Sun's tests;&lt;/li&gt;
&lt;li&gt;make its compiler &lt;strong&gt;disable&lt;/strong&gt; the Microsoft language extensions by default;&lt;/li&gt;
&lt;li&gt;show a &lt;strong&gt;warning&lt;/strong&gt; when a developer turned them on, saying the code might not run on other Java virtual machines;&lt;/li&gt;
&lt;li&gt;and stop &lt;strong&gt;adding any new keywords or compiler directives&lt;/strong&gt; to its Java tools.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Hejlsberg himself filed a declaration in the case. A later order cites it to explain that WFC, his Windows library, used the &lt;code&gt;delegate&lt;/code&gt; and &lt;code&gt;multicast&lt;/code&gt; keywords (&lt;a href="https://www.courtlistener.com/opinion/2307572/sun-microsystems-inc-v-microsoft-corp/" rel="noopener noreferrer"&gt;87 F. Supp. 2d 992, Jan 2000&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;While that order stood, Microsoft's Java couldn't get a single new language feature. The case went to the Ninth Circuit and back, and a new order came in January 2000. But Visual J++ 6.0 was the last version.&lt;/p&gt;

&lt;h2&gt;
  
  
  Project Cool
&lt;/h2&gt;

&lt;p&gt;Around the start of 1999, Hejlsberg put together a small team to design a new language. Its working name was Cool, for "C-like Object Oriented Language". He has said the core design team was about six people, meeting three times a week for two hours (&lt;a href="https://newsletter.pragmaticengineer.com/p/typescript-c-and-turbo-pascal-with" rel="noopener noreferrer"&gt;The Pragmatic Engineer&lt;/a&gt;). In 2001, he described the goal as the power and expressiveness of C++ with the ease of Visual Basic (&lt;a href="https://www.hpcwire.com/2001/04/27/tuning-up-for-c/" rel="noopener noreferrer"&gt;CNET interview, via HPCwire&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;This time, nobody else owned the language. Microsoft could add any keyword it wanted. So delegates went in, not as an extension but as a core part of the language. Multicast went in too, built into every delegate. And on top of it came a new keyword, &lt;code&gt;event&lt;/code&gt;. Properties, events and a button wired straight to a method were built into the language from day one.&lt;/p&gt;

&lt;h2&gt;
  
  
  C# ships
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;June 26, 2000:&lt;/strong&gt; Microsoft &lt;a href="https://news.microsoft.com/source/2000/06/26/microsoft-introduces-highly-productive-net-programming-language-c/" rel="noopener noreferrer"&gt;announced C#&lt;/a&gt; and submitted it to the standards body ECMA. An early preview followed at the Professional Developers Conference in Orlando, on July 11.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;January 23, 2001:&lt;/strong&gt; the Java case &lt;a href="https://news.microsoft.com/source/2001/01/23/microsoft-reaches-agreement-to-settle-contract-dispute-with-sun-microsystems/" rel="noopener noreferrer"&gt;settled&lt;/a&gt;. Microsoft paid Sun $20 million, the 1996 license was terminated, and Microsoft could keep shipping its existing Java products for seven years.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;December 2001:&lt;/strong&gt; ECMA approved &lt;a href="https://ecma-international.org/wp-content/uploads/ECMA-334_1st_edition_december_2001.pdf" rel="noopener noreferrer"&gt;ECMA-334&lt;/a&gt;, the C# standard, based on a submission from HP, Intel and Microsoft.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;February 13, 2002:&lt;/strong&gt; Bill Gates &lt;a href="https://news.microsoft.com/source/2002/02/13/microsoft-launches-xml-web-services-revolution-with-visual-studio-net-and-net-framework/" rel="noopener noreferrer"&gt;launched .NET and Visual Studio .NET&lt;/a&gt;. C# shipped with them.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The twist
&lt;/h2&gt;

&lt;p&gt;In April 2004, the two companies made peace. Microsoft paid Sun $700 million to settle antitrust issues and $900 million for patents (&lt;a href="https://news.microsoft.com/source/2004/04/02/microsoft-and-sun-microsystems-enter-broad-cooperation-agreement-settle-outstanding-litigation/" rel="noopener noreferrer"&gt;Microsoft announcement&lt;/a&gt;). Microsoft's Java virtual machine was retired for good at the end of 2007.&lt;/p&gt;

&lt;p&gt;And Java? In 2014, Java 8 added lambdas and method references, so you could finally hand a button a method directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addActionListener&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;onClick&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It isn't the same thing as a C# delegate under the hood (Java still compiles it to an object that implements a functional interface). But it solves the problem Sun's white paper had called unnecessary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why C# exists
&lt;/h2&gt;

&lt;p&gt;Microsoft wasn't allowed to change a language it didn't own, so it built one it could change. The next time you write &lt;code&gt;+=&lt;/code&gt; on an event, remember that the feature behind it was once at the center of a federal court case.&lt;/p&gt;

&lt;p&gt;If Sun had said yes to delegates in 1998, would C# exist at all? Tell me what you think in the comments.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I make Vlad's Stack, stories and explainers about how the tools you use every day actually work, for people who write code: &lt;a href="https://www.youtube.com/channel/UCUO8Uo5LsEy1b9eRkrH6JNg" rel="noopener noreferrer"&gt;https://www.youtube.com/channel/UCUO8Uo5LsEy1b9eRkrH6JNg&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>dotnet</category>
      <category>java</category>
      <category>history</category>
    </item>
  </channel>
</rss>
