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

Using Enum in linq query throws exception in v3.1.4 #20968

Closed
canertosuner opened this issue May 15, 2020 · 6 comments
Closed

Using Enum in linq query throws exception in v3.1.4 #20968

canertosuner opened this issue May 15, 2020 · 6 comments
Labels
closed-no-further-action The issue is closed and no further action is planned. customer-reported

Comments

@canertosuner
Copy link

canertosuner commented May 15, 2020

I have base and sub classes and their mappings as blow, seperated them using HasDiscriminator method

public abstract class Provider 
{
    public string ClientId { get; protected set; }
    public abstract BankEnum Bank { get; }
}
public class XBankProvider : Provider
{
    public string DomainName { get; protected set; }
    public override BankEnum Bank => BankEnum.XBank;
}
public class YBankProvider : Provider
{
    public string Name { get; protected set; }
    public override BankEnum Bank => BankEnum.YBank;
}	
	
protected override void Map(EntityTypeBuilder<Provider> eb)
{
   eb.Property(b => b.Bank).HasColumnName("BankId").HasColumnType("int");
   eb.Property(b => b.ClientId).HasColumnType("varchar(100)");

   eb.ToTable("Provider")
       .HasDiscriminator<BankEnum>("BankId")
       .HasValue<XBankProvider>(BankEnum.XBank)
       .HasValue<YBankProvider>(BankEnum.YBank);
}

public class XBankProviderMapper : BaseTypeEntityMap<XBankProvider, Provider>
{
    protected override void Map(EntityTypeBuilder<XBankProvider> eb)
    {
        eb.Property(b => b.DomainName).HasColumnType("varchar(100)");
        eb.ToTable("Provider");
    }
}
public class YBankProviderMapper : BaseTypeEntityMap<YBankProvider, Provider>
{
    protected override void Map(EntityTypeBuilder<YBankProvider> eb)
    {
        eb.Property(b => b.Name).HasColumnType("varchar(100)");
        eb.ToTable("Provider");
    }
}
	
public abstract class BaseTypeEntityMap<T, U> where T : class where U : class, IEntity
{
    protected abstract void Map(EntityTypeBuilder<T> eb);

    public void BaseTypeMap(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<T>(bi =>
        {
            bi.HasBaseType<U>();
            Map(bi);
        });
    }
}

ase.OnModelCreating(modelBuilder);
}

when I try to make a query like this;

var provider = _providerRepository.FindBy(c => c.Bank == requestModel.Bank);

it throws this exception ;

The LINQ expression 'DbSet 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.

Before v3.x it was working but it's not working after v3.1.

How to solve this problem ?

Further technical details

EF Core version: 3.1.4
Database provider: PostgreSql
Target framework: (e.g. .NET Core 3.1)
Operating system:
IDE: (e.g. Visual Studio 2019 16.3)

@ajcvickers
Copy link
Contributor

@canertosuner What is the implementation of FindBy?

@canertosuner
Copy link
Author

canertosuner commented May 15, 2020

@canertosuner What is the implementation of FindBy?

It’s a method in GenericRepo class and the ProviderRepository is inherited from this class.

public abstract class GenericRepository<T>
    {
        private readonly MyDbContext _dbContext;
        private readonly DbSet<T> _dbSet;

        protected GenericRepository(MyDbContext dbContext)
        {
            this._dbContext = dbContext;
            this._dbSet = _dbContext.Set<T>();
        }

public IQueryable<T> FindBy(Expression<Func<T, bool>> predicate)
        {
            return _dbSet.Where(predicate);
        }
}

@ajcvickers
Copy link
Contributor

@canertosuner What is the reason for ignoring the navigation property here?

modelBuilder.Entity<Provider>()
        .Ignore(p => p.Bank);

I tried building your model, but it's unclear to me how all the mapping classes you have above work together. It would help us investigate if you could attach a small, runnable project or post a small, runnable code listing that reproduces what you are seeing.

@canertosuner
Copy link
Author

canertosuner commented May 15, 2020

@canertosuner What is the reason for ignoring the navigation property here?

modelBuilder.Entity<Provider>()
        .Ignore(p => p.Bank);

I tried building your model, but it's unclear to me how all the mapping classes you have above work together. It would help us investigate if you could attach a small, runnable project or post a small, runnable code listing that reproduces what you are seeing.

if I don't Ignore it for the base type mapping it throws an exception but it was definetly working on v2.2.

I have attached a sample runnable project about the case. It uses inmemorydb as storage in the sample app. Just send a request to the Get method using swagger UI.
SampleApp.zip
@ajcvickers
May this case be related to this blog post ?

@ajcvickers
Copy link
Contributor

@canertosuner Thanks for the additional info. It seems like the code is using Provider.Bank as a TPH discriminator, but then explicitly excluding this from the EF model and setting up a shadow discriminator instead. This leaves the property not mapped, which means any queries that use it cannot be translated to the server. Running your code against 2.2.6 confirms this with the following log:

warn: Microsoft.EntityFrameworkCore.Query[20500]
      The LINQ expression 'where (Convert([c].Bank, Int32) == Convert(__b_0, Int32))' could not be translated and will be evaluated locally.
warn: Microsoft.EntityFrameworkCore.Query[20500]
      The LINQ expression 'SingleOrDefault()' could not be translated and will be evaluated locally.
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (7ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT [c].[Id], [c].[BankId], [c].[ClientId], [c].[DomainName], [c].[Name]
      FROM [Provider] AS [c]
      WHERE [c].[BankId] IN (2, 1)

Client evaluation like this is no longer supported in EF Core 3+. Hence for 3.1 an exception is thrown.

As the exception message says, you can get back the 2.x behavior by explicitly adding client evaluation to the query using AsEnumerable, ToList, or similar.

Another option is to allow EF to use your Provider.Bank property as the discriminator. This requires that the property has a setter (see #10140) but it can be private/internal/protected. This will then allow the query to be translated to the server. For example:

public abstract class Provider
{
    public Guid Id { get; set; }
    public string ClientId { get; set; }
    public abstract BankEnum Bank { get; internal set; }
}

public enum BankEnum
{
    XBank = 1,
    YBank = 2,
}

public class XBankProvider : Provider
{
    public string DomainName { get; protected set; }
    public override BankEnum Bank
    {
        get => BankEnum.XBank;
        internal set { }
    }
}

public class YBankProvider : Provider
{
    public string Name { get; protected set; }
    public override BankEnum Bank
    {
        get => BankEnum.YBank;
        internal set { }
    }
}

public class MyDbContext : DbContext
{
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Provider>(b =>
        {
            b.ToTable("Provider");
            b.HasDiscriminator(e => e.Bank)
                .HasValue<XBankProvider>(BankEnum.XBank)
                .HasValue<YBankProvider>(BankEnum.YBank);
        });
   }

    private static readonly ILoggerFactory
        Logger = LoggerFactory.Create(x => x.AddConsole()); //.SetMinimumLevel(LogLevel.Debug));

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder
            .UseLoggerFactory(Logger)
            .EnableSensitiveDataLogging()
            .UseSqlServer(Your.SqlServerConnectionString);
}

public static class Program
{
    public static void Main()
    {
        using (var context = new MyDbContext())
        {
            context.Database.EnsureDeleted();
            context.Database.EnsureCreated();

            context.SaveChanges();
        }

        using (var context = new MyDbContext())
        {
            var b = BankEnum.YBank;
            var result = context.Set<Provider>().SingleOrDefault(c => c.Bank == b); 
        }
    }
}

@ajcvickers ajcvickers added the closed-no-further-action The issue is closed and no further action is planned. label May 18, 2020
@canertosuner
Copy link
Author

canertosuner commented May 19, 2020

Thank you for that @ajcvickers . I tried the way you suggested and it worked for v3.1

@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
Labels
closed-no-further-action The issue is closed and no further action is planned. customer-reported
Projects
None yet
Development

No branches or pull requests

2 participants