I have a table I\'m displaying in a site which is pulling data from a few different SQL tables. For reference, I\'m following this guide to set up a sortable table. Simplify the
So you have a table called Models
, filled with objects of class DataItems
. and you have a table Quotes
. There is a on-to-many relations between DataItems
and Quotes
: Every DataItem
has zero of more Quotes
, every Quote
belongs to exactly one DataItem
, namely the DataItem that the foreign key DataItemId
refers to.
Furthermore, every Quote
has a property QuoteName
.
Note that I changed the identifier of your Data class, to DataItem, so it would be easier for me to talk in singular and plural nouns when referring to one DataItem or when referring to a collection of DataItems.
You want to order your DataItems
, in ascending value of property QuoteName
of the first Quote
of the DataItem
.
I see two problems:
This is the reason, that it usually is better to design a one-to-many relation using virtual ICollection<Quote>
, then using virtual IList<Quote>
. The value of DataItem[3].Quotes[4]
is not defined, hence it is not useful to give users access to the index.
But lets assume, that if you have an IQueryable<Quote>
, that you can define a "the first quote". This can be the Quote with the lowest Id, or the Quote with the oldest Date. Maybe if it the Quote that has been Quoted the most often. In any case, you can define an extension method:
public static IOrderedQueryable<Quote> ToDefaultQuoteOrder(this IQueryable<Quote> quotes)
{
// order by quote Id:
return quotes.OrderBy(quote => quote.Id);
// or order by QuoteName:
return quotes.OrderBy(quote => quote.QuoteName);
// or a complex sort order: most mentioned quotes first,
// then order by oldest quotes first
return quotes.OrberByDescending(quote => quote.Mentions.Count())
.ThenBy(quote => quote.Date)
.ThenBy(quote => quote.Id);
}
It is only useful to create an extension method, if you expect it to be used several times.
Now that we've defined a order in your quotes, then from every DataItem you can get the first quote:
DataItem dataItem = ...
Quote firstQuote = dataItem.Quotes.ToDefaultQuoteOrder()
.FirstOrDefault();
Note: if the dataItem has no Quotes at all, there won't be a firstQuote, so you can't get the name of it. Therefore, when concatenating LINQ statements, it is usually only a good idea to use FirstOrDefault()
as last method in the sequence.
So the answer of your question is:
var result = _context.DataItems.Select(dataItem => new
{
DataItem = dataItem,
OrderKey = dataItem.Quotes.ToDefaultQuoteOrder()
.Select(quote => quote.QuoteName)
.FirstOrDefault(),
})
.OrderBy(selectionResult => selectionResult.OrderKey)
.Select(selectioniResult => selectionResult.Data);
The nice thing about the extension method is that you hide how your quotes are ordered. If you want to change this, not order by Id, but by Oldest quote date, the users won't have to change.
One final remark: it is usually not a good idea to use Include as a shortcut for Select. If DataItem [4] has 1000 Quotes, then every of its Quote will have a DataItemId with a value of 4. It is quite a waste to send this value 4 for over a thousand times. When using Select you can transport only the properties that you actually plan to use:
.Select(dataItem => new
{
// Select only the data items that you plan to use:
Id = dataItem.Id,
Name = dataItem.Name,
...
Quotes = dataItem.Quotes.ToDefaultQuoteOrder().Select(quote => new
{
// again only the properties that you plan to use:
Id = quote.Id,
...
// not needed, you know the value:
// DataItemId = quote.DataItemId,
})
.ToList(),
});
In entity framework always use Select to select data and select only the properties that you really plan to use. Only use include if you plan to change / update the included data.
Certainly don't use Include because it saves you typing. Again: whenever you have to do something several times, create a procedure for it:
As an extension method:
public static IQueryable<MyClass> ToPropertiesINeed(this IQueryable<DataItem> source)
{
return source.Select(item => new MyClass
{
Id = item.Id,
Name = item.Name,
...
Quotes = item.Quotes.ToDefaultQuoteOrder.Select(...).ToList(),
});
}
Usage:
var result = var result = _context.DataItems.Where(dataItem => ...)
.ToPropertiesINeed();
The nice thing about Select is that you separate the structure of your database from the actually returned data. If your database structure changes, users of your classes won't have to see this.
Ok, I think I figured it out (at least partially**). I believe I was getting the error because what I had was really just not correct syntax for a Linq query--that is I was trying to use a list member in a query on a table that it didn't exist in (maybe?)
Correcting the syntax I was able to come up with this, which works for my current purposes. The downside is that it's only sorting by the first item in the link. I'm not sure how you'd do this for multiple items--would be interested to see if anyone else has thoughts
dataIQ = dataIQ.OrderByDescending(d => d.Quotes.FirstOrDefault().QuoteName);
**Edit: confirmed this is only partially fixing my issue. I'm still getting the original error if I try to access a child object of Quotes. Anyone have suggestions on how to avoid this error? The below example still triggers the error:
IQueryable<Models.Data> dataIQ = _context.Data
.Include(d => d.Quotes).ThenInclude(q => q.Owner)
.Include(d => d.Location);
dataIQ = dataIQ.OrderByDescending(d => d.Quotes.FirstOrDefault().Owner.OwnerName);