Entity Framework, MVC 3, OrderBy in LINQ To Entities - asp.net-mvc-3

I've got the following query:
model.Page = db.Pages
.Where(p => p.PageId == Id)
.Include(p => p.Series
.Select(c => c.Comics
.Select(col => col.Collection)))
.SingleOrDefault();
This works great, although I now need to order the Comics by a property called 'ReadingOrder'.
I've tried:
model.Page = db.Pages
.Where(p => p.PageId == Id)
.Include(p => p.Series.Select(c => c.Comics.OrderBy(o => o.ReadingOrder)
.Select(col => col.Collection)))
.SingleOrDefault();
But this results in the following 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
Any ideas what this error means?
Thanks in advance
EDIT:
My models:
public class Page
{
public int PageId { get; set; }
public string Title { get; set; }
public ICollection<Series> Series { get; set; }
}
public class Series
{
public int SeriesId { get; set; }
public int PageId { get; set; }
public string Title { get; set; }
public Page Page { get; set; }
public ICollection<Comic> Comics { get; set; }
}
public class Comic
{
public int ComicId { get; set; }
public string Title { get; set; }
public int ReadingOrder { get; set; }
public string Subtitle { get; set; }
public int CollectionId { get; set; }
public Collection Collection { get; set; }
}
public class Collection
{
public int CollectionId { get; set; }
public string Title { get; set; }
public ICollection<Comic> Comics { get; set; }
}

The exception "...Include path expression must refer to a navigation property..." basically complains that c.Comics.OrderBy is not a navigation property. (It's a legitimate complaint, I think.)
Actually it's not supported by EF to apply sorting (and also filtering) in eager loading statements (Include).
So, what can you do?
Option 1:
Sort in memory after you have loaded the entity:
model.Page = db.Pages
.Where(p => p.PageId == Id)
.Include(p => p.Series.Select(c => c.Comics
.Select(col => col.Collection)))
.SingleOrDefault();
if (model.Page != null)
{
foreach (var series in model.Page.Series)
series.Comics = series.Comics.OrderBy(c => c.ReadingOrder).ToList();
}
Ugly, but because you are loading apparently only a single Page object by id it's possibly faster (LINQ to Objects in memory) than the following options (if Series and Comics collections are not extraordinarily long).
Option 2:
Break down the query in parts which mix eager and explicite loading:
model.Page = db.Pages
.Where(p => p.PageId == Id)
.Include(p => p.Series) // only Series collection is included
.SingleOrDefault();
if (model.Page != null)
{
foreach (var series in model.Page.Series)
db.Entry(series).Collection(s => s.Comics).Query()
.Include(c => c.Collection)
.OrderBy(c => c.ReadingOrder)
.Load(); // one new DB query for each series in loop
}
Option 3:
Projection?
Here and here is by the way something about the dangers of complex Include chains of multiple navigation properties. It can load huge amounts of duplicated data. Include ensures that you only have one DB roundtrip but possibly at the cost of much more transfered data. Explicite loading has multiple roundtrips but with possibly less data in total.
(I know, I gave you this Include...Select...Select...Select... chain, but how could I know that you would take me serious :). Well, depending on the size of your nested collections it can still be the best option.)

Off the top of my head, untested:
model.Page = db.Pages
.Where(p => p.PageId == Id)
.Include(p => p.Series
.Select(c => c.Comics
.Select(col => col.Collection)
.OrderBy(o => o.ReadingOrder)))
.SingleOrDefault();

Related

How to write more efficient linq to entities query using EF6

I have an one-to-many relation in my entities:
public class Header
{
public Header()
{
Items = new List<Item>
}
public int Id {get; set;}
public virtual ICollection<Item> Items {get; set;}
// other properties
}
public class Item
{
public int Id {get; set;}
public virtual Header Header { get; set; }
public string Title { get; set; }
public DateTime CreationDate { get; set; }
public int Weight { get; set; }
}
I want to load Header and some of its Items, so I wrote this linq to entity query(EF6):
using(var ctx = new MyContext())
{
var result = ctx.Headers.Where(someConditions)
.AsNoTracking()
.Select(header => new {
HeaderId = header.Id,
//fetch other header properties here
LastItemCreationDate = header.Items.OrderByDescending(item => item.CreationDate)
.Select(item => item.Title)
.FirstOrDefault(),
LastItemTitle = header.Items.OrderByDescending(item => item.CreationDate)
.Select(item => item.CreationDate)
.FirstOrDefault(),
LastItemWeight = header.Items.OrderByDescending(item => item.CreationDate)
.Select(item => item.Weight)
.FirstOrDefault()
}).ToList();
}
This query generate a sql script with 3 times join Header and Item tables, is there any more efficent way to write this query to join Header and Item tables one time?
Since you are using Select, you don't need AsNoTracking since the resulting query will not load any entities. The key performance impacts in your case would be indexes in the Header table suitability for your Where clause, then also whether there is a Descending Index available on the CreationDate in the Items table.
Another improvement would be to alter the projection slightly:
var result = ctx.Headers.Where(someConditions)
.Select(header => new {
HeaderId = header.Id,
LatestItem = header.Items
.OrderByDescending(item => item.CreatedDate)
.Select(item => new
{
Title = item.Title,
CreationDate = item.CreationDate,
Weight = item.Weight
}).FirstOrDefault()
}).ToList();
This will change the resulting anonymous type structure a bit, but should result in a nicer, single join.
You will get a result.HeaderId, then a null-able result.LastestItem containing the latest item's properties. So result.LatestItem?.Title instead of result.Title.

SQL query with Group By and Having Clause in LINQ structure

how do I write this query in LINQ (c# EF6)?
Can someone please help me here - I am new to Entity Framework Structure - So bit hard for me to use different clauses
SELECT
sum(remainingamount) TotalSiteCreditAmount,
max(expirationutcdatetime) MaxExpiryDate
FROM
WholesaleCredits
WHERE
ExpirationUTCDateTime > Getdate()
GROUP BY
registeredcustomerid,
siteid
HAVING
registeredcustomerid = :registeredCustomerId
AND siteid = :siteId
Tried below thing as of now :
var data = context.WholesaleCredit
.Where(x => x.ExpirationUTCDateTime > DateTime.Now)
.GroupBy (x => x.RegisteredCustomerId)
Entity Used in code:
public partial class WholesaleCredits
{
public virtual int Id { get; set; }
public virtual decimal CreditAmount { get; set; }
public virtual decimal RemainingAmount { get; set; }
public virtual Site Site { get; set; }
public virtual DateTime GeneratedUTCDateTime { get; set; }
public virtual DateTime ExpirationUTCDateTime { get; set; }
public virtual int RegisteredCustomerId { get; set; }
}
You do not need HAVING here and Grouping should be provided by constant, because you have filter on grouping keys:
var data = context.WholesaleCredit
.Where(x => x.ExpirationUTCDateTime > DateTime.Now && x.RegisteredCustomerId == registeredCustomerId && x.Site.Id == siteid)
.GroupBy(x => 1)
.Select(g => new
{
TotalSiteCreditAmount = g.Sum(x => x.RemainingAmount),
MaxExpiryDate = g.Max(x => x.ExpirationUTCDateTime)
})
.First();

Update table to not IN condition

I have a strongly typed list called "attendeeList" that contains all active attendees:
List<tb_Attendees> attendeeList
public partial class tb_Attendees
{
public int AttendeeId { get; set; }
public int MeetingId { get; set; }
public string AttendeeName { get; set; }
public bool IsActive { get; set; }
.....
I need update the following table:
DataContext.tb_Attendees
such that that for a given MeetingId, for all AttendeeIds are not in the list, it will update those records to IsActive = false. Basically I am doing a NOT IN.
I want to do something to the effect:
DataContext.tb_Attendees.Where(p => p.MeetingId == meetingId && !attendeeList.Contains(p => p.AttendId) ).ToList().ForEach(x => x.IsActive = false);
DataContext.SaveChanges();
First of all, is this correct? I get an error saying cannot convert from int to Model.tb_Attendees. Do I need to have the list as in int list or any way to accomplish this.
You're close, but it sounds like you need a list of IDs, not a list of attendees:
var attendeeIDList = attendeeList.Select(a => a.AttendId);
DataContext.tb_Attendees
.Where(p => p.MeetingId == meetingId
&& !attendeeIDList.Contains(p => p.AttendId) )
.ToList()
.ForEach(x => x.IsActive = false);
I would also note that the ForEach is just syntactic sugar and would be functionally equivalent to:
foreach(var x in
DataContext.tb_Attendees
.Where(p => p.MeetingId == meetingId
&& !attendeeIDList.Contains(p => p.AttendId) ))
{
x.IsActive = false;
}

How and where to use AddRange() method

I want to display related data from second table with each value in first table
i have tried this query
public ActionResult Index()
{
List<EmployeeAtt> empWithDate = new List<EmployeeAtt>();
var employeelist = _context.TblEmployee.ToList();
foreach (var employee in employeelist)
{
var employeeAtt = _context.AttendanceTable
.GroupBy(a => a.DateAndTime.Date)
.Select(g => new EmployeeAtt
{
Date = g.Key,
Emp_name = employee.EmployeeName,
InTime = g.Any(e => e.ScanType == "I") ? g.Where(e =>
e.ScanType == "I").Min(e =>
e.DateAndTime.ToShortTimeString())
.ToString() : "Absent",
OutTime = g.Any(e => e.ScanType == "O") ? g.Where(e =>
e.ScanType == "O").Max(e =>
e.DateAndTime.ToShortTimeString())
.ToString() : "Absent"
});
empWithDate.AddRange(employeeAtt);
}
return View(empWithDate);
}
Here is my attendance Table
AttendanceTable
Results
I want to display the shortest time with "I" Column value against each employee and last time with "O" Column value as out time. I think i am not using AddRange() at proper place. Where it should go then?
public partial class TblEmployee
{
public TblEmployee()
{
AttendanceTable = new HashSet<AttendanceTable>();
}
public int EmpId { get; set; }
public string EmployeeName { get; set; }
public virtual ICollection<AttendanceTable> AttendanceTable { get; set; }
}
public partial class AttendanceTable
{
public int Id { get; set; }
public int AttendanceId { get; set; }
public int EmployeeId { get; set; }
public string ScanType { get; set; }
public DateTime DateAndTime { get; set; }
public virtual TblEmployee Employee { get; set; }
}
The actual problem is not related to AddRange(), you need a where clause before GroupBy() to limit attendances (before grouping) to only records related to that specific employee, e.g.
_context.AttendanceTable
.Where(a => a.Employee == employee.EmployeeName)
.GroupBy(a => a.DateAndTime.Date)
...
Depended on your model, it is better to use some kind of ID instead of EmployeeName for comparison if possible.
Also you can use SelectMany() instead of for loop and AddRange() to combine the results into a single list. like this:
List<EmployeeAtt> empWithDate = _context.TblEmployee.ToList()
.SelectMany(employee =>
_context.AttendanceTable
.Where(a => a.Employee == employee.EmployeeName)
.GroupBy(a => a.DateAndTime.Date)
.Select(g => new EmployeeAtt
{
...
})
);
...

How to select an entity from database based on nested list item match?

I have a DbSet that contains a list of Item, Now I want to search an Item from the database based on its nested list item matching.
Item Model
public int ItemID{ get; set; }
public string Cover { get; set; }
public List<SlideModel> Slides { get; set; }
Slide Model
public int SlideID{ get; set; }
public int ItemID{ get; set; }
public string Slide{ get; set; }
Now I will pass a search string of Slide and it will search for the Item who have the Slide in its List<SlideModel> and return the Item
item = await context.Items
.Include(i => i.Slides)
.Where(...todo-maybe...)
.FirstOrDefaultAsync();
How should I write the Query method to get the item based on the slide
That's the thing that you want? Hope to help, my friend :))
string inputSlide = "abc";
item = await context.Items
.Include(i => i.Slides)
.Where(i => i.Slides.Any(i => i.Slide.ToLower() == inputSlide.ToLower()))
.FirstOrDefaultAsync();

Resources