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.73k stars 3.18k forks source link

Use the configured ValueComparer in ModificationCommandComparer #13507

Closed AndriySvyryd closed 4 years ago

ajcvickers commented 6 years ago

The KeyComparer of the property is the appropriate one to use in this case.

ajcvickers commented 6 years ago

The KeyComparer should be used so that keys of any type that do not compare appropriately with the default Equals (e.g. IGeometry) will be sorted correctly.

ajcvickers commented 4 years ago

While investigating this it became apparent that there are currently implicit requirements for types used as key properties. Testing indicates:

  1. Class key types must override reference equality (or ensure singleton instances)
  2. Querying requires that struct key types have equality operator overloads. (Exception below.)
  3. The update pipeline currently requires key types to be IComparable. (Exception below.)

Proposal:

Exception when key structs don't implement equality operator:

System.InvalidOperationException : The binary operator Equal is not defined for the types 'System.Nullable`1[Microsoft.EntityFrameworkCore.BuiltInDataTypesTestBase`1+KeyStruct[Microsoft.EntityFrameworkCore.BuiltInDataTypesSqlServerTest+BuiltInDataTypesSqlServerFixture]]' and 'System.Nullable`1[Microsoft.EntityFrameworkCore.BuiltInDataTypesTestBase`1+KeyStruct[Microsoft.EntityFrameworkCore.BuiltInDataTypesSqlServerTest+BuiltInDataTypesSqlServerFixture]]'.
   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 System.Linq.Expressions.Expression.Equal(Expression left, Expression right)
   at Microsoft.EntityFrameworkCore.Query.Internal.NavigationExpandingExpressionVisitor.ExpandingExpressionVisitor.ExpandNavigation(Expression root, EntityReference entityReference, INavigation navigation, Boolean derivedTypeConversion) in C:\aspnet\EntityFrameworkCore\src\EFCore\Query\Internal\NavigationExpandingExpressionVisitor.ExpressionVisitors.cs:line 231
   at Microsoft.EntityFrameworkCore.Query.Internal.NavigationExpandingExpressionVisitor.IncludeExpandingExpressionVisitor.ExpandIncludesHelper(Expression root, EntityReference entityReference) in C:\aspnet\EntityFrameworkCore\src\EFCore\Query\Internal\NavigationExpandingExpressionVisitor.ExpressionVisitors.cs:line 486
   at Microsoft.EntityFrameworkCore.Query.Internal.NavigationExpandingExpressionVisitor.IncludeExpandingExpressionVisitor.ExpandInclude(Expression root, EntityReference entityReference) in C:\aspnet\EntityFrameworkCore\src\EFCore\Query\Internal\NavigationExpandingExpressionVisitor.ExpressionVisitors.cs:line 448
   at Microsoft.EntityFrameworkCore.Query.Internal.NavigationExpandingExpressionVisitor.IncludeExpandingExpressionVisitor.VisitExtension(Expression extensionExpression) in C:\aspnet\EntityFrameworkCore\src\EFCore\Query\Internal\NavigationExpandingExpressionVisitor.ExpressionVisitors.cs:line 325
   at System.Linq.Expressions.Expression.Accept(ExpressionVisitor visitor)
   at Microsoft.EntityFrameworkCore.Query.Internal.NavigationExpandingExpressionVisitor.ExpandingExpressionVisitor.Expand(Expression expression, Boolean applyIncludes) in C:\aspnet\EntityFrameworkCore\src\EFCore\Query\Internal\NavigationExpandingExpressionVisitor.ExpressionVisitors.cs:line 36
   at Microsoft.EntityFrameworkCore.Query.Internal.NavigationExpandingExpressionVisitor.PendingSelectorExpandingExpressionVisitor.Visit(Expression expression) in C:\aspnet\EntityFrameworkCore\src\EFCore\Query\Internal\NavigationExpandingExpressionVisitor.ExpressionVisitors.cs:line 523
   at Microsoft.EntityFrameworkCore.Query.Internal.NavigationExpandingExpressionVisitor.Expand(Expression query) in C:\aspnet\EntityFrameworkCore\src\EFCore\Query\Internal\NavigationExpandingExpressionVisitor.cs:line 75
   at Microsoft.EntityFrameworkCore.Query.QueryTranslationPreprocessor.Process(Expression query) in C:\aspnet\EntityFrameworkCore\src\EFCore\Query\QueryTranslationPreprocessor.cs:line 40
   at Microsoft.EntityFrameworkCore.Query.QueryCompilationContext.CreateQueryExecutor[TResult](Expression query) in C:\aspnet\EntityFrameworkCore\src\EFCore\Query\QueryCompilationContext.cs:line 74
   at Microsoft.EntityFrameworkCore.Storage.Database.CompileQuery[TResult](Expression query, Boolean async) in C:\aspnet\EntityFrameworkCore\src\EFCore\Storage\Database.cs:line 71
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.CompileQueryCore[TResult](IDatabase database, Expression query, IModel model, Boolean async) in C:\aspnet\EntityFrameworkCore\src\EFCore\Query\Internal\QueryCompiler.cs:line 112
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.<>c__DisplayClass9_0`1.<Execute>b__0() in C:\aspnet\EntityFrameworkCore\src\EFCore\Query\Internal\QueryCompiler.cs:line 96
   at Microsoft.EntityFrameworkCore.Query.Internal.CompiledQueryCache.GetOrAddQueryCore[TFunc](Object cacheKey, Func`1 compiler) in C:\aspnet\EntityFrameworkCore\src\EFCore\Query\Internal\CompiledQueryCache.cs:line 84
   at Microsoft.EntityFrameworkCore.Query.Internal.CompiledQueryCache.GetOrAddQuery[TResult](Object cacheKey, Func`1 compiler) in C:\aspnet\EntityFrameworkCore\src\EFCore\Query\Internal\CompiledQueryCache.cs:line 59
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.Execute[TResult](Expression query) in C:\aspnet\EntityFrameworkCore\src\EFCore\Query\Internal\QueryCompiler.cs:line 92
   at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryProvider.Execute[TResult](Expression expression) in C:\aspnet\EntityFrameworkCore\src\EFCore\Query\Internal\EntityQueryProvider.cs:line 79
   at System.Linq.Queryable.Single[TSource](IQueryable`1 source, Expression`1 predicate)
   at Microsoft.EntityFrameworkCore.BuiltInDataTypesTestBase`1.<Can_insert_and_read_back_with_struct_key>g__QueryByStructKey|23_0(DbContext context, KeyStruct id) in C:\aspnet\EntityFrameworkCore\test\EFCore.Specification.Tests\BuiltInDataTypesTestBase.cs:line 1520
   at Microsoft.EntityFrameworkCore.BuiltInDataTypesTestBase`1.Can_insert_and_read_back_with_struct_key() in C:\aspnet\EntityFrameworkCore\test\EFCore.Specification.Tests\BuiltInDataTypesTestBase.cs:line 1527

Exception when key property is not IComparable

System.InvalidOperationException : Failed to compare two elements in the array.
---- System.ArgumentException : At least one object must implement IComparable.
   at System.Collections.Generic.ArraySortHelper`1.Sort(T[] keys, Int32 index, Int32 length, IComparer`1 comparer)
   at System.Array.Sort[T](T[] array, Int32 index, Int32 length, IComparer`1 comparer)
   at System.Collections.Generic.List`1.Sort(Int32 index, Int32 count, IComparer`1 comparer)
   at System.Collections.Generic.List`1.Sort(IComparer`1 comparer)
   at Microsoft.EntityFrameworkCore.Update.Internal.CommandBatchPreparer.BatchCommands(IList`1 entries, IUpdateAdapter updateAdapter)+MoveNext() in C:\aspnet\EntityFrameworkCore\src\EFCore.Relational\Update\Internal\CommandBatchPreparer.cs:line 88
   at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.Execute(IEnumerable`1 commandBatches, IRelationalConnection connection) in C:\aspnet\EntityFrameworkCore\src\EFCore.Relational\Update\Internal\BatchExecutor.cs:line 76
   at Microsoft.EntityFrameworkCore.Storage.RelationalDatabase.SaveChanges(IList`1 entries) in C:\aspnet\EntityFrameworkCore\src\EFCore.Relational\Storage\RelationalDatabase.cs:line 61
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChanges(IList`1 entriesToSave) in C:\aspnet\EntityFrameworkCore\src\EFCore\ChangeTracking\Internal\StateManager.cs:line 1057
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChanges(DbContext _, Boolean acceptAllChangesOnSuccess) in C:\aspnet\EntityFrameworkCore\src\EFCore\ChangeTracking\Internal\StateManager.cs:line 1104
   at Microsoft.EntityFrameworkCore.Storage.ExecutionStrategy.ExecuteImplementation[TState,TResult](Func`3 operation, Func`3 verifySucceeded, TState state) in C:\aspnet\EntityFrameworkCore\src\EFCore\Storage\ExecutionStrategy.cs:line 173
   at Microsoft.EntityFrameworkCore.Storage.ExecutionStrategy.Execute[TState,TResult](TState state, Func`3 operation, Func`3 verifySucceeded) in C:\aspnet\EntityFrameworkCore\src\EFCore\Storage\ExecutionStrategy.cs:line 159
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChanges(Boolean acceptAllChangesOnSuccess) in C:\aspnet\EntityFrameworkCore\src\EFCore\ChangeTracking\Internal\StateManager.cs:line 1084
   at Microsoft.EntityFrameworkCore.DbContext.SaveChanges(Boolean acceptAllChangesOnSuccess) in C:\aspnet\EntityFrameworkCore\src\EFCore\DbContext.cs:line 454
   at Microsoft.EntityFrameworkCore.DbContext.SaveChanges() in C:\aspnet\EntityFrameworkCore\src\EFCore\DbContext.cs:line 417
   at Microsoft.EntityFrameworkCore.BuiltInDataTypesTestBase`1.Can_insert_and_read_back_with_class_key() in C:\aspnet\EntityFrameworkCore\test\EFCore.Specification.Tests\BuiltInDataTypesTestBase.cs:line 1628
----- Inner Stack Trace -----
   at System.Collections.Comparer.Compare(Object a, Object b)
   at System.Collections.Generic.ObjectComparer`1.Compare(T x, T y)
   at Microsoft.EntityFrameworkCore.Update.Internal.ModificationCommandComparer.CompareValue[T](T x, T y) in C:\aspnet\EntityFrameworkCore\src\EFCore.Relational\Update\Internal\ModificationCommandComparer.cs:line 149
   at Microsoft.EntityFrameworkCore.Update.Internal.ModificationCommandComparer.Compare(ModificationCommand x, ModificationCommand y) in C:\aspnet\EntityFrameworkCore\src\EFCore.Relational\Update\Internal\ModificationCommandComparer.cs:line 106
   at System.Collections.Generic.ArraySortHelper`1.InsertionSort(T[] keys, Int32 lo, Int32 hi, Comparison`1 comparer)
   at System.Collections.Generic.ArraySortHelper`1.IntroSort(T[] keys, Int32 lo, Int32 hi, Int32 depthLimit, Comparison`1 comparer)
   at System.Collections.Generic.ArraySortHelper`1.IntrospectiveSort(T[] keys, Int32 left, Int32 length, Comparison`1 comparer)
   at System.Collections.Generic.ArraySortHelper`1.Sort(T[] keys, Int32 index, Int32 length, IComparer`1 comparer)
smitpatel commented 4 years ago

@ajcvickers - Can you share the query you used?

ajcvickers commented 4 years ago

Branch: https://github.com/aspnet/EntityFrameworkCore/tree/BuiltInDataTypesTests1225

smitpatel commented 4 years ago

Filed #19407 for query bug.

ajcvickers commented 4 years ago

Notes from team discussion: