Entity Framework Core Advanced Features
This document delves into advanced features of Entity Framework Core (EF Core) that can help you build more robust, performant, and maintainable data access solutions.
Transactions
EF Core automatically manages transactions for operations that modify the database. When you call SaveChanges()
, EF Core wraps the operations within a database transaction. If any operation fails, the entire transaction is rolled back, ensuring data consistency.
You can also explicitly manage transactions for more control:
using (var context = new MyDbContext())
{
using (var transaction = context.Database.BeginTransaction())
{
try
{
// Perform multiple operations
context.Add(new Product { Name = "New Gadget" });
context.Update(existingItem);
context.SaveChanges();
// If all operations succeed
transaction.Commit();
}
catch (Exception ex)
{
// If any operation fails, rollback
transaction.Rollback();
// Handle the exception
Console.WriteLine($"An error occurred: {ex.Message}");
}
}
}
Concurrency Control
Concurrency control is essential when multiple users or processes might try to update the same data simultaneously. EF Core provides mechanisms to handle this, most commonly through optimistic concurrency.
You can mark a property (e.g., a row version or timestamp) in your entity model that EF Core will use to detect conflicts. When saving changes, EF Core checks if the database value of this property matches the value loaded when the entity was retrieved. If not, an exception (typically DbUpdateConcurrencyException
) is thrown.
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
// Mark a row version for optimistic concurrency
[Timestamp]
public byte[] RowVersion { get; set; }
}
You can then catch this exception and implement strategies like re-fetching data, merging changes, or informing the user.
Performance Tuning
Optimizing EF Core performance is crucial for scalable applications. Key strategies include:
- Efficient Queries: Use LINQ methods that translate effectively to SQL. Avoid loading unnecessary data.
- Asynchronous Operations: Utilize async methods like
ToListAsync()
andSaveChangesAsync()
to prevent blocking the application thread. - Batching: For bulk inserts or updates, consider libraries that support batching operations to reduce database round trips.
- Compiled Queries: For frequently executed queries, compiling them can offer performance benefits.
- Connection Pooling: EF Core uses connection pooling by default, but ensure your database provider is configured correctly.
- Lazy Loading, Eager Loading, Explicit Loading: Choose the appropriate loading strategy for your scenarios to avoid N+1 query problems.
Logging
EF Core provides built-in logging capabilities that are invaluable for debugging and understanding database interactions. You can log SQL commands being executed, parameter values, and more.
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<MyDbContext>(options =>
options.UseSqlServer("YourConnectionString")
.LogTo(Console.WriteLine, LogLevel.Information)); // Log to console
}
You can also configure logging to write to files or other sinks.
Advanced Change Tracking
EF Core meticulously tracks changes to entities. Understanding this process can help you optimize saving operations and diagnose issues.
You can manually control the state of an entity:
// Mark an entity as modified without loading it
context.Entry(product).State = EntityState.Modified;
// Mark an entity as detached
context.Entry(product).State = EntityState.Detached;
EF Core also provides the DetectChanges()
method, which you can call explicitly if you need to ensure all tracked changes are detected before saving.
Executing Raw SQL
Sometimes, you might need to execute raw SQL queries for complex operations or performance optimizations not easily achievable with LINQ. EF Core allows you to do this.
// Execute a query that returns entities
var products = context.Products.FromSqlRaw("SELECT * FROM Products WHERE Category = {0}", "Electronics");
// Execute a command that doesn't return entities
context.Database.ExecuteSqlRaw("UPDATE Products SET Price = Price * 1.1 WHERE Category = {0}", "Books");
Be cautious when using raw SQL, as it bypasses some of EF Core's abstractions and can introduce security vulnerabilities if not handled carefully (e.g., SQL injection).
Calling Stored Procedures
You can call stored procedures from EF Core, either to execute them directly or to map their results to entities.
// Calling a stored procedure with output parameters
var productIdParam = new SqlParameter("@NewProductId", 0) { Direction = System.Data.ParameterDirection.Output };
context.Database.ExecuteSqlRaw("EXEC AddNewProduct @Name, @Price, @NewProductId OUT",
new SqlParameter("@Name", "Super Widget"),
new SqlParameter("@Price", 99.99),
productIdParam);
int newId = (int)productIdParam.Value;
// Calling a stored procedure that returns a set of results
var data = context.MyCustomData.FromSqlInterpolated($"EXEC GetProductsByCategory {categoryName}");
Lazy Loading
Lazy loading allows related entities to be loaded only when they are first accessed. This can simplify code but may lead to performance issues (N+1 problem) if not used judiciously.
To enable lazy loading:
- Install the
Microsoft.EntityFrameworkCore.Proxies
NuGet package. - Configure your
DbContext
to use proxies:options.UseLazyLoadingProxies();
- Make navigation properties
virtual
.
Eager Loading
Eager loading explicitly loads related entities along with the main entity in a single query. This is often more performant than lazy loading when you know you'll need the related data.
var blogs = context.Blogs
.Include(b => b.Posts) // Eager load Posts
.ToList();
You can chain Include()
calls to load multiple levels of related data, and use ThenInclude()
for more complex relationships.
Explicit Loading
Explicit loading provides a middle ground, allowing you to load related data on demand after the main entity has already been retrieved. This avoids the N+1 problem of lazy loading and the potential over-fetching of eager loading.
var blog = context.Blogs.Find(blogId);
// Manually load the Posts for this specific blog
context.Entry(blog).Collection(b => b.Posts).Load();
// Manually load a single related entity
var author = context.Authors.Find(authorId);
context.Entry(author).Reference(a => a.ContactInfo).Load();