<?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: Hellkeeper studio</title>
    <description>The latest articles on DEV Community by Hellkeeper studio (@elite3448).</description>
    <link>https://dev.to/elite3448</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%2F4175934%2F311faf3e-b4e1-4ee2-95c7-39f8cfbe59bd.jpg</url>
      <title>DEV Community: Hellkeeper studio</title>
      <link>https://dev.to/elite3448</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/elite3448"/>
    <language>en</language>
    <item>
      <title>Find N+1 queries in EF Core without changing a line of code</title>
      <dc:creator>Hellkeeper studio</dc:creator>
      <pubDate>Thu, 15 Oct 2026 16:56:12 +0000</pubDate>
      <link>https://dev.to/elite3448/find-n1-queries-in-ef-core-without-changing-a-line-of-code-k85</link>
      <guid>https://dev.to/elite3448/find-n1-queries-in-ef-core-without-changing-a-line-of-code-k85</guid>
      <description>&lt;p&gt;Your endpoint works. Tests are green. In development it answers in 80 ms. In production, for a customer with a long history, it takes two seconds and the database CPU climbs.&lt;/p&gt;

&lt;p&gt;Nine times out of ten with EF Core, the culprit is an &lt;strong&gt;N+1 query&lt;/strong&gt;: one query to load a list, then one more query for &lt;em&gt;each&lt;/em&gt; item of that list.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it looks like
&lt;/h2&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;OrderSummary&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetOrdersAsync&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="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;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;PlacedAt&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&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&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="n"&gt;OrderSummary&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;order&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="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;lines&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;OrderLines&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;OrderId&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="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="nf"&gt;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;                               &lt;span class="c1"&gt;// + 1 query per order&lt;/span&gt;
        &lt;span class="n"&gt;result&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;new&lt;/span&gt; &lt;span class="nf"&gt;OrderSummary&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="n"&gt;Id&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="n"&gt;PlacedAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Sum&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;Quantity&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;UnitPrice&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="n"&gt;result&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;Nothing looks wrong. With 3 orders in your dev database, it runs 4 fast queries. With a customer who has 48 orders, it runs 49 queries, each with a network round trip. Multiply by concurrent requests and you have a performance problem that no unit test will ever show.&lt;/p&gt;

&lt;p&gt;It also hides in places that do not look like queries at all:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lazy loading&lt;/strong&gt; (&lt;code&gt;UseLazyLoadingProxies()&lt;/code&gt;): &lt;code&gt;order.Lines&lt;/code&gt; in a loop silently runs a query per order.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mapping code&lt;/strong&gt;: a &lt;code&gt;Select&lt;/code&gt; in memory, a mapper or a &lt;code&gt;foreach&lt;/code&gt; in a view that touches a navigation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Helper methods&lt;/strong&gt; called in a loop that each run "just one small query".&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to spot it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Read the SQL log.&lt;/strong&gt; EF Core logs every command at the &lt;code&gt;Information&lt;/code&gt; level:&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;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseSqlServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;connectionString&lt;/span&gt;&lt;span class="p"&gt;)&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;Then look for the same &lt;code&gt;SELECT&lt;/code&gt; repeated with a different parameter. It works, but in a real app the log is a wall of text, and it tells you &lt;em&gt;what&lt;/em&gt; ran, not &lt;em&gt;which line of your code&lt;/em&gt; ran it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Count queries per request.&lt;/strong&gt; An interceptor (&lt;code&gt;DbCommandInterceptor&lt;/code&gt;) or a profiler such as MiniProfiler can count commands per HTTP request. More reliable, but it means adding a package and code to your app, and you still have to trace each query back to its origin.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Watch the database.&lt;/strong&gt; Query Store, &lt;code&gt;pg_stat_statements&lt;/code&gt; or Application Insights will show a query executed thousands of times. In production. After your users noticed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three ways to fix it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Project what you need, in one query.&lt;/strong&gt; Usually the best fix: EF Core turns the whole thing into a single SQL statement with a subquery, and loads only the columns you use.&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;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;PlacedAt&lt;/span&gt;&lt;span class="p"&gt;)&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;OrderSummary&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;PlacedAt&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;Lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Sum&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;Quantity&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;UnitPrice&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;&lt;strong&gt;Load the related data with the parent.&lt;/strong&gt; When you need the entities themselves:&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;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;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;Include&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;Lines&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;With several collection &lt;code&gt;Include&lt;/code&gt;s, the single query can return a huge cartesian product: add &lt;code&gt;.AsSplitQuery()&lt;/code&gt; to load each collection with its own query (a fixed number of queries, not N).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Batch the second query.&lt;/strong&gt; When the loop must stay (different contexts, complex logic), load everything the loop needs in one query, then look it up in 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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;ids&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="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="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;linesByOrder&lt;/span&gt; &lt;span class="p"&gt;=&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;OrderLines&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;ids&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;OrderId&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;span class="nf"&gt;ToLookup&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;OrderId&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;order&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="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;linesByOrder&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="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two queries, whatever the number of orders.&lt;/p&gt;

&lt;h2&gt;
  
  
  Catching it at the line that causes it
&lt;/h2&gt;

&lt;p&gt;The hard part is not the fix, it is &lt;em&gt;noticing&lt;/em&gt;. I wanted something that tells me, while I code, "this line ran the same query 48 times in one request".&lt;/p&gt;

&lt;p&gt;So I built it into &lt;strong&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=eflens.schemawise" rel="noopener noreferrer"&gt;Schemawise&lt;/a&gt;&lt;/strong&gt;, a VS Code extension for EF Core. Its Query Profiler shows every SQL query of your app above the line of code that runs it, flags N+1 queries, slow queries and large results, and fixes the N+1 in one click (here, 144 queries become 3):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F04q75g8qnxzvmq1b9ld2.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F04q75g8qnxzvmq1b9ld2.gif" alt="An N+1 query flagged above the loop, then fixed in one click: 144 queries become 3" width="800" height="491"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;How it works, since "no code change" deserves an explanation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;EF Core publishes every command it runs through &lt;code&gt;DiagnosticListener&lt;/code&gt; (the same events its logging uses). A small &lt;strong&gt;.NET startup hook&lt;/strong&gt; (&lt;code&gt;DOTNET_STARTUP_HOOKS&lt;/code&gt;, a runtime feature since .NET Core 3.0) subscribes to them when your app starts. No NuGet package, no line of code in your app.&lt;/li&gt;
&lt;li&gt;For each command, it records the SQL text, the duration, the number of rows read, the parameter names and types (&lt;strong&gt;never their values&lt;/strong&gt;), and the first frames of your own code in the call stack. That is what puts the result on the right line.&lt;/li&gt;
&lt;li&gt;Queries are grouped by request (the root &lt;code&gt;Activity&lt;/code&gt; of ASP.NET Core). The same &lt;code&gt;SELECT&lt;/code&gt; from the same line five times or more in one request is an N+1.&lt;/li&gt;
&lt;li&gt;Everything stays on your machine: the hook writes to a local file that the extension reads. It refuses to run in Production and Staging.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The one-click fix is the "batch the second query" rewrite above: the lightbulb moves the query out of the loop, loads everything with &lt;code&gt;Contains&lt;/code&gt;, groups it with &lt;code&gt;ToLookup&lt;/code&gt;, and shows the change in VS Code's refactor preview before applying it. When the code has another shape, it explains the three fixes instead of guessing.&lt;/p&gt;

&lt;p&gt;The same data catches the other classic EF Core mistakes, each with its fix:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cartesian explosion&lt;/strong&gt;: several collection &lt;code&gt;Include&lt;/code&gt;s in one statement (EF Core's own warning, now at the right line): &lt;code&gt;AsSplitQuery()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Synchronous queries in async methods&lt;/strong&gt;: &lt;code&gt;ToList()&lt;/code&gt; or &lt;code&gt;SaveChanges()&lt;/code&gt; blocking a thread: &lt;code&gt;await ToListAsync()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read-only queries that track entities&lt;/strong&gt; in a DbContext that never saves: &lt;code&gt;AsNoTracking()&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It can also group everything by HTTP request (&lt;code&gt;GET /api/orders/{id}: 49 queries per request&lt;/code&gt;), and read the &lt;strong&gt;estimated execution plans&lt;/strong&gt; of those queries (never running them) to suggest the missing indexes, with the &lt;code&gt;HasIndex(...)&lt;/code&gt; line to add to your model.&lt;/p&gt;

&lt;p&gt;Full disclosure: the profiler is part of Schemawise Pro, free to try for 14 days with no account and no card.&lt;/p&gt;

&lt;p&gt;The extension also has free features for the rest of the EF Core workflow: the effective model of each DbContext, schema drift against a real SQL Server or PostgreSQL database, and the risks of a migration before you apply it.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;N+1 queries are invisible with small dev data: look at &lt;strong&gt;queries per request&lt;/strong&gt;, not at durations.&lt;/li&gt;
&lt;li&gt;Prefer a &lt;strong&gt;projection&lt;/strong&gt; (&lt;code&gt;Select&lt;/code&gt;) to loading entities and walking their navigations.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Include&lt;/code&gt; for entities, &lt;code&gt;AsSplitQuery&lt;/code&gt; when several collections multiply rows, and batching with &lt;code&gt;Contains&lt;/code&gt; when the loop must stay.&lt;/li&gt;
&lt;li&gt;Make the problem visible where you write the code, not in a production dashboard.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you try Schemawise, I would love to hear what it finds in your project, and what it misses: &lt;a href="https://github.com/elite3448/schemawise/issues/new/choose" rel="noopener noreferrer"&gt;issues and ideas are welcome&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>efcore</category>
      <category>csharp</category>
      <category>performance</category>
    </item>
  </channel>
</rss>
