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
});
Related
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 ..."
I have a following code:
var sql = db.Accounts.AsNoTracking()
.Join(db.Customers.AsNoTracking(),
d => d.AccountNr, c => c.CustNr,
(d, c) => new {Accounts = d, Customers = c })
.GroupBy(g => g.Accounts.AccountNr)
.Where(w => w.Accounts.Date == null)
.Select(s => new
{
Company = s.Customers.CompName,
TWQ = s.Customers.TWQ,
AccountNr = s.Accounts.AccountNr,
DocDate = s.Accounts.DocumentDate,
Income = s.Customers.Income
})
.OrderBy(o => o.DocDate);
The issue is that c# underlines the whole WHERE part with an alert saying that: Element IGrouping <string,> has no definition of Accounts and extension method of Accounts can not be found
I don't know where the problem lies. I also tried to use GroupBy in model (instead of using it in the code above) but got the some problem:
var model = (from ss in sql // here I refer to sql outcome I got from the code above
.GroupBy(g => g.AccountNr)
.Skip(page * 15 - 15)
.Take(15)
.AsEnumerable()
select new DocumentsModel
{
Company = s.Customers.CompName,
TWQ = s.Customers.TWQ,
AccountNr = s.Accounts.AccountNr,
DocDate = s.Accounts.DocumentDate,
Income = s.Customers.Income
}).ToList();
At first I would like to say that my English isn't that good.
If you execute an GroupBy, you'll get collection of elements where each element represents a projection over a group and its key.
That's why I execute SelectMany afterwards to work with the model in a normal way.
db.Accounts
.AsNoTracking()
.Join
(
inner: db.Customers.AsNoTracking(),
outerKeySelector: x => x.AccountNr,
innerKeySelector: x => x.CustNr,
resultSelector: (Accounts, Customers) => new
{
Accounts, Customers
}
)
.Where
(
predicate: x => x.Accounts.Date == null
)
.GroupBy
(
keySelector: x => x.Accounts.AccountNr
)
.SelectMany
(
selector: x => x
)
.Select
(
selector: x => new
{
Company = x.Customers.CompName,
TWQ = x.Customers.TWQ,
AccountNr = x.Accounts.AccountNr,
DocDate = x.Accounts.DocumentDate,
Income = x.Customers.Income
}
)
I have this query and I'm having trouble converting into a lambda expression
SELECT [dbo].[Prospects].[Id]
,[UserId]
,[NewId]
,[dbo].[Prospects].[Status]
FROM [dbo].[Prospects] join [dbo].[User] on [dbo].[User].Id = [dbo]. [Prospects].UserId
where [dbo].[Prospects].NewId = 3 and [dbo].[User].IsActive = 1
This is what i got, but it is not working
var result = Workspace.Prospects.Join
(Workspace.Users, pros => pros.UserId,
use => use.Id, (pros, use)
=> new { Prospect = pros, User = use}).Where
(both => both.User.IsActive == true && both.Prospect.NewId == idVacante)
.OrderBy(both => both.Prospect.Id).AsEnumerable().ToList();
List<Prospect> prospects = result.Cast<Prospect>().ToList();
It's not that hard. you can filter the tables before joining them
var result = Workspace.Prospects.Where(x=> x.NewId == 3)
.Join(Workspace.Users.Where(x => x.IsActive == 1),
p => p.UserId,
u => u.Id,
(p, u) => new { p.Id, p.UserId, p.NewId, p.Status })
Data structure looks like:
User(id)
UserApp(user_id, app_id)
UserSkill(user_id, skill_id)
Using linq-to-sql or EF, how would I construct a query to elegantly return only users who possess every requested app and skill?
In addition, how would I adjust the query to return any user who possesses at least one of the requested apps or skills? Essentially an OR vs AND (above).
UPDATE 1:
So I think we're close. Basically I want to only return users who have ALL the requested apps and skills. If we have two arrays of requested ids for skills and apps:
int[] requestedAppIDs // [1, 2, 3]
int[] requestedSkillIDs // [4, 5, 6]
I would only want to return a user if they have apps 1,2,3 AND skills 4,5,6.
var usersWithAllSelectedAppsAndSkills =
context.Users
.GroupJoin(context.UserApp,
k => k.id,
k => k.user_id,
(o, i) => new { User = o, UserApps = i })
.GroupJoin(context.UserSkill,
k => k.User.id,
k => k.user_id,
(o, i) => new { User = o.User, o.UserApps, UserSkills = i })
.Where(w => !requestedAppIDs.Except(w.UserApps.Select(x => x.app_id).ToArray()).Any() && !requestedSkillIDs.Except(w.UserSkills.Select(x => x.skill_id).ToArray()).Any())
.Select(s => s.User)
.ToList();
Obviously, LINQ does not know how to translate the UserSkills.Select().ToArray()'s in my Where() to SQL. How can I accomplish this?
And, secondarily the OR solution as well (user has any one of the requested apps or skills).
This will do the job as long as the user_id – app_id and user_id – skill_id values in the UserApp and UserSkill tables are unique.
var requestedSkillIDs = new[] { 4, 5, 6 };
var skillCount = requestedSkillIDs.Length;
var requestedAppIDs = new[] { 1, 2, 3 };
var appCount = requestedAppIDs.Length;
using (var context = new TestContext()) {
context.Database.CreateIfNotExists();
var appQuery = context.UserApp.Where(p => requestedAppIDs.Contains(p.AppId))
.GroupBy(p => p.UserId)
.Where(p => p.Count() == appCount);
var skillQuery = context.UserSkill.Where(p => requestedSkillIDs.Contains(p.SkillId))
.GroupBy(p => p.UserId)
.Where(p => p.Count() == skillCount);
var result = from a in appQuery
join s in skillQuery on a.Key equals s.Key
join u in context.Users on s.Key equals u.Id
select u;
var users = result.ToList();
}
Here's one way to do it, I hope i got all the syntax right :)
using (var context = new YourContext())
{
var usersWithAllSkills = context.User
.Where(w => w.id == yourId)
.Join(context.UserApp,
k => k.id,
k => k.user_id,
(o,i) => o)
.Join(context.UserSkill,
k => k.id,
k => k.user_id,
(o,i) => o)
.ToList();
var usersWithAnySkill = context.User
.Where(w => w.id == yourId)
.GroupJoin(context.UserSkill,
k => k.id,
k => k.user_id,
(o,i) => new { User = o, UserSkills = i })
.GroupJoin(context.UserApp,
k => k.User.id,
k => k.user_id,
(o,i) => new { User = o.User, o.UserSkills ,UserApps = i })
.Where(w => w.UserSkills != null || w.UserApps != null)
.Select(s => s.User)
.ToList();
}
For the first case (AND) You just need to make inner join like below:
from t1 in db.UserApp
join t2 in db.UserSkill on t1.user_id equals t2.user_id
where t1.app_id == "someID" && t2.skill_id == "someID"
select new { t1.user_id,t1.user_app_id, t2.user_skill}
For the second case just swap &&(AND) with ||(OR).
There is a more direct way to write the required queries using L2E. To write these queries you have to forget thinking in SQL and start thinking in LINQ.
For the first case, look for users which have all the skills:
var usersWithAll = ctx.Users2.Where(u =>
appIds.All(aid => u.Apps.Any(a => a.AppId == aid))
&& skillIds.All(sid => u.Skills.Any(s => s.SkillId == sid))
);
Translated as: get the users where, for all the appIds the user has al leat an app with that application id and for all skillIds the user has at least one skill with that id
And, for the second case, users which have any of the apps and any of the skills:
var usersWithAny = ctx.Users2.Where(u =>
appIds.Any(aid => u.Apps.Any(a => a.AppId == aid))
&& skillIds.Any(sid => u.Skills.Any(s => s.SkillId == sid))
).ToList();
Translated as: get the users where, for at least one appId the user has an app with that application id and for any skillIds the user has at least one skill with that id
If you run this test class, you'll also see the executed query (please, note that, to do so, I'm using the Log property of Database. I think it's only available from EF6 on).
namespace Tests
{
[TestClass]
public class CheckSeveralRelationsAtOnce
{
[TestMethod]
public void HasAllAppsAndSkills()
{
int[] appIds = {1, 2, 3};
int[] skillIds = {6, 7, 8};
using (var ctx = new MyDbContext())
{
ctx.Database.Log = Console.Write;
var usersWithAll = ctx.Users2.Where(u =>
appIds.All(aid => u.Apps.Any(a => a.AppId == aid))
&& skillIds.All(sid => u.Skills.Any(s => s.SkillId == sid))
).ToList();
Assert.IsNotNull(usersWithAll);
}
}
[TestMethod]
public void HasAnyAppsOrSkill()
{
int[] appIds = { 1, 2, 3 };
int[] skillIds = { 6, 7, 8 };
using (var ctx = new MyDbContext())
{
ctx.Database.Log = Console.Write;
var usersWithAny = ctx.Users2.Where(u =>
appIds.Any(aid => u.Apps.Any(a => a.AppId == aid))
&& skillIds.Any(sid => u.Skills.Any(s => s.SkillId == sid))
).ToList();
Assert.IsNotNull(usersWithAny);
}
}
}
}
I believe the answer by codeworx is correct for getting users with all the skills / apps
As an aside - I answered pretty much the same question recently with a pure SQL solution (SQL Server) - that could be turned into a stored procedure (with a table valued parameter) - See here if interested. This will perform better for a large number of values. Entity framework will turn every skill/app in the list into its own SQL parameter, which is much slower.
Unfortunately Entity framework doesn't really support table valued parameters yet - although you can use the entity framework connection to directly call a stored procedure with a table valued parameter (as per this article
Back to the question at hand...
I'll add the (easier) query to select a user with ANY of the skills & ANY of the apps:
var result = from u in context.Users
join _a in (
from a in context.UserApp
where requestedAppIDs.Contains(a.AppId)
select a.UserId;
) on u.Id equals _a
into aGrp
join _s in (
from s in context.UserSkill
where requestedSkillIDs.Contains(s.SkillId)
select s.UserId;
) on u.Id equals _s
into sGrp
where aGrp.Any()
&& sGrp.Any()
select u;
And just for completeness - the ALL solution again:
var skillCount = requestedSkillIDs.Length;
var appCount = requestedAppIDs.Length;
var result = from u in context.Users
join _a in (
from a in context.UserApp
where requestedAppIDs.Contains(a.AppId)
select a.UserId;
) on u.Id equals _a
into aGrp
join _s in (
from s in context.UserSkill
where requestedSkillIDs.Contains(s.SkillId)
select s.UserId;
) on u.Id equals _s
into sGrp
where aGrp.Count() == appCount
&& sGrp.Count() == skillCount
select u;
and finally - an example where the main query body is fixed, but you can add differing where clauses depending upon the AND/OR requirement
bool onlyReturnWhereAllAreMatched = false;
var skillCount = requestedSkillIDs.Length;
var appCount = requestedAppIDs.Length;
IQueryable<User> result;
var query = from u in context.Users
join _a in (
from a in context.UserApp
where requestedAppIDs.Contains(a.AppId)
select a.UserId;
) on u.Id equals _a
into aGrp
join _s in (
from s in context.UserSkill
where requestedSkillIDs.Contains(s.SkillId)
select s.UserId;
) on u.Id equals _s
into sGrp
select new {u, aCount = aGrp.Count(), sCount = sGrp.Count()};
if (onlyReturnWhereAllAreMatched)
{
result = from x in query
where x.aCount == appCount
&& x.sCount == skillCount
select x.u;
} else {
result = from x in query
where x.aCount > 0
&& x.sCount > 0
select x.u;
}
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?