How do you avoid the EF Core 3.0 “Restricted client evaluation” error when trying to order data from a one-to-many join?

馋奶兔 提交于 2020-07-16 08:22:24

问题


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 model, say I have a main class called "Data" which looks like this (while the Quotes class stores the DataID):

namespace MyProject.Models
{
    public class Data
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public int LocationId { get; set; }

        public Models.Location Location { get; set; }
        public IList<Models.Quote> Quotes { get; set; }
    }
}

Then I retrieve the IQueryable object using this code:

    IQueryable<Models.Data> dataIQ = _context.Data
        .Include(d => d.Quotes)
        .Include(d => d.Location);

The "Quotes" table is a one-to-many mapping while the location is a one-to-one. How do I order the IQueryable object by a value in the Quotes table? Specifically I'm trying to do this when the user clicks a filter button. I tried just doing this on the first item in the list (which is guaranteed to be populated) but that throws the client-evaluation error I mentioned in the title. This is the code I'm using to apply the sorting:

//This one throws the client-evaluation error
dataIQ = dataIQ.OrderByDescending(d => d.Quotes[0].QuoteName);

//This one works as expected
dataIQ = dataIQ.OrderByDescending(d => d.Location.LocationName);

回答1:


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:

  • What if a DataItem doesn't have any quotes?
  • Is the term "First Quote` defined: if you look at the tables, can you say: "This is the first Quote of DataItem with Id == 4"?

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.




回答2:


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);


来源:https://stackoverflow.com/questions/62764511/how-do-you-avoid-the-ef-core-3-0-restricted-client-evaluation-error-when-tryin

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!