How to create a where expression in nested entities - entity-framework

I have the following EF query:
Checklist checklist = await context.Checklist.Where(c => c.CHECKLIST_ID == id)
.Include("Employee").Include("EmployeeArchive").Include("ChecklistResponses")
.Include("ChecklistComments").Include("ChecklistResponses.ChecklistQuestion.ChecklistGroup")
.Include("ChecklistResponses.ChecklistQuestion")
.Include("ChecklistResponses.Employee").FirstAsync();
I want to build a where clause on the ChecklistQuestion entity but that is 3 levels deep and it seems I can not build that in the Where clause as it stands. The ChecklistQuestion is a one-to-many relation to the ChecklistResponses.
What I'm getting back is a single entity Checklist with all of the related entities included. The ChecklistResponses will have multiple rows. What I'm trying to do is limit the ChecklistResponses to a ChecklistQuestion.GROUP_ID != 4.
UPDATE: According the Microsoft it is not possible to filter entities with Include with regular Entity Framework. https://learn.microsoft.com/en-us/ef/ef6/querying/related-data?redirectedfrom=MSDN#explicitFilter There are two options I have if I can't do this within the Include. I could filter on the client. (I have a main view with partial views). Or I could do an explicit Load statement.

First off, when using LINQ extension methods it's more common to use the lambda expression syntax, so I'll use that. Then, I think you need the "ThenInclude" extension, which will allow what you are looking for. I left out additional Includes to get this more concise.
Checklist checklist = await context.Checklist.Where(c => c.CHECKLIST_ID == id)
.Include(c => c.ChecklistResponses)
.ThenInclude(c => c.ChecklistResponses.
.Where(q => q.ChecklistQuestion.GROUP_ID != 4))
.FirstOrDefaultAsync();

Related

How do I query and display child and grandchild data in EF 6 and MVC 5?

I have an app using EF 6 and MVC 5 that works fine for inputting data, but now when I try to display some of it I'm having troubles. The basic layout of my entities can be seen in the following diagram:
The first part where I'm having trouble is in querying and filtering the data. I would like to return a list of premises and related data where a survey and signoff exist, but an approval does not. In straight SQL, the query that works now is:
SELECT *
FROM Premises p LEFT OUTER JOIN Approvals a ON a.Id = p.Id
JOIN Surveys s ON s.PremiseId = p.Id
JOIN SignOffs so ON so.Id = s.Id
WHERE a.ApprovedBy IS NULL
The code that I started with is like this:
var premises = Premises.Include(p => p.Approval)
.Include(p => p.Surveys)
.Include(p => p.Surveys.Select(s => s.SignOff));
This appears* to return all records including the child data, but when I try to filter it so I get only records that have a signoff record but do not have an approval, it doesn't work.
var premises = Premises.Include(p => p.Approval).Where(p => p.Approval.ApprovedBy == null)
.Include(p => p.Surveys)
.Include(p => p.Surveys.Select(s => s.SignOff).Where(s => s.Signature != null));
If I use this code, I get this error:
The Include path expression must refer to a navigation property defined on the type. Use dotted paths for reference navigation properties and the Select operator for collection navigation properties.
Parameter name: path
I've changed this query around a lot to try different things, so I'm not sure what all I have done, but I think the first Where statement might work by itself, but the second one definetly causes the error.
How do I need to structure my query to get it to return the requested data properly filtered?
Also, I put an asterisk above on stating that the query appears to return all the data and child data because I can't actually test it. When I'm trying to write my Razor CSHTML page for this, it's not giving me intellisense for the child and grandchild data, and if I enter what I think it should be I get errors. How do I need to reference this data on the page?
You cannot use Include() like this, it is only good for specifying to load a navigation property, not to specify to load an entity when a navigation property is something (not null, in your case).
To do the filtering, I suggest something like this:
var premises = Premises.Include(p => p.Approval).Include(p => p.Surveys).Include(p => p.Surveys.Select(s => s.SignOff))
.Where(p=>p.Approval.ApprovedBy!=null && p.Surveys.Any(s=>s.SignOff.Signature!=null));
So basically, the includes and the filtering have nothing to do with each other. With the includes, you only specify what to load, you can still use the filter on the original entity set.
You're confusing what the Include LINQ method does. It only tells EF to eagerly load that relationship, which is actually unnecessary if your query itself utilizes that relationship; EF will include the relationship by default in that case.
What it doesn't do is allow you filter those relationships. For example, in this portion of your code:
.Include(p => p.Surveys.Select(s => s.SignOff).Where(s => s.Signature != null));
The where clause is applied to Premises, not SignOff as you seem to think. In other words, Where filters the main table being queried, not the table you're including.
There's two paths forward here. You can simply filter Premises by the important parts, i.e.:
var premises = Premises.Where(p => p.Approval.ApprovedBy == null && p => p.Surveys.Any(s => s.SignOff.Signature != null));
That will return only premises where these conditions are true, but the included Surveys collection will contain all surveys related to each premise, not just the ones with null signoff signatures.
If you need to filter the related items as well, then you must explicitly load them:
foreach (premise in premises)
{
context.Entry(premise)
.Collection(p => p.Surveys)
.Query()
.Where(s => s.SignOff.Signature != null)
.Load();
}
Two things of note:
Because of the nature of how this query must be applied, there's no way to do it once for all premises. You'll have to iterate over the premises and explicitly load the Surveys collection for each.
Since this will issue a new query, you want to avoid loading the Surveys collection either lazily or eagerly before this explict load. Otherwise, you're querying the same information twice, which is very inefficient. The easiest way to ensure that is to remove the virtual keyword from the collection property. However, if you do that, then you will have to eager or explicitly load the collection or it will be null. For more information, see: https://msdn.microsoft.com/en-us/library/jj574232(v=vs.113).aspx

Automapper projectTo generated SQL has no where clause

Using Entity Framework Core I'm trying to map an entity containing a child collection.
Without projectTo the generated SQL includes a where clause and only fetches the entities that belong in the child collection. When I add a projectTo, the result is correct but looking at the SQL I see no where clause. It appears to load all the entities of the child type and then do a where in memory.
I've tried all sorts of mappings on the collection but nothing seems to change this behavior.
Is there a way to improve this query?
The code in question is:
var parent = _context
.Parents
.Where(a => a.FamilyId == familyId && a.Id == id)
.Include(r => r.Children)
//.ProjectTo<ParentDetailViewModel>()
.AsNoTracking() // In case projection contains sub entities
.SingleOrDefault();
Without the project to I see the following SQL in the profiler
exec sp_executesql N'SELECT [r].[Id], [r].[FieldOne], [r].[Cid], [r].[FieldTwo], [r].[ParentId]
FROM [Child] AS [r]
WHERE EXISTS (
SELECT TOP(2) 1
FROM [Parent] AS [a]
WHERE (([a].[FamilyId] = #__familyId_0) AND ([a].[Id] = #__id_1)) AND ([r].[ParentId] = [a].[Id]))
ORDER BY [r].[ParentId]',N'#__familyId_0 int,#__id_1 int',#__familyId_0=1,#__id_1=1
With project I see this. No where clause included.
SELECT [r].[ParentId], [r].[Id]
FROM [Child] AS [r]
As a general rule of thumb, I put the ProjectTo as the last thing before your query materialization. Put ProjectTo after AsNoTracking - HOWEVER, the AsNoTracking is pointless. With ProjectTo, you're skipping entities entirely, going straight from SQL to DTO, no entities involved at all.
Also, the Include is pointless. With ProjectTo/Select, EF knows exactly what entities it needs to join, because they're in the Select projection!
Apparently caused by an Entity Framework Core bug. Fixed in 1.1.0 preview

DbSet.Include operator for ef7 accepting string path

EF6 has an overload of DbSet.Include which accepts a string parameter representing a dot-separated list of related objects to return in the query results. It is useful for eager-loading entities in a multi-level object graph. For example:
var order = await _dbContext.Orders
.Include(o => o.Customer)
.Include("OrderDetails.Product") // dot-delimited path
.SingleOrDefaultAsync(o => o.OrderId == id);
This will return both related order details and populate the Product property of each detail by generating a SQL statement that joins OrderDetail and Product tables.
I am looking a way to do this with EF7, but I don't see an overload of DbSet.Include which accepts a string path parameter. Does EF7 provide a way to achieve the same result as the EF6 API?
PS. I just noticed issue #1151 is open, and it looks like it may address my question.
You are correct that #1151 is tracking this scenario. There are also some design meeting notes that summarize the API that will be available in EF7 - https://github.com/aspnet/EntityFramework/wiki/Design-Meeting-Notes:-January-8,-2015.

EF 4.1 loading filtered child collections not working for many-to-many

I've been looking at Applying filters when explicitly loading related entities and could not get it to work for a many-to-many relationship.
I created a simple model:
Brief description:
A Student can take many Courses and a Course can have many Students.
A Student can make many Presentation, but a Presentation can be made by only one Student.
So what we have is a many-to-many relationship between Students and Courses, as well as a one-to-many relationship between Student and Presentations.
I've also added one Student, one Course and one Presentation related to each other.
Here is the code I am running:
class Program
{
static void Main()
{
using (var context = new SportsModelContainer())
{
context.Configuration.LazyLoadingEnabled = false;
context.Configuration.ProxyCreationEnabled = false;
Student student = context.Students.Find(1);
context.
Entry(student).
Collection(s => s.Presentations).
Query().
Where(p => p.Id == 1).
Load();
context.
Entry(student).
Collection(s => s.Courses).
Query().
Where(c => c.Id == 1).
Load();
// Trying to run Load without calling Query() first
context.Entry(student).Collection(s => s.Courses).Load();
}
}
}
After loading the presentations I see that the count for Presentations changed from 0 to 1: . However, after doing the same with Courses nothing changes:
So I try to load the courses without calling Query and it works as expected:
(I removed the Where clause to further highlight the point - the last two loading attempts only differ by the "Query()" call)
Now, the only difference I see is that one relationship is one-to-many while the other one is many-to-many. Is this an EF bug, or am I missing something?
And btw, I checked the SQL calls for the last two Course-loading attempts, and they are 100% identical, so it seems that it's EF that fails to populate the collection.
I could reproduce exactly the behaviour you describe. What I got working is this:
context.Entry(student)
.Collection(s => s.Courses)
.Query()
.Include(c => c.Students)
.Where(c => c.Id == 1)
.Load();
I don't know why we should be forced also to load the other side of the many-to-many relationship (Include(...)) when we only want to load one collection. For me it feels indeed like a bug unless I missed some hidden reason for this requirement which is documented somewhere or not.
Edit
Another result: Your original query (without Include) ...
context.Entry(student)
.Collection(s => s.Courses)
.Query()
.Where(c => c.Id == 1)
.Load();
... actually loads the courses into the DbContext as ...
var localCollection = context.Courses.Local;
... shows. The course with Id 1 is indeed in this collection which means: loaded into the context. But it's not in the child collection of the student object.
Edit 2
Perhaps it is not a bug.
First of all: We are using here two different versions of Load:
DbCollectionEntry<TEntity, TElement>.Load()
Intellisense says:
Loads the collection of entities from
the database. Note that entities that
already exist in the context are not
overwritten with values from the
database.
For the other version (extension method of IQueryable) ...
DbExtensions.Load(this IQueryable source);
... Intellisense says:
Enumerates the query such that for
server queries such as those of
System.Data.Entity.DbSet,
System.Data.Objects.ObjectSet,
System.Data.Objects.ObjectQuery,
and others the results of the query
will be loaded into the associated
System.Data.Entity.DbContext,
System.Data.Objects.ObjectContext or
other cache on the client. This is
equivalent to calling ToList and then
throwing away the list without the
overhead of actually creating the
list.
So, in this version it is not guaranteed that the child collection is populated, only that the objects are loaded into the context.
The question remains: Why gets the Presentations collection populated but not the Courses collection. And I think the answer is: Because of Relationship Span.
Relationship Span is a feature in EF which fixes automatically relationships between objects which are in the context or which are just loaded into the context. But this doesn't happen for all types of relationships. It happens only if the multiplicity is 0 or 1 on one end.
In our example it means: When we load the Presentations into the context (by our filtered explicit query), EF also loads the foreign key of the Presentation entites to the Student entity - "transparently", which means, no matter if the FK is exposed as property in the model of not. This loaded FK allows EF to recognize that the loaded Presentations belong to the Student entity which is already in the context.
But this is not the case for the Courses collection. A course does not have a foreign key to the Student entity. There is the many-to-many join-table in between. So, when we load the Courses EF does not recognize that those courses belong to the Student which is in the context, and therefore doesn't fix the navigation collection in the Student entity.
EF does this automatic fixup only for references (not collections) for performance reasons:
To fix relationship, EF transparently
rewrites the query to bring
relationship info for all relations
which has multiplicity of 0..1 or1 on
the other end; in other words
navigation properties that are entity
reference. If an entity has
relationship with multiplicity of
greater then 1, EF will not bring back
the relationship info because it could
be performance hit and as compared to
bringing a single foreign along with
rest of the record. Bringing
relationship info means retrieving all
the foreign keys the records has.
Quote from page 128 of Zeeshan Hirani's in depth guide to EF.
It is based on EF 4 and ObjectContext but I think this is still valid in EF 4.1 as DbContext is mainly a wrapper around ObjectContext.
Unfortunately rather complex stuff to keep in mind when using Load.
And another Edit
So, what can we do when we want to explicitely load one filtered side of a many-to-many relationship? Perhaps only this:
student.Courses = context.Entry(student)
.Collection(s => s.Courses)
.Query()
.Where(c => c.Id == 1)
.ToList();

Linq to entities - how to select entities with a where condition on their entitycollection?

I found several times people asking for the same question but it seems that the answer was never satisfying altough it should be pretty easy (in theory). Here is my question :
I have an entity called "Company" inside which I have an entityCollection "Employees" (one to many). I need to retrieve all Companies and for each of them, I only want the employees with an Age greater than 21.
I tried :
Return context.Companies.Include("Employees").Where(c => c.Employees.Where(e => e.Age > 21).Count() > 0)
That doesn't work as it gives me all employees for each company if there is at least one above 21 (it is actually the same than .Any() )
I tried :
Return context.Companies.Include("Employees").Select(c => New Company {
.Id = c.Id,
.Employees = c.Employees.Where(Function(e) e.Age > 24)
}).ToList()
That didn't work either (although it would have been perfect), it gives me the following error : The entity or complex type 'MyModel.Company' cannot be constructed in a LINQ to Entities query.
How can you select all my companies with only, for each of them, the employees being above 21 ? At the moment, I select all and on the client side, I filter my employees but I don't like that solution.
Can anybody help me ?
Thank you Morteza Manavi-Parast, it will do the work !
Nevertheless, I hardly convince myself that doing so in a unique query has not be implemented in the Entity framework. It is such a relatively common situation ... As a prove, there are numbers of questions like mine on this forum.
I am surprised ... Maybe for the next release ?
To be clear, I need a list of Companies as I am directly binding the result of my query to a datagrid. For your information, when I click on a row of my datagrid (so selecting a company), I have a second Grid which is populated with its employees (above 21 years old) coming from the entityCollection.
There is no way to have a "Conditional Eager Loading" with include in LINQ to Entities. There are 2 workarounds exist though. The first one is Filtered Projection and it's the one that Justin suggested but might not be desirable in all situations as it gives a collection of anonymous type objects.
The second way is called Two Tracked Queries which gives you a collection of strongly types Companies whose their employees satisfy a condition and I believe that's what you are looking for. Here is the code for it:
var companies = context.Companies.ToList();
var employees = context.Employee.Where(e => e.Age > 21);
foreach (var employee in employees) {
companies.Single(c => c.CompanyID == employee.CompanyID).Employees.Add(employee);
}
Please take a look at Conditional Eager Loading for another example.
Instead of using the type Company, have you tried selecting a new anonymous type:
Return context.Companies.Include("Employees").Select(c => New With {
.Id = c.Id,
.Employees = c.Employees.Where(Function(e) e.Age > 24)
}).ToList()
(Sorry if the syntax is a little off, it's been a while since I've done LINQ/Anonymous Types in VB.NET)
You might be over-thinking this one. If you have the Company => Employee relationship bi-directionally mapped, then just do the select on Employee with the where clause and include company.
Return context.Employee.Include("Company").Where(e => e.Age > 21)