EF Query with conditional include that uses Joins - c#

This is a follow up to another user's question. I have 5 tables
CompanyDetail
CompanyContacts FK to CompanyDetail
CompanyContactsSecurity FK to CompanyContact
UserDetail
UserGroupMembership FK to UserDetail
How do I return all companies and include the contacts in the same query? I would like to include companies that contain zero contacts.
Companies have a 1 to many association to Contacts, however not every user is permitted to see every Contact. My goal is to get a list of every Company regardless of the count of Contacts, but include contact data.
Right now I have this working query:
var userGroupsQueryable = _entities.UserGroupMembership
.Where(ug => ug.UserID == UserID)
.Select(a => a.GroupMembership);
var contactsGroupsQueryable = _entities.CompanyContactsSecurity;//.Where(c => c.CompanyID == companyID);
/// OLD Query that shows permitted contacts
/// ... I want to "use this query inside "listOfCompany"
///
//var permittedContacts= from c in userGroupsQueryable
//join p in contactsGroupsQueryable on c equals p.GroupID
//select p;
However this is inefficient when I need to get all contacts for all companies, since I use a For..Each loop and query each company individually and update my viewmodel. Question: How do I shoehorn the permittedContacts variable above and insert that into this query:
var listOfCompany = from company in _entities.CompanyDetail.Include("CompanyContacts").Include("CompanyContactsSecurity")
where company.CompanyContacts.Any(
// Insert Query here....
// b => b.CompanyContactsSecurity.Join(/*inner*/,/*OuterKey*/,/*innerKey*/,/*ResultSelector*/)
)
select company;
My attempt at doing this resulted in:
var listOfCompany = from company in _entities.CompanyDetail.Include("CompanyContacts").Include("CompanyContactsSecurity")
where company.CompanyContacts.Any(
// This is concept only... doesn't work...
from grps in userGroupsQueryable
join p in company.CompanyContactsSecurity on grps equals p.GroupID
select p
)
select company;

Perhaps something like this.
var q = from company in _entities.CompanyDetail
where
(from c in userGroupsQueryable
join p in contactsGroupsQueryable on c equals p.GroupID
where company.CompanyContacts.Any(cc => cc.pkCompanyContact == p.fkCompanyContact)
select p
).Any()
select new
{
Company = company,
Contacts = company.CompanyContacts
};

The following code queries all companies, and appends only the contacts the user is permitted to see. I don't know how efficient this is, or if there is a way to make it faster, but it works.
var userGroupsQueryable = _entities.UserGroupMembership.Where(ug => ug.UserID == UserID)
.Select(a => a.GroupMembership);
var  contactsGroupsQueryable = _entities.CompanyContactsSecurity;
var listOfCompanies =
from company in _entities.CompanyDetail
select new
{
Company = company,
Contacts = (from c in userGroupsQueryable
join p in contactsGroupsQueryable on c equals p.GroupID
where company.CompanyContacts.Any(cc => cc.CompanyID == p.CompanyID)
select p.CompanyContacts)
};

Related

Left join not bringing all results when you also select a list of other objects in EFCore 3.1.11

Let me just preface this with acknowledging that this bug no longer occurs with newer versions of EFCore (5+), but unfortunately we cannot update our project right now.
I created a sample code in order to better show how this bug happens. In this sample code I have a DbContext with three tables: Companies, Locations and Employees. A company can have 1 or more employees, and 0 or more locations. I want to create a query of companies and locations that would return like this:
Company | Location | Employees
-----------------------------------------------
Company 1 | Location 1 | Employee 1, Employee 2
Company 1 | Location 2 | Employee 1, Employee 2
Company 2 | | Employee 3, Employee 4
Different locations for the same company should be in separate lines, and if the company doesn't have a location, it should still show up.
I came up with the following query using Linq, and it does what I described above in EFCore 5+, but not in 3.1.11
(from c in ctx.Companies
join _l in ctx.Locations on c.Id equals _l.CompanyId into lGroup
from l in lGroup.DefaultIfEmpty()
where
c.Id == new Guid("FFF461F3-9E38-4DA0-8B31-AC4D0C90A4D0") // Id of Company 1 in the code block above
select new
{
CompanyName = c.Name,
LocationName = l == null ? string.Empty : l.Name,
Employees = c.Employees.Select(e => e.Name)
}).AsNoTracking().ToList();
In EFCore 5+ this returns two items in the resulting list, exactly like I wanted. But in 3.1.11 it returns only one line, with the first location. The second line with the second location is missing.
The thing is, if you change the query to not use the DefaultIfEmpty() (basically changing it from a left join to an inner join) it works exactly as intended, but in that case if you try querying a company with no locations, it won't show up (since it's an inner join with location).
Does anyone know if there's a way I could achieve this in an efficient manner?
Thank you in advance.
EDIT: This is the generated query:
SELECT [c].[Name], CASE
WHEN [l].[Id] IS NULL THEN N''
ELSE [l].[Name]
END, [c].[Id], [e].[Name], [e].[Id]
FROM [Companies] AS [c]
LEFT JOIN [Locations] AS [l] ON [c].[Id] = [l].[CompanyId]
LEFT JOIN [Employees] AS [e] ON [c].[Id] = [e].[CompanyId]
WHERE [c].[Id] = 'f2c44d70-d713-42dc-b92a-e93b175fc351'
ORDER BY [c].[Id], [e].[Id]
You can add join with Employees and handle the result in memory:
(from c in ctx.Companies
join _l in ctx.Locations on c.Id equals _l.CompanyId into lGroup
from l in lGroup.DefaultIfEmpty()
join _e in ctx.Employees on c.Id equals _e.CompanyId into eGroup
from e in eGroup.DefaultIfEmpty()
where
c.Id == new Guid("FFF461F3-9E38-4DA0-8B31-AC4D0C90A4D0")
select new
{
CId = c.Id,
CompanyName = c.Name,
LId = l.Id,
LocationName = l == null ? string.Empty : l.Name,
EmployeeName = e.Name
})
.AsNoTracking()
.AsEnumerable()
.GroupBy(r => new {CId, LId})
.Select(g = > new
{
CompanyName = g.First().CompanyName ,
LocationName = g.First().LocationName ,
Employees = g.Select(e => e.EmployeeName)
})
.ToList();
try this:
var q= ctx.Locations
.Include(i=>i.Company)
.Select (i=> new
{
CompanyName = i.Company.Name,
LocationName =i.Name,
Employees = i.Company.Employees.Select(e => e.Name)
}).AsNoTracking().ToList()
.Union(
ctx.Companies
.Where(i=>i.Locations==null)
.Select (i=> new
{
CompanyName = i..Name,
LocationName =stringEmpty,
Employees = i.Employees.Select(e => e.Name)
}).AsNoTracking().ToList());
The workaround for EFC 3.1 is to split the query on two parts: server side (LINQ to Entities) query selecting only the data needed, but with "normal" collection type projection of correlated sub collections, and client side (LINQ to Objects) query doing the desired flattening / multiplication.
Something like this
(from c in ctx.Companies
select new
{
CompanyName = c.Name,
Locations = c.Locations.Select(e => new { e.Name }), // <-- the data needed
Employees = c.Employees.Select(e => e.Name),
})
.AsEnumerable() // <-- switch to client side context
.SelectMany(c => c.Locations.DefaultIfEmpty(), (c, l) => new
{
c.CompanyName,
LocationName = l == null ? string.Empty : l.Name,
c.Employees
}).ToList();
In case you don't have c.Locations collection navigation property (as you should), replace it with its equivalent
ctx.Locations.Where(e => e.CompantyId == c.Id)
which is what EFC does automatically with collection navigation properties inside L2E query.

Is possible to get dictionary of values with linq?

I have solution in my mind, but I am convinced that there must be better solution for the problem.
Lets think I have two tables - User (n..n) Group. In my app I wish to get list of Users, each with all his Groups. In SQL I can achieve similar result with:
SELECT TOP (1000) u.Name, g.Name
FROM [User] u
join [GroupUser] gu on u.Id = gu.UserId
join [Group] g on gu.GroupId = g.Id
In the code I am able to write something like this (please, don't judge the correctness, it's just for illustration):
var result = new Dictionary<Model.User, List<Model.Group>>();
List<string> names = request.Names;
var query = this.dbContext.Set<Model.User>().AsQueryable();
query = from user in query
where names.Contains(user.Name, StringComparer.OrdinalIgnoreCase)
select user;
foreach(var user in await query.ToListAsync())
{
var groupQuery =
from g in this.dbContext.Set<Model.Group>()
join gu in this.dbContext.Set<Model.GroupUser>() on g.Id equals gu.GroupId
join u in this.dbContext.Set<Model.User>() on gu.UserId equals u.Id
where u.Name.Equals(user.Name, StringComparison.OrdinalIgnoreCase)
select g;
result.Add(user, await groupQuery.ToListAsync())
}
My question is -- is possible to achieve something like this with single linq query? Or do I really need to enumerate all the Users and fire new query for each of them? This code looks very resource demanding. It is quite simple, only one cycle, but it contains Users.Count+1 query evaluations.
Thanks in advance for hints.
Assuming you are using LINQ to SQL, you can combine the queries:
var requestedUsers = from user in query
where names.Contains(user.Name, StringComparer.OrdinalIgnoreCase)
select user;
var result = (from u in requestedUsers
join gu in this.dbContext.Set<Model.User>() on u.UserId equals gu.Id
join g in this.dbContext.Set<Model.Group>() on gu.UserId equals g.Id into gj
select new { u, gj })
.ToDictionary(ugj => ugj.u, ugj => ugj.gj.ToList());
If you have an entity like the following:
public class User
{
// other properties
public virtual List<Group> Groups { get; set; }
}
You can query both with an Include:
var users = dbContext.Users.Include(u => u.Groups)
.Where(u => names.Contains(u.Name, StringComparer.OrdinalIgnoreCase));

Linq EF group by to get latest entries into list of objects

I am trying to move from simple SQL to EF.
But there are some complex queries(joins) that it seems to hard to generate the linq for.
At first I tried to use sqltolinq tool to generate the linq but it gives error as some of the things are not supported in the query.
here is the linq:
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
});
The table entry has many entries that I would like to group and get the latest for each group. But then I would need to select into a view model object which will be bind to gridview.
I dont know where I can implement the logic to group by and get the latest from each and be able to get values from join table into viewModel object.
somewhere I need to add
group entry by new
{
entry.aID,
entry.bCode,
entry.Date,
entry.FCode
}
into groups
select groups.OrderByDescending(p => p.ID).First()
in the above linq to retrieve latest from each group.
You can insert group by right after the joins:
var query =
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
group new { ep, e, t } by new { e.aID, e.bCode, e.Date, e.FCode } into g
let r = g.OrderByDescending(x => x.e.ID).FirstOrDefault()
select new
{
UID = r.e.OwnerID,
TID = r.e.TID,
Title = r.t.Title,
EID = r.e.EID
};
The trick here is to include what you need after the grouping between group and by.
However, the above will be translated to CROSS APPLY with all joins included twice. If the grouping key contains fields from just one table, it could be better to perform the grouping/selecting the last grouping element first, and then join the result with the rest:
var query =
from e in (from e in dbContext.tbl_Entry
where e.OwnerID == user.UID
group e by new { e.aID, e.bCode, e.Date, e.FCode } into g
select g.OrderByDescending(e => e.ID).FirstOrDefault())
join ep in dbContext.tbl_EntryPoint on e.EID equals ep.EID
join t in dbContext.tbl_Title on e.TID equals t.TID
select new
{
UID = e.OwnerID,
TID = e.TID,
Title = t.Title,
EID = e.EID
};

Entity Framework inner join

I have two tables something like this:
Customers:
Email
username
Products:
Username
Product
I'd like to fill a class with username, product, and email.
This is what I have so
var res = from H in db.Products
join C in db.customers on H.Username equals C.Username
where C.Username == H.Username
select H;
Results results = res.Single();
However the catch is that I'm not sure exactly how this works, can anyone break it down for me?
var res = from H in db.Products
join C in db.customers on H.Username equals C.Username
where C.Username == H.Username
select H;
Results results = res.Single();
The code above makes an inner join between C and H (the where clause is not necessary) and results in all elements given back where an appropriately connected products and customers entry exist.
The select H "just" gives back all the data from the products. As you stated that you want a mix you need to do that differently.
I myself would be using an anonymous type (or a dto object).
For an anonymous type:
select new { Username = H.Username, Product = H.Product, EMail = C.EMail}
Put together:
var res = from H in db.Products
join C in db.customers on H.Username equals C.Username
where C.Username == H.Username
select new { Username = H.Username, Product = H.Product, EMail = C.EMail}
Results results = res.Single();
For a DTO (Data Transfer Object) you would create a class with public set/get properties and use select new myClass(){....} instead of the select new {}.
DTOs are better in terms of reuse and that you have less problems with writing the names wrong that you use, ....
Just simply using select H, C sadly can't work as Linq wants to return just single objects per row (and additionally always the same datatype). Thus you need to put a container around the objects you want to return. In this case either anonymous types or DTOs.

Linq one-to-many Union

I'm developing a MVC web application using asp.net C# and VS2012 Express.
I have a table (Organizations) with one-to-many relationships with two other tables (Comments and Proposals). All three tables contain an OrganizationID field to maintain the relationships. All three tables have an AddedBy string field.
I want to find all Organizations where either the Organization.AddedBy="Joe" or Comments.AddedBy="Joe" or Proposals.AddedBy="Joe".
These queries do a join, but I'm looking for a union that contains only the Organizations' fields.
// Find organizations created by this person.
IQueryable<Organization> org = from m in context.Organizations
where m.AddedBy.Equals("Joe")
select m;
// Find Comments created by this person.
IQueryable<Comment> comment = from m in context.Comments
where m.AddedBy.Equals("Joe")
select m;
// Join our two queries.
IQueryable<Comment> organizations = (from item in org
join c in comment on item.OrganizationID equals c.OrganizationID
select item).Distinct();
// Find Proposals created by this person.
IQueryable<Proposal> proposal = from m in context.Proposals
where m.AddedBy.Equals("Joe")
select m;
// Join our two queries.
organizations = (from item in organizations
join c in proposal on item.OrganizationID equals c.OrganizationID
select item).Distinct();
Thanks for your help.
If you are using Entity Framework you can do either:
var orgs = context.Organizations
.Where(O => O.AddedBy.Equals("Joe") ||
O.Comments.Any(C => C.AddedBy.Equals("joe")) ||
O.Proposals.Any(P => P.AddedBy.Equals("joe")));
As EF maintaining the parent-child relationship with navigation properties.
Hope this will help !!
So you're looking for three different sets, all combined. Just query each of those three things and then combine them using Union:
string user = "Joe";
var addedOrganizations = context.Organizations.Where(org => org.AddedBy == user);
var orgsWithUsersComments = from org in context.Organizations
join c in context.Comments
on org.OrganizationID equals c.OrganizationID
where c.AddedBy == user
select org;
var orgsWithUsersProposals = from org in context.Organizations
join p in context.Proposals
where p.AddedBy == user
select org;
var combinedResults = addedOrganizations.Union(orgsWithUsersComments)
.Union(orgsWithUsersProposals);

Categories