Find parent based on children properties linq to sql - c#

Lets say we have a sql relationship that can be modeled like this using C# classes
public class Parent
{
public int ID { get; set; }
public List<Child> Children { get; set; }
}
public class Child
{
public int ID { get; set; }
public Parent Parent { get; set; }
public int Number { get; set; }
}
I also know that the parent only has two children. I would I find all the parents where the children both satisfy a different criteria? Say One Child has a number = 0 and the other Child has a number = 1

Goin' out on a limb here...
(from p in context.Parents
where p.Children.Count == 2
where p.Children.Any(c => c.Number == 0)
select p).Where(p => p.Children.Any(c => c.Number == 1));

from p in context.Parents
where p.Children.Count == 2 // sounds like you can skip this one
where p.Children.Any(c => c.Number == 0)
where p.Children.Any(c => c.Number == 1)
select p;

from o in context.Parents where o.Children.Count == 2 && o.Children.Any(x => x.Number == 0) && x.Children.Any(x => x.Number == 1) select o;

Related

Parent Child Search at same table LINQ C#

I have a query to search in single table where records are at Parent, Child Relationship like below
Table
Id
ParentId
Name
Status
so my query is like
var projects = from p in _projectSetupRepository.Table
from all in _projectSetupRepository.Table
where p.Status == status && p.ParentId == null &&
all.Status == status && ((all.ParentId == null && all.Id == p.Id) || all.ParentId == p.Id)
select p;
if (!string.IsNullOrEmpty(search))
projects = projects.Where(c => c.Name.Contains(search)).OrderBy(c => c.Name);
but I don't get actual results of parents if search with the child's name. What was the issue in the query?
PS
table contains thousands of data and performance is very important
PS
public class ProjectSetup
{
public int Id { get; set; }
public int? ParentId { get; set; }
public string Name { get; set; }
public bool Status { get; set; }
public ProjectSetup Project{ get; set; }
public virtual ICollection<ProjectSetup> SubProjects { get; set; }
}
Id
ParentId
Name
Status
1
null
P1
true
2
1
T1
true
3
1
T2
true
4
3
Q1
true
You can find all parents with specific child name(this query searches Level1 childs only):
var status = true;
var search = "T";
var projects = (from parent in context.Projects
join child in context.Projects on parent.Id equals child.ParentId into joinedT
from pd in joinedT.DefaultIfEmpty()
where parent.Status == status
&& parent.ParentId == null //filter top level parents only?
&& pd.Name.Contains(search)
select parent).Distinct().OrderBy(c => c.Name);
foreach(var p in projects)
{
Console.WriteLine(p.Id+":"+p.Name);
}
Console.WriteLine("Found results:"+ projects.Count());
here's fiddle: https://dotnetfiddle.net/PaCidA
If you are looking for multi-level solution I suggest you to take a look at hierarchyid data type and its usage in EF LINQ
https://softwarehut.com/blog/tech/hierarchyid-entity-framework
https://kariera.future-processing.pl/blog/hierarchy-in-the-entity-framework-6-with-the-hierarchyid-type/
Check for performance the folloging algorithm. Idea that we can Include several Project and check on the client what to load again. Algorithm uses EF Core navigation properties fixup.
var query = in_projectSetupRepository.Table
.Include(p => p.Project.Project.Project) // you can increase count of loaded parents
.Where(p => p.Status == status)
.AsQueryable();
var loadedDictionary = new Dictionary<int, ProjectSetup>>();
var projects = query;
if (!string.IsNullOrEmpty(search))
projects = projects.Where(c => c.Name.Contains(search));
while (true)
{
var loaded = projects.ToList();
// collect loaded with parents
foreach(var p in loaded)
{
var current = p;
do
{
if (!loadedDictionary.ContainsKey(current.Id))
loadedDictionary.Add(current.Id, current);
current = current.Project;
}
while (current != null)
}
// collect Ids of incomplete
var idsToLoad = loadedDictionary.Values
.Where(x => x.ParentId != null && x.Project == null)
.Select(x => x.ParentId.Value)
.ToList();
// everything is ok
if (idsToLoad.Count == 0)
break;
// make query to load another portion of objects
projects = query.Where(p => idsToLoad.Contains(p.Id));
}
var result = loadedDictionary.Values
.OrderBy(c => c.Name)
.ToList();

EF Core how to add a filter to .ThenIncludes

I'm using EF Core 5.0.4
I have these three entities that have relations.
public class Parent
{
[Key]
public int ParentId { get; set; }
[Key]
public short ChildId { get; set; }
public string ParentProp1 { get; set; }
public string ParentProp2 { get; set; }
public DateTime ProcessedDate { get; set; }
public DateTime Anniversary { get; set; }
public Child Child { get; set; }
}
public class Child
{
public Child()
{
Animals = new HashSet<Animals>();
}
[Key]
public short ChildId { get; set; }
public string ChildName { get; set; }
public virtual ICollection<Animals> Animals { get; set; }
//UPDATED TO INCLUDE PARENT
public Parent Parent {get;set;}
}
public class Animals
{
[Key]
public short AnimalId { get; set; }
[ForeignKey("ChildId")]
public short ChildId { get; set; }
public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; }
public DateTime MAnniversary { get; set; }
public DateTime PAnniversary { get; set; }
public virtual Child Child { get; set; }
}
In my repository I'm trying to return a List of Parent. How do I add the filter ..see commented code lines... on the IQueryable()?
public List<Parent> Get(Request request)
{
var data = _context.Parents
.Include(r => r.Child)
.ThenInclude(a => a.Animals)
.AsQueryable().AsNoTracking();
data = data.Where(x => x.ProcessedDate == request.ProcessedDate);
// Here is the filter I'm trying to add but can't because data is an IQueryable() :
// Animals needs to be filtered based off a query like:
// data.Child.Animals = data.Child.Animals.Where( d => d.StartDate <= data.ProcessedDate && (
// d.EndDate == null || data.ProcessDate <= d.EndDate
// )
// && d.ChildId == data.ChildId && data.Anniversary >= d.MAnniversary
// ).ToList();
return data;
}
When returning just Parent I'm able to add the filter query like below without any issues because 'data' is not an IQueryable() :
public Parent Get(int id)
{
var data = _context.Parents
.Include(r => r.Child)
.ThenInclude(a => a.Animals)
.FirstOrDefault(x => x.ParentId == id);
data = data.Where(x => x.ProcessedDate == request.ProcessedDate);
data.Child.Animals = data.Child.Animals.Where(d => d.StartDate <= data.ProcessedDate && (
d.EndDate == null || data.ProcessDate <= d.EndDate
)
&& d.ChildId == data.ChildId && data.Anniversary >= d.MAnniversary
).ToList();
return data;
}
I've tried adding a filter on the .ThenInlude() like below but it doesn't work because I can't access the properties needed.
var data = _context.Parents
.Include(r => r.Child)
.ThenInclude(a => a.Animals.Where(x => x.StartDate <= "this doesn't work because can't access Parent or Child properties")
.AsQueryable().AsNoTracking();
UPDATE
I added navigation Parent navigation property to Child and changed the query to be based on the inner relations. It looks something like this:
var data = _context.Animals
.Include(r => r.Child)
.ThenInclude(a => a.Parent)
.Where(r => r.StartDate <= r.Child.Parent.ProcessedDate && (
r.EndDate == null || r.Child.Parent.ProcessDate <= r.EndDate
)
&& r.ChildId == r.Child.Parent.ChildId &&
r.Child.Parent.Anniversary >= r.MAnniversary
).AsQueryable().AsNoTracking();
I added Parent navigation property to Child and changed the query to be based on the inner relations. It looks something like this:
var data = _context.Animals
.Include(r => r.Child)
.ThenInclude(a => a.Parent)
.Where(r => r.StartDate <= r.Child.Parent.ProcessedDate && (
r.EndDate == null || r.Child.Parent.ProcessDate <= r.EndDate
)
&& r.ChildId == r.Child.Parent.ChildId &&
r.Child.Parent.Anniversary >= r.MAnniversary
).AsQueryable().AsNoTracking();

Join 2 tables to retrieve name and count linq Entity Framework

I have 2 tables Orders and Items and I am trying to query these 2 tables for statistics retrieval.
Orders has columns OrderID[PK], ItemID[FK], OrderStatus etc.
Items has columns ItemID[PK], ItemName, ItemPrice etc.
I am fetching list of orders based on date range and then I am returning their counts based on their status.
Below is my StatisticsResponse.cs to return the response.
public class StatisticsResponse
{
public int CancelledOrderCount { get; set; }
public int CompletedOrderCount { get; set; }
public int InProgressOrderCount { get; set; }
public int TotalOrders { get; set; }
public Dictionary<string,int> ItemOrders { get; set;}
}
This is how I am retrieving Orders between 2 dates.
var orders = _unitOfWork.OrderRepository
.GetMany(x => (x.OrderStatus == "Pending"
&& x.OrderDate.Value.Date >= dtStartDate
&& x.OrderDate.Value.Date < dtEndDate) ||
((x.OrderStatus == "Completed" || x.OrderStatus == "Cancelled")
&& x.DeliveryDate.Date >= dtStartDate || x.DeliveryDate.Date < dtEndDate) || (x.LastUpdated.Value.Date >= dtStartDate || x.LastUpdated.Value.Date < dtEndDate)).ToList();
if (orders != null)
{
return new StatisticsResponse()
{
TotalOrders = orders.Count(),
CancelledOrderCount = orders.Where(x => x.OrderStatus == "Cancelled").Count(),
CompletedOrderCount = orders.Where(x => x.OrderStatus == "Completed").Count(),
InProgressOrderCount = orders.Where(x => x.OrderStatus != "Completed" && x.OrderStatus != "Cancelled").Count()
}
}
Now, in the ItemOrders property, which is of type Dictionary<string,int>, I want to group each item with their name and count. I have ItemID in my orders list, and I would like to join 2 tables to get the name before storing.
I have tried to use GroupBy as below but am totally stuck on how to get the name for the Item after grouping
ItemOrders = new Dictionary<string, int>
{
orders.GroupBy(x=>x.ItemID)// Stuck here
}
I also read about GroupJoin but couldn't quite make sure whether it can fit in here.
Could someone please let me know how I can join these 2 tables to get their name based on their ID?
You can use something along this:
using System.Entity.Data; //to use Include()
...
Dictionary<string,int> itemOrders = dbContext.Orders.Include(o=> o.Item)
.GroupBy(o=> o.Item)
.ToDictionary(g => g.Key.Name, g => g.Count());
This is assuming:
There is a navigation property set up from Order to Item
Each Order has one Item
So, I was able to achieve this with GroupJoin through various online example as below:
if (orders != null)
{
var items = _unitOfWork.ItemRepository.GetAll();
var itemOrders = items.GroupJoin(orders,
item => item.ItemID,
ord => ord.ItemID,
(item, ordrs) => new
{
Orders = ordrs,
itemName = item.ItemName
});
StatisticsResponse statsResponse = new StatisticsResponse()
{
//...
};
foreach (var item in itemOrders)
{
statsResponse.ItemOrders.Add(item.itemName, item.Orders.Count());
}
return statsResponse;
}

EF get list of parent entities that have list of child

I have to next 2 entities in my project
public class Product
{
public Product()
{
this.ProductImages = new HashSet<ProductImage>();
this.ProductParams = new HashSet<ProductParam>();
}
public int ID { get; set; }
public int BrandID { get; set; }
public int CodeProductTypeID { get; set; }
public string SeriaNumber { get; set; }
public string ModelNumber { get; set; }
public decimal Price { get; set; }
public bool AvailableInStock { get; set; }
public virtual Brand Brand { get; set; }
public virtual CodeProductType CodeProductType { get; set; }
public virtual ICollection<ProductImage> ProductImages { get; set; }
public virtual ICollection<ProductParam> ProductParams { get; set; }
}
public class ProductParam
{
public int Id { get; set; }
public int ProductId { get; set; }
public int CodeProductParamId { get; set; }
public string Value { get; set; }
public virtual Product Product { get; set; }
public virtual CodeProductParam CodeProductParam { get; set; }
}
and I want to get list of Products which has list of specified parameters
var prodParamCritria = new List<ProductParam>()
{
new ProductParam(){CodeProductParamId =1, Value="Black" },
new ProductParam(){CodeProductParamId =2, Value="Steal"}
};
in sql I can do it by using EXISTS clause twise
SELECT *
FROM Products p
WHERE EXISTS (
SELECT *
FROM ProductParams pp
WHERE pp.ProductId = p.ID
AND (pp.CodeProductParamId = 1 AND pp.[Value] = N'Black')
)
AND EXISTS (
SELECT *
FROM ProductParams pp
WHERE pp.ProductId = p.ID
AND pp.CodeProductParamId = 2
AND pp.[Value] = N'Steal'
)
How can i get same result by EF methods or linq
Try this:
var products= db.Products.Where(p=>p.ProductParams.Any(pp=>pp.CodeProductParamId == 1 && pp.Value == "Black") &&
p.ProductParams.Any(pp=>pp.CodeProductParamId == 2 && pp.Value == "Steal"));
Update
The problem in work with that list of ProductParam to use it as a filter is that EF doesn't know how to translate a PodructParam object to SQL, that's way if you execute a query like this:
var products2 = db.Products.Where(p => prodParamCritria.All(pp => p.ProductParams.Any(e => pp.CodeProductParamId == e.CodeProductParamId && pp.Value == e.Value)));
You will get an NotSupportedException as you comment in the answer of #BostjanKodre.
I have a solution for you but probably you will not like it. To resolve that issue you could call the ToList method before call the Where. This way you will bring all products to memory and you would work with Linq to Object instead Linq to Entities, but this is extremely inefficient because you are filtering in memory and not in DB.
var products3 = db.Products.ToList().Where(p => prodParamCritria.All(pp => p.ProductParams.Any(e => pp.CodeProductParamId == e.CodeProductParamId && pp.Value == e.Value)));
If you want filter by one criteria then this could be more simple and you are going to be able filtering using a list of a particular primitive type. If you, for example, want to filter the products only by CodeProductParamId, then you could do this:
var ids = new List<int> {1, 2};
var products = db.Products.Where(p => ids.All(i=>p.ProductParams.Any(pp=>pp.CodeProductParamId==i))).ToList();
This is because you are working with a primitive type and not with a custom object.
I suppose something like that should work
db.Product.Where(x => x.ProductParams.FirstOrDefault(y => y.CodeProductParamId == 1) != null && x.ProductParams.FirstOrDefault(y => y.CodeProductParamId == 2) != null).ToList();
or better
db.Product.Where(x => x.ProductParams.Any(y => y.CodeProductParamId == 1) && x.ProductParams.Any(y => y.CodeProductParamId == 2)).ToList();
Ok, if you need to make query on parameters in list prodParamCriteria it will look like this:
db.Product.Where(x => prodParamCritria.All(c=> x.ProductParams.Any(p=>p.CodeProductParamId == c.CodeProductParamId && p.Value== c.Value))).ToList();
I forgot that complex types cannot be used in query database, so i propose you to convert your prodParamCriteria to dictionary and use it in query
Dictionary<int, string> dctParams = prodParamCritria.ToDictionary(x => x.CodeProductParamId , y=>y.Value);
db.Product.Where(x => dctParams.All(c => x.ProductParams.Any(p=> p.CodeProductParamId == c.Key && p.Value== c.Value))).ToList();
another variation:
IEnumerable<Int32> lis = prodParamCritria.Select(x => x.CodeProductParamId).ToList();
var q = Products.Select(
x => new {
p = x,
cs = x.ProductParams.Where(y => lis.Contains(y.Id))
}
).Where(y => y.cs.Count() == lis.Count()).
ToList();
with a named class like (or maybe without, but not in linqpad)
public class daoClass {
public Product p {get; set;}
public Int32 cs {get; set;}
}
IEnumerable<Int32> lis = prodParamCritria.Select(x => x.CodeProductParamId).ToList();
var q = Products.Select(
x => new daoClass {
p = x,
cs = x.ProductParams.Where(y => lis.Contains(y.Id))
}
).Where(y => y.cs.Count() == lis.Count()).
SelectMany(y => y.p).
ToList();

Grouping and flattening list with linq and lambda

I have the following class
public class SolicitacaoConhecimentoTransporte
{
public long ID { get; set; }
public string CodigoOriginal { get; set; }
public DateTime Data { get; set; }
public List<CaixaConhecimentoTransporte> Caixas { get; set; }
}
I would like to know if there is a way of achiveing the same behavior of the code below using Linq (with lambda expression syntax),
List<SolicitacaoConhecimentoTransporte> auxList = new List<SolicitacaoConhecimentoTransporte>();
foreach (SolicitacaoConhecimentoTransporte s in listaSolicitacao)
{
SolicitacaoConhecimentoTransporte existing =
auxList.FirstOrDefault(f => f.CodigoOriginal == s.CodigoOriginal &&
f.Data == s.Data &&
f.ID == s.ID);
if (existing == null)
{
auxList.Add(s);
}
else
{
existing.Caixas.AddRange(s.Caixas);
}
}
return auxList;
In other words, group all entities that have equal properties and flat all lists into one.
Thanks in advance.
Use anonymous object to group by three properties. Then project each group to new SolicitacaoConhecimentoTransporte instance. Use Enumerable.SelectMany to get flattened sequence of CaixaConhecimentoTransporte from each group:
listaSolicitacao.GroupBy(s => new { s.CodigoOriginal, s.Data, s.ID })
.Select(g => new SolicitacaoConhecimentoTransporte {
ID = g.Key.ID,
Data = g.Key.Data,
CodigoOriginal = g.Key.CodigoOriginal,
Caixas = g.SelectMany(s => s.Caixas).ToList()
}).ToList()

Categories

Resources