Entity Framework Core: Advanced Scenarios
Tip: This section assumes you have a solid understanding of the fundamental concepts of Entity Framework Core.
Introduction to Advanced Scenarios
Entity Framework Core (EF Core) is a powerful Object-Relational Mapper (ORM) that simplifies database access in .NET applications. While basic CRUD operations are straightforward, real-world applications often require more sophisticated approaches to handle complex data interactions, performance optimizations, and integration with existing systems.
This documentation explores several advanced scenarios that can significantly enhance your EF Core development experience and unlock the full potential of the framework.
1. Raw SQL Queries
Sometimes, you need to execute SQL queries directly for performance reasons, to leverage database-specific features, or when EF Core's LINQ translation becomes complex or inefficient. EF Core provides several ways to execute raw SQL.
Executing a SQL Query that Returns Entities
You can execute a raw SQL query and have EF Core map the results to your entity types.
// Using FromSqlInterpolated to execute a SQL query
var products = await _context.Products
.FromSqlInterpolated($"SELECT * FROM Products WHERE Category = {categoryName}")
.ToListAsync();
Executing a SQL Query that Returns Scalar Values
For simple queries that return a single value (e.g., a count or sum), you can use ExecuteSqlRaw
or ExecuteSqlInterpolated
.
int count = await _context.Products.FromSqlRaw("SELECT COUNT(*) FROM Products").CountAsync();
Executing Stored Procedures
EF Core allows you to call stored procedures. The approach depends on whether the stored procedure returns entities or just performs actions.
// Example for a stored procedure that modifies data
_context.Database.ExecuteSqlRaw("EXEC UpdateProductPrice @ProductId, @NewPrice", productId, newPrice);
2. Batch Operations
Executing many individual INSERT, UPDATE, or DELETE statements can be inefficient due to the overhead of round trips to the database. EF Core provides mechanisms for batching these operations.
Using a Batching Provider
Third-party libraries like EntityFrameworkCore.BatchUpdate
can significantly improve the performance of bulk updates and deletes.
// Example using a hypothetical batching provider (syntax may vary)
await _context.Products
.Where(p => p.IsDeleted)
.BatchDeleteAsync();
3. Concurrency Control
Concurrency occurs when multiple users or processes attempt to modify the same data simultaneously. EF Core offers strategies to detect and handle these conflicts.
Optimistic Concurrency
This is the most common approach, where you add a row versioning property (e.g., a timestamp or a `uint` field) to your entity.
In your entity:
public class Product {
public int ProductId { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
[Timestamp] // Or use a RowVersion property
public byte[] RowVersion { get; set; }
}
When SaveChanges
is called and a conflict is detected, an DbUpdateConcurrencyException
is thrown. You can catch this exception and implement a conflict resolution strategy.
try {
await _context.SaveChangesAsync();
} catch (DbUpdateConcurrencyException ex) {
// Handle concurrency conflict, e.g., reload data, prompt user
var entry = ex.Entries.Single();
var databaseValues = await entry.GetDatabaseValuesAsync();
if (databaseValues == null) {
// The entity was deleted by another user
} else {
var databaseProduct = (Product)databaseValues.ToObject();
// Compare databaseProduct with entry.Entity and decide how to resolve
entry.OriginalValues.SetValues(databaseValues); // Reload original values
await _context.SaveChangesAsync(); // Try saving again
}
}
4. Global Query Filters
Global query filters allow you to apply common filtering logic to all queries for a specific entity type, ensuring that certain data is always included or excluded.
A common use case is soft deletion:
public class MyDbContext : DbContext {
public DbSet Products { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder) {
modelBuilder.Entity().HasQueryFilter(p => !p.IsDeleted);
}
// ... other configurations
}
Now, any query on Products
will automatically include WHERE IsDeleted = 0
(or equivalent).
5. Shadow Properties
Shadow properties are properties that are not defined in your entity classes but are mapped to the database schema. This is useful for properties that are managed entirely by the database, such as audit fields.
modelBuilder.Entity().Property<DateTime>("LastModified");
You can then set and retrieve these properties using the Entry
API.
var productEntry = _context.Entry(product);
productEntry.Property<DateTime>("LastModified").CurrentValue = DateTime.UtcNow;
6. Owned Types
Owned types (formerly Complex Types) allow you to map types that don't have their own primary key to columns in the same table as their owner. This is great for encapsulating related data.
In your entity:
public class Order {
public int OrderId { get; set; }
public Address ShippingAddress { get; set; } // ShippingAddress is an owned type
}
public class Address {
public string Street { get; set; }
public string City { get; set; }
public string ZipCode { get; set; }
}
In OnModelCreating
:
modelBuilder.Entity<Order>().OwnsOne(o => o.ShippingAddress, sa => {
sa.Property(a => a.Street).HasColumnName("ShippingStreet");
sa.Property(a => a.City).HasColumnName("ShippingCity");
sa.Property(a => a.ZipCode).HasColumnName("ShippingZipCode");
});
The ShippingAddress
properties will be mapped to columns like ShippingStreet
, ShippingCity
, etc., in the Orders
table.
Conclusion
Mastering these advanced scenarios will empower you to build more robust, performant, and maintainable data access layers with Entity Framework Core. Remember to always profile your queries and choose the approach that best suits your application's specific needs.