EntityFramework - Load IDs to levels down without loading other columns - c#

I have 3 entities: Banners, Packs and Files.
One Banner has many packs and one pack has many files.
I need to get all the files ids for all banners. I tried the following:
IList<BannerModel> banners = context.Banners
.OrderBy(x => Guid.NewGuid())
.Take(count)
.Select(x =>
new BannerModel {
Images = x.Packs.SelectMany(p => p.Files.Select(f => f.Id)).ToList()
}).ToList();
However, I am getting an error on the Files id selecting:
System.NotSupportedException: LINQ to Entities does not recognize the
method 'System.Collections.Generic.List1[System.Int32]
ToList[Int32](System.Collections.Generic.IEnumerable1[System.Int32])'
method, and this method cannot be translated into a store expression.
at
System.Data.Objects.ELinq.ExpressionConverter.MethodCallTranslator.DefaultTranslator.Translate(ExpressionConverter
parent, MethodCallExpression call) at ...
Any idea of what might be wrong?
NOTE: I have Lazy Loading disabled.

var allFileIDs = context.Banners.SelectMany(b =>
b.Packs.SelectMany(p => p.Files.Select(f => f.ID)))
.ToList();
Resulting SQL will look like:
SELECT [t2].[ID]
FROM [Banners] AS [t0], [Packs] AS [t1], [Files] AS [t2]
WHERE ([t1].[BannerID] = [t0].[ID]) AND ([t2].[PackID] = [t1].[ID])
No lazy-loading, single query.
Misunderstood your question about getting all file ids. If you need to project each banner to BannerModel:
context.Banners.Select(b => new BannerModel {
Images = b.Packs.SelectMany(p => p.Files.Select(f => f.ID))
}).ToList();
You see exception, because you have ToList() in expression, which cannot be converted into SQL. Change BannerModel.Images to be of type IEnumerable<int> instead of List<int>, and remove ToList() call from select statement.
One more option if you don't want to change type of BannerModel.Images:
context.Banners.Select(b => b.Packs.SelectMany(p => p.Files.Select(f => f.ID)))
.ToList() // brings next projection into memory
.Select(ids => new BannerModel { Images = ids.ToList() })
.ToList();

Related

Problem with LINQ query: Select first task from each goal

I'm looking for suggestions on how to write a query. For each Goal, I want to select the first Task (sorted by Task.Sequence), in addition to any tasks with ShowAlways == true. (My actual query is more complex, but this query demonstrates the limitations I'm running into.)
I tried something like this:
var tasks = (from a in DbContext.Areas
from g in a.Goals
from t in g.Tasks
let nextTaskId = g.Tasks.OrderBy(tt => tt.Sequence).Select(tt => tt.Id).DefaultIfEmpty(-1).FirstOrDefault()
where t.ShowAlways || t.Id == nextTaskId
select new CalendarTask
{
// Member assignment
}).ToList();
But this query appears to be too complex.
System.InvalidOperationException: 'Processing of the LINQ expression 'OrderBy<Task, int>(
source: MaterializeCollectionNavigation(Navigation: Goal.Tasks(< Tasks > k__BackingField, DbSet<Task>) Collection ToDependent Task Inverse: Goal, Where<Task>(
source: NavigationExpansionExpression
Source: Where<Task>(
source: DbSet<Task>,
predicate: (t0) => Property<Nullable<int>>((Unhandled parameter: ti0).Outer.Inner, "Id") == Property<Nullable<int>>(t0, "GoalId"))
PendingSelector: (t0) => NavigationTreeExpression
Value: EntityReferenceTask
Expression: t0
,
predicate: (i) => Property<Nullable<int>>(NavigationTreeExpression
Value: EntityReferenceGoal
Expression: (Unhandled parameter: ti0).Outer.Inner, "Id") == Property<Nullable<int>>(i, "GoalId"))),
keySelector: (tt) => tt.Sequence)' by 'NavigationExpandingExpressionVisitor' failed. This may indicate either a bug or a limitation in EF Core. See https://go.microsoft.com/fwlink/?linkid=2101433 for more detailed information.'
The problem is the line let nextTaskId =.... If I comment out that, there is no error. (But I don't get what I'm after.)
I'll readily admit that I don't understand the details of the error message. About the only other way I can think of to approach this is return all the Tasks and then sort and filter them on the client. But my preference is not to retrieve data I don't need.
Can anyone see any other ways to approach this query?
Note: I'm using the very latest version of Visual Studio and .NET.
UPDATE:
I tried a different, but less efficient approach to this query.
var tasks = (DbContext.Areas
.Where(a => a.UserId == UserManager.GetUserId(User) && !a.OnHold)
.SelectMany(a => a.Goals)
.Where(g => !g.OnHold)
.Select(g => g.Tasks.Where(tt => !tt.OnHold && !tt.Completed).OrderBy(tt => tt.Sequence).FirstOrDefault()))
.Union(DbContext.Areas
.Where(a => a.UserId == UserManager.GetUserId(User) && !a.OnHold)
.SelectMany(a => a.Goals)
.Where(g => !g.OnHold)
.Select(g => g.Tasks.Where(tt => !tt.OnHold && !tt.Completed && (tt.DueDate.HasValue || tt.AlwaysShow)).OrderBy(tt => tt.Sequence).FirstOrDefault()))
.Distinct()
.Select(t => new CalendarTask
{
Id = t.Id,
Title = t.Title,
Goal = t.Goal.Title,
CssClass = t.Goal.Area.CssClass,
DueDate = t.DueDate,
Completed = t.Completed
});
But this also produced an error:
System.InvalidOperationException: 'Processing of the LINQ expression 'Where<Task>(
source: MaterializeCollectionNavigation(Navigation: Goal.Tasks (<Tasks>k__BackingField, DbSet<Task>) Collection ToDependent Task Inverse: Goal, Where<Task>(
source: NavigationExpansionExpression
Source: Where<Task>(
source: DbSet<Task>,
predicate: (t) => Property<Nullable<int>>((Unhandled parameter: ti).Inner, "Id") == Property<Nullable<int>>(t, "GoalId"))
PendingSelector: (t) => NavigationTreeExpression
Value: EntityReferenceTask
Expression: t
,
predicate: (i) => Property<Nullable<int>>(NavigationTreeExpression
Value: EntityReferenceGoal
Expression: (Unhandled parameter: ti).Inner, "Id") == Property<Nullable<int>>(i, "GoalId"))),
predicate: (tt) => !(tt.OnHold) && !(tt.Completed))' by 'NavigationExpandingExpressionVisitor' failed. This may indicate either a bug or a limitation in EF Core. See https://go.microsoft.com/fwlink/?linkid=2101433 for more detailed information.'
This is a good example for the need of full reproducible example. When trying to reproduce the issue with similar entity models, I was either getting a different error about DefaulIfEmpty(-1) (apparently not supported, don't forget to remove it - the SQL query will work correctly w/o it) or no error when removing it.
Then I noticed a small deeply hidden difference in your error messages compared to mine, which led me to the cause of the problem:
MaterializeCollectionNavigation(Navigation: Goal.Tasks (<Tasks>k__BackingField, DbSet<Task>)
specifically the DbSet<Task> at the end (in my case it was ICollection<Task>). I realized that you used DbSet<T> type for collection navigation property rather than the usual ICollection<T>, IEnumerable<T>, List<T> etc., e.g.
public class Goal
{
// ...
public DbSet<Task> Tasks { get; set; }
}
Simply don't do that. DbSet<T> is a special EF Core class, supposed to be used only from DbContext to represent db table, view or raw SQL query result set. And more importantly, DbSets are the only real EF Core query roots, so it's not surprising that such usage confuses the EF Core query translator.
So change it to some of the supported interfaces/classes (for instance, ICollection<Task>) and the original problem will be solved.
Then removing the DefaultIfEmpty(-1) will allow successfully translating the first query in question.
I don't have EF Core up and running, but are you able to split it up like this?
var allTasks = DbContext.Areas
.SelectMany(a => a.Goals)
.SelectMany(a => a.Tasks);
var always = allTasks.Where(t => t.ShowAlways);
var next = allTasks
.OrderBy(tt => tt.Sequence)
.Take(1);
var result = always
.Concat(next)
.Select(t => new
{
// Member assignment
})
.ToList();
Edit: Sorry, I'm not great with query syntax, maybe this does what you need?
var allGoals = DbContext.Areas
.SelectMany(a => a.Goals);
var allTasks = DbContext.Areas
.SelectMany(a => a.Goals)
.SelectMany(a => a.Tasks);
var always = allGoals
.SelectMany(a => a.Tasks)
.Where(t => t.ShowAlways);
var nextTasks = allGoals
.SelectMany(g => g.Tasks.OrderBy(tt => tt.Sequence).Take(1));
var result = always
.Concat(nextTasks)
.Select(t => new
{
// Member assignment
})
.ToList();
I would recommend you start by breaking up this query into individual parts. Try iterating through the Goals in a foreach with your Task logic inside. Add each new CalendarTask to a List that you defined ahead of time.
Overall breaking this logic up and experimenting a bit will probably lead you to some insight with the limitations of Entity Framework Core.
I think we might separate the query into two steps. First, query each goals and get the min Sequence task and store them(maybe with a anonymous type like {NextTaskId,Goal}). Then, we query the temp data and get the result. For example
Areas.SelectMany(x=>x.Goals)
.Select(g=>new {
NextTaskId=g.Tasks.OrderBy(t=>t.Sequence).FirstOrDefault()?.Id,
Tasks=g.Tasks.Where(t=>t.ShowAlways)
})
.SelectMany(a=>a.Tasks,(a,task)=>new {
NextTaskId = a.NextTaskId,
Task = task
});
I tried to create the linq request but I'm not sure about the result
var tasks = ( from a in DbContext.Areas
from g in a.Goals
from t in g.Tasks
join oneTask in (from t in DbContext.Tasks
group t by t.Id into gt
select new {
Id = gt.Key,
Sequence = gt.Min(t => t.Sequence)
}) on new { t.Id, t.Sequence } equals new { oneTask.Id,oneTask.Sequence }
select new {Area = a, Goal = g, Task = t})
.Union(
from a in DbContext.Areas
from g in a.Goals
from t in g.Tasks
where t.ShowAlways
select new {Area = a, Goal = g, Task = t});
I currently don't have EF Core, but do you really need to compare this much?
Wouldn't querying the tasks be sufficient?
If there is a navigation property or foreign key defined I could imaging using something like this:
Tasks.Where(task => task.Sequence == Tasks.Where(t => t.GoalIdentity == task.GoalIdentity).Min(t => t.Sequence) || task.ShowAlways);

Refactor Linq query

I am trying to cut this linq down
var sys = db.tlkpSystems
.Where(a => db.tlkpSettings.Where(e => e.Hidden < 3)
.Select(o => o.System)
.ToList().Contains(a.System)) //cannot get this part in?
.OrderBy(a => a.SystemName).ToList();
foreach (var item in sys)
model.Add(new SettingSystem {
System = item.System,
SystemName = item.SystemName
});
I have tried the following:
List<SettingSystem> model = new List<SettingSystem>();
model = db.tlkpSettings.Where(e => e.Hidden < 3)
.OrderBy(e => e.Setting)
.Select(e => new SettingSystem
{
System = e.System,
SystemName = e.Setting
}).ToList();
How can I call the .Contains(a.System) part in my query?
Thanks
Some general rules when working with LINQ to Entities:
Avoid using ToList inside the query. It prevents EF to build a correct SQL query.
Don't use Contains when working with entities (tables). Use Any or joins.
Here is your query (in case System is not an entity navigation property):
var sys = db.tlkpSystems
.Where(a => db.tlkpSettings.Any(e => e.Hidden < 3 && e.System == a.System))
.OrderBy(a => a.SystemName).ToList();
As an addendum, there is also AsEnumerable for when you must pull a query into memory (such as calling methods within another clause). This is generally better than ToList or ToArray since it'll enumerate the query, rather than enumerating, putting together a List/Array, and then enumerating that collection.

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)

LINQ to Entities does not recognize the method (Trying to get Previous and Next record)

I am trying to get the Previous and Next Record, but I am getting an exception at run time. What am I doing wrong? I found this solution to my problem on the internet. I get the basic idea, but I am not very experienced with LINQ.
LINQ to Entities does not recognize the method 'System.Linq.IQueryable1[CXW__DAQ___Web.Models.Report] SkipWhile[Report(System.Linq.IQueryable1[CXW_DAQWeb.Models.Report],System.Linq.Expressions.Expression1[System.Func2[CXW__DAQ_Web.Models.Report,System.Boolean]])' method, and his method cannot be translated into a store expression.
here is my relevant code.
var NextRecord = db.Reports.OrderBy(i => i.ID)
.SkipWhile(i => i.ID != id)
.Skip(1)
.First();
int nextID = NextRecord.ID;
var PrevRecord = db.Reports.OrderBy(i => i.ID)
.Reverse()
.SkipWhile(i => i.ID != id)
.Skip(1)
.Last();
int prevID = PrevRecord.ID;
What the error message is trying to tell you is that LINQ to Entities has no way to translate the IEnumerable.SkipWhile() method into SQL. You'll first have to force the remainder of the query to use LINQ to Objects first instead:
var nextRecord =
db.Reports.OrderBy(i => i.ID)
.AsEnumerable()
.SkipWhile(i => i.ID != id)
.Skip(1)
.First();
The obvious downside here being that your entire table is going to be ordered and then read into memory.

Linq using Distinct() in C# Lambda Expression

SFC.OrderFormModifiedMonitoringRecords
.SelectMany(q => q.TimeModify, w => w.DateModify)
.Distinct()
.OrderBy(t => t)
.SelectMany(t => new { RowID = t.rowID, OFnum = t.OFNo });
It's Error did i missed something or is it Completely Coded The Wrong Way? After this i'm gonna use this on a Foreach method to gather up multiple data without the duplicates.
The delegate you pass to SelectMany must return an IEnumerable and is for collapsing multiple collections into one. So yes, something's definitely wrong here. I think you've confused it with Select which simply maps one collection to another.
Without knowing what your goal is, it's hard to know exactly how to fix it, but I'm guessing you want something like this:
SFC.OrderFormModifiedMonitoringRecords
.OrderBy(t => t.DateModify)
.ThenBy(t => t.TimeModify)
.Select(t => new { RowID = t.rowID, OFnum = t.OFNo })
.Distinct();
Or in query syntax:
(from t in SFC.OrderFormModifiedMonitoringRecords
orderby t.DateModify, t.TimeModify
select new { RowID = t.rowID, OFnum = t.OFNo })
.Distinct();
This will order the records by DateModify then by TimeModify, select two properties, rowID and OFNo and return only distinct pairs of values.

Categories

Resources