Convert simple Left Outer Join and group by SQL statement into Linq - c#

2 tables: User and Alarm
Table:User
UserID(int),
FullName(varchar)
Table:Alarm
AssignedTo(int),
Resolved(bool)
Query:
SELECT u.Fullname, COUNT(resolved) as Assigned, SUM(CONVERT(int,Resolved)) as Resolved, COUNT(resolved) - SUM(CONVERT(int,Resolved)) as Unresolved
FROM Alarm i LEFT OUTER JOIN Users u on i.AssignedTo = u.UserID
GROUP BY u.Fullname
Results:
Fullname Assigned Resolved Unresolved
User1 204 4 200
User2 39 9 30
User3 235 200 35
User4 1 0 1
User5 469 69 400
For the life of me I can't figure out how to make this into a Linq query. I am having trouble with the grouping function.
I've looked a countless examples and none have my combination of Left Outer join with grouping or they are so complicated that I can't figure out how to make it work with mine. Any help here would be Greatly appreciated!!!
Update:
I may not have been clear in what I'm looking for. I am looking for the alarms grouped by the AssignedTo Column which is a userid... Except, I want to replace that userid with the FullName that is located in the users table. Someone had posted and deleted something close except it gave me all users in the user table which is not what I'm looking for..
Update 2: See my answer below

Assuming that you have the following models:
This is the model for Alarm:
public class Alarm
{
public int id { get; set; }
public int AssignedTo { get; set; }
[ForeignKey("AssignedTo")]
public virtual User User { get; set; }
public bool Resolved { get; set; }
}
This is the model for User:
public class User
{
public int UserID { get; set; }
public string FullName { get; set; }
public virtual ICollection<Alarm> Alarms { get; set; }
public User()
{
Alarms = new HashSet<Alarm>();
}
}
This is the model that will hold the alarm statistics for each user:
public class UserStatistics
{
public string FullName { get; set; }
public int Assigned { get; set; }
public int Resolved { get; set; }
public int Unresolved { get; set; }
}
You can then do the following:
var query = context.Users.Select(
user =>
new UserStatistics
{
FullName = user.FullName,
Assigned = user.Alarms.Count,
Resolved = user.Alarms.Count(alarm => alarm.Resolved),
Unresolved = user.Alarms.Count(alarm => !alarm.Resolved)
});
var result = query.ToList();
By the way, you can also modify the query and remove Unresolved = user.Alarms.Count(alarm => !alarm.Resolved), and then make the Unresolved property a calculated property like this:
public class UserStatistics
{
public string FullName { get; set; }
public int Assigned { get; set; }
public int Resolved { get; set; }
public int Unresolved
{
get { return Assigned - Resolved; }
}
}
This will make the generated SQL query simpler.

I finally figured it out.
This:
var results = alarms.GroupBy(x => x.AssignedTo)
.Join(users, alm => alm.Key , usr => usr.UserID, (alm, usr) => new {
Fullname = usr.FullName,AssignedNum = alm.Count(),
Resolved = alm.Where(t=>t.resolved == true).Select(y => y.resolved).Count(),
Unresolved = alm.Where(t=>t.resolved == false).Select(y => y.resolved).Count() });
Reproduces This:
SELECT u.Fullname, COUNT(resolved) as Assigned, SUM(CONVERT(int,Resolved)) as Resolved,
COUNT(resolved) - SUM(CONVERT(int,Resolved)) as Unresolved
FROM Alarm i LEFT OUTER JOIN Users u on i.AssignedTo = u.UserID
GROUP BY u.Fullname
The result is grouped by the AssignedTo (int) but AssignedTo is not selected. Instead FullName is selected from the joined user table.
Many thanks to everyone that tried to help! I learned a lot from your answers.
For bonus points, how would I write my lamdbda answer in a SQL like syntax?

Try this :
from u in context.User
join a in context.Alarm on u.UserID equals a.AssignedTo into g1
from g2 in g1.DefaultIfEmpty()
group g2 by u.Fullname into grouped
select new { Fullname = grouped.Key, Assigned = grouped.Count(t=>t.Resolved != null), Resolved = grouped.Sum
(t => int.Parse(t.Resolved)), Unresolved = (grouped.Count(t=>t.Resolved != null) - grouped.Sum
(t => int.Parse(t.Resolved)))}

I guess it is not necessarily to use "Grouping" for this query in Linq because the combination of "LEFT JOIN" + "GROUP BY" changed them over to "INNER JOIN".
var results =
from u in users
join a in alarms on u.UserID equals a.AssignedTo into ua
select new
{
Fullname = u.FullName,
Assigned = ua.Count(),
Resolved = ua.Count(a => a.Resolved),
Unresolved = ua.Count(a => !a.Resolved)
};
foreach (var r in results)
{
Console.WriteLine(r.Fullname + ", " + r.Assigned + ", " + r.Resolved + ", " + r.Unresolved);
}

Related

LINQ - Grouping by left join queries

I have three tables as follows:
public class Employee
{
public Guid Id { get; set; }
public string FirstName { get; set; } = null!;
public string LastName { get; set; } = null!;
}
public class Answer
{
public Guid Id { get; set; }
public Guid QuestionId { get; set; }
public Guid AppraiserId { get; set; }//Employee who appraises
public Guid AppraisedId { get; set; }//Employee who has been appraised
}
public class FinalizedEmployee
{
public long Id { get; set; }
public Guid AppraiserId { get; set; }
public Guid AppraisedId { get; set; }
}
Every employee has an entry in the answers table by default. and when they answer all the questions we add a record in FinalizedEmployees.
Now I want to write a query to show all employees whether they have answered all the questions or not.
The output will be something like this:
Employee
Has answered all the questions
Employee 1
True
Employee 2
False
Employee 3
True
This is what I have tried so far:
var attendees = from answers in dbContext.Answers
join employees in dbContext.Employees
on answers.AppraiserId equals employees.Id
join finalize in dbContext.FinalizedEmployees
on answers.AppraiserId equals finalize.AppraisedId into finalized
from completed in finalized.DefaultIfEmpty()
where answers.AppraisalId == request.appraisalId
group employees by new { appraiser=answers.AppraiserId,hasComplete=completed!=null} into attendee
select new {attendee.Key.appraiser,attendee.Key.hasComplete };
The output is correct but only gives me employee Ids instead of the employee itself.
How can I write this query?
Is there any better way?
Every employee has an entry in answers table. and if they have answered all the questions there is a record in FinalizedEmployee table.
To check whether that the Employee has answered all the questions, you need a LEFT JOIN query for Employee to FinalizedEmployee tables.
With LEFT JOIN:
Guarantee all the records from Employee (LEFT) table will be queried.
If the employee's Id exists in FinalizedEmployee, the HasAnsweredAllQuestion will be true.
(from a in dbContext.Employees
join b in dbContext.FinalizedEmployees on a.Id equals b.AppraisedId into ab
from b in ab.DefaultIfEmpty()
select new
{
Id = a.Id,
Name = a.FirstName + " " + a.LastName,
HasAnsweredAllQuestion = b != null
}).ToList();
Another approach suggested by #Corey which was achieved with EXISTS will be:
(from a in dbContext.Employees
select new
{
Id = a.Id,
Name = a.FirstName + " " + a.LastName,
HasAnsweredAllQuestion = dbContext.FinalizedEmployees.Any(x => x.AppraisedId == a.Id)
}).ToList();

.Net 5 Entity Framework removing unneccessary calls from Query involving FK 1:M relationship

I have the following 2 DTOs that are used to return the information in the API in order to filter out fields that I don't want the client to be able to view
public record CommentDto
{
public int? CommentId { get; set; }
public string AuthorUid { get; set; }
public string Text { get; set; }
public int? ParentCommentId { get; set; }
public string CommentPn { get; set; }
public virtual UserDto User { get; set; }
}
public record UserDto
{
public string Uid { get; set; }
public string Username { get; set; }
}
I'm querying the postgres database using the following:
var comments = await dbSet.
Where(c => c.commentPn == sentPn)
.Select(c => new CommentDto
{
CommentId = c.CommentId,
CommentPn = c.CommentPn,
AuthorUid = c.AuthorUid,
Text = c.Text,
ParentCommentId = c.ParentCommentId,
User = new UserDto
{
Username = dbSet.Select(u => u.User.Username).Single(),
Uid = dbSet.Select(u => u.User.Uid).Single()
},
}).ToListAsync();
While this works and the correct data is returned, I notice what I believe are unnecessary call(s) being included in the query.
SELECT d1.comment_id AS "CommentId", d1.comment_pn AS "CommentNiin", d1.author_uid AS "AuthorUid", d1.comment_text AS "Text", d1.parent_comment_id AS "ParentCommentId", (
SELECT u.username
FROM database_item_comments AS d
INNER JOIN users AS u ON d.author_uid = u.uid
LIMIT 1) AS "Username", (
SELECT u0.uid
FROM database_item_comments AS d0
INNER JOIN users AS u0 ON d0.author_uid = u0.uid
LIMIT 1) AS "Uid"
FROM database_item_comments AS d1
I know it's due to the way I'm retrieving the user values being incredibly inefficient, what would be the correct way to complete this query while only making a single call to the users table?
Preferably just querying the comment table directly, and returning the full Comment entity sans DTO, without having to map the variables, while creating the UserDto for the Comment and mapping the values
Your Comment entity should have a reference to a User entity, which would see your query corrected to:
var comments = await dbSet.
Where(c => c.commentPn == sentPn)
.Select(c => new CommentDto
{
CommentId = c.CommentId,
CommentPn = c.CommentPn,
AuthorUid = c.AuthorUid,
Text = c.Text,
ParentCommentId = c.ParentCommentId,
User = new UserDto
{
UId = c.User.UId,
Username = c.User.Username
});
}).ToListAsync();
Your example would potentially fail if you have multiple comments in the DB as to populate the user DTO you were effectively telling it to load all comments (DbSet.Select) to get at the User and expect only 1 result via Single(). Then there is the fact that you are executing that twice, once to select the ID, and then to select the name.

Dapper Multi Mapping Not Splitting on Named Parameters

New to Dapper here! Having an issue with multi-mapping. This is my query:
var sql = #"select distinct a.*,
c.Id as 'GenreId', c.Active as 'GenreActive', c.Link as 'GenreLink', c.Name as 'GenreName', c.DateCreated as 'GenreDateCreated', c.DateEdited as 'GenreDateEdited',
d.Id as 'CommentId', d.ReviewId as 'CommentReviewId', d.Name as 'CommentName', d.Email as 'Comment.Email', d.Content as 'CommentContent', d.Active as 'CommentActive', d.DateCreated as 'CommentDateCreated', d.DateEdited as 'CommentDateEdited', d.CommentId as 'ReplyCommentId'
from Review a " +
"left join ReviewGenre b on a.Id = b.ReviewId " +
"left join Genre c on c.Id = b.ReviewId " +
"left join Comment d on a.Id = d.ReviewId " +
"where a.Active = 1 " +
"order by a.DatePublished desc;"
;
And my entities are (shortened for brevity):
public class Review
{
public int Id {get;set;}
public IEnumerable<Genre> Genres { get; set; }
public IEnumerable<Comment> Comments { get; set; }
}
public class Genre
{
public int Id {get;set;}
public string Name {get;set;}
}
public class Comment
{
public int Id {get;set;}
public int Content {get;set;
}
My query using Dapper tries to split on the renamed columns for Genre.Id and Comment.Id. The query appears to be working fine except none of the Genres and Comments appear to be mapping to the Review class. This is how I am trying to execute the query:
using (var connection = new SqlConnection(_ConnectionString))
{
var reviews = await connection.QueryAsync<Review, Genre, Comment, Review>(
sql,
(review, genre, comment) =>
{
review.Genres = new List<Genre>();
review.Comments = new List<Comment>();
if (genre != null)
{
review.Genres.ToList().Add(genre);
}
if (comment != null)
{
review.Comments.ToList().Add(comment);
}
return review;
},
commandType: CommandType.Text,
splitOn: "GenreId,CommentId");
return reviews;
}
I have researched throughout tutorials and SO on the subject and not finding what could be causing the mapping to not happen.
I would appreciate any suggestions (newbie to Dapper). Thanks!
At this line
review.Genres.ToList().Add(genre);
you are creating each time a new list (.ToList()). This method returns/creates new instance, but the new instance is never assigned back to the model property. It's like doing something like that:
var list = new List<int>();
new List<int>().Add(1);
The two instances are separate objects.
What you can do is to changed your models to work like this (the lists are instantiated with the creation of the object):
public class Review
{
public int Id {get;set;}
public List<Genre> Genres { get; set; } = new List<Genre>();
public List<Comment> Comments { get; set; } = new List<Comment>();
}
and then adding elements like this:
review.Genres.Add(genre);
Or you can check the original dapper tutorial where they are using dictionary as state manager to remove duplicates.

Linq query with first or default join

I have the following data model:
public class Course
{
public int CourseId { get; set; }
public int StateId { get; set; }
}
public class CompletedCourse
{
public int CompletedCourseId { get; set; }
public int UserId { get; set; }
public Course Course { get; set; }
public string LicenseNumber { get; set; }
}
public class License
{
public int LicenseId { get; set; }
public int UserId { get; set; }
public int StateId { get; set; }
public string LicenseNumber { get; set; }
}
I'm trying to come up with an IQueryable for CompletedCourses and I would like to populate CompletedCourse.LicenseNumber with the LicenseNumber property of the FirstOrDefault() selection from my Licenses table where UserId and StateId match the completed course records.
Here is my query, but I don't think this will handle duplicate licenses correctly:
var entries =
(from course in context.CompletedCourses
join license in context.Licenses on course.UserId equals license.UserId
where license.StateId == course.Course.StateId
select course)
.Include(x => x.Agent)
.Include(x => x.Course.State);
Is this something that can be done in a single query? Thanks in advance.
Here is how you can do that:
var entries =
(from course in context.CompletedCourses
join license in context.Licenses
on new { course.UserId, course.Course.StateId }
equals new { license.UserId, license.StateId }
into licenses
let licenseNumber = licenses.Select(license => license.LicenseNumber).FirstOrDefault()
select new { course, licenseNumber });
But please note that with this type of projection you cannot have Includes in your query (you can, but they will not be in effect).
The EF generated query I'm getting from the above is:
SELECT
[Extent1].[CompletedCourseId] AS [CompletedCourseId],
[Extent1].[UserId] AS [UserId],
[Extent1].[LicenseNumber] AS [LicenseNumber],
[Extent1].[Course_CourseId] AS [Course_CourseId],
(SELECT TOP (1)
[Extent2].[LicenseNumber] AS [LicenseNumber]
FROM [dbo].[Licenses] AS [Extent2]
INNER JOIN [dbo].[Courses] AS [Extent3] ON [Extent3].[StateId] = [Extent2].[StateId]
WHERE ([Extent1].[Course_CourseId] = [Extent3].[CourseId]) AND ([Extent1].[UserId] = [Extent2].[UserId])) AS [C1]
FROM [dbo].[CompletedCourses] AS [Extent1]
It can be noticed that EF effectively ignores the join, so the same result can be obtained by simple natural query:
var entries =
(from course in db.CompletedCourses
let licenseNumber =
(from license in db.Licenses
where license.UserId == course.UserId && license.StateId == course.Course.StateId
select license.LicenseNumber).FirstOrDefault()
select new { course, licenseNumber });
#IvanStoev's answer was very helpful in joining on anonymous types, but ultimately I couldn't use it because I needed Includes. Here is the solution I went with that results in two DB queries instead of one which is fine for my situation.
var entries = context.CompletedCourses
.Include(x => x.Agent)
.Include(x => x.Course);
var courses = entries.ToList();
var courseIds = entries.Select(x => x.CompletedCourseId);
var licenses =
(from course in entries
join license in context.Licenses
on new { course.AgentId, course.Course.StateId }
equals new { AgentId = license.UserId, license.StateId }
where courseIds.Contains(course.CompletedCourseId)
select license);
foreach (var course in courses)
{
var license = agentLicenses.FirstOrDefault(x => x.UserId == course.AgentId &&
x.StateId == course.Course.StateId);
if (license != null)
{
course.LicenseNumber = license.LicenseNumber;
}
}
return courses;

Select TOP Elements in a Group with EF

I have following entities, basically User and Message have M2M relationship via MsgGroup. Also first message in a thread (or group) has ThreadId=MessageId, where others share the same ThreadId
public class User
{
public int UserId { get; set; }
[System.ComponentModel.DataAnnotations.Schema.InverseProperty("Received")]
public virtual System.Collections.Generic.ICollection<MsgGroup> ReceivedGroups { get; set; }
}
public class Message
{
public int MessageId { get; set; }
public int ThreadId { get; set; }
public virtual System.Collections.Generic.ICollection<MsgGroup> MsgGroups { get; set; }
}
public class MsgGroup
{
public int MsgGroupId { get; set; }
public int UserId { get; set; }
public virtual User Received { get; set; }
public int MessageId { get; set; }
public virtual Message Message { get; set; }
}
I want to get lates messages in groups for a given user. Particularly the following SQL query:
select * from messages where MessageId IN (
SELECT MaxId FROM (
select ThreadId , MAX(MessageId) as MaxId from messages where ThreadId in (
select distinct MessageId from msggroups where UserId = 1
)
GROUP BY ThreadId
) AS t1
)
I have tried:
var query = from grp in db.MsgGroups.Where(g => g.UserId == userId)
select new
{
f = (from msg in db.Messages where (msg.ThreadId == grp.MessageId) select msg).OrderByDescending(m => m.Date).Take(1)
};
however it creates a very complex query where there are subqueries for each field of message.
Is there a solution? Also can I ask the same question in method based format and query expression format (since I can't figure out in query expression format at all)
I think you may be looking to do a join, maybe something like this, this will get you the most recent messages for a particular user, ordered by thread ID, you can replace 100 in the Take section with how many messages you want:
var query = (from grp in db.MsgGroups
.Join(
db.Messages,
g => g.MessageId,
m => m.ThreadId,
(g, m) => new { grp = g, mgs = m })
.Where(x => x.grp.UserId == userId)
select new
{
Message = grp.mgs
})
.Take(100)
.OrderByDescending(t => t.Message.ThreadId);
This will give you a collection of anonymous objects, each one having a single property called Message, which is the Message object you want.
For anyone interested below query exactly did what I requested, only difference is it used JOINs instead of INs in my question:
var query =
from mm in
(
from grp in db.MsgGroups.Where(g => g.UserId == userId)
from msg in db.Messages
where msg.ThreadId == grp.MessageId
group msg by msg.ThreadId into thr
select new { t = thr.Key, m = thr.Max(t => t.MessageId) }
)
join omsg in db.Messages
on mm.m equals omsg.MessageId
select new { t = mm.t, m = omsg.Text } ;
Use the navigation properties and let EF take care of the SQL joins:
var query = from rg in db.MsgGroups
where rg.UserId == userId
group rg by rg.ThreadId into thread
select new {
Thread = thread.Key,
LastMessage = thread.Select(t => t.Message)
.OrderByDescending(m => m.Date)
.FirstOrDefault()
};
This gives you the last message for each thread the user has messages in.

Categories

Resources