left join selector can't access single left table name - c#

I am coding my way through the 101 linq examples, and I'm now at #106.
I am trying to rewrite query expressions in method/lambda syntax for my own learning.
Here is the example code:
List<Customer> customers = GetCustomerList();
List<Supplier> suppliers = GetSupplierList();
var custSuppliers =
from cust in customers
join sup in suppliers on cust.Country equals sup.Country into ss
from s in ss.DefaultIfEmpty()
orderby cust.CompanyName
select new
{
Country = cust.Country,
CompanyName = cust.CompanyName,
SupplierName = s == null ? "(No suppliers)" : s.SupplierName
};
Here is what I have so far:
var custSuppliers =
customers.GroupJoin(suppliers, c => c.Country, s => s.Country, (c, s) => new { Customers = customers, Suppliers = suppliers })
.OrderBy(i => i.Customers)
.SelectMany(x => x.Suppliers.DefaultIfEmpty(), (x, p) => // p is the many field (i.e. customers)
new
{
CompanyName = x.CompanyName, // no definition for CompanyName
Country = p.Country,
SupplierName = p.SupplierName == null ? "(No suppliers)" : p.SupplierName
});
My understanding is the SelectMany takes parameters X and P where X is the "left" table (i.e. there is at most 1 of them) and P is the "right" table where there might be N of them (or a null).
But instead of the X variable holding a single customer, it holds a collection of suppliers and customers.
Can anyone explain what is going on here?

Related

Join where lambda query in EF Core 3.1

I have a problem to write in lambda query this sql query:
select c.Id, c.Name, c.SomeNumber, count(*) from TableA a
inner join TableB b
on a.Id = b.aId
inner join TableC c
on c.BId = b.Id
where b.Status = N'Approved'
and c.Scope = N'Scope1'
group by a.Id, a.Name, a.SomeNumber
Can you guys help me with this one ? I want to write lambda query to execute this in code. I'm using EF Core 3.1
This is what I ended up so far:
var query = await _dbContext.TableA.Where(a => a.TableB.Any(b => b.Status.Equals("Approved")
&& b.TableC.Any(c => c.Scope.Equals("Scope1"))))
.GroupBy(g => new { Id = g.Id, Name = g.Name, SomeNumber = g.SomeNumber })
.Select(s => new { Id = s.Key.Id, Name = s.Key.Name, SomeNumber = s.Key.SomeNumber, Count = s.Count() })
.GroupBy(g => g.Id).Select(s => new {Id = s.Key, Count = s.Count()}).ToListAsync();
Well, this is corrected query. I have used Query syntax which is more readable when query has lot of joins or SelectMany.
var query =
from a in _dbContext.TableA
from b in a.TableB
from c in b.TableC
where b.Status == "Approved" && c.Scope == "Scope1"
group a by new { a.Id, a.Name, a.SomeNumber } into g
select new
{
g.Key.Id,
g.Key.Name,
g.Key.SomeNumber,
Count = g.Count()
}
var result = await query.ToListAsync();
It's maybe easier to start from the many end and work up through the navigation properties
tableC
.Where(c => c.Scope == "Scope1" && c.BEntity.Status == "Approved")
.GroupBy(c => new
{
c.BEntity.AEntity.Id,
c.BEntity.AEntity.Name,
c.BEntity.AEntity.SomeNumber
})
.Select(g => new { g.Key.Id, g.Key.Name, g.Key.SomeNumber, Ct = g.Count()})
EF knows how to do joins when you navigate around the object tree in the where. By starting at the many and and working up to the 1 end of the relationship it means you don't have to get complex with asking "do any of the children of this parent have a status of ..."

Linq GroupJoin add default entries

var storeIds = repository.Get<Store>()
.Select(s => s.Id)
.ToList();
var storeReceipts = repository.Get<Receipt>()
.Where(r => DbFunctions.TruncateTime(r.LogDate) == today)
.GroupBy(r => r.StoreId)
.Select(g => new { Id = g.Key, Sales = g.Sum(r => r.TotalPrice) })
.GroupJoin(storeIds, x => x.Id, s => s, (x, s) => x ?? new { Id = s, Sales = 0 });
Basically I want the GroupJoin to add an entry to the sequence for any Store that doesn't have Receipt records.
My syntax above with the ?? doesn't compile (even if it did I am not sure its correct).
If you want to join the two tables, you may want to try this.
var storeSales = from s in repository.Get<Store>()
join r in repository.Get<Receipt>() on s.Id equals r.StoreId into g
select new {
StoreId = s.Id,
Sales = g.Sum(x => (decimal?)x.TotalPrice) ?? 0
};
It selects from the Stores first, so that you will get an entry for each store. It will next join the Receipts by matching store id into a group g that joins the two.
That allows the selection of your output shape, one item per store. In this case, the Id of the store as StoreId, and the sum of the TotalPrice values for each receipt as the Sales are selected.
If there were no receipts for a store, this sum will end up being 0.

Entity Framework Join 3 Tables

I'm trying to join three tables but I can't understand the method...
I completed join 2 tables
var entryPoint = dbContext.tbl_EntryPoint
.Join(dbContext.tbl_Entry,
c => c.EID,
cm => cm.EID,
(c, cm) => new
{
UID = cm.OwnerUID,
TID = cm.TID,
EID = c.EID,
}).
Where(a => a.UID == user.UID).Take(10);
I would like to include tbl_Title table with TID PK and get Title field.
Thanks a lot
I think it will be easier using syntax-based query:
var entryPoint = (from ep in dbContext.tbl_EntryPoint
join e in dbContext.tbl_Entry on ep.EID equals e.EID
join t in dbContext.tbl_Title on e.TID equals t.TID
where e.OwnerID == user.UID
select new {
UID = e.OwnerID,
TID = e.TID,
Title = t.Title,
EID = e.EID
}).Take(10);
And you should probably add orderby clause, to make sure Top(10) returns correct top ten items.
This is untested, but I believe the syntax should work for a lambda query. As you join more tables with this syntax you have to drill further down into the new objects to reach the values you want to manipulate.
var fullEntries = dbContext.tbl_EntryPoint
.Join(
dbContext.tbl_Entry,
entryPoint => entryPoint.EID,
entry => entry.EID,
(entryPoint, entry) => new { entryPoint, entry }
)
.Join(
dbContext.tbl_Title,
combinedEntry => combinedEntry.entry.TID,
title => title.TID,
(combinedEntry, title) => new
{
UID = combinedEntry.entry.OwnerUID,
TID = combinedEntry.entry.TID,
EID = combinedEntry.entryPoint.EID,
Title = title.Title
}
)
.Where(fullEntry => fullEntry.UID == user.UID)
.Take(10);

Linq SelectMany

Hi I am coding my way through the MS 101 linq examples.
The "JoinOperators" are giving me a hard time since I am trying to refactor the query expressions to lambda syntax and vice versa.
Anyway, on example 105 I see this query expression:
var supplierCusts =
from sup in suppliers
join cust in customers on sup.Country equals cust.Country into cs
from c in cs.DefaultIfEmpty() // DefaultIfEmpty preserves left-hand elements that have no matches on the right side
orderby sup.SupplierName
select new
{
Country = sup.Country,
CompanyName = c == null ? "(No customers)" : c.CompanyName,
SupplierName = sup.SupplierName
};
And I tried implementing it as a lambda this way:
// something is not right here because the result keeps a lot of "Join By" stuff in the output below
var supplierCusts =
suppliers.GroupJoin(customers, s => s.Country, c => c.Country, (s, c) => new { Customers = customers, Suppliers = suppliers })
.OrderBy(i => i.Suppliers) // can't reference the "name" field here?
.SelectMany(x => x.Customers.DefaultIfEmpty(), (x, p) => // does the DefaultIfEmpty go here?
new
{
Country = p.Country,
CompanyName = x == null ? "(No customers)" : p.CompanyName,
SupplierName = p // not right: JoinOperators.Program+Customer ... how do I get to supplier level?
});
For some reason I can't access the supplier-level information this way. When I switch out the customers with suppliers I can't access the customer-level information.
Is there some overload of SelectMany() that lets me pull from the field-level of both objects?
Also, I don't understand why the GroupJoin() appears to return an object with 2 collections (suppliers and customers). Isn't it supposed to join them somehow?
I guess I don't understand how GroupJoin() works.
You have wrong result selector in group join, that's where problems started. Here is fixed query:
var supplierCusts =
suppliers
.GroupJoin(customers,
sup => sup.Country,
cust => cust.Country,
(sup, cs) => new { sup, cs })
.OrderBy(x => x.sup.Name)
.SelectMany(x => x.cs.DefaultIfEmpty(), (x, c) =>
new
{
Country = x.sup.Country,
CompanyName = c == null ? "(No customers)" : c.CompanyName,
SupplierName = x.sup.Name
});
If you want to learn translating the query expressions into lambda's, I suggest you check out LinqPad which can do that by default. For example, your query is translated as follows:
Suppliers
.GroupJoin (
Customers,
sup => sup.Country,
cust => cust.Country,
(sup, cs) =>
new
{
sup = sup,
cs = cs
}
)
.SelectMany (
temp0 => temp0.cs.DefaultIfEmpty (),
(temp0, c) =>
new
{
temp0 = temp0,
c = c
}
)
.OrderBy (temp1 => temp1.temp0.sup.CompanyName)
.Select (
temp1 =>
new
{
Country = temp1.temp0.sup.Country,
CompanyName = (temp1.c == null) ? "(No customers)" : temp1.c.CompanyName,
SupplierName = temp1.temp0.sup.CompanyName
}
)
That being said, I typically find SelectMany to be easier to code and maintain using the query syntax instead of the lambda syntax.
The GroupJoin in this example is used to accomplish the left join (via the .DefaultIfEmpty clause).
Try this:
var supplierCusts =
suppliers.GroupJoin(customers, s => s.Country, c => c.Country, (s, c) => new { Supplier = s, Customers = c })
.OrderBy(i => i.Supplier.SupplierName)
.SelectMany(r => r.Customers.DefaultIfEmpty(), (r, c) => new
{
Country = r.Supplier.Country,
CompanyName = c == null ? "(No customers)" : c.CompanyName,
SupplierName = r.Supplier.SupplierName
});

Linq with conditional joins

I couldn't convert the following left join SQL to linq:
select Students.StudentID, StudentAddresses.state
from Students left join Studentaddresses on (Students.StudentID = Studentaddresses.StudentID and StudentAddresses.Active=1)
where (StudentAddresses.Rank =1 or StudentAddresses.StudentID is null)
and Students.StudentID =3
A student can have zero record or multiple records in the Student Address table, but only one of the records can be active and rank=1.
I was able to do a left join in linq and make it work for normal situation. But if a student has two inactive records in the Student table, I don't know how to make the student record appear only once in the final result. Can anyone please help?
Use Distinct() to collapse duplicates.
var LeftJoin = (from student in Students
join address in (from address1 in StudentAddresses where address.Active select address1)
on student.StudentID equals address.StudentId
into JoinedStudentAddress
from joined in JoinedStudentAddress.DefaultIfEmpty()
select new
{
StudentID = student.StudentID,
State = joined != null ? joined.State : null
}).Distinct();
Alternate syntax
var LeftJoin = Students.GroupJoin( StudentAddress.Where( a => a.Active ),
s => s.StudentID,
a => a.StudentID,
(s,a) => new { Student = s, Addresses = a } )
.SelectMany( j = > j.Addresses.DefaultIfEmpty()
(s,a) => new {
StudentID = s.Student.StudentID,
State = a != null ? a.State : null
})
.Distinct();
This would be the SQL statement converted to linq:
var q = from student in Students
from adress in Studentaddresses.Where(x => student.StudentID == x.StudentID && x.Active).DefaultIfEmpty()
where (adress.Rank == 1 || adress.StudentID == null) && student.StudentID == 3
select new
{
StudentID = student.StudentID,
State = adress.state
};

Categories