<?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: Deepanshu Kumar</title>
    <description>The latest articles on DEV Community by Deepanshu Kumar (@dev-deepanshu-kumar).</description>
    <link>https://dev.to/dev-deepanshu-kumar</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%2F2071142%2Fb180a611-a0e2-4197-ad6b-59587b2291f8.png</url>
      <title>DEV Community: Deepanshu Kumar</title>
      <link>https://dev.to/dev-deepanshu-kumar</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dev-deepanshu-kumar"/>
    <language>en</language>
    <item>
      <title>Entity Framework N+1 in a Report Loop: The Pre-Fetch Fix Was Going to Crash Large Tenants</title>
      <dc:creator>Deepanshu Kumar</dc:creator>
      <pubDate>Mon, 03 Aug 2026 17:11:03 +0000</pubDate>
      <link>https://dev.to/dev-deepanshu-kumar/entity-framework-n1-in-a-report-loop-the-pre-fetch-fix-was-going-to-crash-large-tenants-pdm</link>
      <guid>https://dev.to/dev-deepanshu-kumar/entity-framework-n1-in-a-report-loop-the-pre-fetch-fix-was-going-to-crash-large-tenants-pdm</guid>
      <description>&lt;p&gt;A report that had been slow since the day it shipped. Not slow like "takes a few seconds." Slow like fifteen minutes. Support tickets going back years, all saying the same thing: the expenditure summary report just doesn't load. Users would click generate, walk away, come back, and it still wasn't done.&lt;/p&gt;

&lt;p&gt;I picked it up expecting a missing index or a bad query plan. What I found was a loop inside a loop, each iteration firing multiple database round-trips, each round-trip returning the entire tenant's data table so the application could filter it down to one row.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the code was doing
&lt;/h2&gt;

&lt;p&gt;The report listed every location in the tenant. For each location, it broke down work order costs by category: labor, parts, equipment. Simple enough conceptually. The implementation had an outer loop over locations, an inner loop over categories, and inside that inner loop, several data retrieval calls.&lt;/p&gt;

&lt;p&gt;Here's a genericized version of the pattern:&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;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;location&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;locations&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;workOrders&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_workOrderService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetWorkOrders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;locationId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;location&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="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;category&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;workOrders&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;w&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Category&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;Distinct&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;labors&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_laborService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetAllLabors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cultureId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;     &lt;span class="c1"&gt;// full table&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;parts&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_partsService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetAllParts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cultureId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;      &lt;span class="c1"&gt;// full table&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;equipment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_equipmentService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetAllEquipment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cultureId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// full table&lt;/span&gt;

        &lt;span class="c1"&gt;// filter down to this location + category&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;locationLabors&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;labors&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;l&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WorkOrderId&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Category&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="c1"&gt;// ... same for parts, equipment&lt;/span&gt;
        &lt;span class="c1"&gt;// ... calculate totals&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 data retrieval methods returned &lt;code&gt;IQueryable&amp;lt;T&amp;gt;&lt;/code&gt; backed by table-valued functions. Each call invoked the TVF and returned the full tenant table. The filtering happened afterward in memory.&lt;/p&gt;

&lt;p&gt;For a small tenant this was fine. Slow, but fine. For a tenant with hundreds of locations and several WO categories each, the math became:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;N work order queries (one per location)&lt;/li&gt;
&lt;li&gt;N × C labor queries (one per location × category combination)&lt;/li&gt;
&lt;li&gt;N × C parts queries&lt;/li&gt;
&lt;li&gt;N × C equipment queries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A tenant with 200 locations averaging 4 categories each: roughly 2,600 database calls per report run. A larger tenant with 500 locations: over 6,000. Each call returning the full labor table, the full parts table, the full equipment table, hundreds of thousands of rows per call, discarded immediately after the filter ran.&lt;/p&gt;

&lt;p&gt;The 15-minute report times made sense once I saw this. The mystery was why nobody had fixed it yet.&lt;/p&gt;




&lt;h2&gt;
  
  
  The obvious fix, and why it would have been worse
&lt;/h2&gt;

&lt;p&gt;The first instinct was a global pre-fetch. Run each query once before the loop, hold the results in memory, filter from there:&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;// Pull everything once&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;allLabors&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_laborService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetAllLabors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cultureId&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;allParts&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_partsService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetAllParts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cultureId&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;allEquipment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_equipmentService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetAllEquipment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cultureId&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="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;location&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;locations&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;workOrders&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_workOrderService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetWorkOrders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;locationId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;location&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="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;category&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;workOrders&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;w&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Category&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;Distinct&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;locationLabors&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;allLabors&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;l&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WorkOrderId&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Category&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="c1"&gt;// etc.&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;This works. Three database calls instead of thousands. The report that took fifteen minutes would finish in seconds.&lt;/p&gt;

&lt;p&gt;I had the branch open, the change written, and was about to push it when I thought about the larger tenants.&lt;/p&gt;

&lt;p&gt;The labor and parts tables hold the full history for the tenant: every work order ever completed, every part ever used, going back years. For a small tenant that's manageable. For a large one with tens of thousands of work orders per year, multiple sites, years of history, calling &lt;code&gt;.ToList()&lt;/code&gt; with no predicate could pull millions of rows into the application server's memory in a single shot.&lt;/p&gt;

&lt;p&gt;The TVF sources took only a culture ID parameter. No way to pass a filter into them. They returned the full tenant table and relied on the caller to narrow things down, so calling &lt;code&gt;.ToList()&lt;/code&gt; with no predicate before it meant pulling everything regardless.&lt;/p&gt;

&lt;p&gt;For the tenants with the worst report times, the ones that had been waiting years for this fix, a global pre-fetch might replace a 15-minute report with an out-of-memory crash. That's not a fix.&lt;/p&gt;




&lt;h2&gt;
  
  
  The approach that actually worked
&lt;/h2&gt;

&lt;p&gt;The constraint was: we can't pass a predicate into the TVF, and we can't fetch everything at once either. But we also can't fetch per-loop. That was the original problem.&lt;/p&gt;

&lt;p&gt;The answer was to split it into two phases.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 1&lt;/strong&gt; — run the location loop normally, collecting work order IDs as you go:&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;woIdList&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;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;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;location&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;locations&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;workOrders&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_workOrderService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetWorkOrders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;locationId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;location&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;woIdList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddRange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;workOrders&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;w&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;w&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="n"&gt;woIdList&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;woIdList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Distinct&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is still N database calls, one per location, no change there. But now we have a bounded list of exactly the work order IDs that appear in this report, scoped to the date range the user selected. Not the full tenant. Not all history. Just the IDs relevant to this specific report run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 2&lt;/strong&gt; — one pre-fetch per data type, filtered to that ID list:&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;allLabors&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_laborService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetAllLabors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cultureId&lt;/span&gt;&lt;span class="p"&gt;)&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;l&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;woIdList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WorkOrderId&lt;/span&gt;&lt;span class="p"&gt;))&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;allParts&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_partsService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetAllParts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cultureId&lt;/span&gt;&lt;span class="p"&gt;)&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;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;woIdList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Contains&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;WorkOrderId&lt;/span&gt;&lt;span class="p"&gt;))&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;allEquipment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_equipmentService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetAllEquipment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cultureId&lt;/span&gt;&lt;span class="p"&gt;)&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;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;woIdList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WorkOrderId&lt;/span&gt;&lt;span class="p"&gt;))&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;EF6 translates &lt;code&gt;.Contains()&lt;/code&gt; on an integer list to a SQL &lt;code&gt;IN&lt;/code&gt; clause. The database does the filtering. You get back only the rows for work orders that actually appear in the report.&lt;/p&gt;

&lt;p&gt;Then the inner category loop runs against in-memory collections. Zero additional database calls.&lt;/p&gt;

&lt;p&gt;Total queries: N (location work order fetches) + 3 (the scoped pre-fetches). For a tenant with 200 locations that's 203 queries instead of 2,600. For a tenant with 500 locations it's 503 instead of 6,000-plus.&lt;/p&gt;




&lt;h2&gt;
  
  
  The EF6 parameter limit
&lt;/h2&gt;

&lt;p&gt;One more thing to watch: EF6 generates &lt;code&gt;IN&lt;/code&gt; clauses as inline values, not parameterized. SQL Server has a 2,100-parameter limit on parameterized queries, but inline &lt;code&gt;IN&lt;/code&gt; lists bypass that. The risk is query plan caching. Different ID lists produce different SQL strings, so the plan cache can't reuse them. For this use case that was acceptable. The report runs infrequently enough that plan cache pressure wasn't a concern.&lt;/p&gt;

&lt;p&gt;If the ID list ever grew large enough to cause problems, the right move would be passing the IDs as a table-valued parameter and joining inside the query. That gives the plan cache something stable to reuse. For the typical report run the &lt;code&gt;IN&lt;/code&gt; clause approach was clean and measurably fast.&lt;/p&gt;




&lt;h2&gt;
  
  
  What it looked like after
&lt;/h2&gt;

&lt;p&gt;Before the fix: the main data-collection endpoint averaged around two minutes on the test environment, and the test environment was more powerful than production. In production, the same report ran five to fifteen minutes depending on the tenant.&lt;/p&gt;

&lt;p&gt;After: four to eight seconds.&lt;/p&gt;

&lt;p&gt;The fix had been obvious once the pattern was visible. The loop-inside-a-loop, the TVF sources that returned full tables, the in-memory filtering that discarded almost everything. All of it was right there in the code. The report had shipped like that and nobody had looked closely until the runtime climbed high enough to generate support tickets.&lt;/p&gt;

&lt;p&gt;I don't know how many tenants had been quietly tolerating slow report times without filing tickets. The ones that filed them waited an average of eight months from first report to fix. The ones that didn't file tickets are harder to account for.&lt;/p&gt;




&lt;p&gt;Until next time,&lt;br&gt;
&lt;strong&gt;Deepanshu&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Backend engineer writing about production bugs, distributed systems, and engineering patterns learned the hard way.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://deepanshu-kumar.dev" rel="noopener noreferrer"&gt;Portfolio&lt;/a&gt; · &lt;a href="https://medium.com/@dev-deepanshu-kumar" rel="noopener noreferrer"&gt;Medium&lt;/a&gt; · &lt;a href="https://www.linkedin.com/in/deepanshu-kumar-dev/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; · &lt;a href="https://github.com/Dev-Deepanshu-Kumar" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>entityframework</category>
      <category>database</category>
      <category>csharp</category>
      <category>backend</category>
    </item>
    <item>
      <title>SQL Server "Login Does Not Exist" via AG Listener After Windows Auth Migration: The Server Name Is a Lie</title>
      <dc:creator>Deepanshu Kumar</dc:creator>
      <pubDate>Thu, 23 Jul 2026 12:40:28 +0000</pubDate>
      <link>https://dev.to/dev-deepanshu-kumar/sql-server-login-does-not-exist-via-ag-listener-after-windows-auth-migration-the-server-name-is-1of0</link>
      <guid>https://dev.to/dev-deepanshu-kumar/sql-server-login-does-not-exist-via-ag-listener-after-windows-auth-migration-the-server-name-is-1of0</guid>
      <description>&lt;p&gt;There's an address in our connection strings that looks like a server name but isn't one: &lt;code&gt;ag1-listener.sd.local&lt;/code&gt;. We'd seen it for years. It never mattered. When we switched to SQL Auth, it mattered a lot.&lt;/p&gt;

&lt;p&gt;For context: the app had always used Windows Authentication to connect to SQL Server. One flag, no credentials, nothing to manage. When services moved to Linux containers on EKS, that stopped working — Linux has no Kerberos domain context. We needed SQL Auth: explicit username and password. Store credentials in AWS Secrets Manager, fetch at runtime, substitute into the connection string.&lt;/p&gt;

&lt;p&gt;Seemed mechanical. It worked locally. It worked across every integration environment. In production-like testing, every connection failed with the same error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Login failed for user 'svc_app'.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First thing to check — whether the login actually exists on the node you're connected to:&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="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;server_principals&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'svc_app'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Empty result set. The login wasn't there. But it existed in every other environment. That's when it got interesting.&lt;/p&gt;




&lt;h2&gt;
  
  
  The connection string that caused it
&lt;/h2&gt;

&lt;p&gt;The app is multi-tenant. Each client has its own database. Connection strings are stored centrally. When a request arrives, the app reads the stored connection string, injects credentials fetched at runtime, and connects.&lt;/p&gt;

&lt;p&gt;A stored connection string looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="err"&gt;data&lt;/span&gt; &lt;span class="py"&gt;source&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;ag1-listener.sd.local;&lt;/span&gt;
&lt;span class="err"&gt;initial&lt;/span&gt; &lt;span class="py"&gt;catalog&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;ClientDatabase;&lt;/span&gt;
&lt;span class="err"&gt;user&lt;/span&gt; &lt;span class="py"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;{{UserId}};&lt;/span&gt;
&lt;span class="py"&gt;password&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;{{Password}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The credentials come from Secrets Manager at runtime. The question is: which secret do you fetch?&lt;/p&gt;

&lt;p&gt;In lower environments, the stored addresses were direct server names — &lt;code&gt;sql-dev-01&lt;/code&gt;, &lt;code&gt;sql-dev-02&lt;/code&gt;. One secret per server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;/dev/sql-dev&lt;/span&gt;&lt;span class="mi"&gt;-01&lt;/span&gt;&lt;span class="err"&gt;_credentials&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="err"&gt;→&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"userId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"svc_app"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"password"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;/dev/sql-dev&lt;/span&gt;&lt;span class="mi"&gt;-02&lt;/span&gt;&lt;span class="err"&gt;_credentials&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="err"&gt;→&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"userId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"svc_app"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"password"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the connection string said &lt;code&gt;sql-dev-01&lt;/code&gt;, the provider fetched the matching secret. Clean.&lt;/p&gt;

&lt;p&gt;In the production-like environment, the stored connection strings said &lt;code&gt;ag1-listener.sd.local&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;And &lt;code&gt;ag1-listener.sd.local&lt;/code&gt; is not a server name.&lt;/p&gt;




&lt;h2&gt;
  
  
  What a listener actually is
&lt;/h2&gt;

&lt;p&gt;An Availability Group listener is a virtual DNS name that always points to whoever is currently the primary SQL Server node in a cluster. Today it routes to &lt;code&gt;sql-server-01&lt;/code&gt;. After a failover, it routes to &lt;code&gt;sql-server-02&lt;/code&gt;. The app never has to change anything — the listener handles it transparently.&lt;/p&gt;

&lt;p&gt;That's the design. The listener exists specifically so you don't know or care which physical server you're on.&lt;/p&gt;

&lt;p&gt;SQL Auth breaks this.&lt;/p&gt;

&lt;p&gt;With Windows Authentication, your identity was a domain account with rights on every SQL Server node. "Which server?" was irrelevant — the same Kerberos token worked everywhere.&lt;/p&gt;

&lt;p&gt;With SQL Auth, credentials are server-level objects. A SQL Login is created on a specific SQL Server instance. If the login exists on &lt;code&gt;sql-server-01&lt;/code&gt; but not &lt;code&gt;sql-server-02&lt;/code&gt;, it authenticates on one and fails on the other.&lt;/p&gt;

&lt;p&gt;So when we tried to extend the per-server credential approach:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/prod/sql-server-01_credentials  →  works if listener routes here
/prod/sql-server-02_credentials  →  works if listener routes here
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We needed to know which server the listener was pointing to before choosing which secret to fetch. But to find that out, you have to connect. To connect, you need credentials. To get credentials, you need to know which server the listener points to.&lt;/p&gt;

&lt;p&gt;That's a circle. You can't break into it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why lower environments never showed this
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Local:&lt;/strong&gt; If you're running locally, your connection string provider likely short-circuits to &lt;code&gt;Integrated Security=True&lt;/code&gt; against &lt;code&gt;localhost&lt;/code&gt;. No SQL Auth, no listener, no Secrets Manager. Zero signal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Integration environments:&lt;/strong&gt; No Availability Groups. Single standalone SQL Server instances. Direct server names in the connection strings. Per-server credential lookup works exactly as designed. Still zero signal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Production-like environment:&lt;/strong&gt; Real Availability Groups. Real listeners in the connection strings. SQL Auth enabled. All three conditions together for the first time. The bug had always been there — it just had nowhere to show up.&lt;/p&gt;




&lt;h2&gt;
  
  
  The conversation with the DBA
&lt;/h2&gt;

&lt;p&gt;When it broke, we spent time in the application code first. The provider looked correct. The secret naming looked right. It took the DBA explaining AG architecture for the root cause to land.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"The listener is intentionally abstract. After any failover it points to a different physical server. Any credential mapping that depends on knowing the server name will break the moment a failover happens."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So per-server credentials were the wrong model entirely, not just broken in this one case.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"The SQL login needs to exist on all nodes with the same username and password. Then it doesn't matter which node the listener routes to — the credential is always valid."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That reframed the whole problem. The listener answers &lt;em&gt;where do I connect?&lt;/em&gt; The credential needs to answer &lt;em&gt;how do I authenticate?&lt;/em&gt; — independently of where. Those two concerns have to be separated.&lt;/p&gt;




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

&lt;p&gt;A SQL Login is a server-level object. It has to be created on each SQL Server instance individually — it doesn't replicate automatically across AG nodes.&lt;/p&gt;

&lt;p&gt;Create the login on every node in the cluster:&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;-- Run on each node in the Availability Group&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;LOGIN&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;svc_app&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;PASSWORD&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'your-password'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then inside each database on that node, create the corresponding user and grant access:&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="n"&gt;USE&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ClientDatabase&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;USER&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;svc_app&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="n"&gt;LOGIN&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;svc_app&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="n"&gt;sp_addrolemember&lt;/span&gt; &lt;span class="s1"&gt;'db_datareader'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'svc_app'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="n"&gt;sp_addrolemember&lt;/span&gt; &lt;span class="s1"&gt;'db_datawriter'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'svc_app'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Store the credentials once — one secret, fetched at runtime, applied to every connection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"userId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="s2"&gt;"svc_app"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"password"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The connection string template stays the same. At runtime:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="err"&gt;data&lt;/span&gt; &lt;span class="py"&gt;source&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;ag1-listener.sd.local;&lt;/span&gt;
&lt;span class="err"&gt;initial&lt;/span&gt; &lt;span class="py"&gt;catalog&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;ClientDatabase;&lt;/span&gt;
&lt;span class="err"&gt;user&lt;/span&gt; &lt;span class="py"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;svc_app;&lt;/span&gt;
&lt;span class="py"&gt;password&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;...;&lt;/span&gt;
&lt;span class="py"&gt;MultiSubnetFailover&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The listener routes to whichever node is currently primary. That node has the login. Authentication succeeds. If a failover happens and the listener routes to a different node, same result — the login is there too.&lt;/p&gt;




&lt;h2&gt;
  
  
  The scenario that still breaks it
&lt;/h2&gt;

&lt;p&gt;One gap remains after this is working.&lt;/p&gt;

&lt;p&gt;A new SQL Server cluster gets added to the environment. DB-ops provisions it, adds databases, starts assigning clients to it. But nobody ran &lt;code&gt;CREATE LOGIN&lt;/code&gt; on the new nodes.&lt;/p&gt;

&lt;p&gt;The login exists on every older node. Not on the new cluster. Clients on the new cluster fail with the exact same error. Everyone else is fine.&lt;/p&gt;

&lt;p&gt;From the outside it looks like a data problem. Some clients broken, some not. Seemingly random. The diagnosis is one query on the suspected node:&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="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;server_principals&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'svc_app'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- Empty = login not created on this node&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fix: run &lt;code&gt;CREATE LOGIN&lt;/code&gt; and &lt;code&gt;CREATE USER&lt;/code&gt; on each new node before any clients land there. Simple operation. The risk is forgetting it exists.&lt;/p&gt;




&lt;h2&gt;
  
  
  What would have caught this earlier
&lt;/h2&gt;

&lt;p&gt;One question during design: &lt;em&gt;"What format are the connection strings stored in production?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Answer: listener addresses.&lt;/p&gt;

&lt;p&gt;That single fact collapses the per-server credential model immediately. You can't map credentials to a virtual DNS name — you need credentials that work on whatever physical server the name resolves to. That conversation should have happened in the design meeting. It happened in production testing instead.&lt;/p&gt;

&lt;p&gt;The lower environments used standalone servers with direct names, so everything appeared to work. If production-like connection strings had been used in integration testing — or if the DBA had been in the design review — this would have been an hours-long problem. It wasn't.&lt;/p&gt;

&lt;p&gt;Infrastructure assumptions tend to be invisible until the environment changes. Windows Auth worked everywhere because the domain handled it silently. SQL Auth makes every assumption explicit. The assumption that you can map credentials to a server name was always there — it just never mattered until the listener made "server name" mean something ambiguous.&lt;/p&gt;




&lt;p&gt;Until next time,&lt;br&gt;
&lt;strong&gt;Deepanshu&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Backend engineer writing about production bugs, distributed systems, and engineering patterns learned the hard way.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://deepanshu-kumar.dev" rel="noopener noreferrer"&gt;Portfolio&lt;/a&gt; · &lt;a href="https://medium.com/@dev-deepanshu-kumar" rel="noopener noreferrer"&gt;Medium&lt;/a&gt; · &lt;a href="https://www.linkedin.com/in/deepanshu-kumar-dev/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; · &lt;a href="https://github.com/Dev-Deepanshu-Kumar" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>sqlserver</category>
      <category>debugging</category>
      <category>database</category>
      <category>infrastructure</category>
    </item>
    <item>
      <title>Why the Same Bug Keeps Coming Back - And How to Stop It</title>
      <dc:creator>Deepanshu Kumar</dc:creator>
      <pubDate>Tue, 21 Jul 2026 10:31:41 +0000</pubDate>
      <link>https://dev.to/dev-deepanshu-kumar/why-the-same-bug-keeps-coming-back-and-how-to-stop-it-4hd4</link>
      <guid>https://dev.to/dev-deepanshu-kumar/why-the-same-bug-keeps-coming-back-and-how-to-stop-it-4hd4</guid>
      <description>&lt;p&gt;A few months ago, I fixed a bug on a Friday afternoon. Status filter on a dashboard widget returning empty results for some users. Found the problem, added one line, tests passed, shipped it. Felt good about it.&lt;/p&gt;

&lt;p&gt;The following Tuesday someone filed another ticket. Different widget, same symptom. Assignee filter, empty results, same users affected. I opened the file, found the missing line in about thirty seconds, and just sat there for a moment.&lt;/p&gt;

&lt;p&gt;I hadn't fixed the bug. I fixed one place where the bug lived. The same broken pattern was sitting in three other handlers, and I hadn't thought to check.&lt;/p&gt;




&lt;h2&gt;
  
  
  What actually happened
&lt;/h2&gt;

&lt;p&gt;The app was a multi-tenant SaaS where users have permissions scoped to specific warehouses. Dashboard widgets query orders, but only the ones the user is allowed to see. Simple enough.&lt;/p&gt;

&lt;p&gt;We had six of these query handlers. Someone had written the first one correctly, copy-pasted it five times as new widgets were added, and somewhere along the way one important line kept getting dropped.&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;// Priority widget handler — written correctly&lt;/span&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;Priority&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GetPrioritiesQuery&lt;/span&gt; &lt;span class="n"&gt;query&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;permittedWarehouses&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;_warehouseService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetPermittedWarehouseIds&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="n"&gt;UserId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;permittedWarehouses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&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;// include orders with no warehouse assigned&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;_repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetPriorities&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;permittedWarehouses&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Status widget handler — the one that broke&lt;/span&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;Status&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GetStatusesQuery&lt;/span&gt; &lt;span class="n"&gt;query&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;permittedWarehouses&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;_warehouseService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetPermittedWarehouseIds&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="n"&gt;UserId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// that null append never made it here&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;_repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetStatuses&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;permittedWarehouses&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 &lt;code&gt;null&lt;/code&gt; append matters because some orders aren't assigned to any warehouse. Without it, those orders get filtered out entirely. For users whose entire order history happened to be unassigned, the widget showed nothing at all.&lt;/p&gt;

&lt;p&gt;One line. Easy fix. But I only fixed it in the one handler that had broken visibly.&lt;/p&gt;

&lt;p&gt;When I finally audited all six:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GetOrderStatuses     → ❌ missing null append
GetOrderPriorities   → ✅ has null append  
GetOrderAssignees    → ❌ missing null append
GetOrderLocations    → ✅ has null append
GetOrderCategories   → ❌ missing null append
GetOrderCosts        → ✅ has null append
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three broken, three fine. The broken ones just hadn't surfaced yet, either because fewer users hit those widgets or because the affected users had quietly assumed the data wasn't there and moved on without filing a ticket.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why this keeps happening
&lt;/h2&gt;

&lt;p&gt;The code didn't start inconsistent. Someone wrote the first handler carefully. The inconsistency came from copy-paste and the very reasonable assumption that "this is basically the same as that other handler." Nobody set out to leave three bugs behind.&lt;/p&gt;

&lt;p&gt;The trap is how bugs announce themselves. They show up as individual tickets, individual screens, individual user complaints. The natural instinct is to find the broken thing and fix it. That instinct is right, but it stops one step too early.&lt;/p&gt;

&lt;p&gt;In my case the pattern was "handlers that filter by warehouse permission don't consistently include unassigned orders." I fixed one handler. I should have searched for the pattern and fixed the category.&lt;/p&gt;




&lt;h2&gt;
  
  
  The one that never got reported
&lt;/h2&gt;

&lt;p&gt;After I found the other three broken handlers, I thought about &lt;code&gt;GetOrderCategories&lt;/code&gt;. It had been broken since the original commit. Same missing line, same bug. No ticket had ever been filed for it.&lt;/p&gt;

&lt;p&gt;Two explanations. Either none of our users had an order history composed entirely of unassigned-warehouse orders filtered by category. Or some of them had, looked at the empty widget, assumed the data wasn't there, and closed the tab.&lt;/p&gt;

&lt;p&gt;I don't know which. Probably both, at different customers, at different times. The second one bothers me more than the bug itself. A real user looked at a broken screen, formed the wrong mental model of their own data, and moved on without telling anyone.&lt;/p&gt;




&lt;h2&gt;
  
  
  Before you close the PR
&lt;/h2&gt;

&lt;p&gt;The habit that changed how I work: before writing the fix, write down what's wrong in one sentence of plain English. Not which file, not which method. The rule that got violated.&lt;/p&gt;

&lt;p&gt;"GetOrderStatuses doesn't append null to the warehouse list" is a file reference. "Warehouse-permission queries don't consistently include null to catch unassigned orders" is a pattern. The second version tells you what to search for everywhere else.&lt;/p&gt;

&lt;p&gt;Then search:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"GetPermittedWarehouseIds"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.cs"&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read every result. For each one: should this also have the fix? If yes, it goes in the same PR. Not a follow-up ticket, not later. The same PR, while the context is fresh and the intent is clear.&lt;/p&gt;

&lt;p&gt;The audit took me twenty minutes. The follow-up ticket that would have found &lt;code&gt;GetOrderAssignees&lt;/code&gt; and &lt;code&gt;GetOrderCategories&lt;/code&gt; would have taken two separate Tuesdays. That math is easy.&lt;/p&gt;




&lt;p&gt;The bug that got reported was the one someone noticed. The ones that didn't get reported were waiting for the right user, the right data, or someone patient enough to sit with empty results and eventually wonder why.&lt;/p&gt;

&lt;p&gt;Until next time,&lt;br&gt;
&lt;strong&gt;Deepanshu&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Backend engineer writing about production bugs, distributed systems, and engineering patterns learned the hard way.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://deepanshu-kumar.dev" rel="noopener noreferrer"&gt;Portfolio&lt;/a&gt; · &lt;a href="https://medium.com/@dev-deepanshu-kumar" rel="noopener noreferrer"&gt;Medium&lt;/a&gt; · &lt;a href="https://www.linkedin.com/in/deepanshu-kumar-dev/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; · &lt;a href="https://github.com/Dev-Deepanshu-Kumar" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>debugging</category>
      <category>softwareengineering</category>
      <category>webdev</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
