Some Best Practices in EF core
1. Not using AsNoTracking()
EF Core implements the Unit of Work pattern, keeping track of changes to our entities and then ultimately sending those changes back to our database engine. The approach has many advantages when modifying data, but comes with a noticeable performance cost. Instantiating a DbContext instance is a relatively cheap operation, where most of the cost of using EF Core comes from expression parsing, network operations, and object tracking. We can opt-out of object tracking by using the AsNoTracking method on our LINQ queries. Opting out of object tracking can speed up read scenarios like read-only endpoints on an HTTP API.
var customers = await _dbContext.Customers
.AsNoTracking()
.ToListAsync(ct);
2. Getting all columns in a query
EF Core makes it very easy to query out entity instances, and then use those instances in code. However, querying entity instances can frequently pull back more data than necessary from your database.
var customers = await _dbContext.Customers
.AsNoTracking().ToListAsync(ct);
Notice that we are returning all columns from the dbo.Customers table? That’s so the entity in C# has got all the data. Sometimes this is what we want, but if we are only interested in a few properties, then we are returning data we do not need which can add to memory overhead. If we are only interested in the customer name and phone number, we can use add the Select LINQ operation or the ProjectToType<T>()
method in the Mapster library.
public class Customer
{
public string Id { get; set; } = Guid.NewGuid().ToString("N");
public string FirstName { get; set; } = null!;
public string LastName { get; set; } = null!;
public string PhoneNumber { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime? UpdatedAt { get; set; }
public bool IsDeleted { get; set; }
}
var customers = await _dbContext.Customers
.AsNoTracking()
.ProjectToType<GetCustomerResponse>()
.ToListAsync(ct);
public class GetCustomerResponse
{
public string? FirstName { get; set; }
public string? LastName { get; set; }
public string FullName => $"{FirstName} {LastName}";
punlic string PhoneNumber { get; set; }
}
The above method shown is called Projection. This concept when used returns smaller data sizes and better execution times.
3. No Pagination
Pagination refers to retrieving results in pages, rather than all at once; this is typically done for large result sets, where a user interface is shown that allows the user to navigate to the next or previous page of the results. A common way to implement pagination with databases is to use the Skip and Take LINQ operators.
var customers = await _dbContext.Customers
.AsNoTracking()
.Skip(filter.PageIndex * filter.PageSize)
.Take(filter.PageSize)
.ProjectToType<GetCustomerResponse>()
.ToListAsync(ct);
4. Non-Cancellable Queries
Cancellation tokens are a powerful mechanism in .NET Core for controlling the cancellation of asynchronous operations.They are used to signal that an operation should be canceled, allowing you to gracefully stop the operation without causing exceptions or leaks. This is particularly useful in scenarios where long-running database operations need to be canceled to free up resources, handle timeouts, or respond to user actions such as pressing a cancel button in a user interface. In EF Core, many asynchronous methods accept a CancellationToken as a parameter. This includes methods like SaveChangesAsync, FindAsync, ToListAsync, and many others. By passing a CancellationToken to these methods, you can request cancellation of the database operation.
var customers = await _dbContext.Customers
.AsNoTracking()
.Skip(filter.PageIndex * filter.PageSize)
.Take(filter.PageSize)
.ProjectToType<GetCustomerResponse>()
.ToListAsync(cancellationToken);
When an operation is canceled, an OperationCanceledException is thrown. You should handle this exception to perform any necessary cleanup or user notifications. Using CancellationToken effectively helps in building responsive applications that can handle long-running operations gracefully, providing a better user experience and more efficient resource management.
5. Casting IQueryable to IEnumerable
Casting IQueryable to IEnumerable in Entity Framework Core can lead to several issues related to performance, deferred execution, and query composition. Here is a detailed explanation of the problems that can arise from such a cast:
Understanding IQueryable and IEnumerable
IQueryable:
- Represents a query against a data source.
- Supports LINQ operations that are translated to the data source's query language (e.g., SQL for a relational database).
- Provides deferred execution, meaning the query is not executed until the data is actually enumerated (e.g., with ToList(), ToArray(), or a foreach loop). IEnumerable*:
- Represents a collection of objects that can be enumerated.
- Does not support the same level of query translation as IQueryable.
- When enumerating an IEnumerable, any associated query has already been executed and the data is in memory.
IEnumerable:
- Represents a collection of objects that can be enumerated.
- Does not support the same level of query translation as IQueryable.
- When enumerating an IEnumerable, any associated query has already been executed and the data is in memory.
Problems with Casting IQueryable to IEnumerable:
- Loss of Query Composition and Deferred Execution When you cast an IQueryable to an IEnumerable, the query is executed immediately, and the result is loaded into memory. This breaks the deferred execution model of IQueryable.
IQueryable<Customers> query = context.Customers;
IEnumerable<Customers> enumerable = (IEnumerable<Customers>)query;
After casting to IEnumerable, further LINQ operations (e.g., Where, Select) will be performed in memory, rather than being translated to SQL and executed on the database server. This can lead to inefficient queries and poor performance, especially for large data sets.
- Increased Memory Usage Casting IQueryable to IEnumerable causes the query to execute and load all the results into memory immediately. For large result sets, this can lead to high memory usage and potential performance degradation.
- Inability to Optimize Queries Once the data is loaded into memory, you lose the ability to optimize queries using database indexes, execution plans, and other database engine features. The database is generally much more efficient at filtering and processing data than the application layer.
Example Scenario Consider the following example where casting to IEnumerable can lead to inefficiencies:
IQueryable<Customers> query = context.Customers;
// Cast to IEnumerable
IEnumerable<Customers> enumerable = (IEnumerable<Customers>)query;
// Further filtering in memory
var filteredCustomers = enumerable.Where(p => p.Price > 100);
Here’s what happens:
- The entire customers table is loaded into memory when casting to IEnumerable.
- The Where clause is then applied in memory, filtering the products.
This is inefficient because:
- The initial query retrieves all products, which could be a very large set.
- The filtering is done in memory, which is much slower and less efficient than filtering in the database.
Correct Approach To avoid these issues, keep the operations in the IQueryable context until you need to execute the query and materialize the results:
IQueryable<Product> query = context.Customers;
// Apply filtering in the database
var filteredCustomers = query.Where(p => !p.IsDeleted).ToList();
In this case:
- The Where clause is translated to SQL and executed on the database server.
- Only the filtered products are retrieved and loaded into memory.
6. Efficient Updates and Deletes
Efficient updates and deletes in Entity Framework Core are crucial for maintaining application performance and ensuring that database operations are executed optimally. Here are some best practices and techniques to perform efficient updates and deletes:
Efficient Updates:
- Bulk Updates For updating multiple records efficiently, consider using bulk update operations. eg
context.Blogs
.Where(b => b.Rating < 3)
.ExecuteUpdate(setters => setters.SetProperty(b => b.IsVisible, false));
- Partial Updates Update only the necessary fields to avoid unnecessary data modifications. This can be done by attaching the entity to the context and then marking specific properties as modified.
public async Task UpdateProductPriceAsync(int productId, decimal newPrice)
{
using var context = new ApplicationDbContext();
var product = new Product { Id = productId, Price = newPrice };
context.Products.Attach(product);
context.Entry(product).Property(p => p.Price).IsModified = true;
await context.SaveChangesAsync();
}
- Batching Updates Batching updates can reduce the number of database round trips. EF Core automatically batches commands when SaveChanges or SaveChangesAsync is called.
public async Task BatchUpdateProductsAsync(List<Product> products)
{
using var context = new ApplicationDbContext();
foreach (var product in products)
{
context.Products.Update(product);
}
await context.SaveChangesAsync();
}
Efficient Deletes:
- Bulk Deletes Similar to bulk updates, bulk deletes can be performed using third-party libraries.
context.Blogs.Where(b => b.Rating < 3).ExecuteDelete();
- Conditional Deletes Use the RemoveRange method for conditional deletes, which can be more efficient than removing entities one by one.
public async Task DeleteOldProductsAsync(DateTime cutoffDate)
{
using var context = new ApplicationDbContext();
var oldProducts = context.Products.Where(p => p.CreatedDate < cutoffDate);
context.Products.RemoveRange(oldProducts);
await context.SaveChangesAsync();
}
- Soft Deletes Instead of physically deleting records, mark them as deleted using a flag. This approach can be beneficial for audit purposes and reduces the risk of data loss.
public async Task SoftDeleteProductAsync(int productId)
{
using var context = new ApplicationDbContext();
var product = await context.Products.FindAsync(productId);
if (product != null)
{
product.IsDeleted = true;
await context.SaveChangesAsync();
}
}
Further Readings
Follow this link to learn more.
Was this page helpful?
CHAT SAMMIAT