1

I've a query like this

public IEnumerable<ContractInquiry> ContractInquiry(Guid itemSoldID)
{
  IEnumerable<ContractInquiry> result;
  using (var context = new ContractDbContext(_ctxOptions))
  {
    var qry = from con in context.Contracts
              join product in context.ContractProducts on con.ID equals product.ContractID
              join service in context.ServiceDetails on con.ID equals service.ContractID into tmpService
              from service in tmpService.DefaultIfEmpty()
              where product.ItemSoldID == itemSoldID
                    && product.ProductStatus != ProductStatus.Deleted.ToString()
                    && con.Status != Status.Deleted.ToString()
              select new ContractInquiry
              {
                 ServiceID = con.ID,
                 ServiceType = con.ServiceType,
                 ServiceDate = service.ServiceDate,
                 ServiceNumber = service.ServiceNumber,
                 ServiceManager = con.Contacts.Where(q => q.Role.Contains(ContractRole.ServiceManager.ToString()))
                                  .OrderBy(o => o.ID).FirstOrDefault()
              };
     result = qry.ToList();
   }
   return result;
}

This query was working fine. But when we upgraded to .NET Core 3.1.5 and Entity Framework Core 3.1.5, it started throwing a client-side evaluation error:

"could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to either AsEnumerable(), AsAsyncEnumerable(), ToList(), or ToListAsync()."

So I had to take the following line out from the query:

ServiceManager = con.Contacts.Where(q => q.Role.Contains(ContractRole.ServiceManager.ToString()))
                                  .OrderBy(o => o.ID).FirstOrDefault()

So re-wrote the query like this:

public IEnumerable<ContractInquiry> ContractInquiry(Guid itemSoldID)
{
  List<ContractInquiry> result;
  using (var context = new ContractDbContext(_ctxOptions))
  {
    var result = (from con in context.Contracts
              join product in context.ContractProducts on con.ID equals product.ContractID
              join service in context.ServiceDetails on con.ID equals service.ContractID into tmpService
              from service in tmpService.DefaultIfEmpty()
              where product.ItemSoldID == itemSoldID
                    && product.ProductStatus != ProductStatus.Deleted.ToString()
                    && con.Status != Status.Deleted.ToString()
              select new ContractInquiry
              {
                 ServiceID = con.ID,
                 ServiceType = con.ServiceType,
                 ServiceDate = service.ServiceDate,
                 ServiceNumber = service.ServiceNumber,
                 Contacts = con.Contacts
              }).ToList();
   }
   
   result.ForEach(con => con.Contacts.Where(q => q.Role.Contains(ContractRole.ServiceManager.ToString()))
                                  .OrderBy(o => o.ID).FirstOrDefault();

   return result;
}

Here

con.Contacts

is a table collection in Contract.cs class

I've added a property like this in ContractInquiry.cs class:

[JsonIgnore]
public IEnumerable<Contact> Contacts { set; get; }

This is working fine as well.

Question: Doing like this will work fine but at run time, the table collection "con.Contacts" will be in memory right? And that will impact the performance of the query right if the table is a huge collection? So is there a work around for this instead of using a memory table? How can I take out the "ServiceManager = .." from the select clause in my first query?

UPDATE: Can someone answer my question?

Brian
  • 101
  • 1
  • 1
  • 6
  • Instead of taking it out from the query, it would be better if you find out which part of the expression is causing the issue. Most likely EFC shows the whole expression, but the problem is in one of its parts (`Where`, `OrderBy` etc). So start removing them one by one (I would start with `Where` clause) until it works. – Ivan Stoev Jul 17 '20 at 18:15
  • I checked that and found that the issue is with the following line in the select clause "ServiceManager = con.Contacts.Where(q => ........" . If I comment out this line it works. I hope the issue is with the .Contains(). That's why I needed to take that client-side evaluation out. – Brian Jul 17 '20 at 21:21
  • You need to focus on that expression rather than the whole. For instance, it's unclear from the shown code what is `ContractRole` - class variable? static member? Also the type of `ContractRole.ServiceManager` property and the need for `.ToString()` call. The first thing I would try in such case is to move that part to variable outside of the query, e.g. `var contractRoleServiceManager = ContractRole.ServiceManager.ToString();` and then use `q.Role.Contains(contractRoleServiceManager)` inside. – Ivan Stoev Jul 18 '20 at 07:17
  • ContractRole is an Enum. I can try what you suggest. But my questions is: Is there a way to take the "con.Contacts" collection outside to evaluate separately? – Brian Jul 22 '20 at 09:28
  • Does anyone could answer my question? – Brian Jul 30 '20 at 11:37
  • @Brian the answer is that the code has two serious bugs already and wasn't working fine. First, it tries to use LINQ, entities and contexts as they were SQL queries, tables and connections. They aren't. EF is an ORM, which means *it* will generate the joins from the relations specified in the DbContext. Second, it tried to use code that just can't be translated to SQL. You'd get an exception in EF 1-6.2 in .NET Framework too. The only reason your code run until now (generating runtime warnings though) is because EF Core 1.x was so limited that even a GroupBy required client-side evaluation – Panagiotis Kanavos Aug 12 '20 at 16:00
  • @Brian so the real answer is to create proper entities and relations so you *don't* have to use hand-coded joins. You'll have to explain what the query is supposed to do - are you trying to filter related entities? This is only coming to EF Core in EF Core 5 -check the (possibly duplicate) [Filtering on Include in EF Core](https://stackoverflow.com/questions/43618096/filtering-on-include-in-ef-core). – Panagiotis Kanavos Aug 12 '20 at 16:14
  • In previous versions, assuming proper relations exist, *maybe* you can write `from con in context.Contracts from cont in con.Contacts where cont.Role.Contains(someString) select ....`. – Panagiotis Kanavos Aug 12 '20 at 16:17

1 Answers1

0

To answer your question:

  • No the whole Contacts table won't be loaded into memory.
  • It will be slower than using a database query but unless you have a crazy amount of records you won't be able to 'humanly' measure it (obv. a stress test will point out that this may be slower by 150ms on 10000 records).

Why this is:

EF Core only loads related data and when it is needed. For example you have 1000 of these ContractInquiry records when calling .ToList(). Every one of these records contain ten contacts. Then EF Core will load only 1000*10 contacts. Due to references if any of these overlap they will share memory location and only a reference to it will be saved.

Some changes you can do to make this even faster:

  • Change .ToList() to .AsEnumerable(). You can do this because you only iterate over that list once, so you save a whole iteration using .AsEnumerable(). (to create a list the program must iterate over it and then you iterate over it again). Also you are returning an IEnumerable so creating a list is pointless (if you are iterating over it once, which is the case here) unless you later cast it back, which I do not recommend.
  • Add .AsNoTracking() in the query. I don't know how you can achieve the same thing with this type of querying (I only use Linq). This will save a lot of time because EF Core will not have to create tracking (and will also save memory).

If you would change the query to a Linq query I would be happy to have a look at it and help you optimise it.

BenceL
  • 804
  • 6
  • 13