efcore icon indicating copy to clipboard operation
efcore copied to clipboard

Polymorph entity (TPH) with select-mapping and FirstOrDefault - could not be translated

Open flobiber opened this issue 1 year ago • 2 comments

File a bug

I have a polymorph entity stored as TPH with the following config:


public class Job
{
    public Guid Id { get; set; }
    public JobType Type { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public uint xmin { get; set; }
}

public class ProtocolJob : Job
{
    public string? Source { get; set; }
        
    public ProtocolSeverity Severity { get; set; }
    
    public string Message { get; set; }
        
    public List<string> Tags { get; set; }
}

public class EmailJob : Job
{
    public string Receiver { get; set; }
    public string Subject { get; set; }
    public string Body { get; set; }
}


public class JobConfiguration : IEntityTypeConfiguration<Job>
{
    public void Configure(EntityTypeBuilder<Job> builder)
    {        
        builder.HasDiscriminator(t => t.Type)
            .HasValue<Job>(JobType.Unknown)
            .HasValue<EmailJob>(JobType.Email)
            .HasValue<ProtocolJob>(JobType.Protocol);
    }
}

So now i run all queries with a DTO-Mapping inside select.

Like that:


return await _database.Jobs.Select(j => (j is ProtocolJob) ? new ProtocolJobDto
                    {
                        Source = ((ProtocolJob)j).Source,
                        Severity = ((ProtocolJob)j).Severity,
                        Message = ((ProtocolJob)j).Message,
                        Tags = EF.Property<List<string>>((ProtocolJob)j, "Tags"),
                        Id = ((ProtocolJob)j).Id,
                        Type = ((ProtocolJob)j).Type,
                        Name = ((ProtocolJob)j).Name,
                        Description = ((ProtocolJob)j).Description,
                        xmin = ((ProtocolJob)j).xmin
                    }
                    : (j is EmailJob) ? new EmailJobDto
                    {
                        Receiver = ((EmailJob)j).Receiver,
                        Subject = ((EmailJob)j).Subject,
                        Body = ((EmailJob)j).Body,
                        Id = ((EmailJob)j).Id,
                        Type = ((EmailJob)j).Type,
                        Name = ((EmailJob)j).Name,
                        Description = ((EmailJob)j).Description,
                        xmin = ((EmailJob)j).xmin
                    }
                    : new JobDto
                    {
                        Id = j.Id,
                        Type = j.Type,
                        Name = j.Name,
                        Description = j.Description,
                        xmin = j.xmin
                    })
                .ToListAsync(cancellationToken)

That works absolute amazing!

The issue i am facing, is if i add a FirstOrDefault to this. FirstOrDefault without filterExpression is working also perfect, but when i add a filterExpression it fails:

            return await _database.Jobs.Select(j => (j is ProtocolJob) ? new ProtocolJobDto
                    {
                        Source = ((ProtocolJob)j).Source,
                        Severity = ((ProtocolJob)j).Severity,
                        Message = ((ProtocolJob)j).Message,
                        Tags = EF.Property<List<string>>((ProtocolJob)j, "Tags"),
                        Id = ((ProtocolJob)j).Id,
                        Type = ((ProtocolJob)j).Type,
                        Name = ((ProtocolJob)j).Name,
                        Description = ((ProtocolJob)j).Description,
                        xmin = ((ProtocolJob)j).xmin
                    }
                    : (j is EmailJob) ? new EmailJobDto
                    {
                        Receiver = ((EmailJob)j).Receiver,
                        Subject = ((EmailJob)j).Subject,
                        Body = ((EmailJob)j).Body,
                        Id = ((EmailJob)j).Id,
                        Type = ((EmailJob)j).Type,
                        Name = ((EmailJob)j).Name,
                        Description = ((EmailJob)j).Description,
                        xmin = ((EmailJob)j).xmin
                    }
                    : new JobDto
                    {
                        Id = j.Id,
                        Type = j.Type,
                        Name = j.Name,
                        Description = j.Description,
                        xmin = j.xmin
                    })
                .FirstOrDefaultAsync(cancellationToken)  // <-- WORKS
                //.FirstOrDefaultAsync(j => j.Id == request.Id, cancellationToken) // <-- DONT WORK

In that case i receive the following error:

      The LINQ expression 'DbSet<Job>()
          .Where(j => (j is ProtocolJob) ? (JobDto)new ProtocolJobDto{ 
              Source = ((ProtocolJob)j).Source, 
              Severity = ((ProtocolJob)j).Severity, 
              Message = ((ProtocolJob)j).Message, 
              Tags = EF.Property<List<string>>((ProtocolJob)j, "Tags"), 
              Id = ((ProtocolJob)j).Id, 
              Type = ((ProtocolJob)j).Type,
              Name = ((ProtocolJob)j).Name,
              Description = ((ProtocolJob)j).Description,
              xmin = ((ProtocolJob)j).xmin
          }
           : (j is EmailJob) ? (JobDto)new EmailJobDto{
              Receiver = ((EmailJob)j).Receiver,
              Subject = ((EmailJob)j).Subject,
              Body = ((EmailJob)j).Body,
              Id = ((EmailJob)j).Id,
              Type = ((EmailJob)j).Type,
              Name = ((EmailJob)j).Name,
              Description = ((EmailJob)j).Description,
              xmin = ((EmailJob)j).xmin
          }
           : new JobDto{
              Id = j.Id,
              Type = j.Type,
              Name = j.Name,
              Description = j.Description,
              xmin = j.xmin
          }
          .Id == __request_Id_0)' 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 'AsEnumerable', 'AsAsyncEnumerable', 'ToList', or 'ToListAsync'. See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.

provider and version information

EF Core version: Microsoft.EntityFrameworkCore 8.0.1 Database provider: Npgsql.EntityFrameworkCore.PostgreSQL 8.0.0 Target framework: .NET 8 Operating system: Windows IDE: Rider

flobiber avatar Feb 12 '24 09:02 flobiber

/cc @maumar Can you take a look at this? Could be a dupe.

ajcvickers avatar Feb 14 '24 14:02 ajcvickers

example of a dupe: https://github.com/dotnet/efcore/issues/31279

.FirstOrDefaultAsync(j => j.Id == request.Id, cancellationToken) is effectively

.Where(j => j.Id == request.Id).FirstOrDefaultAsync(cancellationToken)

so now your custom DTO ctors are not in final projection (which allows us to run it as the last step), but rather need to be evaluated before the filter. Since we have no way of knowing what logic is inside those DTO ctors, we throw.

@flobiber In this particular case you can move filter before DTO projections, and things will work.

            return await _database.Jobs.Where(j => j.Id == request.Id).Select(j => (j is ProtocolJob) ? new ProtocolJobDto
                    {
                        Source = ((ProtocolJob)j).Source,
                        Severity = ((ProtocolJob)j).Severity,
                        Message = ((ProtocolJob)j).Message,
                        Tags = EF.Property<List<string>>((ProtocolJob)j, "Tags"),
                        Id = ((ProtocolJob)j).Id,
                        Type = ((ProtocolJob)j).Type,
                        Name = ((ProtocolJob)j).Name,
                        Description = ((ProtocolJob)j).Description,
                        xmin = ((ProtocolJob)j).xmin
                    }
                    : (j is EmailJob) ? new EmailJobDto
                    {
                        Receiver = ((EmailJob)j).Receiver,
                        Subject = ((EmailJob)j).Subject,
                        Body = ((EmailJob)j).Body,
                        Id = ((EmailJob)j).Id,
                        Type = ((EmailJob)j).Type,
                        Name = ((EmailJob)j).Name,
                        Description = ((EmailJob)j).Description,
                        xmin = ((EmailJob)j).xmin
                    }
                    : new JobDto
                    {
                        Id = j.Id,
                        Type = j.Type,
                        Name = j.Name,
                        Description = j.Description,
                        xmin = j.xmin
                    })
                .FirstOrDefaultAsync(cancellationToken);

maumar avatar Feb 16 '24 05:02 maumar