Select TOP Elements in a Group with EF - c#

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.

Related

Query from a many to many relationship table

I have two tables Documents and Group like below. I joined the two tables creating a DocumentsGroup using code first.
Documents Table:
public class Documents
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<Group> Groups { get; set;
}
Groups Table:
public class Group
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<Documents> Documents { get; set; }
}
Here is the DocumentsGroup table. This is the junction table and it does not have a model rather just showing how its looking.
{
public int DocumentsId { get; set; }
public int GroupsId{ get; set; }
}
I am trying to get all the documents which belong to one group from the junction table. I have the Group ID so am trying to get all the documents which belong to that ID like below:
int groupId = 4;
var documents = _database.Groups.Where(d => d.Id == groupId).Include(i => i.Documents).ToList();
I tried that but am not getting all the documents belonging to that group. Is there anything am doing wrong?
Use the following query:
int groupId = 4;
var query =
from g in _database.Groups
from d in g.Documents
where g.Id == groupId
select d;
var documents = query.ToList();
Or via method chain syntax:
int groupId = 4;
var documents = _database.Groups
.Where(g => g.Id == groupId)
.SelectMany(g => g.Documents)
.ToList();
Try reach the the Documents through the mapping table
int groupId = 4;
var documents = _database.DocumentsGroup
.Where(x => x.GroupId == groupId)
.Include(x => x.Documents)
.Select(x => new Documents
{
Id = x.Documents.Id,
// add all props you need
})
.ToList();
But if you don't have the mapping table just create it or you can try:
int groupId = 4;
var documents = _database.Groups
.Include(x => x.Documents)
.Where(x => x.Id == groupId )
.ToList();
The query you have written will return a result set of groups, not the documents. If "Group" table's "Id" column is unique, you should write this as:
var group = dbContext.Groups.Include(g => g.Documents).FirstOrDefault(g => g.Id == 4); //Given group id is 4
if (group != null) {
var documents = group.Documents.ToList(); // Here you should get the desired Documents, given that the the tables are correctly configured
}

Left outer join using LINQ Query Syntax EF Core C#

I have a question in regards with the below,
Left outer join of two tables who are not connected through Foreign Key.
Order by the results matched in second table.
I would like this to be done in LINQ Query method syntax as I am adding lots of conditions depending on the input provided along with skip and limit.
If we have below Product and Favorite tables
So the output that I would like to have is:
meaning with the favorites as part of first set and which are not favorites should be behind them. Below are the tries that I did.
I am able to join the tables get the output but not sure how I can make sure that in the first page I get all the favs.
This answer was very near to what I thought but it gets the result and then does the ordering which will not be possible in my case as I am doing pagination and using IQueryable to get less data.
Group Join and Orderby while maintaining previous query
Open to any solutions to achieve the same.
[Table("Product")]
public class ProductModel
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid ProductId { get; set; }
public string ProductName {get; set;}
public bool IsFavorite { get; set; }
}
[Table("UserFavorite")]
public class UserFavoriteModel
{
[Required]
public Guid UserId { get; set; }
[Required]
public Guid Identifier { get; set; }
[Required]
public FavoriteType Type { get; set; }
}
// Gets products
private async Task<List<ProductModel>> GetProductsAsync(
Guid categoryId,
Guid subCategoryId,
int from,
int limit)
{
var query = _context.Products.AsQueryable();
if (!string.IsNullOrEmpty(categoryId))
query = query.Where(product => product.CategoryId == categoryId);
if (!string.IsNullOrEmpty(subCategoryId))
query = query.Where(product => product.SubCategoryId == subCategoryId);
query = query.Skip(from).Take(limit);
var products = await query.ToListAsync();
query = query.GroupJoin(
_context.Favorites.AsNoTracking()
.Where(favorite => favorite.Type == FavoriteType.FASHION)
// This user Id will come from context just adding for overall picture.
.Where(favorite => favorite.UserId == userId),
//This orderby if I add will not make any difference.
//.OrderByDescending(favorite => favorite.Identifier),
v => v.ProductId,
f => f.Identifier,
(product, fav) => new { product, fav }).
SelectMany(x => x.Fav.DefaultIfEmpty(),
(x, y) => SetFavorite(x.Project, y));
}
private static ProductModel SetFavorite(ProductModel v, UserFavoriteModel si)
{
v.IsFavorite = (si != null);
return v;
}
I would do something like this:
var query =
_context.Products.AsQueryable().Select(p => new ProductModel {
ProductId = p.ProductId,
ProductName = p.ProductName,
IsFavorite =
_context.Favorites.Any(f =>
f.Identifier = p.ProductId &&
f.Type == FavoriteType.FASHION &&
f.UserId == userId
)
}).OrderByDescending(favorite => favorite.Identifier);

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;

Get Distinct List using LINQ

I want to translate this query in LINQ format:
select m.MenuName,m.ParentID from Menu m where Id in(
select distinct m.ParentID from Menu m inner join MenuRole mr on mr.MenuID=m.Id)
This is what I have tried
var _employee = _db.Employees.AsEnumerable().Where(e => e.Id == Int32.Parse(Session["LoggedUserId"].ToString()))
.FirstOrDefault();
var _dashboardVM = new DashboardVM
{
MenuParentList = _employee.Designation.Role.MenuRoles
.Select(x => new SMS.Models.ViewModel.DashboardVM.MenuParent
{
MenuParentID=x.Menu.ParentID ,
MenuParentName=x.Menu.MenuName
})
.Distinct().ToList()
};
I am getting all list instead of distinct List
Dashboard VM
public class DashboardVM
{
public class MenuParent
{
public int? MenuParentID { get; set; }
public string MenuParentName { get; set; }
}
public List<MenuParent> MenuParentList { get; set; }
public List<Menu> MenuList { get; set; }
public User User { get; set; }
}
The Distinct() method checks reference equality for reference types. This means it is looking for literally the same object duplicated, not different objects which contain the same values.
Can you try the following? You may need to tweek as I have no testing environment:
MenuParentList = _employee.Designation.Role.MenuRoles.GroupBy ( r => r.Menu.ParentID + r.Menu.MenuName ).
.Select (y => y.First ())
.Select(x => new SMS.Models.ViewModel.DashboardVM.MenuParent
{
MenuParentID=x.Menu.ParentID ,
MenuParentName=x.Menu.MenuName
}).ToList();

How can I group by a navigation property member in linq to sql?

I have three tables holding Users Groups and their association, UserGroups as laid out in this fiddle:
I am trying to obtain the maximum level among the users' groups as shown in the query in the fiddle using linq2sql.
However, EntityFramework obfuscates the join table, TblUserGroup and instead just gives me the navigation properties: TblGroups.Users or User.TblGroups
This is what I have put together thus far but Linqpad tells me it cannot execute:
var maxGroup = from ua in ctx.TblGroups
group ua by ua.TblUsers.Select(s=>s.UserId)
into g
select new
{
UserId= g.Key,
MaxLevel = g.Max(s => s.GroupLevel)
};
Seems you can do it like this:
var result = users.Select(u => new
{
UserId = u.Id,
MaxLevel = u.Groups.Max(g => g.GroupLevel)
});
Having:
class User
{
public int UserId { get; set; }
public string UserName { get;set; }
public List<Group> Groups { get; set; }
}
class Group
{
public int GroupId { get; set; }
public string GroupName { get; set; }
public int GroupLevel { get; set; }
public List<User> Users { get; set; }
}
Does it work for you?
var maxGroup = ctx.TblUsers
.Where(u => u.TblUserGroups != null)
.Select(u => new
{
UserId = u.UserId,
MaxGroupLevel = u.TblUserGroups.TblGroups.Max(g => g.GroupLevel)
}
);

Categories

Resources