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

"Binary operator Equal is not defined" on custom struct types used as keys #21372

Closed
QuantumToasted opened this issue Jun 22, 2020 · 2 comments
Closed
Labels
area-change-tracking closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. customer-reported type-bug
Milestone

Comments

@QuantumToasted
Copy link

(As mentioned in this comment on an older, closed issue #12290, I have moved to create an issue for my specific problem.)

When attempting to use a custom struct with a defined value conversion to and from ulong as a key, performing a FindAsync() query will generate an exception:

System.InvalidOperationException: The binary operator Equal is not defined for the types 'Disqord.Snowflake' and 'Disqord.Snowflake'.
  at System.Linq.Expressions.Expression.GetEqualityComparisonOperator(ExpressionType binaryType, String opName, Expression left, Expression right, Boolean liftToNull)
  at System.Linq.Expressions.Expression.Equal(Expression left, Expression right, Boolean liftToNull, MethodInfo method)
  at Microsoft.EntityFrameworkCore.Internal.EntityFinder`1.BuildPredicate(IReadOnlyList`1 keyProperties, ValueBuffer keyValues, ParameterExpression entityParameter)
  at Microsoft.EntityFrameworkCore.Internal.EntityFinder`1.BuildLambda(IReadOnlyList`1 keyProperties, ValueBuffer keyValues)
  at Microsoft.EntityFrameworkCore.Internal.EntityFinder`1.FindAsync(Object[] keyValues, CancellationToken cancellationToken)
  at Microsoft.EntityFrameworkCore.Internal.InternalDbSet`1.FindAsync(Object[] keyValues)

I am not sure if this is a regression from issue #12290 which was supposedly fixed, or a completely separate issue altogether. If needed, I am happy to test this on non-super-unstable-and-new versions of EF Core. This just happened to be the version I was using.

Steps to reproduce

*Note: This Snowflake struct is a simplified version of this struct from a library I am using. If you believe the full struct would assist in any way for finding a solution you are welcome to use this instead for triage or testing.

Below is a full one-file example project I created to test this. The code does not perform any migrations or raw database operations so for testing, migrations will need to be created and applied to the testing database.

public readonly struct Snowflake : IEquatable<ulong>, 
    IEquatable<Snowflake>, 
    IComparable<ulong>, 
    IComparable<Snowflake>
{
    public ulong RawValue { get; }

    public Snowflake(ulong rawValue)
    {
        RawValue = rawValue;
    }

    public bool Equals(ulong other)
        => RawValue == other;

    public bool Equals(Snowflake other)
        => RawValue == other.RawValue;

    public override bool Equals(object obj)
    {
        if (obj is Snowflake snowflake)
            return snowflake.RawValue == RawValue;

        if (obj is ulong rawValue)
            return rawValue == RawValue;

        return false;
    }

    public override int GetHashCode()
        => RawValue.GetHashCode();

    public override string ToString()
        => RawValue.ToString();

    public int CompareTo(ulong other)
        => RawValue.CompareTo(other);

    public int CompareTo(Snowflake other)
        => RawValue.CompareTo(other);

    public static implicit operator Snowflake(ulong value)
        => new Snowflake(value);

    public static implicit operator ulong(Snowflake value)
        => value.RawValue;
}

public sealed class Guild
{
    public Snowflake Id { get; set; }

    public string Name { get; set; }
}

public sealed class SnowflakeValueConverter : ValueConverter<Snowflake, ulong>
{
    public SnowflakeValueConverter() 
        : base(x => x.RawValue, x => new Snowflake(x))
    { }
}

public class GuildContext : DbContext
{
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Guild>(g =>
        {
            g.HasKey(x => x.Id);
            g.Property(x => x.Id)
                .HasConversion(new SnowflakeValueConverter());
        });
    }

    public DbSet<Guild> Guilds { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder
            .UseNpgsql("YOUR_DATABASE_CONNECTION_STRING");
}

public class Program
{
    private static readonly Snowflake ExampleId = 132349439506382848;

    public static async Task Main()
    {
        using (var context = new GuildContext())
        {
            context.Guilds.Add(new Guild {Id = ExampleId, Name = "Test"});
            await context.SaveChangesAsync();
        }

        using (var context = new GuildContext())
        {
            try
            {
                var guild = await context.Guilds.FindAsync(ExampleId);
                Console.WriteLine($"Guild found with ID {ExampleId} and name {guild.Name}!");
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Guild NOT found with ID {ExampleId}.");
                Console.WriteLine(ex);
            }
            Console.ReadKey();
        }
    }
}

Assuming everything is set up correctly, when Main() is called, it should enter the catch section and complain about the Guild not being found, and printing a similar if not identical exception message to console.

Further technical details

EF Core version: 5.0.0-preview.5.20278.2
Database provider: Npgsql.EntityFrameworkCore.PostgreSQL, version 5.0.0-preview5
Target framework: .NET 5.0 preview (5.0.100-preview.4.20258.7)
Operating system: Windows 10 x64
IDE: Visual Studio 2019 16.7.0 Preview 2.0

@QuantumToasted QuantumToasted changed the title System.InvalidOperationException about a missing binary Equal operator on custom struct types used as keys "Binary operator Equal is not defined" on custom struct types used as keys Jun 22, 2020
@ajcvickers ajcvickers added this to the 5.0.0 milestone Jun 29, 2020
@ajcvickers ajcvickers self-assigned this Jun 29, 2020
@smitpatel
Copy link
Contributor

#21742 should fix this. BuildPredicate is updated to generated object.Equals for custom structs.

@smitpatel smitpatel added the verify-fixed This issue is likely fixed in new query pipeline. label Aug 11, 2020
@ajcvickers
Copy link
Contributor

Verified this is fixed with the latest daily.

@ajcvickers ajcvickers modified the milestones: 5.0.0, 5.0.0-rc1 Aug 25, 2020
@ajcvickers ajcvickers added closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. and removed verify-fixed This issue is likely fixed in new query pipeline. labels Aug 25, 2020
@ajcvickers ajcvickers modified the milestones: 5.0.0-rc1, 5.0.0 Nov 7, 2020
@ajcvickers ajcvickers removed their assignment Sep 1, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-change-tracking closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. customer-reported type-bug
Projects
None yet
Development

No branches or pull requests

3 participants