Entity Framework Core & .NET Performance

Entity Framework Core Performance Optimization:
Practical Techniques for Faster .NET Applications

Practical techniques for optimizing EF Core queries, data access, SQL Server workloads and enterprise .NET applications.

August 2026 • 14 min read • By Kshitij Mehta

Entity Framework Core performance optimization becomes increasingly important as a .NET application grows from a small database-backed application into a production enterprise system. EF Core makes data access productive, but query design, change tracking, loading strategies, indexes and database behavior still determine how efficiently the application performs.

A Windows POS system is a practical example. A real POS application can work with sales, sale items, customers, customer debt, wallet transactions, products, inventory and synchronization data. Some operations need transactional consistency, while others are read-heavy and can be optimized for smaller result sets and fewer database round trips.

In a system such as WinPOS, EF Core can coexist with Dapper and direct ADO.NET where each approach fits the workload. This article focuses on practical EF Core performance optimization techniques that can be applied to real SQL Server-backed enterprise .NET applications without assuming that one data-access strategy is correct for every operation.

Key Takeaways
  • Measure generated SQL and database time before changing EF Core code.
  • Use no-tracking queries for read-only workloads where appropriate.
  • Project only the columns required by the POS workflow.
  • Control eager, explicit and lazy loading to avoid unnecessary database work.
  • Use suitable SQL Server indexes and pagination for large operational datasets.
  • Keep transactional operations focused and choose Dapper or ADO.NET when they better fit a specific workload.

1. Measure EF Core Performance Before Optimizing

Start with evidence. A slow EF Core operation may be caused by generated SQL, database execution, materialization, tracking or application processing. Measuring the SQL duration and the complete request gives you a better basis for optimization.


var stopwatch = Stopwatch.StartNew();

var sale = await dbContext.Sales
    .FirstOrDefaultAsync(
        x => x.Id == saleId,
        cancellationToken);

stopwatch.Stop();

logger.LogInformation(
    "Sale lookup completed in {ElapsedMs} ms",
    stopwatch.ElapsedMilliseconds);
                

For a POS application, measure important workflows such as sale retrieval, customer lookup, open debt queries, wallet history and synchronization operations separately.


2. Project Only the Data the POS Screen Needs

One of the simplest EF Core performance optimizations is selecting only the properties required by the current operation instead of materializing a complete entity graph.


var sales = await dbContext.Sales
    .Where(x => x.CustomerId == customerId)
    .OrderByDescending(x => x.Id)
    .Select(x => new SaleSummaryDto
    {
        Id = x.Id,
        InvoiceNumber = x.InvoiceNumber,
        Date = x.Date,
        Total = x.Total
    })
    .Take(50)
    .ToListAsync(cancellationToken);
                

Smaller projections reduce the amount of data transferred from SQL Server and reduce the work needed to materialize objects in the .NET process.


3. Use AsNoTracking for Read-Only POS Queries

EF Core normally tracks entity instances when they are loaded into a tracking DbContext. Tracking is useful when changes need to be persisted, but it can add unnecessary work for read-only screens.


var products = await dbContext.Products
    .AsNoTracking()
    .Where(x => x.IsActive)
    .Select(x => new ProductLookupDto
    {
        Id = x.Id,
        Name = x.Name,
        Price = x.Price
    })
    .ToListAsync(cancellationToken);
                

Product browsing, customer search, historical sale views and other read-only POS operations are common candidates for no-tracking queries when the returned entities do not need to be modified and saved through the same context.


4. Control Change Tracking for Write-Heavy Workflows

Tracking is useful when EF Core needs to detect changes to entities. In transactional POS operations, however, tracking should be limited to the entities that actually participate in the update.


var sale = await dbContext.Sales
    .SingleAsync(
        x => x.Id == saleId,
        cancellationToken);

sale.Status = "Completed";

await dbContext.SaveChangesAsync(
    cancellationToken);
                

Avoid loading large collections into a tracking context merely to update one or two fields. Where appropriate, use targeted update operations or a focused entity load.


5. Avoid Unnecessary Include Calls

Eager loading with Include is useful when a workflow genuinely requires related entities. Excessive Includes can produce large joins and large result sets.


var sale = await dbContext.Sales
    .Include(x => x.Items)
    .SingleOrDefaultAsync(
        x => x.Id == saleId,
        cancellationToken);
                

If a POS screen needs only the sale total and item summary, project those values rather than loading every navigation property associated with the sale.


6. Consider Split Queries for Large Include Graphs

When multiple collection navigations are loaded together, a single SQL query can produce a large joined result. Depending on the model and workload, split queries can avoid certain forms of result-set multiplication.


var sale = await dbContext.Sales
    .AsSplitQuery()
    .Include(x => x.Items)
    .Include(x => x.Payments)
    .SingleOrDefaultAsync(
        x => x.Id == saleId,
        cancellationToken);
                

Split queries are not automatically faster. Compare the generated SQL, number of round trips and total execution time for the actual workload.


7. Avoid N+1 Queries in POS Screens

An N+1 pattern occurs when an application loads one set of records and then performs another query for each individual record.


Load 100 sales
    ↓
Query customer for sale 1
Query customer for sale 2
Query customer for sale 3
...
Query customer for sale 100
                

Prefer a suitable join, projection or explicit loading strategy that retrieves the required information in a controlled number of database operations.


8. Paginate Large Product, Sales and Customer Lists

POS administration screens can eventually contain large product catalogs, sales histories, customer records and debt transactions. Loading everything into memory does not scale.


var customers = await dbContext.Customers
    .AsNoTracking()
    .OrderBy(x => x.Name)
    .Skip((page - 1) * pageSize)
    .Take(pageSize)
    .ToListAsync(cancellationToken);
                

Keep page sizes reasonable and evaluate the query and indexing strategy as the database grows.


9. Align EF Core Queries with SQL Server Indexes

EF Core generates SQL; SQL Server ultimately executes it. The query performance therefore depends on suitable indexes for the access patterns generated by the application.


SELECT
    ID,
    InvoiceNumber,
    Date,
    Total
FROM dbo.tblSale
WHERE CustomerID = @CustomerID
ORDER BY ID DESC;
                

For frequently executed POS queries, inspect the actual execution plan and evaluate indexes supporting the WHERE, JOIN and ORDER BY patterns.


10. Consider Compiled Queries for Extremely Hot Paths

EF Core supports compiled queries for scenarios where the same query shape executes repeatedly and query-compilation overhead is measurable.


private static readonly
    Func<PosDbContext, int, Task<Customer?>>
    GetCustomer =
        EF.CompileAsyncQuery(
            (PosDbContext db, int id) =>
                db.Customers
                    .AsNoTracking()
                    .FirstOrDefault(x => x.Id == id));
                

Compiled queries are a specialized optimization. Measure first; they should not be introduced everywhere simply because they exist.


11. Keep EF Core Query Shapes Predictable

Repeated, well-defined query patterns are easier to measure and optimize. Avoid constructing unnecessarily complex dynamic queries when a small set of known application operations can cover the required behavior.


var openDebts = await dbContext.SaleDebts
    .AsNoTracking()
    .Where(x =>
        x.CompanyId == companyId &&
        x.Status == "Open" &&
        x.RemainingAmount > 0)
    .OrderBy(x => x.CreatedDate)
    .Take(50)
    .ToListAsync(cancellationToken);
                

This type of query maps directly to a clear business operation and can be investigated independently in SQL Server.


12. Keep SaveChanges Operations Deliberate

EF Core can track multiple modifications and send changes during SaveChangesAsync. The application should nevertheless keep the unit of work focused.


sale.Status = "Completed";

salePayment.Amount = paymentAmount;

await dbContext.SaveChangesAsync(
    cancellationToken);
                

Avoid keeping unrelated changes tracked in the same context for long periods. Shorter units of work make application behavior easier to reason about and can reduce unnecessary change-tracking overhead.


13. Use Explicit Transactions for Multi-Step POS Operations

POS workflows sometimes require multiple database changes to succeed or fail together, such as recording a sale, payment and associated financial data.


await using var transaction =
    await dbContext.Database
        .BeginTransactionAsync(
            cancellationToken);

try
{
    // Update sale
    // Insert payment
    // Update related records

    await dbContext.SaveChangesAsync(
        cancellationToken);

    await transaction.CommitAsync(
        cancellationToken);
}
catch
{
    await transaction.RollbackAsync(
        cancellationToken);

    throw;
}
                

Keep the transaction focused on database work. Do not normally hold a SQL transaction open while waiting for unrelated external operations.


14. Optimize Wallet and Financial Queries Carefully

The WinPOS wallet workflow uses SQL Server to store wallet activity and calculate balances. Financial queries should favor correctness first and then be optimized around measured access patterns.


SELECT
    ISNULL(SUM(Amount), 0)
FROM dbo.tblWalletActivity
WHERE CompanyID = @CompanyID;
                

For frequently executed balance and history operations, evaluate the execution plan, row counts and indexing strategy. Avoid loading an entire wallet history when a summary or limited history view is all the screen needs.


15. Optimize Customer Debt and Payment Queries

Customer debt is another example of a transactional workflow where EF Core query design matters. A debt summary should return the fields required to display or calculate the current state.


SELECT TOP 1
    ID,
    SaleID,
    CompanyID,
    OriginalAmount,
    RemainingAmount,
    CreatedDate,
    Status
FROM dbo.tblSaleDebt
WHERE SaleID = @SaleID
ORDER BY ID DESC;
                

Repeated debt lookup and payment-processing operations should be evaluated together with the indexes supporting SaleID, CompanyID and the relevant status filters.


16. Use Bulk Techniques for Large Data Imports

EF Core is productive for normal entity operations, but high-volume imports may need a different data-access strategy. The WinPOS codebase demonstrates this broader principle by using direct database tooling for bulk work.

When a workflow involves thousands of rows, compare normal EF Core inserts against suitable bulk or set-based techniques rather than assuming entity-by-entity processing is optimal.


17. EF Core and Dapper Can Coexist in the Same Application

The choice does not have to be EF Core versus Dapper for the entire system. A mature enterprise application can use each tool where its characteristics fit the workload.


POS Application
      │
      ├── Entity Framework Core
      │     └── Entity-oriented workflows
      │
      ├── Dapper
      │     └── Targeted SQL / read-heavy operations
      │
      └── ADO.NET
            └── Direct SQL / specialized processing
                

In WinPOS, the repository layer already demonstrates a Dapper-based approach for wallet and debt operations. EF Core can remain valuable for other entity-oriented workflows without requiring the entire application to use one technology exclusively.


18. Connect EF Core Performance to ASP.NET Core API Design

An efficient EF Core query still needs to fit an efficient API workflow. Avoid loading far more data than the HTTP response requires.


ASP.NET Core API
       ↓
Application Service
       ↓
EF Core Query
       ↓
SQL Server
       ↓
Small DTO / Response
                

Clear application boundaries make it easier to measure SQL time, identify slow endpoints and replace a data-access implementation where appropriate.


19. Cache Stable POS Reference Data

Product categories, country lists, configuration and other relatively stable reference data can sometimes be cached where the business rules allow it.


var settings =
    await cache.GetOrCreateAsync(
        "pos-reference-data",
        async entry =>
        {
            entry.AbsoluteExpirationRelativeToNow =
                TimeSpan.FromMinutes(30);

            return await repository
                .GetReferenceDataAsync();
        });
                

Do not use the same strategy for dynamic sale state, wallet balances or other data where stale information could produce an incorrect business result.


20. Log Slow EF Core Operations

Performance troubleshooting is much easier when the application can identify which workflow was slow.


logger.LogInformation(
    "Wallet history loaded for company {CompanyId}",
    companyId);
                

Combine application-level operation logging with database diagnostics and avoid logging sensitive customer, payment or authentication information unnecessarily.


21. Benchmark EF Core Against Real POS Workloads

A query that performs well on a development database may behave differently when the application contains a large product catalog, many sales, years of debt transactions and substantial wallet history.

  • Test realistic row counts.
  • Test representative search parameters.
  • Measure SQL execution time and logical reads.
  • Test concurrent cashier and administration workloads.
  • Compare generated SQL before and after optimization.
  • Measure the complete API or desktop workflow as well as the database query.

Entity Framework Core Performance Optimization Checklist

  • Have you measured the generated SQL and execution time?
  • Are read-only queries using no-tracking where appropriate?
  • Are projections returning only the required POS data?
  • Are unnecessary Include calls and N+1 patterns avoided?
  • Are large product, customer and sales lists paginated?
  • Do SQL Server indexes support the actual EF Core query patterns?
  • Are transactions and DbContext lifetimes kept focused?
  • Are bulk workloads using an appropriate data-access strategy?
  • Are EF Core and Dapper each used where they best fit the workload?
  • Have you measured realistic POS workloads before introducing advanced optimizations?

Conclusion

Effective Entity Framework Core performance optimization is not about applying one setting to every query. It is about understanding the SQL generated by the application, controlling object tracking and loading, returning only the required data and aligning the queries with SQL Server's indexing and execution behavior.

WinPOS is a useful example because a Windows POS application has a mixture of workloads: product and customer lookups, sales and payment processing, customer debt, wallet transactions and synchronization. Those workloads do not all require the same data-access technique.

EF Core can provide productive entity-oriented access while Dapper and ADO.NET remain valuable for targeted SQL and specialized database operations. The strongest enterprise .NET architecture is usually the one that measures the workload and chooses the simplest technology that fits it.


About the Author

I'm Kshitij Mehta, a Senior .NET Developer and ASP.NET Core Consultant with over 14 years of experience designing and developing enterprise software solutions.

My work includes C#, ASP.NET Core, SQL Server, Dapper, Entity Framework, WPF desktop applications, POS systems, logistics platforms, shipping software and business applications.

This article focuses on practical Entity Framework Core performance techniques that can be applied to real database-driven enterprise .NET applications.