ASP.NET Core Cache: Middleware vs. Controller Showdown

October 12, 2025 · 8 min

Caching is one of the most effective strategies for boosting your ASP.NET Core application’s performance. By storing and reusing frequently accessed data, you can significantly reduce server load, decrease latency, and create a snappier user experience. But when it comes to implementation, ASP.NET Core offers a couple of primary approaches: caching at the middleware level and caching at the controller level.

Choosing the right strategy depends on your specific needs. Do you need a broad, application-wide caching rule, or do you require precise control over individual endpoints? Let’s break down the differences to help you make an informed decision.

Understanding the Core Concepts

Before we compare, it’s crucial to understand the two main players in this context: Response Caching Middleware and the [ResponseCache] attribute.

  • Response Caching Middleware: This is a component you register in your application’s request processing pipeline. It inspects incoming requests and outgoing responses to decide whether to serve a response from its cache or to cache a new response based on HTTP headers. It’s the engine that performs the actual server-side caching.
  • [ResponseCache] Attribute: This is a filter attribute you apply to your controller actions or globally. Its primary job is to set the appropriate HTTP Cache-Control header on the response. These headers instruct clients (like browsers) and intermediate proxies on how they should cache the response.

A …

...

Read more

Stream Responses in ASP.NET Core with Middleware

October 11, 2025 · 6 min

When building APIs that handle large datasets, memory consumption can quickly become a critical bottleneck. The default behavior in ASP.NET Core is to buffer the entire response in memory before sending it to the client. For a small JSON object, this is fine. For a 500 MB CSV export, it’s a recipe for disaster, potentially leading to high memory usage and even OutOfMemoryException.

The solution is response streaming. Instead of building the entire payload at once, we write it to the client in chunks as it’s generated. This drastically reduces the server’s memory footprint and improves the time to first byte (TTFB), making your application feel more responsive.

In this post, we’ll explore how to build a clean, reusable pattern for enabling response streaming using custom ASP.NET Core middleware.


The Problem with Response Buffering

Let’s visualize the default behavior. Imagine an endpoint that generates a large report:

  1. A request comes in for /api/reports/annual-sales.
  2. Your application logic queries a database and processes a massive amount of data.
  3. It serializes this data into a single, large string or byte array in memory. Let’s say it’s 200 MB.
  4. Only after the entire 200 MB payload is ready does ASP.NET Core begin sending the response to the client.

If 10 users request this report simultaneously, your server’s memory usage spikes by 2 GB just for these responses. This doesn’t scale well. Streaming sends the data piece by …

...

Read more

Master ASP.NET Core Rate Limiting for Robust APIs

October 8, 2025 · 9 min

When you’re building APIs, especially those exposed to the public internet, you’re not just building features; you’re also building defenses. A high-traffic API without protection is vulnerable to everything from accidental infinite loops in a client application to deliberate Denial-of-Service (DoS) attacks. This is where rate limiting becomes an essential part of your application’s architecture.

Starting with .NET 7, ASP.NET Core introduced a powerful and flexible rate-limiting middleware right into the framework. Gone are the days of relying solely on third-party packages or complex manual implementations. Let’s dive into how you can use this middleware to make your APIs more resilient and reliable.

What Exactly is Rate Limiting?

At its core, rate limiting is a defensive mechanism that controls the amount of incoming traffic to your API from a specific source in a given period. It’s like a bouncer at a club who only lets a certain number of people in per minute to prevent overcrowding.

By implementing rate limiting, you can:

  • Prevent Resource Exhaustion: Stop a single user or service from overwhelming your servers, database, or other downstream dependencies.
  • Ensure Fair Usage: Guarantee that all clients get a fair share of the available resources.
  • Improve Security: Mitigate brute-force attacks on login endpoints and reduce the effectiveness of DoS attacks.
  • Manage Costs: If you rely on paid third-party services, rate limiting can prevent …
...

Read more

Middleware vs Filters: A Guide for Cross-Cutting Concerns

October 2, 2025 · 9 min

When building modern web applications in ASP.NET Core, we constantly deal with “cross-cutting concerns.” These are the essential but repetitive tasks that cut across our application’s features: logging, authentication, authorization, caching, and error handling. The framework gives us two powerful tools for this job: Middleware and Filters.

While both can solve similar problems, they operate at different levels of the application stack. A common point of confusion for developers is choosing the right tool. Let me be direct: for truly global, application-wide cross-cutting concerns, Middleware is almost always the superior choice.

This post will break down why Middleware holds a strategic advantage over Filters for these scenarios and clarify when Filters still have their essential place.


Understanding the Players: The Pipeline is Everything

The key to this discussion is understanding where Middleware and Filters live within the ASP.NET Core request processing pipeline.

What is Middleware?

Think of Middleware as a series of components chained together to form the fundamental request and response pipeline. Each piece of middleware inspects an incoming HttpContext, performs an action, and then either passes the request to the next component in the chain or short-circuits the process by generating a response itself.

It’s low-level, efficient, and completely agnostic of what will ultimately handle the request. It doesn’t know about MVC controllers, …

...

Read more

Inheritance Mapping in EF Core: TPH vs TPT vs TPC in SQL Server

September 29, 2025 · 12 min

Your e-commerce system has different types of products: physical products, digital downloads, and subscriptions. Each type has common properties (name, price) and specific properties (shipping weight for physical, download URL for digital, billing frequency for subscriptions).

How you map this inheritance hierarchy to SQL Server tables dramatically affects query performance, storage efficiency, and maintainability.

The Three Inheritance Strategies

EF Core provides three approaches to map inheritance hierarchies to relational databases:

Table-per-Hierarchy (TPH)

All types stored in a single table with a discriminator column

Table-per-Type (TPT)

Shared properties in base table, specific properties in derived tables

Table-per-Concrete-Class (TPC)

Each concrete type gets its own table with all properties

Let’s examine each approach with a practical example.

Domain Model Setup

public abstract class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    public string Description { get; set; }
    public DateTime CreatedAt { get; set; }
    public bool IsActive { get; set; }
}

public class PhysicalProduct : Product
{
    public double Weight { get; set; }
    public string Dimensions { get; set; }
    public decimal ShippingCost { get; set; }
    public int StockQuantity { get; set; }
}

public class DigitalProduct : Product
{
    public string DownloadUrl { get; set; }
    public long FileSizeBytes { get; set …

Read more

Table Splitting in EF Core: When to Use It (and Why It Hurts Sometimes)

September 28, 2025 · 10 min

Your Customer entity has 47 properties. Most queries only need the basic information: name, email, and contact details. But every time you load a customer, EF Core pulls all 47 columns from the database, including the large ProfileData JSON blob that’s rarely used.

This is where table splitting can help: split your large entity into multiple classes that map to the same table, loading only what you need when you need it.

But table splitting is a double-edged sword. Use it wrong, and you’ll make performance worse, not better.

Understanding Table Splitting

Table splitting maps multiple entity classes to the same database table:

// Single table with many columns
CREATE TABLE Customers (
    Id int PRIMARY KEY,
    Name nvarchar(100),
    Email nvarchar(200),
    Phone nvarchar(20),
    -- Frequently accessed columns above
    
    ProfileData nvarchar(max),
    Preferences nvarchar(max), 
    MarketingMetadata nvarchar(max),
    LastLoginDetails nvarchar(max),
    -- Rarely accessed columns below
    CreatedAt datetime2,
    ModifiedAt datetime2
);

Instead of one large entity, you create multiple focused entities:

// Frequently accessed data
public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
    public string Phone { get; set; }
    
    // Navigation to extended data
    public CustomerExtended Extended { get; set; }
}

// Infrequently accessed data
public class CustomerExtended
{ …

Read more

Stored Procedures vs EF Core LINQ: When to Drop Down to SQL

September 27, 2025 · 9 min

Your EF Core LINQ query for monthly sales reporting takes 45 seconds to execute. You rewrite it as a stored procedure and it runs in 3 seconds. But now your business logic is split between C# and SQL, and your team is wondering if they made the right choice.

The decision between EF Core LINQ and stored procedures isn’t black and white. Each approach has strengths that match different scenarios.

When EF Core LINQ Excels

EF Core handles most database operations beautifully with clean, maintainable code:

// Clean, readable, and performs well
var customerOrders = await context.Customers
    .Where(c => c.Country == "USA")
    .Include(c => c.Orders.Where(o => o.OrderDate >= DateTime.Now.AddMonths(-3)))
    .Select(c => new CustomerOrderSummary
    {
        CustomerId = c.Id,
        CustomerName = c.Name,
        RecentOrderCount = c.Orders.Count(),
        TotalAmount = c.Orders.Sum(o => o.Total)
    })
    .ToListAsync();

EF Core generates efficient SQL for this type of query:

SELECT [c].[Id], [c].[Name], 
       COUNT([o].[Id]) AS RecentOrderCount,
       COALESCE(SUM([o].[Total]), 0.0) AS TotalAmount
FROM [Customers] AS [c]
LEFT JOIN [Orders] AS [o] ON [c].[Id] = [o].[CustomerId] 
    AND [o].[OrderDate] >= @__AddMonths_0
WHERE [c].[Country] = N'USA'
GROUP BY [c].[Id], [c].[Name]

This performs well and keeps all logic in your application layer.

When to Consider Stored Procedures

Complex Business Logic with Multiple Steps

Some …

Read more

SQL Server Execution Plans: What EF Core Developers Should Actually Look At

September 26, 2025 · 8 min

You’re looking at a SQL Server execution plan that looks like a spider web of boxes and arrows. Your EF Core query takes 800ms, but you have no idea where to start optimizing.

Execution plans contain overwhelming detail, but EF Core developers only need to focus on specific elements. Here’s what actually matters for optimizing your LINQ queries.

Getting Execution Plans from EF Core

First, you need to capture the SQL that EF Core generates:

// Enable query logging to see generated SQL
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder.UseSqlServer(connectionString)
        .LogTo(Console.WriteLine, LogLevel.Information)
        .EnableSensitiveDataLogging(); // Shows parameter values
}

Run your EF Core query and copy the logged SQL:

var expensiveOrders = await context.Orders
    .Include(o => o.Customer)
    .Include(o => o.OrderItems)
        .ThenInclude(oi => oi.Product)
    .Where(o => o.OrderDate >= DateTime.Now.AddDays(-30))
    .ToListAsync();

EF Core generates SQL like this:

SELECT [o].[Id], [o].[CustomerId], [o].[OrderDate], [o].[Total],
       [c].[Id], [c].[Name], [c].[Email],
       [o0].[Id], [o0].[OrderId], [o0].[ProductId], [o0].[Quantity],
       [p].[Id], [p].[Name], [p].[Price]
FROM [Orders] AS [o]
INNER JOIN [Customers] AS [c] ON [o].[CustomerId] = [c].[Id]
LEFT JOIN [OrderItems] AS [o0] ON [o].[Id] = [o0].[OrderId]
LEFT JOIN [Products] AS [p] ON [o0].[ProductId] = [p].[Id]
WHERE [o …

Read more

EF Core Compiled Queries: When They’re Worth It in High-Throughput Apps

September 25, 2025 · 8 min

Your API handles 50,000 requests per second, and each request executes the same customer lookup query. Profiling shows 15% of CPU time is spent translating LINQ expressions to SQL, not actually running the database query.

This is where EF Core compiled queries shine. They pre-compile the LINQ-to-SQL translation, eliminating that CPU overhead for frequently executed queries.

But compiled queries aren’t a magic performance solution. They have specific use cases and trade-offs that many developers misunderstand.

How EF Core Query Compilation Works

Every time you execute a LINQ query, EF Core goes through several steps:

  1. Parse the LINQ expression tree
  2. Translate LINQ to database-specific SQL
  3. Cache the translation result
  4. Execute the SQL query
// Normal query - goes through full pipeline every time
var customer = await context.Customers
    .Where(c => c.Id == customerId)
    .Include(c => c.Orders)
    .FirstOrDefaultAsync();

EF Core caches query plans, but complex LINQ expressions still require parsing and validation on each execution. For simple queries executed thousands of times per second, this overhead becomes significant.

Compiled Query Implementation

Compiled queries pre-compile the LINQ expression, skipping steps 1-2 on subsequent executions:

public static class CompiledQueries
{
    // Compiled query with single parameter
    public static readonly Func<CustomerContext, int, Task<Customer>> GetCustomerById =
        EF.CompileAsyncQuery(( …

Read more

Handling Transactions in EF Core: SaveChanges vs Explicit Transactions

September 20, 2025 · 6 min

Your application works perfectly in development, but in production, you’re seeing partial data updates and mysterious consistency issues. The problem isn’t your business logic, it’s how you’re handling transactions.

EF Core gives you two ways to manage transactions: the automatic approach with SaveChanges, and explicit transaction control. Each has its place, but using the wrong one can cost you data integrity or performance.

How EF Core SaveChanges Handles Transactions

SaveChanges automatically wraps all your changes in a single database transaction. This means multiple entity operations either all succeed or all fail together:

// This entire operation is one transaction
using var context = new OrderContext();

var customer = new Customer { Name = "John Doe" };
var order = new Order { CustomerId = customer.Id, Total = 100.50m };
var orderItem = new OrderItem { OrderId = order.Id, ProductId = 1 };

context.Customers.Add(customer);
context.Orders.Add(order);
context.OrderItems.Add(orderItem);

await context.SaveChangesAsync(); // All or nothing

If any part fails, everything rolls back. Your database stays consistent without any extra code.

When SaveChanges Transactions Aren’t Enough

Multiple SaveChanges Calls

The automatic transaction only covers a single SaveChanges call. If your business logic requires multiple save operations, you need explicit control:

// Problem: Two separate transactions
public async Task ProcessOrderAsync(Order …

Read more

EF Core Tenant Isolation: Global Query Filters for Secure Multi-Tenant SaaS

September 18, 2025 · 6 min

In one of our enterprise SaaS projects last year, we discovered a critical bug during a routine audit. Customer A could see order data belonging to Customer B when filtering by a specific date range. The root cause? A missing WHERE TenantId = @tenantId clause in a complex reporting query.

This wasn’t just embarrassing. It was a potential GDPR violation that could have resulted in significant penalties. That incident taught us that manual tenant filtering is prone to human error, especially in large codebases with multiple developers.

EF Core’s global query filters solved this problem by automatically applying tenant isolation at the ORM level. Here’s how we implemented bulletproof tenant isolation that passes SOC2 and HIPAA compliance requirements.

The Multi-Tenant Entity Foundation

Every entity in our system includes a TenantId property. This isn’t optional, it’s the foundation of data isolation.

public class Order
{
    public int Id { get; set; }
    public Guid TenantId { get; set; }
    public string CustomerName { get; set; }
    public decimal Amount { get; set; }
    public DateTime CreatedAt { get; set; }
}

public class Product
{
    public int Id { get; set; }
    public Guid TenantId { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
}

The TenantId is resolved through dependency injection using a tenant provider service:

public interface ITenantProvider
{
    Guid TenantId { get; }
}

public …
...

Read more

EF Core: One-to-One vs. One-to-Many (A Painful Lesson)

September 14, 2025 · 6 min

The difference between a One-to-One and a One-to-Many relationship in Entity Framework Core isn’t just academic. On my last project, choosing the wrong one led to two weeks of painful migrations, late nights, and a whole lot of dotnet ef database update anxiety.

It started with a simple feature: each tenant in our multi-tenant app needed a settings object. A Tenant has one Settings. Simple. So, we modeled it as a One-to-One relationship. What could go wrong?

Six months later, the requirements changed. Now, the business wanted an audit trail. They needed to see who changed a setting and when. Suddenly, our single Settings record per tenant was a huge problem. We needed a history, a collection of settings over time. Our rigid One-to-One schema was now a roadblock, and I was the one who had to fix it.

This experience taught me a hard lesson: your data model has to account for the future, not just the current sprint.

The Basics: What’s the Actual Difference?

Most of us get the concept, but let’s quickly recap how EF Core sees them.

A One-to-One relationship is a tight coupling. Think User and UserProfile. A user has exactly one profile. The profile can’t exist without the user. In the database, the primary key of UserProfile is also its foreign key back to User.

// The principal entity
public class User
{
    public int Id { get; set; }
    public string Username { get; set; }

    // Navigation property to the dependent
    public UserProfile Profile { …

Read more

5 EF Core Patterns for Faster ASP.NET Core APIs

September 13, 2025 · 7 min

I once inherited a dashboard page that took over five seconds to load. The API endpoint behind it looked innocent enough, but digging in, I found a classic case of death by a thousand cuts: lazy loading, bloated entities, and chatty database calls. It was a textbook example of default EF Core behavior backfiring under real-world load.

After fixing that mess (and many others like it), I’ve developed a small playbook of go-to optimizations. These aren’t wild, complex tricks. They’re five fundamental patterns that I apply to almost every high-traffic ASP.NET Core project.

Here’s what I do to keep my data layers fast and lean.

1. Ditch Full Entities, Project to DTOs

This one is my golden rule for read queries. If you’re just displaying data, stop loading full-blown EF Core entities.

The problem is that when you pull an entity, EF Core has to hydrate every single property. Even the ones you don’t need. This results in bigger SQL queries that join more tables and pull back way more data than your API client will ever see.

It’s pure waste.

Instead, use Select to project directly into a Data Transfer Object (DTO).

// The slow way
var users = await _context.Users
    .Include(u => u.Profile) // Pulls everything from two tables
    .ToListAsync();

// The fast, clean way
var users = await _context.Users
    .Select(u => new UserSummaryDto 
    {
        Id = u.Id,
        Name = u.Name,
        Email = u.Email
    })
    .ToListAsync();

On a …

...

Read more

Fix Slow EF Core Queries with Projections

September 10, 2025 · 6 min

I’ve seen this happen a dozen times. An API endpoint is dog-slow, taking 2-3 seconds to respond. Everyone blames the network or the database. We check the query execution plan, and it’s fine—the database returns data in 50 milliseconds. So what’s the problem?

The problem is the 900KB of data we’re pulling from the database, serializing, and then throwing away for every single request. The culprit is a silent performance killer in Entity Framework Core: fetching full entities when you only need a few properties.

The Default Behavior is a Trap

It’s easy to write code like this. It’s clean, it’s simple, and it works.

var orders = await _dbContext.Orders
    .Where(o => o.Status == "Active")
    .ToListAsync();

But under the hood, EF Core is trying to be helpful by generating a SELECT * style query. If your Orders table has a few large NVARCHAR(MAX) columns for notes, addresses, or serialized metadata, you’re in for a bad time.

Here’s the SQL it probably generates:

SELECT [o].[Id], [o].[CustomerId], [o].[OrderDate], [o].[Status], 
       [o].[Notes], [o].[ShippingAddress], [o].[BillingAddress],
       [o].[InternalComments], [o].[AuditData], [o].[SerializedMetadata]
FROM [Orders] AS [o]
WHERE [o].[Status] = 'Active'

If your API only needs to display the order ID, customer name, and total, you just paid a heavy price in network I/O and memory allocation for nothing. That SerializedMetadata column alone could be megabytes.

The Fix: Project Only What You Need

The …

Read more

How to Find and Fix N+1 Queries in EF Core

September 9, 2025 · 6 min

The API was screaming. A single endpoint, perfectly fine on my machine, was bringing our database to its knees in production. After digging through the logs, I found the culprit: one request was generating 241 separate SQL queries.

This one burned me badly early in my career. It’s a classic trap that every developer using an ORM like Entity Framework Core will eventually fall into: the N+1 query problem. It’s silent in development but absolutely lethal at scale.

So, What’s an N+1 Query Anyway?

It’s a sneaky performance bug. You ask EF Core for a list of items, and it happily obliges with one query. That’s the “1”.

Then, you loop through that list, and for each item, you access a related property (like a post’s author). If you haven’t told EF Core to load that related data upfront, it goes back to the database for every single item. That’s the “N”.

So, to get 100 blog posts and their authors, you end up with:

  • 1 query to get the 100 posts.
  • 100 more queries to get each post’s author.
  • Total: 101 queries for what should have been a simple operation.

In that production disaster I mentioned, fixing the N+1 pattern dropped the query count from 241 to just 3. The response time fell by over 85%.

How to Spot an N+1 Ambush

You can’t fix what you can’t see. Here are my go-to methods for hunting down these hidden performance killers.

1. Just Watch the Logs

This is the most direct way to see the problem. Turn on …

Read more

5 EF Core Defaults to Change Before Production

September 8, 2025 · 6 min

I once spent half a day chasing a performance bug in an API endpoint. It was a simple read operation, but it was inexplicably slow and chewing up memory under load. The culprit? A single, innocent-looking Entity Framework Core default that was tracking thousands of objects for changes that would never happen.

EF Core is fantastic, but its defaults are designed for getting-started tutorials, not production applications. They prioritize ease of use over performance and safety. Here are the five settings I change in every single production project before writing a single line of business logic.

1. The Silent Killer: QueryTrackingBehavior.TrackAll

By default, every time you query for data, EF Core’s DbContext assumes you’re going to change it. It loads the entities and sets up change tracking, which consumes memory and CPU cycles to keep an eye on every property.

This is a complete waste for the 90% of queries that are read-only: fetching data for an API response, a report, or a UI display. I’ve seen this add 20-40% overhead on high-traffic endpoints. It’s the number one performance gotcha I find in code reviews.

The Fix: Turn it off globally.

// In your Program.cs or Startup.cs
services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(connectionString)
           .UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking));

This simple change makes all queries non-tracking by default. For the rare cases where you do need to fetch and then …

Read more

Fix Slow EF Core Queries with Database Indexing

September 7, 2025 · 8 min

Of course. Here is the rewritten blog post, following your guidelines.


We’ve all been there. The EF Core query is blazing fast on your machine with 1,000 test records. Then it hits production with two million rows, and that same query goes from 50ms to 3 seconds. Suddenly, alerts are firing and your app is crawling.

The culprit is almost always the same: missing database indexes. This performance cliff between dev and prod is a classic “gotcha,” and it’s burned me more than once. Here’s how to spot it and fix it for good.

Why Your Query Is Fast Locally but Slow in Prod

When you write a clean piece of LINQ, EF Core does its job and generates the SQL you’d expect.

var user = await context.Users
    .Where(u => u.Email == "[email protected]")
    .FirstOrDefaultAsync();

Looks harmless, right? But without an index on the Email column, you’re asking SQL Server to do a full table scan. It literally has to check every single row to find a match. On a small local database, that’s instant. On a production table with millions of records, it’s a disaster.

Let’s be clear: EF Core isn’t slow. Your database is just doing what you told it to do. Our job is to give it the tools (indexes) to work smarter, not harder.

A Quick Sanity Check for Indexes

Not sure if a column needs an index? Here’s the first thing I do.

Step 1: See the SQL EF Core is actually running.

The ToQueryString() method is your best friend. It shows you the …

Read more

Safely Log EF Core SQL Queries in Production

September 6, 2025 · 6 min

Of course. Here is the rewritten blog post, following your style guide and prompt.


Stop Flying Blind: Safely Log EF Core SQL in Production

I remember the incident vividly. P95 latency for our main API shot through the roof overnight. Our logs showed a bunch of slow HTTP requests, but nothing about why they were slow. We were flying blind. After hours of frantic debugging, we found the culprit: a seemingly innocent LINQ query was generating a monster SQL join, causing a full table scan on a massive table.

We had no visibility into what Entity Framework Core was doing. This one burned me, and I promised myself: never again.

Most developers face this. They either accept the black box and pray, or they enable dangerous logging flags that leak sensitive user data. There’s a much better way to see exactly what SQL EF Core is executing in production without compromising security.

TL;DR: Use a custom DbCommandInterceptor to log SQL, execution time, and parameter metadata (but never the values). Correlate logs with a TraceId and use sampling or thresholds to avoid log noise.

The Common Mistakes That’ll Get You Paged at 3 AM

Before we get to the right way, let’s talk about the traps. I’ve seen these “temporary fixes” make their way into production code, and the results are always painful.

  • Never use EnableSensitiveDataLogging() in production. I can’t stress this enough. It logs parameter values. That means passwords, personal user info, and …

Read more

Why EF Core AutoInclude is a Performance Trap

September 5, 2025 · 6 min

I remember the first time AutoInclude() burned me in production. We had an Order entity, and someone thought it’d be convenient to always load the Customer and OrderItems. Local tests were snappy. But once we deployed, a simple dashboard API that just needed to show order statuses started timing out. The database CPU was pegged at 100%.

The culprit? A seemingly harmless configuration was forcing massive, unnecessary joins on every single query.

Here’s why AutoInclude() is a performance trap and what you should do instead.

What’s actually happening under the hood?

When you use AutoInclude() in your OnModelCreating configuration, you’re telling EF Core to always run a JOIN for that relationship. Every time. No exceptions.

// In your DbContext's OnModelCreating method
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Order>()
        .Navigation(o => o.Customer)
        .AutoInclude();
        
    modelBuilder.Entity<Order>()
        .Navigation(o => o.OrderItems)
        .AutoInclude();
}

With this in place, any query for an Order will automatically fetch the Customer and the list of OrderItems.

The Production Nightmare: Silent and Deadly Joins

The real problem with AutoInclude() is that it hides the database cost. The performance penalty isn’t visible in your application code; it’s buried deep in the model configuration.

When your code looks this simple:

var orders = await _context. …
...

Read more

Why Lazy Loading is a Performance Trap in ASP.NET Core

September 3, 2025 · 6 min

We’ve all been there. Your new API endpoint flies on your machine, returning customer orders in 50ms. You deploy it, and suddenly… it’s taking two seconds. The logs don’t show any obvious errors, but your app is crawling. The culprit is often a feature designed for convenience: lazy loading.

Lazy loading in Entity Framework Core feels like magic during development, but it can hide a massive performance tax that will absolutely cripple your application at scale.

So What Exactly is Lazy Loading?

Lazy loading means EF Core automatically grabs related data from the database only when you access it. Instead of you explicitly telling EF what you need upfront, EF Core reacts to your code. When you touch a navigation property for the first time, boom, it fires off a new database query to get that data.

Sounds handy, right? The problem is that each of these property accesses can trigger a separate, hidden database round-trip. What looks like a simple line of C# can become a silent storm of SQL queries.

The Sneaky Performance Killer: The N+1 Query Problem

The most common side effect of lazy loading is the infamous N+1 query problem. It starts with one simple query and then spirals.

Take a look at this standard ASP.NET Core controller action that fetches 50 orders:

public async Task<IActionResult> GetOrderSummary()
{
    var orders = await _context.Orders.Take(50).ToListAsync();
    
    // The problem kicks in right here, usually during serialization
    var …

Read more