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

Case Insensitive string comparison in Composite Foreign Keys #27467

Closed
Hylaean opened this issue Feb 17, 2022 · 13 comments
Closed

Case Insensitive string comparison in Composite Foreign Keys #27467

Hylaean opened this issue Feb 17, 2022 · 13 comments

Comments

@Hylaean
Copy link

Hylaean commented Feb 17, 2022

I fond an issue in a model were navigability of entities didn't work when casing changed between the target and source tables.
When a case insensitive comparer is added, it seems the inner and outer key selectors can't be translated.

Exception:

   System.InvalidOperationException : The LINQ expression 'DbSet<Product>()
        .Where(p => p.SubscriptionId == __subscriptionId_0 || p.SubscriptionId == -1)
        .Where(p => p.ProductCode == __productCode_1 && !(p.IsDeleted))
        .Join(
            inner: DbSet<ProductDefinition>(), 
            outerKeySelector: p => new object[]
            { 
                (object)EF.Property<int?>(p, "SubscriptionId"), 
                (object)EF.Property<string>(p, "ProductCode"), 
                (object)EF.Property<string>(p, "CustomerCode") 
            }, 
            innerKeySelector: p0 => new object[]
            { 
                (object)EF.Property<int?>(p0, "SubscriptionId"), 
                (object)EF.Property<string>(p0, "ProductCode"), 
                (object)EF.Property<string>(p0, "CustomerCode") 
            }, 
            resultSelector: (o, i) => new TransparentIdentifier<Product, ProductDefinition>(
                Outer = o, 
                Inner = i
            ))' 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.

  Stack Trace: 
    QueryableMethodTranslatingExpressionVisitor.<VisitMethodCall>g__CheckTranslated|15_0(ShapedQueryExpression translated, <>c__DisplayClass15_0& )
    QueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
    InMemoryQueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
    MethodCallExpression.Accept(ExpressionVisitor visitor)
    ExpressionVisitor.Visit(Expression node)
    QueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
    InMemoryQueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
    MethodCallExpression.Accept(ExpressionVisitor visitor)
    ExpressionVisitor.Visit(Expression node)
    QueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)

version information

EF Core version: 6.0.2
Database provider: *
Target framework: .NET 6.0

@ajcvickers
Copy link
Contributor

This issue is lacking enough information for us to be able to fully understand what is happening. Please attach a small, runnable project or post a small, runnable code listing that reproduces what you are seeing so that we can investigate.

@leppie
Copy link

leppie commented Feb 22, 2022

I ran into the same issue, this has been a bug since ef core 2 as I could repro on it. Was running on SQL server.

I suspect @Hylaean tried a case-insensitive comparer as a workaround, but IMO the join should be working without a special comparer.

So basically you have a composite key where one column has a foreign key.

Table A(name)
A
B
C

Table AB(name, code)
A, 1
A, 2
a, 3
A, 4
B, 1
C, 2
C, 1

When you get related AB's on A for name=A, only values returned are up to be not including the row where the casing changes. The rest are ignored. IOW:

A, 1
A, 2

If the casing was a, 1 (and all others uppercase), no rows are returned.

The queries are correct, but it seems when it joins in code, it does a case-sensitive comparison (seemingly iterating till the key does not match any more and stops).

@ajcvickers let me know if you need more info.

@Hylaean
Copy link
Author

Hylaean commented Feb 22, 2022

Thanks @leppie, sorry I haven't yet come about to fix this for good.

There is indeed the navigability issue, using a custom case insensitive comparer leads to EF Core not being able to generate the query when more two or more properties are needed for the join.

@leppie
Copy link

leppie commented Feb 23, 2022

Here is a blog post about this issue and perhaps more details

https://qszhuan.github.io/dotnetcore/2018/10/03/dotnetcore-foreign-key-case-sensitive

@leppie
Copy link

leppie commented Feb 23, 2022

@Hylaean
Copy link
Author

Hylaean commented Feb 23, 2022 via email

@roji
Copy link
Member

roji commented Feb 23, 2022

Duplicate of dotnet/EntityFramework.Docs#2979

@roji roji marked this as a duplicate of dotnet/EntityFramework.Docs#2979 Feb 23, 2022
@roji
Copy link
Member

roji commented Feb 23, 2022

Setting up a value comparer is indeed the right way to work around this.

@Hylaean
Copy link
Author

Hylaean commented Feb 23, 2022

I'm sorry @roji, my point is that I did that and setting up a value comparer leads to exception mentioned when the foreign key looks like that.

        .HasPrincipalKey(d => new { d.SubscriptionId, d.ProductCode, d.CustomerCode })
        .HasForeignKey(d => new { d.SubscriptionId, d.ProductCode, d.CustomerCode })

I've forked the repo trying to find a unit test I could use as inspiration to demonstrate this bug. Can you point me to one?

@roji
Copy link
Member

roji commented Feb 23, 2022

@Hylaean sorry, I didn't notice your comment above.

But in any case, what we're missing here is a full, runnable code sample - it's difficult to understand from the above bits and pieces exactly what's going on. Ideally you would just write a simple console program that reproduces the error; if there's a problem in your code (as opposed to in EF), this would also help you isolate it.

@Hylaean
Copy link
Author

Hylaean commented Feb 23, 2022

Sorry @roji I hadn't time to reproduce this outside.
Ok, so I think the following tests illustrate the issue:

#nullable disable
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xunit;

namespace EfBug;

public class EFComparerCompositeKeyBug
{
    public class Genus
    {
        public string Name { get; set; }
        public string FamilyName { get; set; }
        public ICollection<Specy> Species { get; set; }
    }
    public class Specy
    {
        public string Name { get; set; }
        public string GenusName { get; set; }
        public string FamilyName { get; set; }
        public Genus Genus { get; set; }
    }
    internal static class Comparers
    {
        public static ValueComparer<string>? CI { get; } = new(
            (l, r) => string.Equals(l, r, StringComparison.OrdinalIgnoreCase),
            v => StringComparer.OrdinalIgnoreCase.GetHashCode(v));
    }
    public class TaxonomyContext : DbContext
    {
        protected override void OnModelCreating(ModelBuilder model)
        {
            model.Entity<Genus>(entity =>
            {
                entity.HasKey(e => new { e.Name, e.FamilyName });
                entity.Property(e => e.Name).IsRequired().Metadata.SetValueComparer(Comparers.CI);
                entity.Property(e => e.FamilyName).IsRequired().Metadata.SetValueComparer(Comparers.CI);
            });


            model.Entity<Specy>(entity =>
            {
                entity.HasKey(e => new { e.Name, e.GenusName, e.FamilyName });
                entity.Property(e => e.Name).IsRequired().Metadata.SetValueComparer(Comparers.CI);
                entity.Property(e => e.FamilyName).IsRequired().Metadata.SetValueComparer(Comparers.CI);
                entity.Property(e => e.GenusName).IsRequired().Metadata.SetValueComparer(Comparers.CI);
                entity.HasOne(e => e.Genus).WithMany()
                    .HasPrincipalKey(e => new { GenusName = e.Name, e.FamilyName })
                    .HasForeignKey(e => new { e.GenusName, e.FamilyName });
            });

        }

        protected override void OnConfiguring(DbContextOptionsBuilder options)
        {
            base.OnConfiguring(options);
            options.UseInMemoryDatabase("Taxonomy");
        }

        public virtual DbSet<Genus> Genera { get; set; } = null!;
        public virtual DbSet<Specy> Species { get; set; } = null!;

    }

    [Fact]
    public void Passes()
    {
        using var taxo = new TaxonomyContext();
        var species = taxo.Species.ToList();
    }

    [Fact]
    public void Fails()
    {
        using var taxo = new TaxonomyContext();
        var ex = Assert.Throws<InvalidOperationException>(() => (from s in taxo.Species.Include(e => e.Genus)
                                                         where s.Genus.FamilyName == "Ornithorhynchidae"
                                                         select s).ToList());
        Assert.Contains("could not be translated", ex.Message);
    }

    [Fact]
    public void AlsoFails()
    {
        using var taxo = new TaxonomyContext();
        var ex = Assert.Throws<InvalidOperationException>(() => (from s in taxo.Species
                                                where s.Genus.FamilyName == "Ornithorhynchidae"
                                                select s).ToList());
        Assert.Contains("could not be translated", ex.Message);
    }

    [Fact]
    public void StrangelyDoesntFail()
    {
        using var taxo = new TaxonomyContext();
        (from g in taxo.Genera.Include(e => e.Species)
        where g.FamilyName == "Ornithorhynchidae"
        from s in g.Species
        select s).Distinct().ToList();
    }
}

@Hylaean Hylaean closed this as completed Feb 23, 2022
@roji roji marked this as not a duplicate of dotnet/EntityFramework.Docs#2979 Feb 23, 2022
@roji
Copy link
Member

roji commented Feb 23, 2022

Duplicate of #27495

@ajcvickers
Copy link
Contributor

See also #27526

@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

4 participants