Why aren't my included entities actually included? - c#

I'm using Entity Framework 4 in Visual Studio 2010, with C#.
I have a method used in a repository that returns a object set with various navigation properties included. Up until recently, this method looked like this...
private IEnumerable<VRTSystem> GetSystems() {
return ctx
.Include(s => s.Customer.CustomerType)
.Include(s => s.VRTSystemProductConfigurations);
}
...where ctx is an ObjectSet of generic type VRTSystem. The full method had a lot more .Include()s than this, but this is enough to show the point.
This worked fine, however I needed to add some code to ensure that only VRTSystemProductConfigurations that had the Active flag set to true were returned. Following the advice commonly given for such situations, I changed the code to look like this...
private IEnumerable<VRTSystem> GetSystems() {
return ctx
.Include(s => s.Customer.CustomerType)
.Include(s => s.VRTSystemProductConfigurations)
.Select(s => new {
System = s,
VRTSystemProductConfigurations = s.VRTSystemProductConfigurations.Where(pc => pc.Active)
})
.Select(s => s.System);
}
However, this new version does not include any of the navigation properties, they are all null.
Anyone any idea why?

This is because Entity Framework is not entirely stupid. It sees that in the end only Systems are queried, so it cuts everything in between and returns Systems only. And part of the trick you're executing here is to disable lazy loading, so the navigation properties are null and will remain null.
You have to remove the last Select out of the scope of the EF query provider by adding an AsEnumerable:
return ctx
.Include(s => s.Customer.CustomerType)
.Select(s => new {
System = s,
VRTSystemProductConfigurations = s.VRTSystemProductConfigurations.Where(pc => pc.Active)
})
.AsEnumerable()
.Select(s => s.System);
And you don't want to include VRTSystemProductConfigurations, because that's the collection you want to load partly.

Related

Net Core 3.1 Razor Pages Multiple Nested Many to Many queries with Linq

I'm using .NET Core 3.1, which has some differences to pre .NET Core 3.0 around Linq queries from what I can gather: https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-3.0/breaking-changes#linq-queries-are-no-longer-evaluated-on-the-client.
There seems to be a lot of information out there for earlier versions of .Net core that don't quite work for me.
All join tables and entities should be configured correctly for automatic creation, so I won't add them all unless someone needs to see them.
In light of that link I also need to be careful that I don't download the entire notes database, which could be millions of lines eventually. Some advice on tracking or notracking, firstordefaultasync etc might be helpful to if it makes a difference.
#Rena helped me with this earlier to get this example working for a single many to many query, thanks heaps Rena:
public async Task<List<Note>> GetAllNotesForNoteTypeAsync(string notetype)
{
var noteData = await context.Note
.Include(n => n.NoteNoteTypeJoins)
.ThenInclude(t => t.NoteType)
.ToListAsync();
var noteDataWithTypes = noteData.Where(i => i.NoteNoteTypeJoins.Any(x => x.NoteType.Type.ToString() == notetype))
.ToList();
return noteDataWithTypes;
}
I have another query I need, which goes one level deeper. These two attempts below obviously don't work but they explain what I am trying to do, trying to filter notes by notetype group text, which is four tables removed via two many to many relationships:
public async Task<List<Note>> GetAllNotesForNoteTypeGroupAsync(string notetypegroup)
{
var noteData = await context.Note
.Include(n => n.NoteNoteTypeJoins)
.ThenInclude(t => t.NoteType)
.ThenInclude(gt => gt.NoteTypeNoteTypeGroupJoins)
.ThenInclude(g => g.NoteTypeGroup)
.Where(g => g.NoteTypeGroup.Group == notetypegroup)
.ToListAsync();
return noteData;
}
or:
public async Task<List<Note>> GetAllNotesForNoteTypeGroupAsync(string notetypegroup)
{
var noteData = await context.Note
.Select(note => new
{
mytypejoin = note.NoteNoteTypeJoins
.Select(notetypejoin => new
{
mynotetype = notetypejoin.NoteType
.Select(notetype => new
{
mynotetype = notetype.NoteTypeNoteTypeGroupJoins
.Select(notetypegroupjoins => new
{
mytypegroup = notetypegroupjoins
.Where(i => i.NoteTypeNoteTypeGroupJoins
.Any(x => x.NoteTypeGroup.Group.ToString() == notetypegroup)
}),
}),
}),
});
return noteData;
Any help would be greatly appreciated, I'm fairly new to this, Thank you!
Here is the answer compliments of Chris:
var noteData = await context.Note.Where(n =>
n.NoteNoteTypeJoins.Any(ntj =>
ntj.NoteType.NoteTypeNoteTypeGroupJoins.Any(ntg => ntg.NoteTypeGroup.Group == notetypegroup)))
.ToListAsync();
You'd need to use Any recursively to dig into the lower relationships, but it does get ugly.
var noteData = await context.Note.Where(n =>
n.NoteTypeJoins.Any(ntj =>
ntj.NoteType.Any(nt =>
nt.NoteTypeNoteTypeGroupJoins.Any(ntntgj =>
ntntgj.NoteTypeGroup.Group == notetypegroup)))))
.ToListAsync();
You don't need all the Include and ThenInlude, as EF will automatically issue the necessary joins in order to make the query in the first place, since it includes all these relationships.
It's also important to note (and will become more problematic with deeper level queries like this), that it's not going to just return NoteTypes, for example, that are in this group. It's going to return Notes (since that's the base entity you're querying) that have any NoteTypes in this group, with all the related NoteTypes, whether they're in the group or not. You just won't have notes where there are zero associated note types in this group.
If you're looking to filter the actual relationships, you'll have to explicitly load. See: https://learn.microsoft.com/en-us/ef/core/querying/related-data#querying-related-entities

Select a specific column with multiple includes in Entity Framework

I'm trying to get specific columns from a context with several includes, but when I try:
Context.Include(i => i.c)
.Include(i => i.l).Select(s=> new LocationCatalog { Name = s.Name})
.Include(i => i.p)
.Include(i => i.li)
.Include(i => i.pcl)
VS throws an error after the select.
How can I achieve this? I want to specify the columns for each include.
The error says that for example: i.p doesn't contains definition for i.l
That's not possible with Entity Framework. You either include the entire table (with Include, as you are doing) or you don't include it at all.
If you want to load only specific columns, you can do this, but see how it's a manual process:
Context
.Select(i => new YourType
{
c = i.l,
l = i.l,
x = new X
{
a = i.x.a // only the properties you want here
}
...
});
The moment you use Select, Include is completely ignored, so you cannot use both.

Entity Framework, Getting part of an entity

I have an entity with one field I do not want to return sometimes.
I am setting it to null right now. Is there a way to specify this in the query itself instead of clearing it out like I am here?
public async Task<IQueryable<XYZXY>> GetStuff()
{
histories =
_db.Stuffs
.Where(n => n.NationId == User.NationId)
.OrderBy(x => x.DateSent);
await histories.ForEachAsync(d => d.Attachment = null);
return histories;
}
What you are looking for is called projection, this is what stuff do you want to project into your result set from the server.
In EF projection is done by a combination of the select line of your query and any Includes which you have done.
If attachment is a second table accessed by a navigation property it wont be returned by your current query (IE it will be null) unless you are doing lazy loading (normally signified by the virtual keyword on the nav property eg public virtual Attachment Attachment {get;set;}). If attachment is a column projection is more complicated
You have 2 options, use an annonomous type eg:
_db.Stuffs
.Where(n => n.NationId == User.NationId)
.OrderBy(x => x.DateSent)
.Select(x=> new { A = x.A, B = x.B .... /*Dont list attachment*/});
or reuse the existing object
_db.Stuffs
.Where(n => n.NationId == User.NationId)
.OrderBy(x => x.DateSent)
.Select(x=> new Stuff { A = x.A, B = x.B .... /*Dont list attachment*/});
Do note custom projections will not be tracked so changing a property and calling save wont work.

Filter nested models on property in last node with NHibernate

I am using NHibernate with mapping by code.
I have three models: Solution, Installation and System. There are one-to-many relations between them. So that each Solution has a list of Installations, and each Installation has a list of Systems.
Each system has a property "Type", which can be "1" or "0".
I am trying to write a method in the Solution repository that will return all the Solutions, with their Installations with only the Systems of type "1".
I have tried the Where-keyword in the SystemMap but i get the same result with and without it. Then i tried a few different experiments with QueryOver(???) without success.
How do i go about to filter on information in the last node?
Thank to your answer, i have done the following implementation, but it results in a huge amount of Systems and Solutions. Maybe i have done something wrong?
The Maps are as follows:
public SAPSolutionMap()
{
Id(t => t.YPID);
Property(e => e.ShortName);
Property(e => e.FullName);
Bag(x => x.SapInstallations, colmap =>
{
colmap.Table("SAPInstallation");
colmap.Key(x => x.Column("Solution"));
colmap.Inverse(true);
colmap.Lazy(CollectionLazy.NoLazy);
colmap.Fetch(CollectionFetchMode.Join);
colmap.Cascade(Cascade.None);
}, map => map.OneToMany(m => m.Class(typeof(SAPInstallation))));
}
public SAPInstallationMap()
{
Id(t => t.InstallationNumber);
Bag(x => x.SapSystems, colmap =>
{
colmap.Table("sapgui");
colmap.Key(x => x.Column("Installation"));
colmap.Inverse(true);
colmap.Lazy(CollectionLazy.NoLazy);
colmap.Cascade(Cascade.None);
colmap.Fetch(CollectionFetchMode.Join);
//colmap.Where("Type = 1");
}, map => map.OneToMany(m => m.Class(typeof(SAPSystem))));
ManyToOne(x => x.SapSolution, map =>
{
map.Column("Solution");
map.NotNullable(true);
map.Cascade(Cascade.None);
map.Class(typeof(SAPSolution));
});
}
public SAPSystemMap()
{
Id(t => t.ID, t => t.Generator(Generators.Identity));
Property(e => e.Type);
Property(e => e.ExplanationText);
ManyToOne(x => x.SapInstallation, map =>
{
map.Column("Installation");
map.NotNullable(true);
map.Cascade(Cascade.None);
map.Class(typeof(SAPInstallation));
});
}
And the Query:
public IList<SAPSolution> GetProductionSystems()
{
SAPSystem syst = null;
SAPInstallation installation = null;
var subquery = QueryOver.Of(() => syst)
.JoinQueryOver(x => x.SapInstallation, () => installation)
.Where(() => syst.Type == 1)
.Select(x => installation.SapSolution.YPID);
// main Query
var query = Session.QueryOver<SAPSolution>()
.WithSubquery
.WhereProperty(root => root.YPID)
.In(subquery);
return query.List<SAPSolution>();
}
Thank you!
General solution should be:
// this is a subquery (SELECT ....
System syst = null;
Installation installation = null;
var subquery = QueryOver.Of(() => syst)
.JoinQueryOver(x => x.Installation, () => installation)
.Where(() => syst.Type == 1)
.Select(x => installation.Solution.ID)
;
// main Query
var query = session.QueryOver<Solution>()
.WithSubquery
.WhereProperty(root => root.ID)
.In(subquery)
;
var list = query
.Take(10)
.Skip(10)
.List<Solution>();
What we can see, that Solution, Installation and System
System has property Installation (many-to-one)
Installation has property Solution (many-to-one)
This is expect-able, because it goes side by side with one-to-many (it is the reverse mapping)
So, then we create subquery, which returns just solution ID's which belong to system with searched Type.
Main query is flat (the great benefit) and we can use paging on top of it.
We would be able to do that even if there is only one way (one-to-many). But that will generate more complicated SQL query ... and does not make sense. In C# we can have both relations...
EXTEND:
You did a great job. Your mapping and query is really cool. But there is one big but: LAZY is what we should/MUST use. Check this:
NHibernate is lazy, just live with it, by Ayende
So, our, collections cannot be FETCHING with a JOIN, because that will multiply the result (10 solutions * 100 installation * 10 systems == 10000 results)
Bag(x => x.SapSystems, colmap =>
{
...
// THIS IS not good way
colmap.Lazy(CollectionLazy.NoLazy);
colmap.Fetch(CollectionFetchMode.Join);
We should use LAZY as possible. To avoid later 1 + N issue, we can use batch-fetching (for example check this)
How to Eager Load Associations without duplication in NHibernate?
So, our collections should be mapped like this:
Bag(x => x.SapSystems, colmap =>
{
...
// THIS IS not good way
colmap.Lazy(CollectionLazy.Lazy);
colmap.BatchSize(100);
With this setting, the query will really use only the root object and related collections will be loaded very effectively

Visiting Entity Framework's Include method

I'm trying to visit the Entity Framework's Include method using QueryResultCache class which is motioned here. It's a very popular article and a lot of query caching libraries are using it.
When I try an expression like:
var exp1 = context.Products.Include(x => x.Tags)
.Where(x => x.Tags.Any(y => y.Name.Contains("Test")))
.Select(x => new {x.ProductId}).Expression;
with it, it produces this string:
value(System.Data.Entity.Core.Objects.ObjectQuery`1
[EfSecondLevelCaching.Test.Models.Product]).MergeAs(AppendOnly).IncludeSpan
(value(System.Data.Entity.Core.Objects.Span))
.Where(x => x.Tags.Any(y => y.Name.Contains("Test")))
.Select(x => new <>f__AnonymousType5`1(ProductId = x.ProductId))
As you can see, the result doesn't contain the parameters of Include method (x => x.Tags). So most of the linq caching libraries on the net can't create a valid unique query key for the EF queries. How can I fix this?
Edit:
If I remove the select method, it will produce:
value(System.Data.Entity.Core.Objects.ObjectQuery`1
[EfSecondLevelCaching.Test.Models.Product])
.MergeAs(AppendOnly)
.IncludeSpan(value(System.Data.Entity.Core.Objects.Span))
.Where(x => x.Tags.Any(y => y.Name.Contains("Test")))
So here there is no difference between Include(x=>x.Tags) and Include(x=>x.Users).
The query will only return what is in your Select expression. In this case Select(x => new {x.ProductId}) means that only a single field ProductId will be returned.
Your Include would have made a difference if you were returning Products as they contain Tags, but makes no difference if you just have ProductId.
See this MSDN article for more information on eager loading (Include ensures eager loading)

Categories

Resources