I have 3 classes and trying to use LINQ methods to perform an INNER JOIN and a LEFT JOIN. I'm able to perform each separately, but no luck together since I can't even figure out the syntax.
Ultimately, the SQL I'd write would be:
SELECT *
FROM [Group] AS [g]
INNER JOIN [Section] AS [s] ON [s].[GroupId] = [g].[Id]
LEFT OUTER JOIN [Course] AS [c] ON [c].[SectionId] = [s].[Id]
Classes
public class Group {
public int Id { get; set; }
public int Name { get; set; }
public bool IsActive { get; set; }
public ICollection<Section> Sections { get; set; }
}
public class Section {
public int Id { get; set; }
public int Name { get; set; }
public int GroupId { get; set; }
public Group Group { get; set; }
public bool IsActive { get; set; }
public ICollection<Course> Courses { get; set; }
}
public class Course {
public int Id { get; set; }
public int UserId { get; set; }
public int Name { get; set; }
public int SectionId { get; set; }
public bool IsActive { get; set; }
}
Samples
I want the result to be of type Group. I successfully performed the LEFT JOIN between Section and Course, but then I have an object of type IQueryable<a>, which is not what I want, sinceGroup`.
var result = db.Section
.GroupJoin(db.Course,
s => s.Id,
c => c.SectionId,
(s, c) => new { s, c = c.DefaultIfEmpty() })
.SelectMany(s => s.c.Select(c => new { s = s.s, c }));
I also tried this, but returns NULL because this performs an INNER JOIN on all tables, and the user has not entered any Courses.
var result = db.Groups
.Where(g => g.IsActive)
.Include(g => g.Sections)
.Include(g => g.Sections.Select(s => s.Courses))
.Where(g => g.Sections.Any(s => s.IsActive && s.Courses.Any(c => c.UserId == _userId && c.IsActive)))
.ToList();
Question
How can I perform an INNER and a LEFT JOIN with the least number of calls to the database and get a result of type Group?
Desired Result
I would like to have 1 object of type Group, but only as long as a Group has a Section. I also want to return the Courses the user has for the specific Section or return NULL.
I think what you ask for is impossible without returning a new (anonymous) object instead of Group (as demonstrated in this answer). EF will not allow you to get a filtered Course collection inside a Section because of the way relations and entity caching works, which means you can't use navigational properties for this task.
First of all, you want to have control over which related entities are loaded, so I suggest to enable lazy loading by marking the Sections and Courses collection properties as virtual in your entities (unless you've enabled lazy loading for all entities in your application) as we don't want EF to load related Sections and Courses as it would load all courses for each user anyway.
public class Group {
public int Id { get; set; }
public int Name { get; set; }
public bool IsActive { get; set; }
public virtual ICollection<Section> Sections { get; set; }
}
public class Section {
public int Id { get; set; }
public int Name { get; set; }
public int GroupId { get; set; }
public Group Group { get; set; }
public bool IsActive { get; set; }
public virtual ICollection<Course> Courses { get; set; }
}
In method syntax, the query would probably look something like this:
var results = db.Group
.Where(g => g.IsActive)
.GroupJoin(
db.Section.Where(s => s.IsActive),
g => g.Id,
s => s.GroupId,
(g, s) => new
{
Group = g,
UserSections = s
.GroupJoin(
db.Course.Where(c => c.IsActive && c.UserId == _userId).DefaultIfEmpty(),
ss => ss.Id,
cc => cc.SectionId,
(ss, cc) => new
{
Section = ss,
UserCourses = cc
}
)
})
.ToList();
And you would consume the result as:
foreach (var result in results)
{
var group = result.Group;
foreach (var userSection in result.UserSections)
{
var section = userSection.Section;
var userCourses = userSection.UserCourses;
}
}
Now, if you don't need additional filtering of the group results on database level, you can as well go for the INNER JOIN and LEFT OUTER JOIN approach by using this LINQ query and do the grouping in-memory:
var results = db.Group
.Where(g => g.IsActive)
.Join(
db.Section.Where(s => s.IsActive),
g => g.Id,
s => s.GroupId,
(g, s) => new
{
Group = g,
UserSection = new
{
Section = s,
UserCourses = db.Course.Where(c => c.IsActive && c.UserId == _userId && c.SectionId == s.Id).DefaultIfEmpty()
}
})
.ToList() // Data gets fetched from database at this point
.GroupBy(x => x.Group) // In-memory grouping
.Select(x => new
{
Group = x.Key,
UserSections = x.Select(us => new
{
Section = us.UserSection,
UserCourses = us.UserSection.UserCourses
})
});
Remember, whenever you're trying to access group.Sections or section.Courses, you will trigger the lazy loading which will fetch all child section or courses, regardless of _userId.
Use DefaultIfEmpty to perform an outer left join
from g in db.group
join s in db.section on g.Id equals s.GroupId
join c in db.course on c.SectionId equals s.Id into courseGroup
from cg in courseGroup.DefaultIfEmpty()
select new { g, s, c };
Your SQL's type is not [Group] (Type group would be: select [Group].* from ...), anyway if you want it like that, then in its simple form it would be:
var result = db.Groups.Where( g => g.Sections.Any() );
However, if you really wanted to convert your SQL, then:
var result = from g in db.Groups
from s in g.Sections
from c in s.Courses.DefaultIfEmpty()
select new {...};
Even this would do:
var result = from g in db.Groups
select new {...};
Hint: In a well designed database with relations, you very rarely need to use join keyword. Instead use navigational properties.
Related
I have the following structure: Training has many Module has many Phase has many Question.
I use the following query to get the above
Context.Trainings
.Include(x => x.Modules)
.ThenInclude(x => x.Phases)
.ThenInclude(y => y.Questions)
Question also has many Comment but that relationship is not defined as navigation property because Comment can have different type of patents. So Comment just has a ParentId that is sometimes Question and sometimes other things.
My question is how do I modify the above query to, for every Question, count the child Comment from the Context.Comments and assign it to Question.CommentCount? Kind of like a manual Include
In my head it's something like this
Context.Trainings
.Include(x => x.Modules)
.ThenInclude(x => x.Phases)
.ThenInclude(y => y.Questions.Select(x=> new Question.Question {
Name = x.Name,
Description = x.Description,
CommentCount = Context.Comments.Where(y=>y.ParentId == x.Id)
}));
But it seems you can't put projections in Include and I don't know how to think about this in another way.
With the entities set up such as ...
public class Training
{
public int Id { get; set; }
public ICollection<Module> Modules { get; set; }
}
public class Module
{
public int Id { get; set; }
public ICollection<Phase> Phases { get; set; }
}
public class Phase
{
public int Id { get; set; }
public ICollection<Question> Questions { get; set; }
}
public class Question
{
public int Id { get; set; }
[NotMapped]
public int CommentCount { get; set; }
}
public class Comment
{
public int Id { get; set; }
public int ParentId { get; set; }
}
// DbContext
public DbSet<Training> Trainings { get; set; }
public DbSet<Module> Modules { get; set; }
public DbSet<Phase> Phases { get; set; }
public DbSet<Question> Questions { get; set; }
public DbSet<Comment> Comments { get; set; }
... it can be done in a single query, but it's quite messy.
// query all nested navigations using projections with extra data
var projected = await context.Trainings
.Select(t =>
new
{
Training = t,
Modules = t.Modules.Select(m =>
new
{
Module = m,
Phases = m.Phases.Select(p =>
new
{
Phase = p,
Questions = p.Questions.Select(q =>
new
{
Question = q,
CommentCount = context.Comments.Count(c => c.ParentId == q.Id)
}
)
}
)
}
)
}
)
.ToListAsync();
// fixup by setting comment count from dto projection to "real" tracked entity
foreach (var q in projected.SelectMany(t => t.Modules).SelectMany(m => m.Phases).SelectMany(m => m.Questions))
{
q.Question.CommentCount = q.CommentCount;
}
// thanks to ef core entity tracker this will still work
var trainings = projected.Select(p => p.Training);
var totalCommentCount = trainings.SelectMany(t => t.Modules).SelectMany(m => m.Phases).SelectMany(p => p.Questions).Sum(q => q.CommentCount);
final query
SELECT [t].[Id], [t0].[Id], [t0].[TrainingId], [t0].[Id0], [t0].[ModuleId], [t0].[Id00], [t0].[PhaseId], [t0].[c]
FROM [Trainings] AS [t]
LEFT JOIN (
SELECT [m].[Id], [m].[TrainingId], [t1].[Id] AS [Id0], [t1].[ModuleId], [t1].[Id0] AS [Id00], [t1].[PhaseId], [t1].[c]
FROM [Modules] AS [m]
LEFT JOIN (
SELECT [p].[Id], [p].[ModuleId], [q].[Id] AS [Id0], [q].[PhaseId], (
SELECT COUNT(*)
FROM [Comments] AS [c]
WHERE [c].[ParentId] = [q].[Id]) AS [c]
FROM [Phases] AS [p]
LEFT JOIN [Questions] AS [q] ON [p].[Id] = [q].[PhaseId]
) AS [t1] ON [m].[Id] = [t1].[ModuleId]
) AS [t0] ON [t].[Id] = [t0].[TrainingId]
ORDER BY [t].[Id], [t0].[Id], [t0].[Id0]
As pointed out in comments, you could benefit from using TPH with real navigation collection back to comments from questions, and you should also probably use split query or multiple queries instead of joining it all up like this. But depending on use case, perhaps a single query might perform better for you.
So the equivalent of this query:
select * from car
left join parts ON car.Id = parts.carId
where parts.MemberId = 1
is this, in EntityFrameworkCore LINQ , using an IQueryable which has already selected car.Include(x => x.parts):
queryable = queryable.Where(x =>
x.parts.Select(y => y.MemberId).Contains(1);
But how can I convert the following SQL to LINQ, so that it includes rows from the left car table that have no respective MemberId entries in the parts table?
select * from car
left join parts ON car.Id = parts.CarId and parts.MemberId = 1
Models:
public class Car
{
public int Id { get; set; }
public virtual ICollection<Part> Parts { get; set; }
}
public class Parts
{
public int Id { get; set; }
public int CarId { get; set; }
public virtual Car { get; set; }
public int MemberId { get; set; }
}
A filtered Include does exactly what you want:
var cars = context.Cars
.Include(c => c.Parts.Where(p => p.MemberId == 1));
This doesn't generate the shorter join statement with a composite condition, but an outer join to a filtered subquery on Parts, to the same effect.
queryable = queryable
.Where(x => x.parts.Select(y => y.MemberId).Contains(1) || !x.parts.Any());
Try it like that:
queryable = queryable.Include(x => x.parts).Where(x =>
x.parts.Any(y => y.MemberId == 1).ToList();
I have SQL query like this
SELECT T.*
FROM
(
SELECT ServiceRecords.DistrictId, Districts.Name as DistrictName, COUNT(Distinct(NsepServiceRecords.ClientRegNo)) AS ClientsServedCount
FROM ServiceRecords
INNER JOIN Districts ON ServiceRecords.DistrictId = Districts.ID
INNER JOIN NsepServiceRecords ON NsepServiceRecords.ServiceRecordId = ServiceRecords.Id
WHERE ServiceRecords.CreatedAtUtc >= #StartDate
AND ServiceRecords.CreatedAtUtc <= #EndDate
AND ServiceRecords.DistrictId = #DistrictId
GROUP BY ServiceRecords.DistrictId, Districts.Name
) AS T
ORDER BY T.DistrictName ASC, T.DistrictId
Query results:
DistrictId DistrictName ClientsServedCount
8d059005-1e6b-44ad-bc2c-0b3264fb4567 Bahawalpur 117
27ab6e24-50a6-4722-8115-dc31cd3127fa Gujrat 492
14b648f3-4912-450e-81f9-bf630a3dfc72 Jhelum 214
8c602b99-3308-45b5-808b-3375d61fdca0 Lodhran 23
059ffbea-7787-43e8-bd97-cab7cb77f6f6 Muzafarghar 22
580ee42b-3516-4546-841c-0bd8cef04df9 Peshawar 211
I'm struggling converting this to LINQ to entities query. I want to get same results (except District Id column) using LINQ.
I have tried like this, but not working as expected. Can somebody tell me what I'm doing wrong?
_dbContext.ServiceRecords
.Include(x => x.District)
.Include(x=>x.NsepServiceRecords)
.GroupBy(x => x.DistrictId)
.Select(x => new DistrictClientsLookUpModel
{
DistrictName = x.Select(record => record.District.Name).FirstOrDefault(),
ClientsServedCount = x.Sum(t=> t.NsepServiceRecords.Count)
});
Model classes are like this
public class BaseEntity
{
public Guid Id { get; set; }
}
public class NsepServiceRecord : BaseEntity
{
public DateTime CreatedAtUtc { get; set; }
public Guid ServiceRecordId { get; set; }
public string ClientRegNo { get; set; }
// other prop .......
public virtual ServiceRecord ServiceRecord { get; set; }
}
public class ServiceRecord : BaseEntity
{
public DateTime CreatedAtUtc { get; set; }
public string DistrictId { get; set; }
public virtual District District { get; set; }
public virtual ICollection<NsepServiceRecord> NsepServiceRecords { get; set; }
}
public class DistrictClientsLookUpModel
{
public string DistrictName { get; set; }
public int ClientsServedCount { get; set; }
}
I'm using Microsoft.EntityFrameworkCore, Version 2.2.4
EDIT
I have also tried like this
var startUniversalTime = DateTime.SpecifyKind(request.StartDate, DateTimeKind.Utc);
var endUniversalTime = DateTime.SpecifyKind(request.EndDate, DateTimeKind.Utc);
return _dbContext.NsepServiceRecords
.Join(_dbContext.ServiceRecords, s => s.ServiceRecordId,
r => r.Id, (s, r) => r)
.Include(i => i.District)
.Where(x => x.DistrictId == request.DistrictId
&& x.CreatedAtUtc.Date >= startUniversalTime
&& x.CreatedAtUtc.Date <= endUniversalTime)
.OrderBy(x => x.DistrictId)
.GroupBy(result => result.DistrictId)
.Select(r => new DistrictClientsLookUpModel
{
DistrictName = r.Select(x=>x.District.Name).FirstOrDefault(),
ClientsServedCount = r.Sum(x=>x.NsepServiceRecords.Count())
});
Another try,
from s in _dbContext.ServiceRecords
join record in _dbContext.NsepServiceRecords on s.Id equals record.ServiceRecordId
join district in _dbContext.Districts on s.DistrictId equals district.Id
group s by new
{
s.DistrictId,
s.District.Name
}
into grp
select new DistrictClientsLookUpModel
{
DistrictName = grp.Key.Name,
ClientsServedCount = grp.Sum(x => x.NsepServiceRecords.Count)
};
It takes too long, I waited for two minutes before I killed the request.
UPDATE
EF core have issues translating GroupBy queries to server side
Assuming the District has a collection navigation property to ServiceRecord as it should, e.g. something like
public virtual ICollection<ServiceRecord> ServiceRecords { get; set; }
you can avoid the GroupBy by simply starting the query from District and use simple projection Select following the navigations:
var query = _dbContext.Districts
.Select(d => new DistrictClientsLookUpModel
{
DistrictName = d.Name,
ClientsServedCount = d.ServiceRecords
.Where(s => s.CreatedAtUtc >= startUniversalTime && s.CreatedAtUtc <= endUniversalTime)
.SelectMany(s => s.NsepServiceRecords)
.Select(r => r.ClientRegNo).Distinct().Count()
});
You don't appear to be doing a join properly.
Have a look at this:
Join/Where with LINQ and Lambda
Here is a start on the linq query, I'm not sure if this will give you quite what you want, but its a good start.
Basically within the .Join method you need to first supply the entity that will be joined. Then you need to decide on what they will be joined on, in this case district=> district.Id, serviceRecord=> serviceRecord.Id.
_dbContext.ServiceRecords
.Join( _dbContext.District,district=> district.Id, serviceRecord=> serviceRecord.Id)
.Join(_dbContext.NsepServiceRecords, Nsep=> Nsep.ServiceRecord.Id,district=>district.Id)
.GroupBy(x => x.DistrictId)
.Select(x => new DistrictClientsLookUpModel
{
DistrictName = x.Select(record => record.District.Name).FirstOrDefault(),
ClientsServedCount = x.Sum(t=> t.NsepServiceRecords.Count)
});
I have a list of the following class:
public class SiloRelationship
{
public int RelationshipType { get; set; }
public string MasterKey { get; set; }
public string SlaveKey { get; set; }
public int QueryId { get; set; }
}
I have a second list of the following class:
public class SiloNode
{
public string Key { get; private set; }
public string Url { get; private set; }
public List<NodeQuery> Queries { get; private set; }
}
Which has a sub-class:
public class NodeQuery
{
public string Query { get; private set; }
public int Seq { get; private set; }
}
Lists:
LandingSilo.Relationships is a list of SiloRelationship
LandingSilo.Nodes is a list of SiloNode.
Here's my query - there is a simple join, after which I need to return the Url and Query properties - the filter should result in a single QueryNode from the list.
What we have is:
SiloRelationship => 1 to 1 SiloNode => 1 to many QueryNode
A Kvp would be adequate for the purpose of the exercise but I can't see the Query property with the code I've got so far.
var query =
from r in LandingSilo.Relationships
join n in LandingSilo.Nodes on r.SlaveKey equals n.Key
where r.RelationshipType == 1 &&
n.Queries.Select(y => y.Seq).Contains(r.QueryId)
Any help appreciated.
Try this:
IEnumerable<string> queries = LandingSilo.Relationships
.Where(r => r.RelationshipType == 1)
.Join(
LandingSilo.Nodes,
r => r.SlaveKey,
n => n.Key,
(r, n) => n.Queries.SingleOrDefault(q => q.Seq == r.QueryId))
.Where(q => q != null)
.Select(q => q.Query);
Line by line: filter all Relationships with type different from 1, join on SlaveKey/Key and select the only query in the node that has the Seq equal to the Relationships QueryId. Filter out null results and select the Query property. This is going to throw an InvalidOperationException if there are multiple queries within one node matching.
This can be also done in the LINQ keyword syntax like this:
IEnumerable<string> queries =
from r in LandingSilo.Relationships
where r.RelationshipType == 1
join n in LandingSilo.Nodes on r.SlaveKey equals n.Key
from q in n.Queries.SingleOrDefault(q => q.Seq == r.QueryId)
where q != null
select q.Query;
You just need to filter the Queries. Change last statement like below
select n.Queries.FirstOrDefault(q => q.Seq == q.QueryId);
My models look like:
public class ReturnItem
{
public int returnItemId { get ; set; }
public int returnRequestId { get; set; }
public int quantity { get; set; }
public string item { get; set; }
}
public class ReturnRequest
{
public int returnRequestId { get; set; }
public string orderNumber { get; set; }
public IEnumerable<ReturnItem> returnItems { get; set; }
}
And I have the following query:
SELECT item, sum(quantity)
FROM ReturnItem
JOIN ReturnRequest
ON ReturnRequest.returnRequestId = ReturnItem.returnRequestId
WHERE ReturnRequest.orderNumber = '1XX'
GROUP BY item
How do I convert the query to Entity Framework and return a List<ReturnItem>? Can I use .Include instead of .Join?
from ri in db.ReturnItems
join rr in db.ReturnRequests
on ri.returnRequestId equals rr.returnRequestId
where rr.orderNumber == "1XX"
group ri by ri.item into g
select new {
Item = g.Key,
Quantity = g.Sum(i => i.quantity)
}
You can't use Include instead of Join because Include translated into Left Outer Join but you need Inner Join here.
But you can use navigation property to perform join implicitly:
db.ReturnRequests
.Where(rr => rr.orderNumber == "1XX")
.SelectMany(rr => rr.returnItems)
.GroupBy(ri => ri.item)
.Select(g => new {
Item = g.Key,
Quantity = g.Sum(ri => ri.quantity)
});