Entity Framework - Selective Condition on Included Navigation Property - c#

Assume I have these simplified EF generated entities...
public class PurchaseOrder
{
public int POID {get;set;}
public int OrderID {get;set;}
public int VendorID {get;set;}
public IEnumerable<Order> Orders {get;set;}
}
public class Order
{
public int OrderID {get;set;}
public decimal Price {get;set;}
public IEnumerable<Item> Items {get;set;}
}
public class Item
{
public int OrderID {get; set;}
public string SKU {get;set;}
public int VendorID {get;set;}
public Order Order {get;set;}
}
Business Logic:
An order can have multiple POs, one for each distinct vendor on the order (vendors are determined at the Item level).
How Can I selectively Include Child Entities?
When querying for POs, I want to automatically include child entites for Order and Item.
I accomplish this, using Include()...
Context.PurchaseOrders.Include("Orders.Items");
This does it's job and pulls back related entities, but, I only want to include Item entities whose VendorID matches the VendorID of the PurchaseOrder entity.
With traditional SQL, I'd just include that in the JOIN condition, but EF builds those internally.
What LINQ magic can I use tell EF to apply the condition, without manually creating the JOINs between the entities?

You can't selectively pull back certain child entities that match a certain condition. The best you can do is manually filter out the relevant orders yourself.
public class PurchaseOrder
{
public int POID {get;set;}
public int OrderID {get;set;}
public int VendorID {get;set;}
public IEnumerable<Order> Orders {get;set;}
public IEnumerable<Order> MatchingOrders {
get {
return this.Orders.Where(o => o.VendorId == this.VendorId);
}
}
}

You can't. EF doesn't allow conditions for eager loading. You must either use multiple queries like:
var pos = from p in context.PurchaseOrders.Include("Order")
where ...
select p;
var items = from i in context.Items
join o in context.Orders on new { i.OrderId, i.VendorId}
equals new { o.OrderId, o.PurchaseOrder.VendorId }
where // same condition for PurchaseOrders
select i;
Or you can use projection in single query:
var data = from o in context.Orders
where ...
select new
{
Order = o,
PurchaseOrder = o.PurchaseOrder,
Items = o.Items.Where(i => i.VendorId == o.PurchaseOrder.VendorId)
};

You could use the IQueryable-Extensions here:
https://github.com/thiscode/DynamicSelectExtensions
The Extension builds dynamically an anonymous type. This will be used for projection as described by #Ladislav-Mrnka.
Then you can do this:
var query = query.SelectIncluding( new List<Expression<Func<T,object>>>>(){
//Example how to retrieve only the newest history entry
x => x.HistoryEntries.OrderByDescending(x => x.Timestamp).Take(1),
//Example how to order related entities
x => x.OtherEntities.OrderBy(y => y.Something).ThenBy(y => y.SomeOtherThing),
//Example how to retrieve entities one level deeper
x => x.CollectionWithRelations.Select(x => x.EntityCollectionOnSecondLevel),
//Of course you can order or subquery the deeper level
//Here you should use SelectMany, to flatten the query
x => x.CollectionWithRelations.SelectMany(x => x.EntityCollectionOnSecondLevel.OrderBy(y => y.Something).ThenBy(y => y.SomeOtherThing)),
});

Related

querying base and subtype with different include

I have a type and sub type
public class Reservation
{
public int Id {get; set;}
public TimeSpan Length {get; set;}
}
public class Appointment : Reservation
{
public Customer Customer {get; set;}
}
DbContext
public DbSet<Reservation> Reservations { get; set; }
public DbSet<Appointment> Appointments { get; set; }
table per hierarchy (TPH) is the current setup since it's the only option for ef core 3.1.
Now I would like to query the superset Reservations and include the Customer when a Reservation is Appointment. I tried to use union on IQueryable like this
var reservations = context.Appointments.Include(e => e.Customer)
.Union(context.Reservations)
But I get
InvalidOperationException: 'When performing a set operation, both
operands must have the same Include operations
While it is possible to perform the Union in memory. I would like to perform one-trip-query that saves the day. I might migrate to EntityFramework core 5 if a solution only exists there.
EDIT: https://github.com/dotnet/efcore/issues/16298
I would suggest this approach, not sure it will work with EF Core, since it has problems with Concat/Union. Also your case needs Concat - UNION ALL, not Union - UNION.
var query1 =
context.Appointments.Select(e => new Appointment
{
Id = e.Id,
Length = e.Length,
Cutomer = e.Customer
});
var query2 =
context.Reservations.Select(e => new Appointment
{
Id = e.Id,
Length = e.Length,
});
var query = query1.Concat(query2);
if you need concrete classes, you have to do transformation on the client side. Also in ChangeTracker will be recorder only Customers.

Pull data from multiple tables in one SQL query using LINQ and Entity Framework (Core)

I want to pull data from multiple tables using LINQ in my .NET Core application. Here's an example:
public class Customer {
public Guid Id { get; set; }
public string Name { get; set; }
public DateTime Created { get; set; }
public HashSet<Transaction> Transactions { get; set; }
}
public class Transaction {
public Guid Id { get; set; }
public decimal Amount { get; set; }
public DateTime Created { get; set; }
public Guid CustomerId { get; set; }
public Customer Customer { get; set; }
}
These have a one-to-many relation in my solution. One customer has many transactions and one transaction has one customer. If I wanted to grab the 10 latest transactions and 10 lastest customers in one LINQ query, how would I do that? I've read that .Union() should be able to do it, but it won't work for me. Example:
var ids = _context
.Customers
.OrderByDescending(x => x.Created)
.Take(10)
.Select(x => x.Id)
.Union(_context
.Transactions
.OrderByDescending(x => x.Created)
.Take(10)
.Select(x => x.CustomerId)
)
.ToList();
This gives me two lists of type Guid, but they contain the same elements. Not sure if it's just me who understands this wrong, but it seems a bit weird. I am happy as long as it asks the database once.
You wrote:
I wanted to grab the 10 latest transactions and 10 latest customers in one LINQ query
It is a bit unclear what you want. I doubt that you want one sequence with a mix of Customers and Transactions. I guess that you want the 10 newest Customers, each with their last 10 Transactions?
I wonder why you would deviate from the entity framework code first conventions. If your class Customer represents a row in your database, then surely it doesn't have a HashSet<Transaction>?
A one-to-many of a Customer with his Transactions should be modeled as follows:
class Customer
{
public int Id {get; set;}
... // other properties
// every Customer has zero or more Transactions (one-to-many)
public virtual ICollection<Transaction> Transactions {get; set;}
}
class Transaction
{
public int Id {get; set;}
... // other properties
// every Transaction belongs to exactly one Customer, using foreign key
public int CustomerId {get; set;}
public virtual Customer Customer {get; set;}
}
public MyDbContext : DbContext
{
public DbSet<Customer> Customers {get; set;}
public DbSet<Transaction> Transactions {get; set;}
}
This is all that entity framework needs to know to detect the tables you want to create, to detect your one-to-many relationship, and to detect the primary keys and foreign keys. Only if you want different names of tables or columns, you'll need attributes and/or fluent API
The major differences between my classes and yours, is that the one-to-many relation is represented by virtual properties. The HashSet is an ICollection. After all, your Transactions table is a collection of rows, not a HashSet
In entity framework the columns of your tables are represented by non-virtual properties; the virtual properties represent the relations between the tables (one-to-many, many-to-many, ...)
Quite a lot of people tend to (group-)join tables, when they are using entity framework. However, life is much easier if you use the virtual properties
Back to your question
I want (some properties of) the 10 newest Customers, each with (several properties of) their 10 latest Transactions
var query = dbContext.Customers // from the collection of Customer
.OrderByDescending(customer => customer.Created) // order this by descending Creation date
.Select(customer => new // from every Customer select the
{ // following properties
// select only the properties you actually plan to use
Id = Customer.Id,
Created = Customer.Created,
Name = Customer.Name,
...
LatestTransactions = customer.Transactions // Order the customer's collection
.OrderBy(transaction => transaction.Created) // of Transactions
.Select(transaction => new // and select the properties
{
// again: select only the properties you plan to use
Id = transaction.Id,
Created = transaction.Created,
...
// not needed you know it equals Customer.Id
// CustomerId = transaction.CustomerId,
})
.Take(10) // take only the first 10 Transactions
.ToList(),
})
.Take(10); // take only the first 10 Customers
Entity framework knows the one-to-many relationship and recognizes that a group-join is needed for this.
One of the slower parts of your query is the transfer of the selected data from the DBMS to your local process. Hence it is wise to limit the selected data to the data you actually plan to use. If Customer with Id 4 has 1000 Transactions, it would be a waste to transfer the foreign key for every Transaction, because you know it has value 4.
If you really want to do the join yourself:
var query = dbContext.Customers // GroupJoin customers and Transactions
.GroupJoin(dbContext.Transactions,
customer => customer.Id, // from each Customer take the primary key
transaction => transaction.CustomerId, // from each Transaction take the foreign key
(customer, transactions) => new // take the customer with his matching transactions
{ // to make a new:
Id = customer.Id,
Created = customer.Created,
...
LatestTransactions = transactions
.OrderBy(transaction => transaction.Created)
.Select(transaction => new
{
Id = transaction.Id,
Created = transaction.Created,
...
})
.Take(10)
.ToList(),
})
.Take(10);
Try following. I models you database _context as a class just so I could test the syntax. Remember that one customer may map to more than one transaction. You may want to use GroupBy ID so you get 10 different customers.
class Program
{
static void Main(string[] args)
{
Context _context = new Context();
var ids = (from c in _context.customers
join t in _context.transactions on c.Id equals t.CustomerId
select new { c = c, t = t})
.OrderByDescending(x => x.c.Created)
.Take(10)
.ToList();
}
}
public class Context
{
public List<Customer> customers { get; set; }
public List<Transaction> transactions { get; set; }
}
public class Customer
{
public Guid Id { get; set; }
public string Name { get; set; }
public DateTime Created { get; set; }
public HashSet<Transaction> Transactions { get; set; }
}
public class Transaction
{
public Guid Id { get; set; }
public decimal Amount { get; set; }
public DateTime Created { get; set; }
public Guid CustomerId { get; set; }
public Customer Customer { get; set; }
}
You may want to try this instead :
var ids = (from c in _context.customers
join t in _context.transactions on c.Id equals t.CustomerId
select new { c = c, t = t})
.OrderByDescending(x => x.c.Created)
.GroupBy(x => x.c.Id)
.SelectMany(x => x.Take(10))
.ToList();
Eliminating the Join will speed up results. You always can get the customer info in another query.
var transactions = _context.transactions
.OrderByDescending(x => x.Created)
.GroupBy(x => x.CustomerId)
.Select(x => x.Take(10))
.ToList();
try this:
var customers = customerService.GetAll().OrderByDescending(c => c.Created).Take(10).ToList().AsQueryable();
var transactions = transactionService.GetAll().OrderByDescending(t => t.Created).Take(10).ToList().AsQueryable();
transactions = transactions.Where(t => customers.Any(c => c.CustomerId == t.Id));

Update a row adding a relationed row in EF (many-to-many)

I have two entities: Order and Item and a many-to-many relationship between them. I have a method which receive the ItemId as parameter like this:
public void AddItems(int OrderId, int ItemId)
{
Item item = db.ItemSet.SingleOrDefault(i => i.Id == ItemId);
Order order = db.OrderSet.SingleOrDefault(o => o.Id == OrderId);
order.Items.Add(item);
db.SaveChanges();
}
There are a lot of rows in the ItemSet table, so the first query is a heavy one. Is there a way that I can add the item to the order without doing a query first on the "ItemSet" table? I mean, can I add the ItemId directly to the Order.Items or something like that?
Item item = new Item { ID = ItemId };
Order order = new Order { ID = OrderId };
db.ItemSet.Attach(item);
db.OrderSet.Attach(order);
order.Items.Add(item);
db.SaveChanges();
Just be sure that you have this in your Order class:
public Order()
{
Items = new List<Item>();
}
So you don't get null pointer exception in order.Items.Add(item);
So you can model the entities, so:
public DbSet<Item> ItemSet {get;set;}
public DbSet<Order> OrderSet {get;set;}
public DbSet<ItemOrder> ItemOrders {get;set;}
public class Item
{
public int Id {get;set;}
}
public class Order
{
public int Id {get;set;}
}
public class ItemOrder
{
[Key, Column(Order = 0)]
public int ItemId {get;set;}
[Key, Column(Order = 1)]
public int OrderId {get;set;}
}
So in order to update:
public void AddItems(int OrderId, int ItemId)
{
var itemExists = db.ItemOrders.FirstOrDefault(x => x.OrderId == OrderId && x.ItemId == ItemId);
if (itemExists != null) return;
db.ItemOrders.Add(new ItemOrder { OrderId = OrderId, ItemId = ItemId });
db.SaveChanges();
}
Then you can query using the LINQ .Join() and is pretty straightforward.
Just for completeness sake the OP mentioned that the navigation properties have now disappeared. So if you know the OrderId then you can simple do:
var items = db.ItemOrders.GroupJoin(
db.ItemSet,
io => io.ItemId,
i => i.Id,
(itemOrder, items) => items)
.ToList();
So to walk you through, GroupJoin asks for the IQueryable/IEnumerable of data you want to join against.
The second param is the field of the initial data set i.e. ItemOrders the set you want to join to.
The third parameter is the field of the set that is being joined in i.e. ItemSet.
The fouth parameter is basically a Select where you are presented with a Func signature of roughly (ItemOrder itemInFirstSet, IEnumerable<Item> itemsThatAreJoined).
You can apply this same logic to the LINQ Join extension. But rather than itemsThatAreJoined it is itemThatIsJoined. So the distinction is GroupJoin will find multiple entities that match, Join will only find one.
This is no way a detailed explanation, more of an overview

Creating LINQ statement against an EF Context with no relationships

I cannot wrap my head around how to write a linq query against my EF context to get what I want.
1) What I have
Database with no foreign keys assigned, and a reverse engineered code first entity framework project. I tried manually adding virtual classes so EF might create implied foreign keys in the DBcontext, but I get errors on my .Include statements still.
Without the include the only thing I can think of is to use left joins, but I haven't gotten it down yet. In the end there will be 21 tables I have to get data from, but the following table outline encapsulates the majority of issues i'm facing.
Sample data structure:
Table Human: HumanId, LastFoodEatenId, FavoriteFoodId, CurrentlyDesiredFoodId
Table Food: FoodId, FoodName, FoodStuff
Table Toys: HumanOwnerId, ToyId, ToyName
Table Pets: HumanOwnerId, PetId, PetName, PetType
Table PetSurgery: PetId, SurgeryId, SurgeryPerformed
2) What I want
Given a HumanID, I want a compsite class or something like it from a single query.
Public Class QueryResult
{
public Human human {get;set;}
public Food LastEatenFood {get;set;}
public Food FavoriteFood {get;set;}
public Food CurrentlyDesiredFood {get;set;}
public IEnumerable<Toy> Toys {get;set;}
public IEnumerable<Pet> Pets {get;set;} //Includes surgeries if any
}
Is it even possible to write a single query to get this kind of information in a single db hit? I'd be fine is someone simply confirmed it is't possible. Then I can at least request we add relationships to our database.
Thanks in advance,
You can use linq to query multiple, non-related tables.
I'm going to assume a LOT about your entities, but here we go...
int humanId = 1234;
using (var context = new MyContext())
{
var human = (from h in context.Humans
join lf in context.Foods on h.LastFoodEatenId equals lf.foodId into lfg
from lf in lfg.DefaultIfEmpty() // left join
join ff in context.Foods on h.FavoriteFoodId equals lf.foodId into ffg
from ff in ffg.DefaultIfEmpty() // left join
join cf in context.Foods on h.CurrentlyDesiredFoodId equals lf.foodId into cfg
from cf in cfg.DefaultIfEmpty() // left join
join p in context.Pets on h.humanId equals p.humanId into pg // group
join t in context.Toys on h.humanId equals t.humanId into tg // group
where h.humanId = humanId
select new QueryResult { human = h, LastEatenFood = lf, FavoriteFood = ff, CurrentlyDesiredFood = cf, Toys = tg, Pets = pg }
).SingleOrDefault();
}
Note: I'm doing this from memory without a syntax checker, so ymmv. Adding surgeries should be possible as well, but may require a subquery.
I tried manually adding virtual classes
I assume you mean virtual collections. You can define relationships in a "code-first" model if they are not in the database. The only condition is that foreign keys must refer to properties that EF knows as primary keys. So you should be able to do LINQ queries using navigation properties in stead of these verbose joins by a model like this (reduced to the essentials):
class Human
{
public int HumanId { get; set; }
public int LastFoodEatenId { get; set; }
public virtual Food LastEatenFood { get; set; }
public int FavoriteFoodId { get; set; }
public virtual Food FavoriteFood { get; set; }
public int CurrentlyDesiredFoodId { get; set; }
public virtual Food CurrentlyDesiredFood { get; set; }
public virtual ICollection<Toy> Toys { get; set; }
public virtual ICollection<Pet> Pets { get; set; }
}
class Food
{
public int FoodId { get; set; }
}
class Pet
{
public int PetId { get; set; }
public int HumanOwnerId { get; set; }
}
class Toy
{
public int ToyId { get; set; }
public int HumanOwnerId { get; set; }
}
And a mapping:
class HumanMapping : EntityTypeConfiguration<Human>
{
public HumanMapping()
{
HasOptional(h => h.LastEatenFood).WithMany()
.HasForeignKey(h => h.LastFoodEatenId);
HasOptional(h => h.FavoriteFood).WithMany()
.HasForeignKey(h => h.FavoriteFoodId);
HasOptional(h => h.CurrentlyDesiredFood).WithMany()
.HasForeignKey(h => h.CurrentlyDesiredFoodId);
HasMany(h => h.Toys).WithOptional().HasForeignKey(t => t.HumanOwnerId);
HasMany(h => h.Pets).WithOptional().HasForeignKey(t => t.HumanOwnerId);
}
}
EF will infer the primary keys by name conventions.
Now you will be able to execute a LINQ statement like:
context.Humans.Where(h => h.HumanId == id)
.Include(h => h.LastEatenFood)
.Include(h => h.FavoriteFood)
.Include(h => h.CurrentlyDesiredFood)
.Include(h => h.Toys)
.Include(h => h.Pets)
From your description I understand that PetSurgery should be a junction class between Pet and another class (Surgery?). Anyway, I think you will manage creating the correct mappings, seeing this example.

Limit child entities without limiting parent entities - NHibernate

I'm trying to limit the result set of a mapped collection.
Here is a simple model:
public class Table1 {
public virtual long Id { get; set; }
public virtual IList<Table2> Table2s { get; set; }
}
public class Table2 {
public virtual long Id { get; set; }
public virtual long Table1Id { get; set; }
public virtual Table1 Table1 { get; set; }
public virtual string Field { get; set; }
}
public class Table1Map : ClassMap<Table1> {
public Table1Map () {
Table("Table1");
Id(x => x.Id).Column("Id").Not.Nullable().CustomType("Int64").GeneratedBy.Native();
HasMany<Table2>(x => x.Table2s).Inverse().Not.LazyLoad().KeyColumns.Add("Table1Id").Fetch.Join();
}
}
public class Table2Map : ClassMap<Table2> {
public Table2Map () {
Table("Table2");
Id(x => x.Id).Column("Id").Not.Nullable().CustomType("Int64").GeneratedBy.Native();
Map(x => x.Table1Id).Column("Table1Id").Not.Nullable().CustomType("Int64");
Map(x => x.Field).Column("Field").Not.Nullable().CustomType("AnsiString").Length(25);
References<Table1>(x => x.Table1, "Table1Id").Cascade.None();
}
}
I want to select all Table1s. I also want to select all Table2s that meet a certain criteria (Table2.Field = 'value'), but I don't want to limit my Table1s, so select null Table2s if they don't meet the criteria. If I want to do this in SQL I'd do the following:
SELECT *
FROM
Table1
LEFT OUTER JOIN Table2 ON Table1.Id = Table2.Table1Id
WHERE
Table2.Field = 'value' or Table2.Field IS NULL
How should I structure my NHibernate query to achieve the desired result? I'd like a list of Table1s, and within each Table1 I'd like either an empty list of Table2s (because no Table2s met the criteria), or a list of Table2s that met the creteria.
I'm trying something like the following, but this will obviously not work:
List<Table1> result = new List<Table1>();
IQueryable<Table1> query = session.Query<Table1>();
if (value != null) {
query = query.Where(x => x.Table2s.Field == value);
}
query = query.OrderBy(x => x.Id);
result = query.ToList();
I think this is not possible the way you do this. Hibernate loads the complete entity with all its properties (if not lazyloading is activated).
What should hibernate do, if you save such a loaded entity of type table1 without all table2's?
You should create some kind of viewobject (dvo) that contains the relevant parts of table1 and a list of table2 childs that fit the criteria. The select could possible be done by projection.
There is pretty good documentation - 16.4. Associations
http://nhibernate.info/doc/nh/en/index.html#queryqueryover-associations
The QueryOver syntax would look like this
IQueryOver<Table1, Table2> myQuery =
session.QueryOver<Table1>()
.Left.JoinQueryOver<Table2>(t => t.Table2s)
.Where(
Restrictions.Or(
Restrictions.On<Table2>((t2) => t2.ID).IsNull,
Restrictions.On<Table2>((t2) => t2.Field).IsLike("value")
)
);
var list = myQuery.List<Table1>();
then the list will return the collection of all combinations meeting the criteria. (Later order by or distinct or other porjections could be added...)

Categories

Resources