Top 7 EF Core tips every developer should know – Part #2

Top 7 EF Core tips every developer should know – Part #2

EF Core is a powerful tool for database access in .NET applications, but there’s always more to learn when it comes to writing efficient and maintainable code. In the first part, we covered key optimizations that many developers overlook. Now, let’s dive even deeper.

Here are more EF Core tips to help you improve your code:

 

1- Avoid Repeated SaveChanges Calls

Calling SaveChangesAsync() multiple times can degrade performance by triggering unnecessary database transactions. However, in some cases, the logic may require separate operations that can’t be batched easily.

C#
				if (order.TotalAmount > OrderConstants.HighValueThreshold)
{
    order.Status = OrderConstants.Approved;
    await context.SaveChangesAsync(); // First transaction
}

if (order.TotalAmount > OrderConstants.DiscountThreshold)
{
    order.TotalAmount -= order.TotalAmount * OrderConstants.DiscountRate;
    await context.SaveChangesAsync(); // Second transaction
}

if (order.DeliveryDate <= DateTime.UtcNow)
{
    order.Status = OrderConstants.Processed;
    await context.SaveChangesAsync(); // Third transaction
}
			

Each SaveChangesAsync() call here triggers a separate database transaction, reducing efficiency.

We can apply all modifications before calling SaveChangesAsync(), ensuring a single database transaction for better performance.

C#
				if (order.TotalAmount > OrderConstants.HighValueThreshold)
    order.Status = OrderConstants.Approved;

if (order.TotalAmount > OrderConstants.DiscountThreshold)
    order.TotalAmount -= order.TotalAmount * OrderConstants.DiscountRate;

if (order.DeliveryDate <= DateTime.UtcNow)
    order.Status = OrderConstants.Processed;

// Save all changes in one transaction
await context.SaveChangesAsync();
			

 

2 – Avoid Executing Deletes in a Loop – Use ExecuteDelete Instead

Each time the loop runs, SaveChangesAsync() is called, resulting in multiple delete transactions. This is inefficient and can cause slowdowns, especially if there are many records to delete.

C#
				foreach (var order in orders)
{
    context.Orders.Remove(order);
    await context.SaveChangesAsync();
}
			

Although SaveChanges() is called once after the loop, this still causes Change Tracker overhead as entities need to be loaded and tracked. Additionally, the in-loop removal process may be slower than a direct mass delete in the bank.

C#
				foreach (var order in orders)
{
    context.Orders.Remove(order);
}

await context.SaveChangesAsync()
			

With ExecuteDelete, deletion is performed much more efficiently. The method goes directly to the database and deletes the records at once, without having to load the entities or involve the Change Tracker.

Furthermore, there is less risk of failures and errors on the part of the developer and the database

C#
				var deletedCount = await context.Orders
    .Where(o => o.Status == OrderStatus.Canceled)
    .ExecuteDeleteAsync();
			

 

3 – Avoid Executing Updates in a Loop – Use ExecuteUpdate Instead

Just like ExecuteDelete, ExecuteUpdate is an efficient way to update multiple records directly in the database, Instead of manually fetching and modifying entities before calling SaveChanges(), we can use ExecuteUpdate to apply changes optimally:

C#
				await context.Orders
    .Where(o => o.Status == OrderStatus.Pending)
    .ExecuteUpdateAsync(setters => setters
        .SetProperty(o => o.Status, OrderStatus.Processing));

			

If you need to combine ExecuteUpdate with other changes to the context, use a transaction to ensure atomicity.

4 – Use Pagination for Large Datasets

Without paging, fetching all records can consume a lot of memory and processing time, pagination allows users to find information in a structured way, without having to deal with gigantic lists.

C#
				var pagedResults = await context.Products
    .AsNoTracking()
    .Where(p => p.Id > lastId)
    .Take(pageSize)
    .ToListAsync();
			

Keyset pagination, An index is used to execute a seek operation at the start of the desired page, It’s not the ideal solution for every situation, but it can be highly beneficial in many scenarios.

This is the visualization using SQL:

SQL
				SELECT TOP (@pageSize) *
FROM Products
WHERE Id > @lastId
ORDER BY Id;
			

If you need to easily navigate to any page of results (e.g. page 3, 10, etc.) or go back to page, OFFSET is simpler. It allows you to directly specify the page and number of items per page.

C#
				var paginatedData = await context.Users
    .AsNoTracking()
    .Where(u => u.IsActive)
    .OrderBy(u => u.Id)
    .Skip((pageNumber - 1) * pageSize)
    .Take(pageSize)
    .ToListAsync();
			

This is the visualization using SQL:

SQL
				SELECT *
FROM Users
WHERE IsActive = 1
ORDER BY Id
OFFSET (@pageNumber - 1) * @pageSize ROWS
FETCH NEXT @pageSize ROWS ONLY;
			

 

5 – Use Compiled Queries

Rather than translating LINQ queries into SQL on every execution:

C#
				var orders = context.Orders
    .Where(o => o.Status == OrderStatus.Pending)
    .ToList();

			

EF Core compiles them once and stores the execution plan. This allows subsequent executions of the same query to bypass the translation step, significantly improving performance.

We can precompile queries to avoid this overhead:

C#
				var compiledQuery = EF.CompileQuery((MyDbContext context, OrderStatus status) =>
    context.Orders.Where(o => o.Status == status).ToList());
			

Now, we can call the query without the need for recompilation:

C#
				var orders = compiledQuery(context, OrderStatus.Pending);

			

 

6 – Use EF Core Queries with CancellationToken

When we work with asynchronous operations in EF Core, we can improve the efficiency and scalability of the application using CancellationToken. It allows you to cancel queries when they are no longer needed, freeing up resources and improving performance.

C#
				return await context.Orders
    .Where(o => o.Status == OrderStatus.Pending)
    .ToListAsync(cancellationToken);
			

By passing a CancellationToken to EF Core’s asynchronous methods like ToListAsync, we can stop execution as soon as a cancellation is requested.

7 – Don’t skip EF Core migrations

Migrations in EF Core allow you to add, modify, or remove database elements (such as tables, columns, indexes) in a programmatic and versioned manner. You can generate files that describe schema changes and apply them in a controlled way, without the need to write SQL manually.

PowerShell
				dotnet ef migrations add AddNewColumnToOrders

dotnet ef database update

			

Using EF Core Migrations is an excellent practice for keeping track of database schema changes in an organized and secure way.

Important point: Although EF Core and Migrations are powerful, we can’t help but learn SQL and tuning. Knowing how to optimize queries and analyze execution plans is essential to ensure good database performance.

Conclusion

I hope these tips on EF Core migrations, performance optimizations, and best practices have provided valuable insights to improve your development workflow. Remember, mastering the tools and understanding the underlying SQL can make a significant difference in building efficient and scalable applications.

Feel free to share your thoughts or let me know if you’d like me to explore more topics on EF Core!

Let’s keep refining our code, step by step.

Thank you for reading until the end!

See you next time!

Share the Post:
plugins premium WordPress