Entity Framework filter nested collection - c#

I have a entity relation diagram as follows.
ClassEntity:
public int id
public int std
public virtual ICollection<StudentEntity> students
StudentEntity:
public int id
public string name
public string gender
public virtual ClassEntity class
public virtual StudentAddressEntity studentAddress
StudentAddressEntity:
public int id
public string address
I need to get the class and its male children.
var classEntity = dbContext.Set<ClassEntity>().Where(t => t.id == classId);
var query = classEntity.Include(c => c.students.Select(s => s.studentAddress))
.FirstOrDefault(c => c.students.Any(s => s.gender == GenderEnum.Male));
But it is returning the class with all the students. How to filter only male students?

I have used joins to accomplish similar results in the past. For eg I've accounts that have addresses nested (1:M). If I want to get, say, all the accounts that belong to a particular country, I would use joins as below:
(from a in accountRepo.GetAll()
join aa in accountAddressRepo.GetAll() on a.AccountId equals aa.AccountId
join ad in addressRepo.GetAll() on aa.AddressId equals ad.AddressId
where ad.CountryId == codeCountryId
select a).ToList();
If you are not using repository pattern you can simply replace accountRepo.GetAll() with DbContext.Set().
In your case you should be able to join Student, Address and Class entities and get similar results. Something like below should work for you:
(from s in DbContext.Set<StudentEntity>
join a in DbContext.Set<StudentAddressEntity> on s.studentAddress.id equals a.id
join c in DbContext.Set<ClassEntity> on s.class.id equals c.id
where c.std == classId && s.gender== GenderEnum.Male
select s).ToList();
please note this is a simple representation based on my understanding of your database and entity names. You may need to tweak this query a bit to make it compilable but the underlying idea should work for you. Please let me know how did it work for you.

You intentionally "can't" do this directly with the EF proxies. For example, consider what would happen when you tried to call SaveChanges() and all of the female students are missing from ClassEntity.Students!
Instead, the usual thing to do if you're just displaying data is to project onto an anonymous type or a DTO, e.g.:
var classOnlyMale = dbContext.Set<ClassEntity>()
.Where(x => x.Id == classId)
.Select(x => new // I'm using an anonymous type here, but you can (and usually should!) project onto a DTO instead
{
// It's usually best to only get the data you actually need!
Id = x.Id
Students = x.Students
.Where(y => y.Gender == GenderEnum.Male)
.Select(y => new { Name = y.Name, ... })
});
Or, if you desperately need to make changes and save them:
var classOnlyMale = dbContext.Set<ClassEntity>()
.Where(x => x.Id == classId)
.Select(x => new
{
Class = x,
MaleStudents = x.Students.Where(y => y.Gender == GenderEnum.Male)
});
I quite strongly recommend the former unless there's no way around it. It's really easy to introduce bugs if you're making changes to filtered data and trying to save it.

The below should load only the male students for each class.
var classEntity = testContext.Set<ClassEntity>().Where(t => t.Id == classId);
var classes = classEntity.ToList().Select(c =>
{
testContext.Entry(c)
.Collection(p => p.Students)
.Query()
.Where(s => s.Gender == GenderEnum.Male)
.Load();
return c;
});

I think join is the right way to go about it as suggested by Manish Kumar.
(from s in DbContext.Set<StudentEntity>
join a in DbContext.Set<StudentAddressEntity> on s.studentAddress.id equals a.id
join c in DbContext.Set<ClassEntity> on s.class.id equals c.id
where c.std == classId && s.gender== GenderEnum.Male
select s).ToList();

Related

Filtering on Include reverted if I perform Select afterwards in EF Core

I am trying to use Filtered Includes in EF Core and I have encountered an issue which I can't seem to pinpoint to a specific cause.
My query looks something like this:
context.Users.Include(u=>u.UserRoles.Where(r => r.Role.Category == 3))
.ThenInclude(r=>r.Role).Where(u => u.userId == currentUserId)
.Select(u=> new UserDTO()
{
UserDisplayName= u.Name,
ListOfRoles = String.Join(",", u.UserRoles.Select(u => u.Role.DisplayName))
}).FirstOrDefaultAsync();
If I omit the Select part from the query and check the object, it is populated only with the appropriate UserRoles, the ones belonging to the category 3, but upon checking the result of this Select, it contains also the Roles that belong to a different category, concatenated into the ListOfRoles.
I would be grateful if anyone had any ideas what could be causing this.
Thank you
Include only applies where you are returning the entity. When you use projection with Select you need to filter the data within the Select expression:
context.Users
.Where(u => u.userId == currentUserId)
.Select(u=> new UserDTO()
{
UserDisplayName= u.Name,
ListOfRoles = String.Join(",", u.UserRoles
.Where(ur => ur.Role.Catecory == 3)
.Select(ur => ur.Role.DisplayName))
}).SingleOrDefaultAsync();
I believe that String.Join would require client-side evaluation in EF Core. This can lead to unexpected data being loaded. A recommendation to avoid this would be to perform the concatination within the DTO so that the Linq query loads the raw data and can translate that to SQL efficiently:
context.Users
.Where(u => u.userId == currentUserId)
.Select(u=> new UserDTO()
{
UserDisplayName= u.Name,
Roles = u.UserRoles
.Where(ur => ur.Role.Catecory == 3)
.Select(ur => ur.Role.DisplayName))
.ToList();
}).SingleOrDefaultAsync();
Where in the DTO you would have:
[Serializable]
public class UserDTO
{
public string UserDisplayName { get; set; }
public IList<string> Roles { get; set; } = new List<string>();
public string ListOfRoles
{
get { return string.Join(",", Roles); }
}
}
This ensures the query can run efficiently and translate fully to SQL, then moves the formatting to the DTO.
Include will work only if you select entities directly. Once you do the projection (Select for example) Include is ignored. You can try to apply category filtering during the concatenation part:
context.Users
.Where(u => u.userId == currentUserId)
.Select(u=> new UserDTO()
{
UserDisplayName= u.Name,
ListOfRoles = String.Join(",", u.UserRoles.Where(r => r.Role.Category == 3).Select(u => u.Role.DisplayName))
})
.FirstOrDefaultAsync();

How to fill NotMapped property with count in Linq?

I have added the following property to PassAllocations class :
[NotMapped]
public int PendingForApprovalCount { get; set; }
var passAllocations = db.PassAllocations.Include(p => p.PassRule)
.Where(p => p.CompanyID == company.CompanyID).ToList();
I am trying to fill each PendingForApprovalCount property in list passAllocations with result of db.PassLinkings.Where(p => p.PassAllocationID == parentid and p.status="Pending").ToList()
Where 'parentid' is PassAllocationID in each passAllocations list
passAllocations
.ForEach(x=>x.PendingForApprovalCount=db.PassLinkings
.Where(p => p.PassAllocationID == x.id &&
p.status="Pending")
.Count())
It might give you correct result but this is not at all advisable solution as it will led to multiple hits to db ,which will eventually hamper performance. A better solution may be this one
var desiredResult=(from a in db.PassAllocations
.Include(pa => pa.PassRule)
.Where(pa => pa.CompanyID == company.CompanyID)
join b in db.PassLinkings
.Where(pl => pl.status="Pending")
.GroupBy(pl=>pl.PassAllocationID)
on a.PassAllocationID equals b.Key
select new {a,b})
.Select(x=>new PassAllocation
{
PassAllocationID=x.a.PassAllocationID,
.
.
. <------------Other Properties
.
.
PendingForApprovalCount=x.b.Count()
}).ToList();
Hope it helps.

Turn SQL into Lambda/Linq

I've been trying to turn a fairly basic piece of SQL code into Lamda or Linq but I'm getting nowhere. Here is the SQL query:
SELECT * FROM Form a
INNER JOIN FormItem b ON a.FormId = b.FormId
INNER JOIN FormFee c ON a.FormId = c.FormId
INNER JOIN FeeType d ON c.FeeTypeId = d.FeeTypeId
WHERE b.StatusId = 7
I tried this but it isn't doing what I want.
public Form GetFormWithNoTracking(int id)
{
return ObjectSet
.Where(x => x.FormId == id &&
(x.FormItem.Any(di => di.StatusId == (short)Status.Paid)))
.AsNoTracking()
.FirstOrDefault();
}
I'm trying to return only the rows from FormItem whose StatusId is Paid. However, the above returns all. I know that .Any() will check if there are any matches and if there are return all, so in this case my data, for this form, does have items who have a StatusId of Paid and some items whose StatusId is not paid so it brings back them all.
var query = (from a in ObjectSet.FormA
join b in ObjectSet.FormB on a.field equals b.field
where b.StatusId = 7
select new { a, b})
You can join rest with same logic.
This should be what you are asking for:
Get the Form with FormId = id
Of that form, return all FormItems that have StatusId = Paid
public IEnumerable<FormItem> GetFormWithNoTracking(int id)
{
return ObjectSet
.SingleOrDefault(x => x.FormId == id)
.Select(f => f.FormItem
.Where(di => di.StatusId == (short)Status.Paid))
.AsNoTracking();
}
If you need the Form itself too, you might want to create a custom type (edit: see #Burk's answer) or return a Tuple<Form,IEnumerable<FormItem>>, a IEnumerable<Tuple<Form,FormItem>> or whatever suits your needs best instead.
Alternatively you could remove all non-paid items of the form.
public Form GetFormWithNoTracking(int id)
{
var form = ObjectSet
.SingleOrDefault(x => x.FormId == id)
.AsNoTracking();
var nonPaid = form.Select(f => f.FormItem
.Where(di => di.StatusId != (short)Status.Paid)).ToList();
foreach(FormItem item in nonPaid)
form.FormItem.Remove(item);
return form;
}

C# : How to get anonymous type from LINQ result

I need to get NewsImage field and list of categories Ids that associated with the news in Many to Many relationship ... but it gives me error:
The type of one of the expressions in the join clause is incorrect.Type inference failed in the call to 'Join'.
My code looks like this
var Result1 = (from c in db.News
join d in db.Categories
on c.NewsId equals d.News.Select(l => l.NewsId)
where c.NewsId == 1
select new { c.NewsImagePath, d.CategoryId }).ToList();
Assuming you have a navigation property defining the n-n relation I would write:
var result = db.News
.Where(x => x.NewsId == 1)
.SelectMany(x => x.Categories,
(news, category) => new { news.NewsImagePath, category.CategoryId })
.ToList();
The problem is inside the on statement.
on c.NewsId equals d.News.Select( l => l.NewsId )
The Select on the right-hand side will return a IEnumerable of news, which is not what you want.
Something like this would technically work:
on c.NewsId equals d.News.Select( l => l.NewsId ).FirstOrDefault()
But it does not make sense logically.
I suspect the whole query should be built differently. I think you want to join when the category list of news contains the news item. In that case, you can't use the join statement, it would look somewhat like this:
from n in db.News
from c in db.Categories
where c.News.Select( ne => ne.NewsId ).Contains( n.NewsId )
select new { n.NewsImagePath, c.CategoryId }

Linq any - How to select

I have some simple classes that looks like this:
Class Favorites
Guid UserId
Guid ObjectId
Class Objects
Guid Id
String Name
With Entity Framework I want to select all the Objects which has been marked as a favorite by a user.
So I tried something like this
context.Objects.Where(
x => x.Id ==
context.Favorite.Where(f => f.UserId == UserId)
.Select(f => f.ObjectId).Any()
);
But I don't get it. I also tried with intersect, but what I understand it most be the same type. One User can have many Favorite objects
you could use join clause:
context.Favorite
.Where(f => f.UserId == UserId)
.Join(context.Objects, t => t.ObjectId, u => u.Id, (t, u) => t);
I'd do a join, my linq would look like:
var matches = from o in context.Objects
join f in context.Favorite on o.Id equals f.ObjectId
where f.UserId == UserId
select o;
from o in context.Objects
join f in context.Favorites on o.Id equals f.ObjectId
where f.UserId == #userId
select //whatever you wants
Use FirstOrDefault() instead of Any()
your favorites class needs a property that links it back to the objects, and instead of the double where clauses you can use an Include
Classes
Class Favorites
Guid UserId
Guid ObjectId
[ForeignKey("Id")]
Objects objs
Class Objects
Guid Id
String Name
Linq
context.Favorites.Include(objs).Where(x => x.UserID == UserID)
Then you Favorites object would have a collection of objects under it.

Categories