EF Core - many queries sent to database for subquery - linq

Using EF Core 2.2.2, I have a table in my database which is used to store notes for many other tables. In other words, it's sortof like a detail table in a master-detail relationship, but with multiple master tables. Consider this simplified EF Model:
public class Person
{
public Guid PersonID { get; set; }
public string Name { set; set; }
}
public class InvoiceItem
{
public Guid InvoiceItemID { get; set; }
public Guid InvoiceID { get; set; }
public string Description { get; set; }
}
public class Invoice
{
public Guid InvoiceID { get; set; }
public int InvoiceNumber { get; set; }
public List<Item> Items { get; set; }
}
public class Notes
{
public Guid NoteID { get; set; }
public Guid NoteParentID { get; set; }
public DateTime NoteDate { get; set; }
public string Note { get; set; }
}
In this case, Notes can store Person notes or Invoice notes (or InvoiceItem notes, though let's just say that the UI doesn't support that).
I have query methods set up like this:
public IQueryable<PersonDTO> GetPersonQuery()
{
return from p in Context.People
select new PersonDTO
{
PersonID = p.PersonID,
Name = p.Name
};
}
public List<PersonDTO> GetPeople()
{
return (from p in GetPersonQuery()
return p).ToList();
}
public IQueryable<InvoiceDTO> GetInvoiceQuery()
{
return from p in Context.Invoices
select new InvoiceDTO
{
InvoiceID = p.InvoiceID,
InvoiceNumber = p.InvoiceNumber
};
}
public List<InvoiceDTO> GetInvoices()
{
return (from i in GetInvoiceQuery()
return i).ToList();
}
These all work as expected. Now, let's say I add InvoiceItems to the Invoice query, like this:
public IQueryable<InvoiceDTO> GetInvoiceQuery()
{
return from p in Context.Invoices
select new InvoiceDTO
{
InvoiceID = p.InvoiceID,
InvoiceNumber = p.InvoiceNumber,
Items = (from ii in p.Items
select new ItemDTO
{
ItemID = ii.ItemID,
Description = ii.Description
}).ToList()
};
}
That also works great, and issues just a couple queries. However, the following:
public IQueryable<InvoiceDTO> GetInvoiceQuery()
{
return from p in Context.Invoices
select new InvoiceDTO
{
InvoiceID = p.InvoiceID,
InvoiceNumber = p.InvoiceNumber,
Items = (from ii in p.Items
select new ItemDTO
{
ItemID = ii.ItemID,
Description = ii.Description
}).ToList(),
Notes = (from n in Context.Notes
where i.InvoiceID = n.NoteParentID
select new NoteDTO
{
NoteID = n.NoteID,
Note = n.Note
}).ToList(),
};
}
sends a separate query to the Note table for each Invoice row in the Invoice table. So, if there are 1,000 invoices in the Invoice table, this is sending something like 1,001 queries to the database.
It appears that the Items subquery does not have the same issue because there is an explicit relationship between Invoices and Items, whereas there isn't a specific relationship between Invoices and Notes (because not all notes are related to invoices).
Is there a way to rewrite that final query, such that it will not send a separate note query for every invoice in the table?

The problem is indeed the correlated subquery versus collection navigation property. EF Core query translator still has issues processing such subqueries, which are in fact logical collection navigation properties and should have been processed in a similar fashion.
Interestingly, simulating collection navigation property with intermediate projection (let operator in LINQ query syntax) seems to fix the issue:
var query =
from i in Context.Invoices
let i_Notes = Context.Notes.Where(n => i.InvoiceID == n.NoteParentID) // <--
select new InvoiceDTO
{
InvoiceID = i.InvoiceID,
InvoiceNumber = i.InvoiceNumber,
Items = (from ii in i.Items
select new ItemDTO
{
ItemID = ii.ItemID,
Description = ii.Description
}).ToList(),
Notes = (from n in i_Notes // <--
select new NoteDTO
{
NoteID = n.NoteID,
Note = n.Note
}).ToList(),
};

Related

Linq query to select any in list against a list

Using EF Core code-first, and I want to find any record with a similar list of a foreign entities to the entity I already have.
public class ClownModel {
public int Id { get; set; }
public List<CarModel> Cars { get; set; }
}
public class CarModel {
public int Id { get; set; }
}
var MyClown = new ClownModel() { /*add properties*/ }
//or maybe an existing record selected from database, just some ClownModel instance
Basically, "Select all the ClownModels where they have any Cars.Id that are in my MyClown.Cars"
Assuming that ClownModel has unique CarModel Id's, you can use the following query:
Matches All Ids
var ids = MyClown.Cars.Select(c => c.Id).ToList();
var query =
from cm in ctx.ClownModel
where cm.Cars.Where(c => ids.Contains(c.Id)).Count() == ids.Count
select cm;
Matches Any Ids
var ids = MyClown.Cars.Select(c => c.Id).ToList();
var query =
from cm in ctx.ClownModel
where cm.Cars.Where(c => ids.Contains(c.Id)).Any()
select cm;

product sale qty sum and name in linq group query

I have a product sales data and want to show the summary of sale grouped by product id.
Summary result should show product name and total sales. How can I select a field along with groupby result and that field is not the key field.
public partial class SaleOrderDetail
{
public int Id { get; set; }
public int ProductId { get; set; }
public string ProductName { get; set; }
public int Quantity { get; set; }
public decimal Price { get; set; }
public decimal LineTotal { get; set; }
}
var query = from saleorder in _dbContext.SaleOrderDetail
group saleorder by saleorder.ProductId into salesummary
select new
{
productid = salesummary.Key,
prdouctname = salesummary.First().ProductName,
totalqty = salesummary.Sum(s => s.Quantity)
};
I got the error invalidoperationException because of First() for product name.
You have to include ProductName in grouping Key.
var query =
from saleorder in _dbContext.SaleOrderDetail
group saleorder by new {saleorder.ProductId, saleorder.ProductName} into salesummary
select new
{
productid = salesummary.Key.ProductId,
prdouctname = salesummary.Key.ProductName,
totalqty = salesummary.Sum(s => s.Quantity)
};
Making SaleOrderDetail as AsEnumerable() did the trick. For SQL Expression it will work if make it as AsEnumerable() or .ToList<> etc.
var query = from saleorder in _dbContext.SaleOrderDetail.AsEnumerable()
group saleorder by saleorder.ProductId into salesummary
select new
{
productid = salesummary.Key,
prdouctname = salesummary.First().ProductName,
totalqty = salesummary.Sum(s => s.Quantity)
};

Linq sort by products but display cateogires

So I have an issue where the code is Selecting a Category
and I need to change sort order to sort by product.Name Then By category.name.
But the problem is I still want to select a category, but how do I
sort by product.name first without adding an extra join or select.
from category in categories
select category.name
orderby category.Name //orderby category name
Later on in view I loop foreach(category.products) and pass in category.product[i] to view to display
But sort order is wrong, order is always by Category.Name
How do I sort by Product.Name first and then by Category.Name?
Will SelectMany help? Again I do not want to disrupt the select
part of my query, just the Order by stuff.
class Product
{
public string Name { get; set; }
public int CategoryID { get; set; }
}
class Category
{
public string Name { get; set; }
public int ID { get; set; }
}
// Specify the first data source.
static List categories = new List()
{
new Category(){Name="Beverages", ID=001},
new Category(){ Name="Condiments", ID=002},
new Category(){ Name="Vegetables", ID=003},
new Category() { Name="Grains", ID=004},
new Category() { Name="Fruit", ID=005}
};
// Specify the second data source.
static List products = new List()
{
new Product{Name="Cola", CategoryID=001},
new Product{Name="Tea", CategoryID=001},
new Product{Name="Mustard", CategoryID=002},
new Product{Name="Pickles", CategoryID=002},
new Product{Name="Carrots", CategoryID=003},
new Product{Name="Bok Choy", CategoryID=003},
new Product{Name="Peaches", CategoryID=005},
new Product{Name="Melons", CategoryID=005},
};
Oh, I saw you query, it was badly formatted.
You need to order by Product.Name group by Category.Name
//LinqPad code
class Category
{
public string Name { get; set; }
public int ID { get; set; }
public List<Product> products { get; set;}
}
class Product
{
public string Name { get; set; }
public int ID { get; set; }
}
void Main()
{
// Specify the second data source.
List<Product> BevProducts = new List<Product>()
{
new Product{Name="ZCola"},
};
// Specify the third data source.
List<Product> CondProducts = new List<Product>()
{
new Product{Name="Sugar"},
};
// Specify the first data source.
List<Category> categories = new List<Category>()
{
new Category(){Name="Beverages", ID=001, products=BevProducts},
new Category(){ Name="Condiments", ID=002, products=CondProducts},
};
var sortedCats = categories.OrderBy(c => c.ID).ToList();
foreach (var category in sortedCats)
{
//display category
System.Console.Out.WriteLine(category.Name);
//Assuming each category contains exactly one product in the list ie 1 to 1 relationship
// how can I sort by product.Name, so if ZCola comes before Sugar, ZCola's Category (Beverages) sorts before Condiments
// so in this Case Beverages, Condiments is the right order, because ZCola comes after Sugar.
var sortedProductsPerCategory = category.products.OrderBy(p => p.Name).ToList();
foreach (var product in sortedProductsPerCategory)
{
//display product
System.Console.Out.WriteLine(" " + product.Name);
}
}
}
class Category
{
public string Name { get; set; }
public int ID { get; set; }
public List<Product> products { get; set;}
}
class Product
{
public string Name { get; set; }
public int ID { get; set; }
}
void Main()
{
// Specify the second data source.
List<Product> BevProducts = new List<Product>()
{
new Product{Name="ZCola"},
};
// Specify the third data source.
List<Product> CondProducts = new List<Product>()
{
new Product{Name="Sugar"},
};
// Specify the first data source.
List<Category> categories = new List<Category>()
{
new Category(){Name="Beverages", ID=001, products=BevProducts},
new Category(){ Name="Condiments", ID=002, products=CondProducts},
};
var sortedCats = categories.OrderBy(c => c.products.Min(p => p.Name)).ToList();
foreach (var category in sortedCats)
{
//display category
System.Console.Out.WriteLine(category.Name);
//Assuming each category contains exactly one product in the list ie 1 to 1 relationship
// how can I sort by product.Name, so if ZCola comes before Sugar, ZCola's Category (Beverages) sorts before Condiments
// so in this Case Beverages, Condiments is the right order, because ZCola comes after Sugar.
var sortedProductsPerCategory = category.products.OrderBy(p => p.Name).ToList();
foreach (var product in sortedProductsPerCategory)
{
//display product
System.Console.Out.WriteLine(" " + product.Name);
}
}
}

How can I use be generic to result of linq query?

Be first, my English is not very good. So I am sorry about that. :)
My question as the title. I have two tables on my database:
Siniflar (SinifId, SinifAdi, Kapasite, OgretmenId)
Ogretmenler (OgretmenId, Ad, Soyad, Brans)
Query:
var siniflar = (from s in db.Siniflar
join o in db.Ogretmenler
on s.OgretmenId equals o.OgretmenId
select new { s.SinifId, s.SinifAdi, s.Kapasite, o.Ad }).ToList();
I want to use the result of the query as a generic, like this:
public List<Siniflar> SiniflariListele()
{
var siniflar = (from s in db.Siniflar
join o in db.Ogretmenler
on s.OgretmenId equals o.OgretmenId
select new { s.SinifId, s.SinifAdi, s.Kapasite, o.Ad }).ToList();
return siniflar;
}
But I get an error. Because the result is anonymous types.
Error: Cannot implicitly convert type System.Collections.Generic.List<AnonymousType#1> to System.Collections.Generic.List<Entity.Siniflar>
How Can I use be generic result. What Can I do for that?
I changed question:
Class:
public class RSiniflar
{
public int SinifId { get; set; }
public string SinifAdi { get; set; }
public int Kapasite { get; set; }
public string OgretmenAdiSoyadi { get; set; }
}
Metod:
public List<RSiniflar> SiniflariListele()
{
List<RSiniflar> siniflar = (from s in db.Siniflar
join o in db.Ogretmenler
on s.OgretmenId equals o.OgretmenId
select new RSiniflar
{
SinifId = s.SinifId,
SinifAdi = s.SinifAdi,
Kapasite = s.Kapasite,
OgretmenAdiSoyadi = o.Ad + ' ' + o.Soyad
}).ToList();
return siniflar;
}
But now error to query: Unable to create a constant value of type 'System.Object'. Only primitive types or enumeration types are supported in this context.
I think, there are a problem on my query... I get an error same when query to var type...
You can return typed list, but you can't return a list of anonymous type. So you need to create a class which will handle your results:
public class A
{
public int SinifId { get; set; }
public int SiniAdi { get; set; }
public int Kapasite { get; set; }
public int Ad { get; set; }
}
And then use it in your query, instead of anonymous objects:
public List<A> SiniflariListele()
{
var siniflar = (from s in db.Siniflar
join o in db.Ogretmenler
on s.OgretmenId equals o.OgretmenId
select new A {
SinifId = s.SinifId,
SinifAdi = s.SinifAdi,
Kapasite = s.Kapasite,
Ad = o.Ad
}).ToList();
return siniflar;
}
You could do the same with an existing class (e.g. Siniflar), but you have to be sure that this class has all necessary properties.
Rather than using new { property1, property2 }, create new Siniflar's in your select.
E.g.
select new Siniflar() {
SinifId = s.SinifId,
SinifAdi = s.SinifAdi,
Kapasite = s.Kapasite }).ToList()
Note: as lazyberezovsky mentioned, your mapping seems to be flawed, as Siniflar does not contain the property Ad.

Append OR subquery in Linq

I am trying to build a simple search against some entities (EF4, if that makes any difference). Passed into my search query is a list of criteria objects. The crieteria object looks like this:
public class ClaimSearchCirtieria
{
public Guid? FinancialYear { get; set; }
public bool AllClaimants { get; set; }
public IList<Guid> ClaimantIds { get; set; }
public bool AllExpenseCategories { get; set; }
public IList<ExpenseCategoryAndTypeCriteria> EpenseCategoryAndTypes { get; set; }
}
And the ExpenseCategoryAndTypeCriteria
public class ExpenseCategoryAndTypeCriteria
{
public Guid ExpenseCategory { get; set; }
public bool AllTypesInCatgeory { get; set; }
public IList<Guid> ExpenseTypes { get; set; }
}
Searching on financial years and claimants needs to be an AND query, then I need the expense categories and expense types to be appended as an OR sub query.
In essence I'm trying to do:
select *
from claims
where <financial year> AND <Claimants> AND (expense type 1 OR expense type 2 or expense category X)
So far I've got this:
public PagedSearchResult<Claim> Search(ClaimSearchCirtieria searchCriteria, int page, int pageSize)
{
var query = All();
if (searchCriteria.FinancialYear.HasValue)
{
query = from claim in query
where claim.FinancialYearId == searchCriteria.FinancialYear
select claim;
}
if (!searchCriteria.AllClaimants)
{
query = from claim in query
where searchCriteria.ClaimantIds.Contains(claim.ClaimantId)
select claim;
}
if (!searchCriteria.AllExpenseCategories)
{
foreach (var item in searchCriteria.EpenseCategoryAndTypes)
{
if (item.AllTypesInCatgeory)
{
//Just search on the category
query = query.Where(claim =>
(from transaction in claim.ClaimTransactions
where item.ExpenseCategory == transaction.ExpenseType.ExpenseCategoryId
select transaction).Count() > 0
);
}
else
{
//Search for the specified types
query = query.Where(claim =>
(from transaction in claim.ClaimTransactions
where item.ExpenseTypes.Contains(transaction.ExpenseTypeId)
select transaction).Count() > 0
);
}
}
}
return PagedSearchResult<Claim>.Build(query, pageSize, page);
}
What I'm currently seeing is that the last expense category requested is the only expense category I get results for. Also, looking at the code, it looks like I would expect this to be building a series of AND queries, rather that the required OR.
Any pointers?
You can do this with LINQKit's PredicateBuilder. You need to use AsExpandable() when composing Entity Framework queries.

Resources