Entity Framework Core has become the preferred Object-Relational Mapper (ORM) for .NET developers because it simplifies database access while integrating seamlessly with ASP.NET Core. However, convenience comes with a cost. Poorly optimized EF Core queries can lead to excessive memory usage, unnecessary database round trips, slow API responses, and scalability issues.

EF Core 10 introduces several improvements, but achieving high performance still depends on how you design and use your data access layer. This article explores four key areas that have the greatest impact on production performance: change tracking, query filters, compiled queries, and bulk operations. You'll also learn when to use each feature and the trade-offs involved.

Understanding EF Core Performance

Where Does EF Core Spend Time?

Before optimizing, it's important to understand where performance bottlenecks originate.

Common sources include:

Framework upgrades alone won't solve these issues. Effective optimization starts with choosing the right EF Core features for each scenario.

Optimizing Change Tracking

Use Tracking Only When Needed

By default, EF Core tracks every entity it retrieves so that changes can be detected automatically during SaveChanges(). While this is useful for updates, it adds memory and CPU overhead for read-only operations.

var products = await context.Products
    .AsNoTracking()
    .ToListAsync();

Why Use AsNoTracking()?

AsNoTracking() tells EF Core not to monitor retrieved entities for changes. This reduces memory consumption and improves query execution for read-only scenarios such as dashboards, reports, and public APIs.

Use tracking only when you intend to modify and save entities.

When Should Tracking Be Enabled?

Tracking is appropriate when:

Avoid disabling tracking globally unless the application primarily performs read-only operations.

Using Global Query Filters

Automatically Filter Data

Many enterprise applications implement soft deletes or multi-tenancy. Without query filters, every query must include repetitive filtering logic.

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Product>()
        .HasQueryFilter(p => !p.IsDeleted);
}

Why Use Global Query Filters?

Instead of adding Where(p => !p.IsDeleted) throughout the application, the filter is automatically applied to every query.

This improves consistency and reduces the risk of accidentally exposing deleted or unauthorized data.

Ignoring Query Filters

Occasionally, administrators may need to retrieve all records.

var products = await context.Products
    .IgnoreQueryFilters()
    .ToListAsync();

This allows privileged operations while preserving default application behavior.

Compiled Queries

Reduce Query Compilation Overhead

EF Core translates LINQ expressions into SQL before executing them. Frequently executed queries can benefit from compilation.

private static readonly Func<AppDbContext, int, Task<Product?>>
GetProductById =
    EF.CompileAsyncQuery(
        (AppDbContext context, int id) =>
            context.Products
                .FirstOrDefault(p => p.Id == id));

Using the compiled query:

var product = await GetProductById(context, productId);

Why Use Compiled Queries?

Normally, EF Core analyzes and translates the LINQ expression every time it executes.

Compiled queries cache this translation, making repeated execution more efficient.

They're particularly useful for:

Avoid compiling queries that execute only occasionally, as the additional complexity may not provide measurable benefits.

Efficient Bulk Operations

Avoid Updating Records One by One

Consider the following update:

foreach (var product in products)
{
    product.IsActive = false;
}

await context.SaveChangesAsync();

For thousands of records, this approach becomes inefficient because every entity is loaded and tracked.

Instead, use EF Core's bulk update capabilities.

await context.Products
    .Where(p => p.Stock == 0)
    .ExecuteUpdateAsync(setters =>
        setters.SetProperty(
            p => p.IsActive,
            false));

Why Is This Better?

ExecuteUpdateAsync() generates a single SQL statement that updates all matching rows directly in the database.

Benefits include:

The same principle applies to bulk deletes.

await context.Products
    .Where(p => p.IsDeleted)
    .ExecuteDeleteAsync();

This avoids loading entities into memory before deletion.

End-to-End Performance Optimization Workflow

Consider an e-commerce application exposing a product catalog.

Client Request
      │
      ▼
ASP.NET Core API
      │
      ▼
Application Service
      │
      ▼
Compiled Query
      │
      ▼
AsNoTracking()
      │
      ▼
Global Query Filter
      │
      ▼
SQL Database

Here's how the request flows:

  1. A client requests available products.

  2. The API forwards the request to the application service.

  3. A compiled query retrieves product data.

  4. AsNoTracking() eliminates unnecessary tracking overhead.

  5. Global query filters automatically exclude soft-deleted products.

  6. Optimized SQL is executed.

  7. Results are returned with minimal overhead.

Each optimization contributes to a more efficient request pipeline without increasing application complexity.

Choosing the Right Optimization

FeatureBest Used ForAvoid When
AsNoTrackingRead-only queriesUpdating entities
Query FiltersSoft delete, multi-tenancyAdministrative reporting requiring all records
Compiled QueriesFrequently executed queriesRarely executed queries
ExecuteUpdateAsyncLarge updatesEntity validation is required
ExecuteDeleteAsyncLarge deletesBusiness rules require entity loading

Choosing the right optimization depends on the application's workload rather than applying every technique everywhere.

Best Practices

Common Mistakes

A common mistake is enabling tracking for every query. While convenient, unnecessary tracking increases memory usage and reduces throughput.

Another issue is loading entire entities when only a few properties are required. Projecting only the necessary fields reduces network traffic and query execution time.

Developers also tend to optimize prematurely. Profile the application first to identify actual bottlenecks before introducing compiled queries or other advanced optimizations.

Testing and Validation

Performance optimizations should never compromise correctness.

Before deploying changes:

Testing ensures that performance improvements do not introduce functional regressions.

Performance Considerations

While the techniques discussed improve performance, they should be applied selectively.

Keep these recommendations in mind:

Remember that database design often has a greater impact on performance than ORM configuration alone.

Security Considerations

Database performance should never come at the expense of security.

Follow these practices:

Security and performance should always be optimized together rather than independently.

Troubleshooting

Queries Are Still Slow

Inspect the generated SQL and review execution plans. The bottleneck may be missing indexes or inefficient joins rather than EF Core itself.

Bulk Updates Don't Trigger Business Logic

ExecuteUpdateAsync() and ExecuteDeleteAsync() operate directly against the database and bypass entity tracking. If validation or domain events are required, update entities through the normal change tracker.

Unexpected Missing Data

Check whether a global query filter is excluding records. Use IgnoreQueryFilters() only when appropriate.

High Memory Usage

Review tracking behavior and verify that read-only queries consistently use AsNoTracking().

Conclusion

EF Core 10 provides powerful features for building high-performance data access layers, but their effectiveness depends on how they're applied. Using AsNoTracking() for read-only queries, global query filters for consistent data access, compiled queries for frequently executed operations, and bulk update APIs for large-scale modifications can significantly improve application efficiency. Rather than optimizing every query indiscriminately, profile your application, identify real bottlenecks, and apply these techniques where they provide measurable value. A balanced approach results in applications that are both scalable and maintainable in production.