dotnet / efcore

EF Core is a modern object-database mapper for .NET. It supports LINQ queries, change tracking, updates, and schema migrations.
https://docs.microsoft.com/ef/
MIT License
13.77k stars 3.18k forks source link

Equal is not defined for the types 'System.Int32' and 'System.Nullable`1[System.Int32] on versions after 2.1.1 #13499

Closed AJHopper closed 6 years ago

AJHopper commented 6 years ago

So, within our codebase we have had a setup that has been working on EF Core up to 2.1.1 however on versions beyond this fails with the Equal not defined issue - curious if this change is by design or a bug.

The issue being on a HasOne().WithMany() one way relationship mapping as detailed below where one of the fields it links on is an int type, and one is nullable int, as mentioned it works in 2.1.1 but tested in 2.1.2 and 2.1.4 its non-working with this issue.

Obviously a workaround would be to make the field nullable on both sides though this is essentially making a field which can never be null... into a nullable field just to get around an issue and since it works fine in 2.1.1 and prior I would like to not do that :) though would also like to be able to update versions without worry of breaking things

Exception message: The binary operator Equal is not defined for the types 'System.Int32' and 'System.Nullable`1[System.Int32]'.

Stack trace:    at Microsoft.EntityFrameworkCore.Metadata.Internal.ClrAccessorFactory`1.Create(PropertyInfo propertyInfo, IPropertyBase propertyBase)
   at Microsoft.EntityFrameworkCore.Metadata.Internal.ClrAccessorFactory`1.Create(IPropertyBase property)
   at Microsoft.EntityFrameworkCore.Internal.NonCapturingLazyInitializer.EnsureInitialized[TParam,TValue](TValue& target, TParam param, Func`2 valueFactory)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.ReadPropertyValue(IPropertyBase propertyBase)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.NavigationFixer.SetForeignKeyProperties(InternalEntityEntry dependentEntry, InternalEntityEntry principalEntry, IForeignKey foreignKey, Boolean setModified)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.NavigationFixer.InitialFixup(InternalEntityEntry entry, ISet`1 handledForeignKeys, Boolean fromQuery)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.NavigationFixer.StateChanged(InternalEntityEntry entry, EntityState oldState, Boolean fromQuery)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntryNotifier.StateChanged(InternalEntityEntry entry, EntityState oldState, Boolean fromQuery)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.FireStateChanged(EntityState oldState)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.EntityGraphAttacher.PaintAction(EntityEntryGraphNode node, Boolean force)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.EntityEntryGraphIterator.TraverseGraph[TState](EntityEntryGraphNode node, TState state, Func`3 handleNode)
   at Microsoft.EntityFrameworkCore.DbContext.SetEntityState[TEntity](TEntity entity, EntityState entityState)

Steps to reproduce

So, steps to reproducing would be having the below class structure (havent tested variants but this is what we have), and doing a query for Entity1 calling .Include() on the Entity2 object.

public abstract class BaseModel
{
public int? Id {get;set;}
}

public class Entity1 : BaseModel
{
public virtual Entity2 Thing2 {get;set;}
public virtual int Entity2Id {get;set;}
}

public virtual Entity2 : BaseModel
{
}

public class Entity1 : IEntityTypeConfiguration<Entity1>
    {
        public void Configure(EntityTypeBuilder<Entity1> builder)
        {
builder.HasKey(x => x.Id);
builder.HasOne(x => x.Thing2).WithMany().HasPrincipalKey(x => x.Id).HasForeignKey(x => x.Entity2Id);
        }
     }

Further technical details

EF Core version: 2.1.4 Database Provider: Microsoft.EntityFrameworkCore.SqlServer Operating system: Windows 10 64 bit Enterprise Edition IDE: Visual Studio 2017 15.8.4

ajcvickers commented 6 years ago

@AJHopper Can you post a runnable project/solution or complete code listing that demonstrates the behavior you are seeing.

AJHopper commented 6 years ago

@ajcvickers Was setting up a small runnable solution that has the issue and seemingly couldnt get it to error - this lead me to delve into our codebase more and I found the issue was a redundant .HasConversion() call - the code runs fine without it but equally, as mentioned above it also runs fine with it in versions 2.1.1 and below, so will still post the code up to repro it.

using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace ConsoleApp2
{
    public abstract class Base
    {
        public int? Id { get; set; }
    }

    public class ObjectA : Base
    {
        public virtual ObjectB SomeObject { get; set; }
        public int ObjectBId { get; set; }
    }

    public class ObjectB : Base
    {
    }

    public class ObjectAMap : IEntityTypeConfiguration<ObjectA>
    {
        public void Configure(EntityTypeBuilder<ObjectA> builder)
        {
            builder.ToTable("FirstObject");
            builder.HasKey(x => x.Id);
            builder.Property(x => x.Id).HasColumnType("int");
            builder.Property(x => x.ObjectBId).HasColumnType("int");
            builder.HasOne(x => x.SomeObject).WithMany().HasForeignKey(x => x.ObjectBId).HasPrincipalKey(x => x.Id);
        }
    }

    public class ObjectBMap : IEntityTypeConfiguration<ObjectB>
    {
        public void Configure(EntityTypeBuilder<ObjectB> builder)
        {
            builder.ToTable("SecondObject");
            builder.HasKey(x => x.Id);
            /*
             * Below line seems to be the cause of the issue - somehow a redundant HasConversion was in one of our mappings.
             * 
             * This does however work on EF Core 2.1.1 - though crashes on 2.1.2+
            */
            builder.Property(x => x.Id).HasColumnType("float").HasConversion(x => x, x => (int)x);
        }
    }

    public class MyDbContext : DbContext
    {
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer(@"Server=ADatabase;Database=TestRelations;Trusted_Connection=True;");
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);
            modelBuilder.ApplyConfiguration(new ObjectAMap());
            modelBuilder.ApplyConfiguration(new ObjectBMap());
        }

        public DbSet<ObjectA> ObjectAs { get; set; }
        public DbSet<ObjectB> ObjectBs { get; set; }
    }   

    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new MyDbContext())
            { 
                var tt = context.ObjectAs.Include(x=> x.SomeObject).ToList();
            }
        }
    }
}
ajcvickers commented 6 years ago

@AJHopper Thanks for the additional info.

Triage: the issue here is that a conversion is defined for int? to int? but then used for an int property. This probably should work given that the reverse (int to int but used for an int?) is supposed to work. For example:

This fails:

builder.Property(x => x.Id).HasConversion(x => x, x => x);

This works:

builder.Property(x => x.Id).HasConversion(new ValueConverter<int,int>(x => x, x => x));