Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

The LINQ expression could not be translated. #20175

Closed
cosmin-ciuc opened this issue Mar 4, 2020 · 6 comments
Closed

The LINQ expression could not be translated. #20175

cosmin-ciuc opened this issue Mar 4, 2020 · 6 comments

Comments

@cosmin-ciuc
Copy link

I have a perfectly functional query which is executed against a Microsoft SQL Server Database. I have optimized the query for EF Core 2.2 to be executed server-side not client-side. The same query in EF Core 3.1 produces the exception:

The LINQ expression 'DbSet<DocumentEntity>
    .Where(d => !(d.IsDeleted))
    .Join(
        outer: DbSet<DocumentRelationEntity>
            .Where(d0 => !(d0.IsDeleted)), 
        inner: d => new { 
            DocumentId = d.Id, 
            ObjectType = 0
         }, 
        outerKeySelector: d0 => new { 
            DocumentId = d0.DocumentId, 
            ObjectType = d0.ObjectType
         }, 
        innerKeySelector: (d, d0) => new TransparentIdentifier<DocumentEntity, DocumentRelationEntity>(
            Outer = d, 
            Inner = d0
        ))
    .Join(
        outer: DbSet<DocumentCaseEntity>
            .Where(d1 => !(d1.IsDeleted)), 
        inner: ti => ti.Inner.ObjectId.ToLower(), 
        outerKeySelector: d1 => d1.Id.ToString().ToLower(), 
        innerKeySelector: (ti, d1) => new TransparentIdentifier<TransparentIdentifier<DocumentEntity, DocumentRelationEntity>, DocumentCaseEntity>(
            Outer = ti, 
            Inner = d1
        ))' 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(). See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.

I do not understand why EF Core 2.2 was able to translate the query and EF Core 3.1 is not.

Steps to reproduce

The query is build like this:

            var query = context.Documents.AsQueryable()
                .Include(document => document.TypeFsCode)
                .Join(
                    context.DocumentRelations.AsQueryable(),
                    doc => new
                    {
                        DocumentId = doc.Id,
                        ObjectType = (int)ObjectType.DocumentCase,
                    },
                    relation => new
                    {
                        relation.DocumentId,
                        relation.ObjectType,
                    },
                    (doc, rel) => new
                    {
                        DocumentEntity = doc,
                        DocumentCaseId = rel.ObjectId,
                    })
                .Join(//// We assume that every document is part of a DoumentCaseArchive. Documents that do not have a DocumentCase relation won't be returned
                    context.DocumentCases.AsQueryable(),
                    doc => doc.DocumentCaseId.ToLower(),
                    documentCase => documentCase.Id.ToString().ToLower(),
                    (doc, documentCase) => new DocumentSearchEntity
                    {
                        DocumentEntity = doc.DocumentEntity,
                        DocumentCaseId = documentCase.Id,
                        DocumentCaseNumber = documentCase.Number,
                        LatestVersion = doc.DocumentEntity.Versions
                                                            .Where(verFinal => verFinal.Status == (int)VersionStatus.Final)
                                                            .OrderByDescending(verFinal => verFinal.CreatedOn)
                                                            .FirstOrDefault(),
                        DraftVersion = includeDraftVersionInResult
                                       ? doc.DocumentEntity.Versions
                                                            .Where(verDraft => verDraft.Status == (int)VersionStatus.Draft)
                                                            .OrderByDescending(verDraft => verDraft.CreatedOn)
                                                            .FirstOrDefault()
                                       : null,
                    })
               .Where(entry => entry.DocumentEntity.Id == id)
               .Select(entry => entry.TransferPropertiesToDocumentEntity(requestInfo))
              .SingleOrDefaultAsync();

The TransferPropertiesToDocumentEntity method is this one:

        public DocumentEntity TransferPropertiesToDocumentEntity(IRequestInfo requestInfo)
        {
            if (DocumentEntity == null)
            {
                return null;
            }

            switch (requestInfo.GetRequestCulture().Name.Substring(0, 2).ToLower())
            {
                case "en":
                    DocumentEntity.TypeName = DocumentEntity.TypeFsCode?.DisplayNameEn;
                    break;
                case "sr":
                    DocumentEntity.TypeName = DocumentEntity.TypeFsCode?.DisplayNameSr;
                    break;
                default:
                    DocumentEntity.TypeName = DocumentEntity.TypeFsCode?.DisplayNameSq;
                    break;
            }

            DocumentEntity.DraftVersion = DraftVersion;
            DocumentEntity.LatestVersion = LatestVersion;
            DocumentEntity.DocumentCaseId = DocumentCaseId;
            DocumentEntity.DocumentCaseNumber = DocumentCaseNumber;
            return DocumentEntity;
        }

With EF Core 2.2 this query gets translated into several SQL statements:

exec sp_executesql N'SELECT TOP(2) [e].[Id], [e].[CreatedBy], [e].[CreatedOn], [e].[DeletedBy], [e].[DeletedOn], [e].[IsDeleted], [e].[Number], [e].[ParentId], [e].[Position], [e].[Type], [e].[UpdatedBy], [e].[UpdatedOn], [document.TypeFsCode].[Id], [document.TypeFsCode].[DisplayNameEn], [document.TypeFsCode].[DisplayNameSq], [document.TypeFsCode].[DisplayNameSr], [t0].[Id], [t0].[Number]
FROM [Document] AS [e]
INNER JOIN [_FsCodes] AS [document.TypeFsCode] ON [e].[Type] = [document.TypeFsCode].[Id]
INNER JOIN (
    SELECT [e0].*
    FROM [DocumentRelation] AS [e0]
    WHERE [e0].[IsDeleted] = 0
) AS [t] ON ([e].[Id] = [t].[DocumentId]) AND (0 = [t].[ObjectType])
INNER JOIN (
    SELECT [e1].*
    FROM [DocumentCase] AS [e1]
    WHERE [e1].[IsDeleted] = 0
) AS [t0] ON LOWER([t].[ObjectId]) = LOWER(CONVERT(VARCHAR(36), [t0].[Id]))
WHERE ([e].[IsDeleted] = 0) AND ([e].[Id] = @__id_1)',N'@__id_1 uniqueidentifier',@__id_1='830D9550-75C0-46F4-5A84-08D7AE2E3249'

exec sp_executesql N'SELECT TOP(1) [e2].[Id], [e2].[CreatedBy], [e2].[CreatedOn], [e2].[DeletedBy], [e2].[DeletedOn], [e2].[DocumentId], [e2].[IsDeleted], [e2].[Label], [e2].[Major], [e2].[Minor], [e2].[Revision], [e2].[Status], [e2].[Type], [e2].[UpdatedBy], [e2].[UpdatedOn]
FROM [Version] AS [e2]
WHERE (([e2].[IsDeleted] = 0) AND ([e2].[Status] = 1)) AND (@_outer_Id = [e2].[DocumentId])
ORDER BY [e2].[CreatedOn] DESC',N'@_outer_Id uniqueidentifier',@_outer_Id='830D9550-75C0-46F4-5A84-08D7AE2E3249'

exec sp_executesql N'SELECT TOP(1) [e3].[Id], [e3].[CreatedBy], [e3].[CreatedOn], [e3].[DeletedBy], [e3].[DeletedOn], [e3].[DocumentId], [e3].[IsDeleted], [e3].[Label], [e3].[Major], [e3].[Minor], [e3].[Revision], [e3].[Status], [e3].[Type], [e3].[UpdatedBy], [e3].[UpdatedOn]
FROM [Version] AS [e3]
WHERE (([e3].[IsDeleted] = 0) AND ([e3].[Status] = 0)) AND (@_outer_Id1 = [e3].[DocumentId])
ORDER BY [e3].[CreatedOn] DESC',N'@_outer_Id1 uniqueidentifier',@_outer_Id1='830D9550-75C0-46F4-5A84-08D7AE2E3249'

exec sp_executesql N'SELECT [e].[Id], [e].[CreatedBy], [e].[CreatedOn], [e].[DeletedBy], [e].[DeletedOn], [e].[DocumentId], [e].[IsDeleted], [e].[Label], [e].[Major], [e].[Minor], [e].[Revision], [e].[Status], [e].[Type], [e].[UpdatedBy], [e].[UpdatedOn]
FROM [Version] AS [e]
WHERE ([e].[IsDeleted] = 0) AND ([e].[DocumentId] = @__get_Item_0)',N'@__get_Item_0 uniqueidentifier',@__get_Item_0='830D9550-75C0-46F4-5A84-08D7AE2E3249'

exec sp_executesql N'SELECT [e].[Id], [e].[BinaryFileId], [e].[CreatedBy], [e].[CreatedOn], [e].[DeletedBy], [e].[DeletedOn], [e].[IsDeleted], [e].[Type], [e].[UpdatedBy], [e].[UpdatedOn], [e].[VersionId]
FROM [Content] AS [e]
WHERE ([e].[IsDeleted] = 0) AND ([e].[VersionId] = @__get_Item_0)',N'@__get_Item_0 uniqueidentifier',@__get_Item_0='97CE469C-2270-4C13-CF2C-08D7AE2E324F'

exec sp_executesql N'SELECT [e].[Id], [e].[CreatedBy], [e].[CreatedOn], [e].[DeletedBy], [e].[DeletedOn], [e].[DocumentId], [e].[IsDeleted], [e].[ObjectId], [e].[ObjectType], [e].[UpdatedBy], [e].[UpdatedOn]
FROM [DocumentRelation] AS [e]
WHERE ([e].[IsDeleted] = 0) AND ([e].[DocumentId] = @__get_Item_0)',N'@__get_Item_0 uniqueidentifier',@__get_Item_0='830D9550-75C0-46F4-5A84-08D7AE2E3249'

SELECT [e].[Id], [e].[CreatedBy], [e].[CreatedOn], [e].[DeletedBy], [e].[DeletedOn], [e].[IsDeleted], [e].[Number], [e].[UpdatedBy], [e].[UpdatedOn]
FROM [DocumentCase] AS [e]
WHERE ([e].[IsDeleted] = 0) AND [e].[Id] IN ('17bb6116-1968-4822-4803-08d7ae212f07')

Further technical details

EF Core version: 3.1.2
Database provider: Microsoft.EntityFrameworkCore.SqlServer
Target framework: .NET Core 3.1
Operating system: Windows 10 x64
IDE: Visual Studio 2019 16.4.3

@maumar
Copy link
Contributor

maumar commented Mar 4, 2020

@cosmin-ciuc Since 2.2 was translating it to multiple queries, it means that part of the query was evaluated on the client. In 3.1 client evaluation is no longer happening (see #12795 for details). Can you provide a full repro (entities, dbcontext etc) so we can see if there is a workaround for your particular case.

@cosmin-ciuc
Copy link
Author

@maumar thank you for answering. As for providing a repository which could replicate the issue that won't happen very soon because it takes time to strip down a bigger Web API solution constructed on UnitOfWork+Repository pattern to isolate this case. I'll try to do it in my free time.
I was hoping that you could understand what's happening in there just by examining the LINQ statements.

@cosmin-ciuc
Copy link
Author

Before starting to strip away my solution to isolate this particular case I would like to ask if the following use case is supported in EF Core 3.1.
Let's say we have the following entities:

Author

  • Id (PK, guid)
  • Name (string)
  • BirthDate (datetime)
  • DateOfDeath (nullalble string)

Book

  • Id (PK, guid)
  • Title (string)
  • Gendre (string)
  • DateOfWriting (datetime)
  • AuthorId (guid, FK_Author)

Edition

  • Id (PK, guid)
  • PublishingDate (datetime)
  • CoverType (string)
  • PublishingHouseName (string)
  • BookId (guid, FK_Book)

Let's say that we have stored all the books that have been published in US in the past 50 years.

I want to retrieve a paginated list of books with a page size of 10. For each book, I want to know its author and whether the author is still living or is dead. For each book, I want to know the name of the publishing house that published the latest hardcover edition and the name of the publishing house that published the first edition of the book. And I want to order this list by the name of the publishing house that published the latest hardcover edition. And maybe I want to filter the list to only the books that were first published by a particular publishing house, or those books that were first published in the year 1978 for example.

Will I be able to write such a LINQ query using EF Core 3.1 that will be executed server-side? I don't want to retrieve in memory any of these tables because they are huge and I want to read only 10 records at a time. The ORDER BY requirement is mandatory and the ordering has to be done server-side for the pagination to properly work.

I guess that what I'm trying to say is that maybe the decision to prohibit the translation of a LINQ query into several SQL statements might not have been a wise one. Maybe we are lucky that we have in our solution just a limited number of complex queries that maybe could be somehow refactored to work correctly in EF Core 3.1. But what if there are in the world big mission-critical solutions, with many complex queries, in which such a refactoring may not be feasible or even possible. What will happen to these solutions? Will they be frozen to EF Core 2.2 because that is the version of EF Core that allows the server-side execution of the complex queries through several SQL statements?

@maumar
Copy link
Contributor

maumar commented Mar 5, 2020

@cosmin-ciuc

I came up with something like this, it may not be exactly what you were looking for, but close enough.

    class Program
    {
        static void Main(string[] args)
        {
            using var ctx = new MyContext();
            ctx.Database.EnsureDeleted();
            ctx.Database.EnsureCreated();

            var page = 1;

            var query = ctx.Books
                .Where(b => b.Editions.OrderBy(e => e.PublishingDate).Select(e => e.PublishingHouseName).FirstOrDefault() == "Penguin Random House")
                .Where(b => b.Editions.Any(e => e.PublishingDate.Year == 1978))
                .OrderBy(b => b.Editions.Where(e => e.CoverType == "Hard").OrderByDescending(e => e.PublishingDate).Select(e => e.PublishingHouseName).FirstOrDefault())
                .Skip((page - 1) * 10).Take(10)
                .Select(b => new
                {
                    Book = b,
                    AuthorName = b.Author.Name,
                    AuthorAlive = b.Author.DateOfDeath == null,
                    LatestHardCover = b.Editions.Where(e => e.CoverType == "Hard").OrderByDescending(e => e.PublishingDate).Select(e => e.PublishingHouseName).FirstOrDefault(),
                    FirstEdition = b.Editions.OrderBy(e => e.PublishingDate).Select(e => e.PublishingHouseName).FirstOrDefault(),
                });

            var result = query.ToList();
        }
    }

    public class Author
    {
        public Guid Id { get; set; }
        public string Name { get; set; }
        public DateTime BirthDate { get; set; }
        public DateTime? DateOfDeath { get; set; }

        public List<Book> Books { get; set; }
    }

    public class Book
    {
        public Guid Id { get; set; }
        public string Title { get; set; }
        public string Genre { get; set; }
        public DateTime DateOfWriting { get; set; }
        
        public Guid AuthorId { get; set; }
        public Author Author { get; set; }

        public List<Edition> Editions { get; set; }
    }

    public class Edition
    {
        public Guid Id { get; set; }
        public DateTime PublishingDate { get; set; }
        public string CoverType { get; set; }
        public string PublishingHouseName { get; set; }
        
        public Book Book { get; set; }
        public Guid BookId { get; set; }
    }

    public class MyContext : DbContext
    {
        public DbSet<Author> Authors { get; set; }
        public DbSet<Book> Books { get; set; }
        public DbSet<Edition> Editions { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Repro20175;Trusted_Connection=True;MultipleActiveResultSets=true");
        }
    }

It all gets translated into one sql statement like so:

SELECT [t].[Id], [t].[AuthorId], [t].[DateOfWriting], [t].[Genre], [t].[Title], [a].[Name] AS [AuthorName], CASE
    WHEN [a].[DateOfDeath] IS NULL THEN CAST(1 AS bit)
    ELSE CAST(0 AS bit)
END AS [AuthorAlive], (
    SELECT TOP(1) [e].[PublishingHouseName]
    FROM [Editions] AS [e]
    WHERE ([t].[Id] = [e].[BookId]) AND ([e].[CoverType] = N'Hard')
    ORDER BY [e].[PublishingDate] DESC) AS [LatestHardCover], (
    SELECT TOP(1) [e0].[PublishingHouseName]
    FROM [Editions] AS [e0]
    WHERE [t].[Id] = [e0].[BookId]
    ORDER BY [e0].[PublishingDate]) AS [FirstEdition]
FROM (
    SELECT [b].[Id], [b].[AuthorId], [b].[DateOfWriting], [b].[Genre], [b].[Title], (
        SELECT TOP(1) [e1].[PublishingHouseName]
        FROM [Editions] AS [e1]
        WHERE ([b].[Id] = [e1].[BookId]) AND ([e1].[CoverType] = N'Hard')
        ORDER BY [e1].[PublishingDate] DESC) AS [c]
    FROM [Books] AS [b]
    WHERE ((
        SELECT TOP(1) [e2].[PublishingHouseName]
        FROM [Editions] AS [e2]
        WHERE [b].[Id] = [e2].[BookId]
        ORDER BY [e2].[PublishingDate]) = N'Penguin Random House') AND EXISTS (
        SELECT 1
        FROM [Editions] AS [e3]
        WHERE ([b].[Id] = [e3].[BookId]) AND (DATEPART(year, [e3].[PublishingDate]) = 1978))
    ORDER BY (
        SELECT TOP(1) [e1].[PublishingHouseName]
        FROM [Editions] AS [e1]
        WHERE ([b].[Id] = [e1].[BookId]) AND ([e1].[CoverType] = N'Hard')
        ORDER BY [e1].[PublishingDate] DESC)
    OFFSET @__p_0 ROWS FETCH NEXT @__p_1 ROWS ONLY
) AS [t]
INNER JOIN [Authors] AS [a] ON [t].[AuthorId] = [a].[Id]
ORDER BY [t].[c]

wrt not allowing client translation - here is (somewhat long) discussion on why we made the decision #12795

TL;DR
Basically, any query that used to work in 2.2 you can make work by applying AsEnumerable() manually and stitching it yourself on the client (although it sometimes requires a lot of effort). The downside was that many people were not aware that the query was partially executing on the client (specifically filters). Everything seemd fine in testing, but when they deploy to production suddenly query would pull 100000 rows and the service would go down.

Also, client eval was a huge problem for us when adding new features - stuff that didn't translate would client eval and seem to work. We then would improve the translation but introduced some bugs in the process. People would experience that as regressions, which was not a great experience.
With the new approach, after the initial break is dealt with (which we anticipated would be a significant pain for folks, but we actually didn't get as much negative feedback as we thought we would) the user experience should be better with every new version, as new translations light up. Worst case users will experience bugs in new functionality, which is much better than regressions.

There are also other, more technical reasons for issuing just one query, e.g. async is easier and we had issues with data consistency when database would get modified between first and second query was executed.

@cosmin-ciuc
Copy link
Author

cosmin-ciuc commented Mar 6, 2020

Thank you @maumar for taking the time to write this very detailed response. Following your example I'll try in the following days to do the same for our particular case.
Our case is just a bit more complicated because the model didn't allow us to define two relations and I had to use Join method. In one situation the PK type is Guid and the FK type is string, and in the second situation only a subset from the dependent table will have a corespondent in the primary table, something like
{ObjectType, ObjectId} == {0, Id}
where ObjectType is int, ObjectId is string and Id is Guid. Imagine that in the books example you can't define the relation between Book and Edition and have to write Join in your query.
I'll keep you posted on my progress.

@cosmin-ciuc
Copy link
Author

cosmin-ciuc commented Mar 10, 2020

I'm sorry I've been misleading. The issue can be replicated with Microsoft.EntityFrameworkCore.SQLite 3.1.2 not with Microsoft.EntityFrameworkCore.SqlServer 3.1.2.
I'll close this issue and open another one with a repository attached for issue replication. https://github.com/dotnet/efcore/issues/20237
Thank you @maumar for your support and please excuse me for the confusion I've created,

@ajcvickers ajcvickers reopened this Oct 16, 2022
@ajcvickers ajcvickers closed this as not planned Won't fix, can't repro, duplicate, stale Oct 16, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants