Skip to content

Instantly share code, notes, and snippets.

@smitpatel
Last active October 15, 2021 17:39
Show Gist options
  • Save smitpatel/d4cb3619e5b33e8d9ea24d3f2a88333a to your computer and use it in GitHub Desktop.
Save smitpatel/d4cb3619e5b33e8d9ea24d3f2a88333a to your computer and use it in GitHub Desktop.
This app shows how to do split queries for multiple levels of collection include in EF Core 3.0
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.EntityFrameworkCore;
namespace EFSampleApp
{
public class Program
{
public static void Main(string[] args)
{
using (var db = new MyContext())
{
// Recreate database
db.Database.EnsureDeleted();
db.Database.EnsureCreated();
// Seed database
db.AddRange(new Customer
{
Address = new Address(),
Orders = new List<Order>
{
new Order
{
OrderDiscount = new OrderDiscount(),
OrderDetails = new List<OrderDetail>
{
new OrderDetail(),
new OrderDetail()
}
},
new Order
{
OrderDiscount = new OrderDiscount(),
OrderDetails = new List<OrderDetail>
{
new OrderDetail(),
new OrderDetail()
}
},
new Order
{
OrderDiscount = new OrderDiscount()
},
new Order()
},
},
new Customer
{
Address = new Address()
},
new Customer());
db.SaveChanges();
}
using (var db = new MyContext())
{
// Run queries
// Tracking and Buffered
Console.WriteLine("Tracking & Buffering");
var query = db.Customers.Include(c => c.Address);
var result = query.ToList();
query.Include(c => c.Orders).ThenInclude(o => o.OrderDiscount).SelectMany(c => c.Orders).Load();
query.SelectMany(c => c.Orders).SelectMany(o => o.OrderDetails).Load();
// Following code is just to print out, above will run queries and stitch up graph
// Since Include is not used for collection navigations,
// the collection properties may be null if no related objects & not initialized
foreach (var customer in result)
{
Console.WriteLine($"CustomerId: {customer.Id}");
Console.WriteLine($"Customer Address: {customer.Address?.Id}");
if (customer.Orders != null)
{
Console.WriteLine($"Customer Orders.Count: {customer.Orders.Count}");
foreach (var order in customer.Orders)
{
Console.WriteLine($"OrderId: {order.Id}");
Console.WriteLine($"Order OrderDiscount: {order.OrderDiscount?.Id}");
if (order.OrderDetails != null)
{
Console.WriteLine($"Order OrderDetails.Count: {order.OrderDetails?.Count}");
foreach (var orderDetail in order.OrderDetails)
{
Console.WriteLine($"OrderDetailId: {orderDetail.Id}");
}
}
}
}
}
}
using (var db = new MyContext())
{
// Run queries
// Tracking and non-buffered
Console.WriteLine("Tracking & Non-buffering");
var customers = db.Customers.Include(c => c.Address);
var orders = customers.Include(c => c.Orders).ThenInclude(o => o.OrderDiscount).SelectMany(c => c.Orders).GetEnumerator();
orders.MoveNext();
var orderDetails = customers.SelectMany(c => c.Orders).SelectMany(o => o.OrderDetails).GetEnumerator();
orderDetails.MoveNext();
// Above will run queries and get enumerators, following code will actually enumerate.
// The following code blocks will move each enumerators upto the point it is needed to generate the current result
// Since Include is not used for collection navigations,
// the collection properties may be null if no related objects & not initialized
foreach (var customer in customers)
{
Console.WriteLine($"CustomerId: {customer.Id}");
Console.WriteLine($"Customer Address: {customer.Address?.Id}");
while (orders.Current?.CustomerId == customer.Id)
{
// Enumerate orders as long as the order is related to customer
if (!orders.MoveNext())
{
break;
}
}
if (customer.Orders != null)
{
Console.WriteLine($"Customer Orders.Count: {customer.Orders.Count}");
foreach (var order in customer.Orders)
{
Console.WriteLine($"OrderId: {order.Id}");
Console.WriteLine($"Order OrderDiscount: {order.OrderDiscount?.Id}");
while (orderDetails.Current?.OrderId == order.Id)
{
// Enumerate orderDetails as long as the orderDetail is related to order
if (!orderDetails.MoveNext())
{
break;
}
}
if (order.OrderDetails != null)
{
Console.WriteLine($"Order OrderDetails.Count: {order.OrderDetails.Count}");
foreach (var orderDetail in order.OrderDetails)
{
Console.WriteLine($"OrderDetailId: {orderDetail.Id}");
}
}
}
}
}
orders.Dispose();
orderDetails.Dispose();
}
using (var db = new MyContext())
{
// Run queries
// Non-tracking
Console.WriteLine("Non-tracking");
var customers = db.Customers.Include(c => c.Address).AsNoTracking();
var orders = customers.Include(c => c.Orders).ThenInclude(o => o.OrderDiscount).SelectMany(c => c.Orders)
.Select(o => new
{
// We connect order to related customer by comparing value of FK to PK.
// If FK property is not shadow then this custom projection is not necessary as you can access o.CustomerId
// If FK property is shadow then project out FK value and use it for comparison.
o.CustomerId, // For shadow property use EF.Property<int>(o, "CustomerId")
o
}).GetEnumerator();
orders.MoveNext();
var orderDetails = customers.SelectMany(c => c.Orders).SelectMany(o => o.OrderDetails)
.Select(od => new
{
od.OrderId,
od
})
.GetEnumerator();
orderDetails.MoveNext();
// Above will run queries and get enumerators, following code will actually enumerate.
// The following code blocks will move each enumerators upto the point it is needed to generate the current result
// And stitch up navigations.
// If you want to buffer the result, create collection to store top level objects.
foreach (var customer in customers)
{
Console.WriteLine($"CustomerId: {customer.Id}");
Console.WriteLine($"Customer Address: {customer.Address?.Id}");
customer.Orders = new List<Order>();
while (orders.Current?.CustomerId == customer.Id)
{
// Add order to collection
customer.Orders.Add(orders.Current.o);
// Set inverse navigation to customer
orders.Current.o.Customer = customer;
// Enumerate orders as long as the order is related to customer
if (!orders.MoveNext())
{
break;
}
}
Console.WriteLine($"Customer Orders.Count: {customer.Orders.Count}");
foreach (var order in customer.Orders)
{
Console.WriteLine($"OrderId: {order.Id}");
Console.WriteLine($"Order OrderDiscount: {order.OrderDiscount?.Id}");
order.OrderDetails = new List<OrderDetail>();
while (orderDetails.Current?.OrderId == order.Id)
{
// Add orderDetail to collection
order.OrderDetails.Add(orderDetails.Current.od);
// Set inverse navigation to order
orderDetails.Current.od.Order = order;
// Enumerate orderDetails as long as the orderDetail is related to order
if (!orderDetails.MoveNext())
{
break;
}
}
Console.WriteLine($"Order OrderDetails.Count: {order.OrderDetails.Count}");
foreach (var orderDetail in order.OrderDetails)
{
Console.WriteLine($"OrderDetailId: {orderDetail.Id}");
}
}
}
orders.Dispose();
orderDetails.Dispose();
}
Console.WriteLine("Program finished.");
}
}
public class MyContext : DbContext
{
public DbSet<Customer> Customers { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
// Select 1 provider
optionsBuilder
.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=_ModelApp;Trusted_Connection=True;Connect Timeout=5;ConnectRetryCount=0;MultipleActiveResultSets=true");
}
}
public class Customer
{
public int Id { get; set; }
public Address Address { get; set; }
public List<Order> Orders { get; set; }
}
public class Address
{
public int Id { get; set; }
public int CustomerId { get; set; }
public Customer Customer { get; set; }
}
public class Order
{
public int Id { get; set; }
public int CustomerId { get; set; }
public Customer Customer { get; set; }
public OrderDiscount OrderDiscount { get; set; }
public List<OrderDetail> OrderDetails { get; set; }
}
public class OrderDetail
{
public int Id { get; set; }
public int OrderId { get; set; }
public Order Order { get; set; }
}
public class OrderDiscount
{
public int Id { get; set; }
public int OrderId { get; set; }
public Order Order { get; set; }
}
}
@smitpatel
Copy link
Author

query.Include(c => c.Orders).ThenInclude(o => o.OrderDiscount).Skip(10).Take(50).SelectMany(c => c.Orders).Load();

@jkatsiotis
Copy link

Thanks!

@Dunge
Copy link

Dunge commented Jun 8, 2020

When adding a Take(1) parameter to the original query to simulate a FirstOrDefault() call, all subsequent queries done with SelectMany/Load will also reselect all base properties of the first query in a join with related data. Without Take(1), it only select the related data making it substantially faster.

var query = context.Container.Include("CommunicationSetting").Where(d => d.Id == id).Take(1);
var result = query.ToList();
query.Include(t => t.Labels).SelectMany(t => t.Labels).Load();

Will be much slower than:

var query = context.Container.Include("CommunicationSetting").Where(d => d.Id == id);
var result = query.ToList();
query.Include(t => t.Labels).SelectMany(t => t.Labels).Load();

When using SQL Profiler, I clearly see in the first case that all Container properties are selected a second time when stepping over the line loading Labels. In the second example, they aren't.

Any idea why?

@bwn-z
Copy link

bwn-z commented Jun 23, 2020

I think you need to add a stopwatch to all blocks of running queries. The user must monitor the result of executed queries in different blocks. User can make decisions about the approach used, depending on the performance.

@NN89
Copy link

NN89 commented Oct 15, 2021

Oh sorry, gotcha. Load all base reference nav in the original query (a join make sense in this case), but collections navs with a separate call. With SelectMany, the properties aren't duplicated.

I'm still having issues with inheritance though, can't use SelectMany if the base type doesn't possess the collections. Edit: A Simple OfType<>() seems to do.

I am facing a similar issue with SelectMany if the base type doesn't possess the collections. I wasn't able to figure out your Edit portion of the comment. Can you give me an example of how you address that?

@Dunge
Copy link

Dunge commented Oct 15, 2021

Your best bet would be to upgrade to EFCore5/EFCore6 and use AsSplitQuery() instead of all that, but if you are stuck on EFCore3:

context.BaseEntitySet.OfType<DerivedEntity>().Include(e => e.YourList).SelectMany(t => t.YourList).Load();  

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment