Why does calling .Include() after a .Join() not work? - c#

I have a DB structure which is not ideal, but I have coded it into EF like this:
[Table("Item")]
public class Item
{
[Key] public int Id { get; set; }
public int CategoryId { get; set; }
public int ItemTypeId { get; set; } // An ItemTypeId of 1 means this row refers to an Article
public int ItemId { get; set; } // this refers to the Article primary key
}
[Table("Article")]
public class Article
{
[Key] public int Id { get; set; }
...
public virtual ICollection<SubArticle> SubArticles { get; set; }
}
[Table("SubArticle")]
public class SubArticle
{
...
public int ArticleId { get; set; }
}
modelBuilder.Entity<Article>().Collection(_ => _.SubArticles).InverseReference(_ => _.Article).ForeignKey(_ => _.ArticleId);
What I want to do is get all articles (with the corresponding sub-articles) that belong to a specific category. I have this query which is working:
var result = await Context.Set<Item>()
.Where(i => i.CategoryId == 200)
.Where(i => i.ItemTypeId == 1)
.Join(
Context.Set<Article>().Include(a => a.SubArticles),
i => i.ItemId,
a => a.Id,
(i,a) => a)
.ToListAsync();
result.SubArticles.First(); // works
My question is why does this query not work:
var result = await Context.Set<Item>()
.Where(i => i.CategoryId == 200)
.Where(i => i.ItemTypeId == 1)
.Join(
Context.Set<Article>(),
i => i.ItemId,
a => a.Id,
(i,a) => a)
.Include(a => a.SubArticles)
.ToListAsync();
result.SubArticles.First(); // error: result.SubArticles is null

Related

Filtering "include" entities in EF Core

I have the following models
public class Person
{
public int PersonId { get; set; }
public string Name { get; set; }
public List<PersonRole> PersonRoles { get; set; }
}
public class RoleInDuty
{
public int roleInDutyId { get; set; }
public string Name { get; set; }
public int typeOfDutyId { get; set; }
public TypeOfDuty typeOfDuty { get; set; }
public List<PersonRole> PersonRoles { get; set; }
}
public class PersonRole
{
public int PersonId { get; set; }
public Person Person { get; set; }
public int RoleInDutyId { get; set; }
public RoleInDuty RoleInDuty { get; set; }
}
And now I can load all people with all their roles using the following code:
var people = _context.Persons
.Include(p => p.PersonRoles)
.ThenInclude(e => e.RoleInDuty).ToList();
But I wantn't load all data to List, I need load PersonRole according entered typeOfDutyId.
I am trying to solve this with the following code
people = _context.Persons
.Include(p => p.PersonRoles
.Where(t=>t.RoleInDuty.typeOfDutyId == Id)).ToList();
But VS throw error
InvalidOperationException: The Include property lambda expression 'p
=> {from PersonRole t in p.PersonRoles where ([t].RoleInDuty.typeOfDutyId == __typeOfDuty_typeOfDutyId_0) select
[t]}' is invalid. The expression should represent a property access:
't => t.MyProperty'. To target navigations declared on derived types,
specify an explicitly typed lambda parameter of the target type, E.g.
'(Derived d) => d.MyProperty'. For more information on including
related data, see http://go.microsoft.com/fwlink/?LinkID=746393.
As I understand I can't access property RoleInDuty.typeOfDutyId because i'm not include it yet.
I solved this problem with the following code
people = _context.Persons
.Include(p => p.PersonRoles)
.ThenInclude(e=>e.RoleInDuty).ToList();
foreach (Person p in people)
{
p.PersonRoles = p.PersonRoles
.Where(e => e.RoleInDuty.typeOfDutyId == Id)
.ToList();
}
Finally, filter in Include with ef core 5. Details in MSDN doc: https://learn.microsoft.com/en-us/ef/core/querying/related-data#filtered-include
Var people = _context.Persons
.Include(p => p.PersonRoles
.Where(t=>t.RoleInDuty.typeOfDutyId == Id))
.ToList();
var blogs = context.Blogs
.Include(e => e.Posts.OrderByDescending(post => post.Title).Take(5)))
.ToList();
devnull show the next How to filter "Include" entities in entity framework?, and there the same problem, I read it, and find the answer. Solve my problem can with the next:
var temp = _context.Persons.Select(s => new
{
Person = s,
PersonRoles= s.PersonRoles
.Where(p => p.RoleInDuty.typeOfDutyId == this.typeOfDuty.typeOfDutyId)
.ToList()
}).ToList();

The most efficient way to calculate total number of all descendants in self reference table

I am looking for an efficient way to calculate total number of all sub categories (all levels) for a given category, without retrieving all the data from the database
Here is my Model:
public class Category
{
public Category()
{
SubCategories = new HashSet<Category>();
}
public Guid CategoryId { get; set; }
public string Title { get; set; }
[ForeignKey("ParentCategory")]
public Guid? ParentCategoryId { get; set; }
//navigation properties
public virtual Category ParentCategory { get; set; }
public virtual ICollection<Category> SubCategories { get; set; }
}
And my ViewModel:
public class CategoryViewModel
{
public CategoryViewModel()
{
SubCategories = new List<CategoryViewModel>();
}
public Guid CategoryId { get; set; }
[Required]
public string Title { get; set; }
public Guid? ParentCategoryId { get; set; }
public virtual CategoryViewModel ParentCategory { get; set; }
public virtual List<CategoryViewModel> SubCategories { get; set; }
public int TotalOfDirectSubCategories { get; set; }
public int TotalOfAllSubCategories { get; set; }
}
My AutoMapping:
cfg.CreateMap<Category, CategoryViewModel>()
.PreserveReferences()
.ReverseMap();
And finally my EF to retrieve data:
data = _ctx.Categories.Where(x => x.ParentCategoryId == categoryId)
//.Include(x => x.SubCategories)
.ToList();
Let's assume a category can contain thousands of sub categories. Since this will be SOA solution, and data will be passed from Web API into my client app, I don't want to pass all subCategories data with every web API call, so I will only need data for requested category, and corresponding counts.
Let assume we have
'cat 1' > 'cat 1 1' > 'cat 1 1 1'
'cat 1' contains exactly 2 sub categories. 'cat 1 1' contains one sub category. I can retrieve 'cat 1' data by calling web API method 'getCategory(null)', and to retrieve 'cat 1 1' data I would call 'getCategory(GuidOfCat11)'
For 'cat 1':
TotalOfDirectSubCategories - 1
TotalOfAllSubCategories - 2
And again, no subCategories will be included
Here is the best I could implement, do you think there is a better approach:
AutoMapper:
cfg.CreateMap<Category, CategoryViewModel>()
.PreserveReferences()
.ForMember(x => x.TotalOfDirectSubCategories, x => x.MapFrom(z => z.SubCategories.Count))
.ReverseMap();
Populating CategoryViewModel:
public List<CategoryViewModel> GetAllCategoriesAndTasksForParentCategoryAndUser(Guid? categoryId)
{
var data = new List<Category>();
if (categoryId == null) // retrieve root level categories
{
data = _ctx.Categories.Where(x => x.ParentCategoryId == null)
.Include(x => x.SubCategories)
.Include(x => x.Tasks)
.ToList();
}
else
{
data = _ctx.Categories.Where(x => x.CategoryId == categoryId)
.Include(x => x.SubCategories)
.Include(x => x.Tasks)
.ToList();
}
var dataToReturn = Mapper.Map<List<Category>, List<CategoryViewModel>>(data);
foreach (var category in data)
{
var vm = dataToReturn.First(x => x.CategoryId == category.CategoryId);
//subCategories
int totalCount = 0;
SubCategoriesCount(ref totalCount, category);
vm.TotalOfAllSubCategories = totalCount;
}
return dataToReturn;
}
And finally, the method:
private void SubCategoriesCount(ref int subCatCount, Category category)
{
if (category.SubCategories != null || category.SubCategories.Count() != 0)
{
subCatCount += category.SubCategories.Count();
foreach (var subCat in category.SubCategories)
{
SubCategoriesCount(ref subCatCount, subCat);
}
}
}

Group by Nested List Identifier

I have an object as follows:
class ObjectParent
{
public List<ObjectChild> ChildNumbers{ get; set; }
public int ParentId { get; set; }
}
class ObjectChild
{
public int ChildId { get; set; }
public string Property { get; set; }
}
I have a List<ObjectParent> obj that I need to group by ChildId which is inside ObjectChild
How can I do so?
this is what i tried
var groupedItemDetails = itemDetails
.GroupBy(u => u.ChildNumbers.ChildId)
.Select(grp => grp.ToList())
.ToList();
You can use SelectMany to flatten the list and then group by ChildId like this:-
var result = parents.SelectMany(x => x.ChildNumbers, (parentObj, childnum) =>
new {
parentObj, childnum
})
.GroupBy(x => x.childnum.ChildId)
.Select(x => new
{
ChildId = x.Key,
Properties = x.Select(z => z.childnum.Property),
ParentIds = x.Select(z => z.parentObj.ParentId)
});
Check this Fiddle I have created with some sample data.

Select common value in navigation table using LINQ lambda expression

I have a table called InvestigatorGroup and a table called InvestigatorGroupUsers which is used to see what groups have what users. I am trying to get the common investigator group between two users
My query is as follows:
public InvestigatorGroup GetCommonGroup(string userId, string investigatorUserId)
{
using (GameDbContext entityContext = new GameDbContext())
{
string[] ids = new[] { userId, investigatorUserId };
return entityContext.InvestigatorGroups
.Where(i => i.IsTrashed == false)
.Include(i => i.InvestigatorGroupUsers)
.Where(i => i.InvestigatorGroupUsers.Any(e => ids.Contains(e.UserId)))
.OrderByDescending(i => i.InvestigatorGroupId)
.GroupBy(i => i.InvestigatorGroupId)
.Where(i => i.Count() > 1)
.SelectMany(group => group).FirstOrDefault();
}
}
The entity InvestigatorGroup is as follows:
public class InvestigatorGroup : IIdentifiableEntity
{
public InvestigatorGroup()
{
this.InvestigatorGroupGames = new HashSet<InvestigatorGroupGame>();
this.InvestigatorGroupUsers = new HashSet<InvestigatorGroupUser>();
}
// Primary key
public int InvestigatorGroupId { get; set; }
public string InvestigatorGroupName { get; set; }
public bool HasGameAssignment { get; set; }
public string GroupRoleName { get; set; }
public bool IsTrashed { get; set; }
// Navigation property
public virtual ICollection<InvestigatorGroupUser> InvestigatorGroupUsers { get; private set; }
public virtual ICollection<InvestigatorGroupGame> InvestigatorGroupGames { get; private set; }
public int EntityId
{
get { return InvestigatorGroupId; }
set { InvestigatorGroupId = value; }
}
}
The problem is that it keeps returning a value of 0. It doesn't see the shared group with a count of 2 between the two users.
I did a test to return the groups (I removed the count>1 condition) and it returned all the groups for both users not only the one they have in common
I believe the issue is with this line: .Where(i => i.InvestigatorGroupUsers.Any(e => ids.Contains(e.UserId)))
Thanks for the help!
I've resolved this by changing my query so that it searches for the rows containing one of the UserId's. Then it queries through those selected rows and selects the ones containing the other UserId (InvestigatorUserId). This way only the rows containing both are returned
My new code is as follows:
public InvestigatorGroup GetCommonGroup(string userId, string investigatorUserId)
{
using (GameDbContext entityContext = new GameDbContext())
{
IEnumerable<InvestigatorGroup> userGroups = entityContext.InvestigatorGroups
.Where(i => i.IsTrashed == false)
.Include(i => i.InvestigatorGroupUsers)
.Where(i => i.InvestigatorGroupUsers.Any(e => e.UserId.Contains(userId)))
.OrderByDescending(i => i.InvestigatorGroupId);
return userGroups.Where(i => i.InvestigatorGroupUsers.Any(e => e.UserId.Contains(investigatorUserId))).FirstOrDefault();
}
}

Order by property of nested object with NHibernate Linq

I have class Request with list of Comments. Each Request can have zero, one or many Comments.
public class Request
{
public virtual string Id
{ get; protected set; }
public virtual DateTime Date
{ get; set; }
public virtual byte RequestStatusId
{ get; set; }
public virtual Payment Payment
{ get; set; }
public virtual IList<RequestComment> RequestComments
{ get; set; }
}
public class RequestComment
{
public virtual int Id
{ get; protected set; }
public virtual DateTime Date
{ get; set; }
public virtual string CommentText
{ get; set; }
public virtual Request Request
{ get; set; }
public virtual User User
{ get; set; }
}
I'm using NHibernate.Linq to get data from database.
When I sort, for example by Request Id, it looks like this:
var query = _session.Query<Request>()
.Where(r => r.RequestStatusId == requestStatusId)
.OrderBy(r => r.Id)
.Skip(pageNo * pageSize)
.Take(pageSize);
return query.ToFuture().AsQueryable();
When I need last comment for Request I get it like this:
public RequestComment GetLastCommentForRequest(string requestId)
{
var query = _session.Query<RequestComment>()
.Where(r => r.Request.Id == requestId)
.OrderByDescending(r => r.Date)
.FirstOrDefault();
return query;
}
Now I need to get Requests, with their last comment (if exists) and all sorted by CommentText. I was trying to do order in Request query with:
.OrderBy(x => x.RequestComments.Where(y => y.Request.Id == x.Id).OrderByDescending(y => y.Date).FirstOrDefault())
But it's not working and I'm getting error »Exception has been thrown by the target of an invocation.«
UPDATE
This is ok, but it's not sorted by last comment, but by first found:
.OrderBy(r => r.RequestComments.Select(x => x.CommentText).FirstOrDefault())
How about this :
.OrderBy(x => x.RequestComments
.OrderByDescending(y => y.Date)
.Select(x => x.CommentText)
.FirstOrDefault()
)
UPDATE :
It seems that linq above translated to query with subquery having order by clause which is not allowed. Try this instead :
.OrderBy(x => x.RequestComments
.Where(y => y.Date == x.RequestComments.Max(o => o.Date))
.Select(u => u.CommentText)
.FirstOrDefault()
)
You could try:
.OrderBy(x => x.RequestComments.Select(y => y.Date).DefaultIfEmpty().Max())

Categories