EntityFramework: Discrepency between Includes with Navigation Property vs. Includes with Select? - c#

This is more of a syntax question than an actual bug or error, as I finally got what I wanted working. But I want to understand and perhaps improve upon my current solution.
Schema
Let's assume I have a Users table, with a one-to-many relationship to a table Posts, and a further one-to-one relationship table of Authors - one for each Post.
I want to write a custom repository function to get all Users, with all Posts, with each Author per Post.
Attempt #1 (Doesn't Work)
I thought I could do something like:
public IQueryable<User> GetUsersWithPostsAndAuthors()
{
var query = GetAll();
// include all details on user object
return query
.Include(user => user.Posts.Select(x => x.Author));
}
it doesn't seem to include the Author entity. Actually, I was getting the following error:
Lambda expression used inside Include is not valid.
Attempt #2 (Also Doesn't Work)
Then I thought that maybe those Posts need to be in the query first, so I tried this:
public IQueryable<User> GetUsersWithPostsAndAuthors()
{
var query = GetAll();
// include all details on user object
return query
.Include(user => user.Posts)
.Include(user => user.Posts.Select(x => x.Author)
}
Unfortunately, I got the same error:
Lambda expression used inside Include is not valid.
Attempt #3 (Works!)
However, if I use the version of Include where you can provide a string navigationPropertyPath (which actually I don't like since it's just a hardcoded string), with something like this:
public IQueryable<User> GetUsersWithPostsAndAuthors()
{
var query = GetAll();
// include all details on user object
return query
.Include(user => user.Posts)
.Include("Posts.Author");
}
The query works as expected!
What is going on here? I thought the Select projection would do the same as Include. (And there seem to be some answers on Stackoverflow suggesting that.)
More importantly, is there a way to accomplish what I want without hardcoding the Posts.Author in the Include call? I'd like to have static type checks here.

What is going on here?
No offense, but nothing more than not quite understanding what Include is for. It's only for including navigation properties, not for projections.
The syntax is quite clear:
Include for navigation properties off of the root of the query:
.Include(user => user.Posts)
ThenInclude for navigation properties off of included navigation properties:
.Include(user => user.Posts).ThenInclude(p => p.Author)
The latter example is equivalent to .Include("Posts.Author"), but the lambda syntax is preferred because of compile-time checking. In the old EF6 version there was no ThenInclude and the syntax for including more levels was as you wrote: .Include(user => user.Posts.Select(x => x.Author)).
A projection is a Select in the LINQ query, not inside an Include statement. For example:
return query.Select(u => new { u.Id, u.Name });
Projections and Includes exclude one another. In the projection there's nothing in which a navigation property can be included. A query like:
return query
.Include(u => u.Posts)
.Select(u => new
{
u.Id,
u.Name,
Posts = u.Posts.Select(p => p.Title)
});
will completely ignore the Include. There's no trace of it in the generated SQL: only Post.Title will be queried, not all Post fields, as an Include would do.

Related

.NET Core Self Accessing QueryFilters

I'm looking for a work around for an EntityFramework Core Bug. I'm trying to write a query which filters on itself.
Disclaimer: I'm doing something a bit more complex than filtering by an explicit userId, I am just using this with a hard-coded value for simplicity, as the exact implementation isn't relevant to my question.
protected override void OnModelCreating(ModelBuilder modelBuilder) =>
modelBuilder.Entity<ConversationSubscription>()
.HasQueryFilter(x => x.Conversation.ConversationSubscriptions
.Select(c => c.UserId).Contains(315)
);
Since the Query Filter is attempting to access the entity in which it's filtering, it ends up in and endless loop. Since we're working with the ModelBuilder and not the DbSet, there is no way to mark it as IgnoreQueryFilters.
Given this, I tried to use the current context to filter itself:
modelBuilder.Entity<ConversationSubscription>().HasQueryFilter(x =>
this.Set<ConversationSubscription().AsNoTracking().IgnoreQueryFilters()
.Where(cs => cs.ConversationId == x.ConversationId)
.Select(c => c.UserId)
.Contains(315)
);
However, this throws an InvalidOperationException, most likely because we're attempting to use the context before OnModelCreating has finished.
I feel like there is a way I can hack around this if I can somehow select the ConversationSubsriptions into an Anonymous Type, such that they're unfiltered.
Edit: I tried to hack around this using an anonymous type, but no luck.
modelBuilder.Entity<ConversationSubscription>().HasQueryFilter(c =>
x => x.Conversation.Messages.Select(m => new {
Value = m.Conversation.ConversationSubscriptions.Distinct()
.Select(cs => cs.UserId).Contains(c.Variable(this._userId))
}).FirstOrDefault().Value
);
Query filters initially didn't support accessing navigation properties or db sets. Looks like EF Core 3.0 removed these limitations (probably because of the new Single SQL statement per LINQ query mode), with the following restrictions/bugs:
AsNoTracking() and AsTracking() - not supported, which makes sense, since the query filter is always translated to SQL.
Include / ThenInclude - allowed, but ignored by the same reason.
IgnoreQueryFilters - not supported. This could be considered as bug since it could have been be used to resolve the next cases.
Cross referencing filters (e.g. entity A filter uses entity B and entity B filter uses entity A) via either navigation properties or db sets - cause StackOverflowException because filters are trying to use each other. This is a bug.
Self referencing filter via navigation properties - same bug as #4, should be like #6.
Self referencing filter via db sets - supported(!), always ignored in filter subquery.
With all that being said, luckily your case is supported by #6, i.e. your second attempt with just unsupported AsNoTracking() and IgnoreQueryFilters() removed:
modelBuilder.Entity<ConversationSubscription>().HasQueryFilter(x =>
this.Set<ConversationSubscription()
.Where(cs => cs.ConversationId == x.ConversationId)
.Select(c => c.UserId)
.Contains(315));
Maybe I'm missing something obvious here Johnny, so if I'm way off the mark, my apologies.
But I feel like you're doing this:
From conversation subscriptions, join conversations, then join conversation subscriptions where user id = 315
While you could/should be doing this:
From conversation subscriptions where user id = 315, join conversations.
I fail to see what the round trip in your query is necessary for, since it looks like you are querying from ConversationSubscription. In such a query, a simple include will return only conversations and conversationsubscriptions that a user has access to. Is that not what you want to return?
var result = context.ConversationSubscriptions.Include(c => c.Conversation).ToList();
private int _userId = 315;
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<ConversationSubscription>()
.HasQueryFilter(x => x.UserId.Contains(_userId));
}

Entity Framework Core SelectMany then Include

I cant seem to figure out how to get EF Core to include / load related objects when using SelectMany.
context.MyObject
.Where(w => w.Id == Id)
.SelectMany(m => m.SubObject)
.Include(i => i.AnotherType)
Would have thought something like the above would work, however the collapsed SubObject collection has the AnotherObject being null and not included.
Been searching for hours.
Any help would be appreciated.
Thanks
Would have thought something like the above would work
It used to work in EF6, but currently is not supported by EF Core - it allows you to use eager load only the entity which the query starts with, as mentioned in the Loading Related Data - Ignored Includes section of the documentation:
If you change the query so that it no longer returns instances of the entity type that the query began with, then the include operators are ignored.
So to get the eager loading work in your scenario, the query should be like this (assuming you have inverse navigation or FK property in SubObject):
context.SubObject
.Where(so => so.Object.Id == Id) // or so.ObjectId == Id
.Include(i => i.AnotherType)

LINQ Include error

I am new to using asp .net and LINQ queries. I wrote the following query but am getting an error.
Include path expression must refer to a navigation property defined on the type
In the above question there is the same error but they don't explain why it is happening at all. I believe it is because I included the inline .First() method on the third line below but again I want to know why this occurs and what it means. Thank you for your help.
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
Query:
IQueryable<User> users = db.Users
.Where(u => u.Email == email)
.Include(cu => cu.CompanyUsers.First())
.Include(c => c.Companies)
.Include(p => p.ParentCompanyAccounts );
The problem lies in the 3rd line of your query. When you are including something using the Include method, you can't just take one of an object. You have to take them all.
So where you have:
.Include(cu => cu.CompanyUsers.First())
Should be:
.Include(cu => cu.CompanyUsers);
For a good look at how to use Include, I recommend taking a look at this MSDN post.
You cannot use First in an Include call. If you're going to use Include, you need to include all of the related values.
You can user a transient property along with a persistent property.
class User
{
....
public virtual ICollection<User> CompanyUsers {get; set;} //persistent property
[NotMapped]
public User FirstCompanyUser //transient property
{
get{ return CompanyUsers.FirstOrDefault(); }
}
}
You can user a partial class to avoid code loss on regeneration if you use data first approach.

EF Lambda: The Include path expression must refer to a navigation property [duplicate]

This question already has answers here:
EF: Include with where clause [duplicate]
(5 answers)
Closed 5 years ago.
Here is my expression:
Course course = db.Courses
.Include(
i => i.Modules.Where(m => m.IsDeleted == false)
.Select(s => s.Chapters.Where(c => c.IsDeleted == false))
).Include(i => i.Lab).Single(x => x.Id == id);
I know the cause is Where(m => m.IsDeleted == false) in the Modules portion, but why does it cause the error? More importantly, how do I fix it?
If I remove the where clause it works fine but I want to filter out deleted modules.
.Include is used to eagerly load related entities from the db. I.e. in your case make sure the data for modules and labs is loaded with the course.
The lamba expression inside the .Include should be telling Entity Framework which related table to include.
In your case you are also trying to perform a condition inside of the include, which is why you are receiving an error.
It looks like your query is this:
Find the course matching a given id, with the related module and lab. As long as the matching module and chapter are not deleted.
If that is right, then this should work:
Course course = db.Courses.Include(c => c.Modules)
.Include(c => c.Lab)
.Single(c => c.Id == id &&
!c.Module.IsDeleted &&
!c.Chapter.IsDeleted);
but why does it cause the error?
I can imagine that sometimes the EF team regrets the day they introduces this Include syntax. The lambda expressions suggest that any valid linq expression can be used to subtly manipulate the eager loading. But too bad, not so. As I explained here the lambdas only serve as a disguised string argument to the underlying "real" Include method.
how do I fix it?
Best would be to project to another class (say, a DTO)
db.Courses.Select(x => new CourseDto {
Id = x.Id,
Lab = x.Lab,
Modules = x.Modules.Where(m => !m.IsDeleted).Select( m => new ModuleDto {
Moudle = m,
Chapters = x.Chapters.Where(c => c.IsDeleted)
}
}).Single(x => x.Id == id);
but that may be a major modification for you.
Another option is to disable lazy loading and pre-load the non-deleted Modules and Chapters of the course in the context by the Load command. Relationship fixup will fill the right navigation properties. The Include for Lab will work normally.
By the way, there is a change request for this feature.

EntityFramework 5 filter an included navigation property

I would like to find a way using Linq to filter a navigation property to a subset of related entities. I know all answers around this subject suggest doing an anonymous selector such as:
query.Where(x => x.Users.Any(y => y.ID == actingUser.ID))
.Select(x => new
{
Event = x,
Discussions = x.Discussions.Where(actingUser.GenerateSecurityFilterFor<Domain.Discussion>())
})
.OrderBy(x => x.Discussions.Count())
.ThenBy(x => x.Event.Name);
However, this is significantly less than ideal due to the general nature of our query generation and also yields significantly horrific sql queries if you throw up profiler.
I would like to be able to accomplish something like:
query.Include(x => x.Discussions.Where(actingUser.GenerateSecurityFilterFor<Domain.Discussion>()))
.OrderBy(x => x.Discussions.Count())
.ThenBy(x => x.Name);
I realize that this is not supported in EF5 (or any version for that matter) but there has to be a way to accomplish constraining the result set through Linq without delving into anonymous type select statements.
I have attempted doing something to the tune of:
query.GroupJoin(discquqery,
x => x.ID,
x => x.Event.ID,
(evt, disc) => evt.Discussions = disc.Where(actingUser.GenerateSecurityFilterFor<Domain.Discussion>())).ToList();
However you cannot have assignment inside a lambda expression and selecting an anonymous type here causes the same dilemma that it does using the select.
I guess I cannot comprehend why EF does not provide a way (that I can find) to generate:
SELECT
--Properties
FROM Event e
LEFT OUTER JOIN Discussions d
ON e.ID = d.EventID AND --Additional constraints
WHERE
--Where conditions
ORDER BY
--Order Conditions
It is so simple to constrain the join in SQL there HAS to be a way to do it through Linq as well.
PS: I have searched stack, MSDN, experts-exchange, etc. Please realize this is not a duplicate. Anything even touching on this subject either has a cop-out "It can't be done" answer or no answer at all. Nothing is impossible... including this.
Anything even touching on this subject either has a cop-out "It can't
be done" answer or no answer at all. Nothing is impossible...
including this.
Sure. It is possible. You can download EF source code and add this feature yourselves. It will be great contribution to open source project and the community. I believe EF team will gladly help you with your effort.
With the current version "it can't be done" is the answer. You can either use projection to anonymous or special unmapped type as you have described in the beginning of your question. Other options are separate explicit query to load related entities for single parent or separate query to load related entities for all parents.
Load relations for single parent:
context.Entry(event)
.Collection(e => e.Discussions)
.Query()
.Where(d => ...)
.Load();
Load relations for all parents (requires lazy loading to be turned off):
// load all parents
var events = query.Where(e => ...).ToList();
// load child filtered by same condition for parents and new condition for children
childQuery.Where(d => e.Event ... && d.Something ...).Load();
The second solution requires child to have navigation property back to parent (for constructing same query condition used initially to loads parent). If you have everything correctly configured and entities are attached EF should automatically fix your relations (collections) in parent entities (but it will not mark collection in dynamic proxy as loaded so that is the reason why you cannot use this together with lazy loading).

Categories

Resources