Nextended.EF
Entity Framework Core extensions for enhanced database operations and query capabilities.
Overview
Nextended.EF provides powerful extensions for Entity Framework Core, including graph loading, automatic inclusion of related entities, alternate query matching, and simplified multi-level includes.
Installation
dotnet add package Nextended.EF
dotnet add package Microsoft.EntityFrameworkCore
Key Features
1. Load Graph Async
Automatically load entity graphs with navigation properties to a specified depth, preventing circular references.
using Nextended.EF;
using Microsoft.EntityFrameworkCore;
// Load an entity with all its navigation properties up to depth 2
var user = await dbContext.Users.FindAsync(userId);
await dbContext.LoadGraphAsync(user, maxDepth: 2);
// Now user.Orders, user.Address, and nested properties are loaded
2. Include All Extensions
Automatically include all navigation properties without manually specifying each one.
using Nextended.EF;
// Include all navigation properties
var users = await dbContext.Users
.IncludeAll(dbContext)
.ToListAsync();
// Include all except specific paths
var users = await dbContext.Users
.IncludeAll(dbContext, new[] { "Orders.OrderItems", "Profile.Avatar" })
.ToListAsync();
// Include all except specific properties using expressions
var users = await dbContext.Users
.IncludeAll(dbContext, u => u.Orders, u => u.Profile)
.ToListAsync();
// For DbSet (context is automatically retrieved)
var products = await dbContext.Products
.IncludeAll(excludePaths: new[] { "Category.Products" })
.ToListAsync();
3. Multi-Level Include Helpers
Simplified syntax for chaining multiple ThenInclude operations.
using Nextended.EF;
// Chain multiple includes at once
var orders = await dbContext.Orders
.MultiInclude(
q => q.Include(o => o.OrderItems),
oi => oi.Product,
oi => oi.Product.Category,
oi => oi.Discounts
)
.ToListAsync();
// Simplified nested includes for collections
var users = await dbContext.Users
.Include(
u => u.Orders,
o => o.OrderItems,
oi => oi.Product
)
.ToListAsync();
4. Alternate Query Match Extensions
Advanced query matching capabilities for flexible search operations.
using Nextended.EF;
using Microsoft.EntityFrameworkCore;
// Search across multiple properties (requires AlternateQueryMatchExtensions)
var searchTerm = "john";
var users = await dbContext.Users
.AlternateQueryMatch(searchTerm)
.ToListAsync();
// This will match users where any text property contains "john"
Usage Examples
Load Complete Entity Graphs
using Nextended.EF;
using Microsoft.EntityFrameworkCore;
public class OrderRepository
{
private readonly ApplicationDbContext _context;
public OrderRepository(ApplicationDbContext context)
{
_context = context;
}
public async Task<Order> GetOrderWithAllRelatedDataAsync(int orderId)
{
var order = await _context.Orders.FindAsync(orderId);
// Load all navigation properties up to 3 levels deep
// Prevents circular references and N+1 queries
await _context.LoadGraphAsync(order, maxDepth: 3);
return order;
// Order now includes:
// - Order.Customer
// - Order.OrderItems
// - Order.OrderItems[].Product
// - Order.OrderItems[].Product.Category
// And so on, up to depth 3
}
public async Task<List<User>> SearchUsersAsync(string searchTerm)
{
return await _context.Users
.AlternateQueryMatch(searchTerm)
.OrderBy(u => u.Name)
.ToListAsync();
}
}
Using IncludeAll for Automatic Loading
public class ProductRepository
{
private readonly ApplicationDbContext _context;
public async Task<List<Product>> GetAllProductsWithRelationsAsync()
{
// Automatically include all navigation properties
return await _context.Products
.IncludeAll(dbContext)
.ToListAsync();
// Loads Product.Category, Product.Supplier, Product.Reviews, etc.
}
public async Task<List<Product>> GetProductsExcludingReviewsAsync()
{
// Include all except Reviews to avoid loading large collections
return await _context.Products
.IncludeAll(_context, new[] { "Reviews", "Reviews.User" })
.ToListAsync();
}
public async Task<List<Product>> SearchProductsAsync(
string searchTerm,
decimal? minPrice = null,
decimal? maxPrice = null)
{
var query = _context.Products
.IncludeAll(_context, p => p.Reviews) // Exclude reviews
.AlternateQueryMatch(searchTerm);
if (minPrice.HasValue)
{
query = query.Where(p => p.Price >= minPrice.Value);
}
if (maxPrice.HasValue)
{
query = query.Where(p => p.Price <= maxPrice.Value);
}
return await query
.OrderByDescending(p => p.CreatedDate)
.ToListAsync();
}
}
Multi-Level Includes Made Easy
public class OrderService
{
private readonly ApplicationDbContext _context;
public async Task<List<Order>> GetOrdersWithDetailsAsync()
{
// Traditional way - verbose
var ordersOld = await _context.Orders
.Include(o => o.OrderItems)
.ThenInclude(oi => oi.Product)
.Include(o => o.OrderItems)
.ThenInclude(oi => oi.Discounts)
.Include(o => o.OrderItems)
.ThenInclude(oi => oi.Product)
.ThenInclude(p => p.Category)
.ToListAsync();
// Nextended way - concise
var orders = await _context.Orders
.MultiInclude(
q => q.Include(o => o.OrderItems),
oi => oi.Product,
oi => oi.Product.Category,
oi => oi.Discounts
)
.ToListAsync();
return orders;
}
public async Task<Order> GetOrderWithAllDataAsync(int orderId)
{
// Load single order with all related data
var order = await _context.Orders
.Include(o => o.Customer, c => c.Address, a => a.City)
.Include(o => o.OrderItems, oi => oi.Product, p => p.Category)
.FirstOrDefaultAsync(o => o.Id == orderId);
return order;
}
}
Complete Data Loading Example
public class ReportService
{
private readonly ApplicationDbContext _context;
public async Task<UserReport> GenerateUserReportAsync(int userId)
{
// Find user
var user = await _context.Users.FindAsync(userId);
if (user == null)
return null;
// Load complete graph of related data
await _context.LoadGraphAsync(user, maxDepth: 3);
// Now we have access to:
// - user.Orders
// - user.Orders[].OrderItems
// - user.Orders[].OrderItems[].Product
// - user.Profile
// - user.Address
// All loaded in a single efficient operation
return new UserReport
{
User = user,
TotalOrders = user.Orders.Count,
TotalSpent = user.Orders.Sum(o => o.Total),
RecentOrders = user.Orders.OrderByDescending(o => o.Date).Take(5).ToList()
};
}
public async Task<List<User>> GetFilteredUsersAsync(UserFilter filter)
{
var query = _context.Users
.IncludeAll(_context, u => u.Orders); // Include all except Orders to control data size
// Apply search term if provided
if (!string.IsNullOrWhiteSpace(filter.SearchTerm))
{
query = query.AlternateQueryMatch(filter.SearchTerm);
}
// Apply date filters
if (filter.CreatedAfter.HasValue)
{
query = query.Where(u => u.CreatedDate >= filter.CreatedAfter.Value);
}
if (filter.CreatedBefore.HasValue)
{
query = query.Where(u => u.CreatedDate <= filter.CreatedBefore.Value);
}
// Apply sorting
query = filter.SortBy switch
{
"name" => query.OrderBy(u => u.Name),
"date" => query.OrderByDescending(u => u.CreatedDate),
"email" => query.OrderBy(u => u.Email),
_ => query.OrderBy(u => u.Id)
};
// Apply pagination
if (filter.PageSize > 0)
{
query = query
.Skip((filter.Page - 1) * filter.PageSize)
.Take(filter.PageSize);
}
return await query.ToListAsync();
}
}
public class UserFilter
{
public string SearchTerm { get; set; }
public DateTime? CreatedAfter { get; set; }
public DateTime? CreatedBefore { get; set; }
public string SortBy { get; set; }
public int Page { get; set; } = 1;
public int PageSize { get; set; } = 20;
}
## Best Practices
### 1. Use AsNoTracking for Read-Only Queries
```csharp
// Better performance for read-only scenarios
var users = await _context.Users
.AsNoTracking()
.IncludeAll(_context)
.AlternateQueryMatch(searchTerm)
.ToListAsync();
2. Avoid N+1 Query Problems with LoadGraphAsync
// Bad - N+1 queries
var users = await _context.Users.ToListAsync();
foreach (var user in users)
{
var orders = await _context.Orders
.Where(o => o.UserId == user.Id)
.ToListAsync();
}
// Good - Use IncludeAll or LoadGraphAsync
var users = await _context.Users
.IncludeAll(_context)
.ToListAsync();
// Or for a single entity
var user = await _context.Users.FindAsync(userId);
await _context.LoadGraphAsync(user, maxDepth: 2);
3. Control Depth to Avoid Loading Too Much Data
// Be careful with depth - higher values load more data
var order = await _context.Orders.FindAsync(orderId);
// Depth 1: Load direct navigation properties only
await _context.LoadGraphAsync(order, maxDepth: 1);
// Depth 3: Load navigation properties, their properties, and one more level
await _context.LoadGraphAsync(order, maxDepth: 3);
4. Exclude Large Collections with IncludeAll
// Exclude collections that might be very large
var products = await _context.Products
.IncludeAll(_context, new[] { "Reviews", "OrderItems" })
.ToListAsync();
// Or use expressions
var products = await _context.Products
.IncludeAll(_context, p => p.Reviews, p => p.OrderItems)
.ToListAsync();
5. Use Pagination for Large Datasets
public async Task<PagedResult<User>> GetPagedUsersAsync(int page, int pageSize)
{
var query = _context.Users
.IncludeAll(_context, u => u.Orders); // Exclude large collections
var total = await query.CountAsync();
var users = await query
.OrderBy(u => u.Name)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
return new PagedResult<User>
{
Items = users,
TotalCount = total,
Page = page,
PageSize = pageSize
};
}
Configuration
DbContext Setup
using Microsoft.EntityFrameworkCore;
using Nextended.EF;
public class ApplicationDbContext : DbContext
{
public DbSet<User> Users { get; set; }
public DbSet<Order> Orders { get; set; }
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// Configure entities
modelBuilder.Entity<User>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Email).IsRequired().HasMaxLength(256);
entity.HasMany(e => e.Orders)
.WithOne(o => o.User)
.HasForeignKey(o => o.UserId);
});
}
}
Supported Frameworks
- .NET 8.0
- .NET 9.0
Dependencies
Nextended.Core- Core utilities and extensionsMicrosoft.EntityFrameworkCore(9.0+) - EF Core framework
Performance Tips
- Use
AsNoTracking()for read-only queries to avoid change tracking overhead - Use
LoadGraphAsyncwith appropriatemaxDepthto control how much data is loaded - Use
IncludeAllwith exclusions to automatically load relations while avoiding large collections - Implement pagination for large datasets
- Use
MultiIncludeto simplify complex include chains - Use compiled queries for frequently executed queries
- Consider splitting very large queries into smaller batches
- Monitor the depth parameter in
LoadGraphAsync- higher values can significantly impact performance
Related Projects
- Nextended.Core - Foundation library
- Nextended.Web - Includes OData support for EF queries