Nextended.EF
Entity Framework Core extensions: graph loading, declarative includes, paging & sorting, query-comfort helpers, DbContext utilities and bulk operations.
Installation
dotnet add package Nextended.EF
dotnet add package Microsoft.EntityFrameworkCore
Feature Overview
| Area | API |
|---|---|
| Graph loading | DbContext.LoadGraphAsync, DbSet.IncludeAll, DbSet.MultiInclude |
| Declarative includes | IncludeDefinitionFor<T>, IncludePathDefinition, IIncludePathDefinition, AttributeIncludePathDefinition<T>, CompositeIncludePathDefinition, PrefixedIncludePathDefinition, FilteredIncludePathDefinition, IncludeDetails, ThenIncludeDetails, Without / WithoutPrefix / WithoutRegex / Except |
| Querying | WhereContains, WhereKeyMatches, WhereBetween, WhereIn, WhereIf |
| Paging & sorting | Page, ToPagedResultAsync, OrderByMember, ThenByMember, OrderByMembers, PagedResult<T> |
| Conditional query | IncludeIf, AsTrackingIf, AsNoTrackingIf, ExistsAsync |
| DbContext helpers | FindEntityType<T>, GetPrimaryKeyPropertyNames<T>, GetPrimaryKeyValues<T>, DetachAll, IsTrackedBy, GetOrAddAsync, GetOrCreateAsync |
| Bulk | BulkInsertAsync, BulkDeleteWhereAsync, UpsertAsync, UpsertRangeAsync |
Graph Loading
LoadGraphAsync
Walks the navigation graph of a single entity and loads everything reachable within maxDepth. Cycle-safe via a visited set.
using Nextended.EF;
var order = await db.Orders.FindAsync(orderId);
await db.LoadGraphAsync(order, maxDepth: 2);
// order.Customer, order.Customer.Address, order.Lines, order.Lines[].Product, …
IncludeAll
Walks the EF model and adds an Include per navigation. Navigations on the dependent side (IsOnDependent) are skipped so cycles are avoided — typically you only get the principal-side collections.
// Everything reachable from the principal side
var bob = await db.Customers.IncludeAll().FirstAsync(c => c.Id == 2);
// Exclude specific paths
var products = await db.Products
.IncludeAll(new[] { "Reviews", "Reviews.User" })
.ToListAsync();
// Exclude by expression
var users = await db.Customers.IncludeAll(c => c.Orders).ToListAsync();
MultiInclude
Compact alternative to repeated Include(...).ThenInclude(...) chains.
var customers = db.Customers.MultiInclude(
c => c.Include(x => x.Orders),
o => o.Lines,
o => o.Customer!);
Declarative Includes — IncludeDefinitionFor<T>
A reusable, composable description of which navigations to load. Definitions are framework-agnostic (live in Nextended.Core.IncludeDefinitions) and become real Include strings when applied via IncludeDetails.
Building a definition
using Nextended.Core.IncludeDefinitions;
using Nextended.EF;
var def = new IncludeDefinitionFor<Customer>()
.Include(c => c.Address)
.Include(c => c.Orders)
.IncludeWithPrefix<Customer, Order>(
c => c.Orders,
new IncludePathDefinition().Include("Lines", "Lines.Product"));
// def.GetPaths() → ["Address", "Orders", "Orders.Lines", "Orders.Lines.Product"]
Applying it
var customers = await db.Customers.IncludeDetails(def).ToListAsync();
// Or on a sub-navigation while building the query:
var orders = await db.Orders
.Include(o => o.Customer)
.ThenIncludeDetails(o => o.Customer!.Address, addressDef)
.ToListAsync();
Reusable + filterable
// Combine two definitions
var combined = new CompositeIncludePathDefinition(customerDef, orderDef);
// Strip subtrees
var lean = def.WithoutPrefix("Orders");
// Drop by expression
var noOrders = def.Without<Customer>(c => c.Orders);
// Glob & regex filtering
var noProducts = def.Without("Orders.Lines.Product");
var noNestedLines = def.WithoutRegex(@"^Orders\.Lines\..*$");
var noTransitive = def.Without("Orders.**");
Attribute-driven
Mark navigations with [IncludeInDetails] and let the definition discover them.
public class Customer
{
public int Id { get; set; }
[IncludeInDetails] public virtual Address? Address { get; set; }
[IncludeInDetails] public virtual ICollection<Order> Orders { get; set; } = new List<Order>();
}
var def = new AttributeIncludePathDefinition<Customer>(maxDepth: 3);
var customers = await db.Customers.IncludeDetails(def).ToListAsync();
Discover by reflection
// Pull in everything virtual (typical "lazy-load convention")
var allVirtual = new IncludeDefinitionFor<Customer>()
.IncludeAllVirtual(maxDepth: 4, includeCollections: true);
// Or any predicate
var onlyAudited = new IncludeDefinitionFor<Customer>()
.IncludeAllWhere(p => p.PropertyType.IsClass && p.Name.EndsWith("History"));
Querying
WhereContains
Case-insensitive substring search across one or more string properties.
var hits = await db.Customers
.WhereContains("cargo", c => c.Name, c => c.Email)
.ToListAsync();
WhereKeyMatches
Match a stringified key against the entity’s primary key (or candidate name properties), with auto-coercion to string / Guid / int.
// Auto-detect PK from the model
var hits = await db.Customers.WhereKeyMatches(db, "2").ToListAsync();
// Or against arbitrary candidate property names
var orders = db.Orders
.WhereKeyMatches(new[] { nameof(Order.OrderNumber) }, "ORD-101");
// Or against selector expressions
var matches = db.Customers
.WhereKeyMatches(new Expression<Func<Customer, object>>[]
{
c => c.Id,
c => c.Email!,
}, "alice@example.com");
WhereBetween / WhereIn / WhereIf
var winter = db.Orders.WhereBetween(o => o.CreatedAt,
new DateTime(2025, 1, 1), new DateTime(2025, 3, 31));
var subset = db.Customers.WhereIn(c => c.Id, new[] { 1, 3, 7 });
// Build queries up incrementally without ugly if/else trees
var query = db.Customers
.WhereIf(!string.IsNullOrWhiteSpace(filter.Term), c => c.Name.Contains(filter.Term!))
.WhereIf(filter.MinCredit.HasValue, c => c.CreditLimit >= filter.MinCredit!.Value);
ExistsAsync
var hasAlice = await db.Customers.ExistsAsync(c => c.Name == "Alice");
Paging & Sorting
Page + ToPagedResultAsync
PagedResult<T> carries Items, TotalCount, PageIndex, PageSize, TotalPages, HasNext, HasPrevious.
var page = await db.Orders
.OrderBy(o => o.Id)
.ToPagedResultAsync(pageIndex: 0, pageSize: 25);
// page.Items, page.TotalCount, page.HasNext, …
// If you already have a count, just slice:
var slice = db.Orders.OrderBy(o => o.Id).Page(2, 25);
Dynamic sorting
String-based ordering (e.g. from a UI table header) with support for dotted property paths and case-insensitive matching.
var customers = db.Customers
.OrderByMember("name") // case-insensitive
.ThenByMember(nameof(Customer.CreditLimit), descending: true)
.ToList();
// Multi-column from any source
var orderings = new[]
{
(nameof(Order.CustomerId), false),
(nameof(Order.TotalCost), true),
};
var sorted = db.Orders.OrderByMembers(orderings);
// Nested paths work too
var sorted = db.Orders.OrderByMember("Customer.Address.City");
Throws ArgumentException if the property doesn’t exist.
Conditional query helpers
var query = db.Customers
.IncludeIf(opts.LoadAddress, c => c.Address)
.IncludeIf(opts.LoadOrders, "Orders")
.AsNoTrackingIf(opts.ReadOnly)
.AsTrackingIf(opts.ForceTracking);
Each helper is a no-op when the condition is false.
DbContext helpers
var entityType = db.FindEntityType<Customer>(); // EF IEntityType
var pkNames = db.GetPrimaryKeyPropertyNames<Customer>(); // e.g. ["Id"]
var pkValues = db.GetPrimaryKeyValues(alice); // composite-key safe
if (db.IsTrackedBy(alice)) { /* … */ }
// Wipe the change tracker (handy between test steps or long-running services)
db.DetachAll();
// Find-or-create. GetOrAddAsync does NOT call SaveChanges — handy when you
// want to insert as part of a larger unit of work. GetOrCreateAsync persists.
var alice = await db.Customers.GetOrAddAsync(
c => c.Email == "alice@example.com",
() => new Customer { Name = "Alice", Email = "alice@example.com" });
var bob = await db.GetOrCreateAsync(
db.Customers,
c => c.Email == "bob@example.com",
() => new Customer { Name = "Bob", Email = "bob@example.com" });
Bulk Operations
Built on EF Core 7+’s ExecuteUpdateAsync / ExecuteDeleteAsync, with a tracker-based fallback for providers that don’t support bulk SQL (e.g. the InMemory provider used in tests).
BulkInsertAsync — batched AddRange + SaveChanges
var products = LoadCatalog(); // tens of thousands of rows
await db.BulkInsertAsync(products, batchSize: 1000);
BulkDeleteWhereAsync
// Single server-side DELETE on relational providers; tracker-based on InMemory.
var removed = await db.OrderLines.BulkDeleteWhereAsync(l => l.UnitPrice < 5m);
UpsertAsync / UpsertRangeAsync
Find by key selector, run updateExisting if found, otherwise insert.
await db.UpsertAsync(
new Product { Id = 2, Name = "Gizmo v2", Price = 25m },
keySelector: p => p.Id,
updateExisting: (existing, incoming) =>
{
existing.Name = incoming.Name;
existing.Price = incoming.Price;
});
await db.UpsertRangeAsync(
incomingProducts,
keySelector: p => p.Id,
updateExisting: (existing, src) =>
{
existing.Name = src.Name;
existing.Price = src.Price;
});
Note: For very large workloads on production-grade providers (SQL Server, PostgreSQL, …) consider a dedicated bulk library —
BulkInsertAsynchere still uses the EF change tracker. The helpers above hit the sweet spot between “obvious” and “fast enough for most cases”.
End-to-end example
public async Task<PagedResult<Customer>> SearchCustomersAsync(
CustomerFilter filter,
CancellationToken ct = default)
{
var query = _db.Customers
.WhereIf(!string.IsNullOrWhiteSpace(filter.Term),
c => c.Name.Contains(filter.Term!) || c.Email!.Contains(filter.Term!))
.WhereIf(filter.MinCredit.HasValue,
c => c.CreditLimit >= filter.MinCredit!.Value)
.IncludeIf(filter.IncludeAddress, c => c.Address)
.IncludeIf(filter.IncludeOrders, c => c.Orders)
.AsNoTrackingIf(filter.ReadOnly);
if (!string.IsNullOrWhiteSpace(filter.SortBy))
query = query.OrderByMember(filter.SortBy, filter.SortDescending);
return await query.ToPagedResultAsync(filter.PageIndex, filter.PageSize, ct);
}
public sealed class CustomerFilter
{
public string? Term { get; set; }
public decimal? MinCredit { get; set; }
public bool IncludeAddress { get; set; }
public bool IncludeOrders { get; set; }
public bool ReadOnly { get; set; } = true;
public string? SortBy { get; set; }
public bool SortDescending { get; set; }
public int PageIndex { get; set; }
public int PageSize { get; set; } = 25;
}
Supported Frameworks
- .NET 8.0 / 9.0 / 10.0
Dependencies
Nextended.CoreMicrosoft.EntityFrameworkCore
Tested with
Nextended.EF.Tests covers the public surface with 60+ MSTest cases against the EF Core InMemory provider — see the Nextended.EF.Tests project in the repo for runnable examples of every helper above.
Related Projects
- Nextended.Core —
IncludeDefinitionFor<T>and friends live here - Nextended.Web — OData support layered on top of
IQueryable