Problem with Eager Loading Nested Navigation Based on Abstract Entity (EF CTP5) - c#

I a portion of my EF model that looks like this:
Summary:
Location has many Posts
Post is an abstract class
Discussion derives from Post
Discussions have many Comments
Now, the query i'm trying to achieve:
Get information about Location Id 1234, including any Discussions and Comments associated with those Discussions.
I can get discussions and the comments like this:
var discussions = ctx.Posts
.OfType<Discussion>()
.Include(x => x.Comments)
.ToList();
But i can't seem to get it based on the Posts navigation on the Location entity.
I've tried this:
var locationWithDiscussionsAndComments = ctx
.Locations
.Include(x => x.Posts
.OfType<Discussion>()
.Select(y => y.Comments))
.SingleOrDefault();
Which compiles, but i get the error:
System.ArgumentException: The include path expression must refer to a property defined by the entity, optionally also with nested properties or calls to Select.
Parameter name: path
Any ideas? I could probably go "backwards" from the Posts:
var locationWithDiscussionsAndComments = ctx
.Posts
.Include(x => x.Location)
.OfType<Discussion>()
.Include(x => x.Comments)
.Where(x => x.LocationId == 1234)
.Select(x => x.Location)
.ToList();
But that is both hairy and semantically wrong in terms of my repositories (i shouldn't have to go through a post repository to get information about a location).
Any ideas?
EDIT
So after having a bigger think about it, i realized that OfType<T> is a filter operation. As as we know, EF does not support filtering with eager loading. The only options are retrieving everything, or using anonymous type projection.
No way i can retrieve everything, as there is far too much meta data involved. So i'm attempting the anonymous type projection.

The new Query method might help you:
var location = context.Locations.SingleOrDefault();
context.Entry(location)
.Collection(l => l.Posts)
.Query()
.OfType<Discussion>()
.Load();
Repository Implementation:
We can add a new LoadProperty generic method to the Repository<T> class that leverages this new QUery method:
public void LoadProperty<TElement>(T entity,
Expression<Func<T, ICollection<TElement>>> navigationProperty,
Expression<Func<TElement, bool>> predicate) where TElement : class
{
_context.Set<T>().Attach(entity);
_context.Entry(entity)
.Collection(navigationProperty)
.Query()
.Where(predicate)
.Load();
}
Using the LoadProperty method:
Location location = _locationRepository.Find(1);
_locationRepository.LoadProperty(location, l => l.Posts, p => p is Discussion);

Related

Why is nHibernate lazy loading and ignoring the results of a future query?

I have a method that retrieves an Entity from a database using nHibernate. It's quite a complex Entity:
Level4 has many Level3s which have many Level2s which have many Level1s which has a Level1Ref
So I use a few futures like this:
var future = this.Session.QueryOver<Level4>()
.Where(x => x.Id == level4Id)
.Fetch(x => x.Level3List).Eager
.Fetch(x => x.Level3List.First().Level2List).Eager
.Left.JoinAlias(x => x.Level3List, () => level3Alias, x => x.AnotherThing.Id == anotherThingId)
.FutureValue();
And some queries like this:
this.Session.QueryOver<Level1>()
.Fetch(x => x).Eager
.Fetch(x => x.Level1Ref).Eager
.Fetch(x => x.Level2).Eager
.Inner.JoinAlias(x => x.Level2, () => level2Alias)
.Inner.JoinAlias(() => level2Alias.Level3, () => level3Alias, x => x.AnotherThing.Id == anotherThingId && level3Alias.Level4.Id == level4Id)
.Future();
And then:
var record = future.Value;
This all generates the SQL that I would expect but when I try to iterate over the Level2.Level1List it lazy loads the records in that list.
The Question:
This is the problem. What have I? done wrong in my query that nHibernate thinks it needs to go to the database for information that it has already got? (I've got a hunch that I need to swap some of my JoinQueryOver bits for eager fetches?
(Questions edited to simplify examples)
After lots of investigation the only way I could get this working was change all of my queries so they have the same TRoot. Ie. Change them so they all start like this:
this.Session.QueryOver<Level4>()
This obviously isn't ideal in situations like this:
FishMonger
Collection of Fish
Collection of Eye
Collection of Bone
It means I have to write two queries and join in Fish twice...
The alternative is to lazy load and batch the queries but the requirement is to make only one round trip to the database.

Issue with many-to-many query with linq to entities

I've got a table
Application
ApplicationID,
NAme
ApplicationSteps
AplicationStepID,
AplicationID,
StepID
ApplicationStepCriterias
ApplicationStepID,
CriteriaID
So I've got one SelectedCriteriaID - a user choose from a dropdown one criteria and he wants all the applications which has this SelectedCriteriaID in the table ApplicationStepCriterias
I tried
var ds = context.Applications
.Where(a => a.ApplicationSteps
.Select(x=>x.ApplicationStepCriterias
.Select(t=>t.CriteriaId))
.Contains(SelectesdCriteria));
But as I have as result IEnumerable<IEnumerable<int>> I cannot use Contains
Just I get a list of all the CriteriaIds for each ApplicationStep(also a sequence). Just I cannot think of way to get in one list all the CriteriIds.
First, let me try to get the names right. This is not a pure many-to-many association, because the junction class is part of the class model. It is what I unofficially call a 1-n-1 association. So you have
Application -< ApplicationSteps >- ApplicationStepCriterias
I'd strongly recommend to use singular names for your classes ...
Application -< ApplicationStep >- ApplicationStepCriterion
... so you can use plural for collection property names without getting confused.
If I'm right so far, you query should be
context.Applications
.Where(a => a.ApplicationSteps
.Any(x => selectedCriteria
.Contains(x.ApplicationStepCriterion.CriteriaId));
(and I'd also prefer CriterionId, probably referring to a Criterion class)
You may try something like this:
var applicationStepIds = context.ApplicationStepCriterias
.Where(i => i.CriteriaID == selectedCriteria)
.Select(i => i.ApplicationStepID)
.Distinct();
var applicationIds = context.ApplicationSteps
.Where(i => applicationStepIds.Contains(i.AplicationStepID))
.Select(i => i.AplicationID)
.Distinct();
var result = context.Applications.Where(i => applicationIds.Contains(i.ApplicationId));

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)

Why aren't my included entities actually included?

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.

Miniprofiler with EF 5 inspecting duplicate queries ( Readers)

I have my object-to-object mapping code (using automapper) in service layer with lazy-loading enabled:
public IEnumerable<TaskViewModel> MapToView(IEnumerable<IRAS_PM_TaskAssignment> models)
{
Mapper.CreateMap<IRAS_PM_TaskAssignment, TaskViewModel>()
.ForMember(t => t.AssetOrShotName, map => map.MapFrom(t => t.IRAS_PM_Asset_Sequence.AssetShotName))
.ForMember(t => t.Days, map => map.MapFrom(t => (t.StartDate.HasValue && t.DeadLine.HasValue)
? t.DeadLine.Value.Subtract(t.StartDate.Value).TotalDays
: 0.0))
.ForMember(t => t.DepartmentName, map => map.MapFrom(t => t.IRAS_PM_DepartmentName.DeptName));
return models.Select(x => Mapper.Map<IRAS_PM_TaskAssignment, TaskViewModel>(x));
}
And my controller call is:
public ActionResult TaskRead([DataSourceRequest] DataSourceRequest request, int? projectId)
{
var tasks = projectId.HasValue
? _taskRepository.MapToView(_taskRepository.FindBy(x => x.ProjectId == projectId).ToList())
: _taskRepository.MapToView(_taskRepository.All.ToList());
return Json(tasks.ToDataSourceResult(request), JsonRequestBehavior.AllowGet);
}
When inspecting EF-queries, miniprofiler warns me for duplicate reader queries:
What am i doing wrong? please help me to remove redundant readers.
The issue is on this line:
.ForMember(t => t.DepartmentName, map => map.MapFrom(t => t.IRAS_PM_DepartmentName.DeptName));
For every IRAS_PM_TaskAssignment entity here, you are loading looking up the IRAS_PM_DepartmentName that is related to that entity, and getting its department name.
Because of Lazy Loading, these are called one at a time, for every row you are loading.
The solution is to perform eager loading on the IRAS_PM_DepartmentName entity relates to your base IRAS_PM_TaskAssignment. The code to do this remove this:
_taskRepository.FindBy(x => x.ProjectId == projectId).ToList();
And instead use something like this:
_taskRepository.FindBy(x => x.ProjectId == projectId)
.Include(x => x.IRAS_PM_TaskAssignment)
.ToList();
You can make a similar change to the unfiltered retrieval.
The Include statement should pull down each of those related items in the initial query, eliminating the need to look each one up subsequently, one at a time.

Categories

Resources