Entity Framework (EF) has been a cornerstone for .NET developers, providing a robust ORM (Object-Relational Mapping) framework to interact with databases using .NET objects. While many developers are familiar with the basics, there are several lesser-known tricks and best practices that can significantly enhance your EF experience. In this article, we will explore some of these hidden gems and also highlight new features introduced in the latest version of Entity Framework Core (EF Core 6.0 and EF Core 7.0).
Leveraging Global Query Filters
Real-Time Scenario
Imagine you are developing a multi-tenant application where each tenant should only see their own data. Implementing this manually in every query can be error-prone and cumbersome.
Feature Explanation
Global Query Filters, introduced in EF Core 2.0, allow you to define filters that apply to all queries for a given entity type. This is particularly useful for implementing multi-tenancy, soft deletes, or any scenario where you need to filter data globally.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Customer>()
.HasQueryFilter(c => !c.IsDeleted);
}
Table Schema
SQL Script
CREATE TABLE Customers (
Id INT PRIMARY KEY,
Name NVARCHAR(100),
IsDeleted BIT
);
Table Format
| Column Name | Data Type | Description |
|---|---|---|
| Id | INT | Primary key |
| Name | NVARCHAR(100) | Name of the customer |
| IsDeleted | BIT | Indicates if the customer is deleted |
Using Value Conversions
Real-Time Scenario
Suppose you have a custom type for representing monetary values in your domain model, but you need to store these values as decimals in the database.
Feature Explanation
Value Conversions, introduced in EF Core 2.1, enable you to map custom types to database types. This is useful when you have domain-specific types that need to be stored in a database-friendly format.
public class Money
{
public decimal Amount { get; set; }
public string Currency { get; set; }
public override string ToString() => $"{Amount} {Currency}";
public static Money Parse(string value)
{
var parts = value.Split(' ');
return new Money { Amount = decimal.Parse(parts[0]), Currency = parts[1] };
}
}
modelBuilder.Entity<Order>()
.Property(o => o.TotalAmount)
.HasConversion(
v => v.ToString(), // Convert to string for storage
v => Money.Parse(v) // Convert back to custom type
);
Table Schema
SQL Script
CREATE TABLE Orders (
Id INT PRIMARY KEY,
CustomerId INT,
TotalAmount NVARCHAR(50),
FOREIGN KEY (CustomerId) REFERENCES Customers(Id)
);
Table Format
| Column Name | Data Type | Description |
|---|---|---|
| Id | INT | Primary key |
| CustomerId | INT | Foreign key to Customers table |
| TotalAmount | NVARCHAR(50) | Custom monetary value stored as string |
Compiled Queries for Performance
Real-Time Scenario
You have a high-traffic application where certain queries are executed frequently, and you need to optimize performance to handle the load.
Feature Explanation
Compiled Queries, introduced in EF Core 2.0, can significantly improve the performance of frequently executed queries by pre-compiling the query plan.
private static readonly Func<YourDbContext, int, Customer> _compiledQuery =
EF.CompileQuery((YourDbContext context, int id) =>
context.Customers.Single(c => c.Id == id));
public Customer GetCustomerById(int id)
{
return _compiledQuery(_context, id);
}
Table Schema
SQL Script
CREATE TABLE Customers (
Id INT PRIMARY KEY,
Name NVARCHAR(100)
);
Table Format
| Column Name | Data Type | Description |
|---|---|---|
| Id | INT | Primary key |
| Name | NVARCHAR(100) | Name of the customer |
Interceptors for Advanced Scenarios
Real-Time Scenario
You need to implement custom logging for all database commands executed by your application to comply with auditing requirements.
Feature Explanation
Interceptors, introduced in EF Core 3.0, allow you to hook into various stages of EF’s operation, such as command execution, saving changes, and more. This is useful for logging, auditing, or modifying behavior dynamically.
public class MyCommandInterceptor : DbCommandInterceptor
{
public override InterceptionResult<int> NonQueryExecuting(
DbCommand command, CommandEventData eventData, InterceptionResult<int> result)
{
// Log or modify the command here
return base.NonQueryExecuting(command, eventData, result);
}
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.AddInterceptors(new MyCommandInterceptor());
}
Table Schema
No specific schema changes are required for interceptors.
Temporal Tables for Historical Data
Real-Time Scenario
Your application needs to maintain a history of changes to certain entities for auditing and compliance purposes.
Feature Explanation
Temporal Tables, supported by EF Core 6.0, allow you to track changes to your data over time. This is useful for auditing and historical analysis.
modelBuilder.Entity<Customer>()
.ToTable("Customers", b => b.IsTemporal());
Table Schema
SQL Script
CREATE TABLE Customers (
Id INT PRIMARY KEY,
Name NVARCHAR(100),
ValidFrom DATETIME2 GENERATED ALWAYS AS ROW START,
ValidTo DATETIME2 GENERATED ALWAYS AS ROW END,
PERIOD FOR SYSTEM_TIME (ValidFrom, ValidTo)
) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.CustomersHistory));
Table Format
| Column Name | Data Type | Description |
|---|---|---|
| Id | INT | Primary key |
| Name | NVARCHAR(100) | Name of the customer |
| ValidFrom | DATETIME2 | Start of the validity period |
| ValidTo | DATETIME2 | End of the validity period |
New Features in the Latest Entity Framework
a. Many-to-Many Relationships
Real-Time Scenario
You are developing a library management system where books can have multiple authors, and authors can write multiple books. Modeling this relationship manually can be tedious.
Feature Explanation
The latest version of EF Core (EF Core 5.0) introduces native support for many-to-many relationships without needing a join entity.
modelBuilder.Entity<Author>()
.HasMany(a => a.Books)
.WithMany(b => b.Authors);
Table Schema
SQL Script
CREATE TABLE Authors (
Id INT PRIMARY KEY,
Name NVARCHAR(100)
);
CREATE TABLE Books (
Id INT PRIMARY KEY,
Title NVARCHAR(100)
);
CREATE TABLE AuthorBook (
AuthorId INT,
BookId INT,
PRIMARY KEY (AuthorId, BookId),
FOREIGN KEY (AuthorId) REFERENCES Authors(Id),
FOREIGN KEY (BookId) REFERENCES Books(Id)
);
Table Format
Authors Table
| Column Name | Data Type | Description |
|---|---|---|
| Id | INT | Primary key |
| Name | NVARCHAR(100) | Name of the author |
Books Table
| Column Name | Data Type | Description |
|---|---|---|
| Id | INT | Primary key |
| Title | NVARCHAR(100) | Title of the book |
AuthorBook Table
| Column Name | Data Type | Description |
|---|---|---|
| AuthorId | INT | Foreign key to Authors table |
| BookId | INT | Foreign key to Books table |
b. Improved LINQ Translation
Real-Time Scenario
You have complex LINQ queries that need to be translated into efficient SQL to ensure optimal performance.
Feature Explanation
EF Core 5.0 and later versions have improved their LINQ translation capabilities, allowing for more complex queries to be translated into efficient SQL.
CustomerIdINTForeign key to Customers tableTotalAmountNVARCHAR(50)Custom monetary value stored as string
c. Split Queries for Related Data
Real-Time Scenario
You need to load large datasets with related data without running into performance issues caused by the N+1 query problem.
Feature Explanation
Split Queries, introduced in EF Core 5.0, allow you to load related data in multiple queries, reducing the risk of the N+1 query problem and improving performance for large result sets.
var data = context.Customers
.Include(c => c.Orders)
.AsSplitQuery()
.ToList();
Table Schema
SQL Script
CREATE TABLE Customers (
Id INT PRIMARY KEY,
Name NVARCHAR(100)
);
CREATE TABLE Orders (
Id INT PRIMARY KEY,
CustomerId INT,
TotalAmount NVARCHAR(50),
FOREIGN KEY (CustomerId) REFERENCES Customers(Id)
);
Table Format
Customers Table
| Column Name | Data Type | Description |
|---|---|---|
| Id | INT | Primary key |
| Name | NVARCHAR(100) | Name of the customer |
Orders Table
| Column Name | Data Type | Description |
|---|---|---|
| Id | INT | Primary key |
| CustomerId | INT | Foreign key to Customers table |
| TotalAmount | NVARCHAR(50) | Custom monetary value stored as string |
d. Savepoints for Transactions
Real-Time Scenario
You are performing a series of operations within a transaction and need to create intermediate points to roll back to in case of errors.
Feature Explanation
Savepoints, introduced in EF Core 7.0, allow you to create intermediate points within a transaction, providing more control over transaction management.
using var transaction = context.Database.BeginTransaction();
try
{
// Perform some operations
var savepoint = transaction.CreateSavepoint("BeforeCriticalOperation");
// Perform critical operation
transaction.RollbackToSavepoint("BeforeCriticalOperation");
transaction.Commit();
}
catch
{
transaction.Rollback();
}
Table Schema
No specific schema changes are required for savepoints.
e. Primitive Collections
Real-Time Scenario
You need to store a list of primitive values, such as strings or integers, directly within an entity without creating a separate table.
Feature Explanation
Primitive Collections, introduced in EF Core 6.0, allow you to store collections of primitive types directly within an entity.
public class Product
{
public int Id { get; set; }
public List<string> Tags { get; set; }
}
modelBuilder.Entity<Product>()
.Property(p => p.Tags)
.HasConversion(
v => string.Join(',', v), // Convert list to comma-separated string
v => v.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList() // Convert string back to list
);
Table Schema
SQL Script
CREATE TABLE Products (
Id INT PRIMARY KEY,
Name NVARCHAR(100),
Tags NVARCHAR(MAX)
);
Table Format
| Column Name | Data Type | Description |
|---|---|---|
| Id | INT | Primary key |
| Name | NVARCHAR(100) | Name of the product |
| Tags | NVARCHAR(MAX) | Comma-separated list of tags |
f. HierarchyId Support
Real-Time Scenario
You are working with hierarchical data, such as organizational structures or file systems, and need to efficiently manage and query this data.
Feature Explanation
HierarchyId support, introduced in EF Core 7.0, allows you to work with hierarchical data types in SQL Server.
public class Category
{
public int Id { get; set; }
public HierarchyId HierarchyId { get; set; }
}
modelBuilder.Entity<Category>()
.Property(c => c.HierarchyId)
.HasConversion(
v => v.ToString(), // Convert HierarchyId to string for storage
v => HierarchyId.Parse(v) // Convert string back to HierarchyId
);
Table Schema
SQL Script
CREATE TABLE Categories (
Id INT PRIMARY KEY,
Name NVARCHAR(100),
HierarchyId HIERARCHYID
);
Table Format
| Column Name | Data Type | Description |
|---|---|---|
| Id | INT | Primary key |
| Name | NVARCHAR(100) | Name of the category |
| HierarchyId | HIERARCHYID | Hierarchical data identifier |
g. Efficient Bulk Operations
Real-Time Scenario
You need to perform bulk insert, update, or delete operations efficiently to handle large datasets.
Feature Explanation
Efficient Bulk Operations, supported by third-party libraries like EFCore.BulkExtensions, allow you to perform bulk operations with high performance.
context.BulkInsert(products);
context.BulkUpdate(products);
context.BulkDelete(products);
Table Schema
SQL Script
CREATE TABLE Products (
Id INT PRIMARY KEY,
Name NVARCHAR(100)
);
Table Format
| Column Name | Data Type | Description |
|---|---|---|
| Id | INT | Primary key |
| Name | NVARCHAR(100) | Name of the product |
h. JSON Column Enhancements
Real-Time Scenario
You need to store and query JSON data within your database, leveraging the flexibility of JSON columns.
Feature Explanation
JSON Column Enhancements, introduced in EF Core 6.0, provide improved support for storing and querying JSON data.
public class Customer
{
public int Id { get; set; }
public string JsonData { get; set; }
}
var query = context.Customers
.Where(c => EF.Functions.JsonContains(c.JsonData, "{\"key\":\"value\"}"))
.ToList();
Table Schema
SQL Script
CREATE TABLE Customers (
Id INT PRIMARY KEY,
Name NVARCHAR(100),
JsonData NVARCHAR(MAX)
);
Table Format
| Column Name | Data Type | Description |
|---|---|---|
| Id | INT | Primary key |
| Name | NVARCHAR(100) | Name of the customer |
| JsonData | NVARCHAR(MAX) | JSON data stored as string |
Sample JSON
{
"key": "value",
"nested": {
"subkey": "subvalue"
},
"array": [1, 2, 3]
}
Conclusion
Entity Framework continues to evolve, offering powerful features and capabilities that can greatly enhance your data access layer. By leveraging these lesser-known tricks and best practices, you can write more efficient, maintainable, and robust code. Stay updated with the latest features and continuously explore the depths of EF to unlock its full potential.