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

SaveChanges() produces an exception when saving multiple entities (of different types) sharing the same base class / repository #654

Closed
brian-vn opened this issue Oct 24, 2018 · 1 comment
Assignees
Labels
Milestone

Comments

@brian-vn
Copy link

Using Entity Framework 6 (Database-First approach), an attempt to save a set of modified entities derived from the same base class failed with "System.ArgumentException: Object must be of type Int32." The entities in question were implemented using the Table-Per-Type (TPT) inheritance strategy, and the derived types happened to have the same number of "key-identifier" fields (as outlined in more detail below...).

Based on what I've observed, there appears to be a limitation / bug in the framework within the EF code responsible for determining the order in which to produce/execute the database commands. I tried version 6.1.3 and 6.2, both with the same results.

The following is the Exception I received...

Exception message: System.ArgumentException: Object must be of type Int32
Stack trace:

at System.Int32.CompareTo(Object value)
   at System.Collections.Comparer.Compare(Object a, Object b)
   at System.Collections.Generic.ObjectComparer`1.Compare(T x, T y)
   at System.Collections.Generic.Comparer`1.System.Collections.IComparer.Compare(Object x, Object y)
   at System.Data.Entity.Core.Common.Utils.ByValueComparer.System.Collections.IComparer.Compare(Object x, Object y)
   at System.Data.Entity.Core.Mapping.Update.Internal.**FunctionUpdateCommand.CompareToType**(UpdateCommand otherCommand)
   at System.Data.Entity.Core.Mapping.Update.Internal.UpdateCommand.CompareTo(UpdateCommand other)
   at System.Collections.Generic.GenericComparer`1.Compare(T x, T y)
   at System.Collections.Generic.SortedSet`1.AddIfNotPresent(T item)
   at System.Data.Entity.Core.Mapping.Update.Internal.Graph`1.TryTopologicalSort(IEnumerable`1& orderedVertices, IEnumerable`1& remainder)
   at System.Data.Entity.Core.Mapping.Update.Internal.UpdateTranslator.ProduceCommands()
   at System.Data.Entity.Core.Mapping.Update.Internal.UpdateTranslator.Update()
   at System.Data.Entity.Core.EntityClient.Internal.EntityAdapter.<Update>b__2(UpdateTranslator ut)
   at System.Data.Entity.Core.EntityClient.Internal.EntityAdapter.Update[T](T noChangesResult, Func`2 updateFunction)
   at System.Data.Entity.Core.EntityClient.Internal.EntityAdapter.Update()
   at System.Data.Entity.Core.Objects.ObjectContext.<SaveChangesToStore>b__35()
   at System.Data.Entity.Core.Objects.ObjectContext.ExecuteInTransaction[T](Func`1 func, IDbExecutionStrategy executionStrategy, Boolean startLocalTransaction, Boolean releaseConnectionOnSuccess)
   at System.Data.Entity.Core.Objects.ObjectContext.SaveChangesToStore(SaveOptions options, IDbExecutionStrategy executionStrategy, Boolean startLocalTransaction)
   at System.Data.Entity.Core.Objects.ObjectContext.<>c__DisplayClass2a.<SaveChangesInternal>b__27()
   at System.Data.Entity.SqlServer.DefaultSqlExecutionStrategy.Execute[TResult](Func`1 operation)
   at System.Data.Entity.Core.Objects.ObjectContext.SaveChangesInternal(SaveOptions options, Boolean executeInExistingTransaction)
   at System.Data.Entity.Core.Objects.ObjectContext.SaveChanges(SaveOptions options)
   at System.Data.Entity.Internal.InternalContext.SaveChanges()
   at System.Data.Entity.Internal.LazyInternalContext.SaveChanges()
   at System.Data.Entity.DbContext.SaveChanges()
   at Rodan.EMIS.ObjectModel.Core.Repository.GenericEFUnitOfWork.SaveChanges(OperationLog opLog, Boolean throwOnError)
   at Rodan.EMIS.Configuration.Core.FieldServices.SaveChangesToDatabase(DataProviderUnitOfWork dataProviderUoW, OperationLog opLog, MergeOptions options) 
   at Rodan.EMIS.Configuration.Core.EMISConfiguration.SaveToDatabase(MergeOptions options) 
   at Rodan.EMIS.Configuration.Core.EMISConfiguration.ImportConfiguration(String sourceXmlFilePath, MergeOptions dbMergeOptions, String optClientNameFilter, String operationLogXmlFilePath) 
   at Rodan.EMIS.Configuration.Core.Tests.EMISConfigurationTests.ImportConfiguration_successfullyImportsThenModifiesNewClientFromXml2DB() 

Steps to reproduce (sort of)

When breaking on the exception, and examining the call-stack at the function highlighted above, I found the following...

  1. The System.ArgumentException occurred when EF was (presumably) trying to establish what order to issue the DB commands in
  2. The FunctionUpdateCommand.CompareToType() comparison function:
    • was comparing a “DataSource” update command with a “DataPoint” update command (meaning both were in the “Modified” state)
    • correctly detected that both objects/commands were directed to the same repository (i.e. “IntegrationNodes”)
    • correctly detected that both objects/commands had the same number of “identifiers” (i.e. 8)
    • incorrectly attempted to compare each identifier assuming they were of the same type
      • although the entities shared the same base type, the Entity Types were in fact different (DataSource vs. DataPoint) as noted above
      • the corresponding key identifiers (and their types), were also different (see image below).
      • specifically, the exception occurred while attempting to compare the DataPoint's "dataRepresentationID" (type Int32) with the DataSource's "ownerVisibilityScopeID" (type Byte)

image

Based on my understanding (albeit limited) of the issue here, I made the following change to the System.Data.Entity.Core.Mapping.Update.Internal.FunctionUpdateCommand.CompareToType function:

  • From
// order by key values
var thisInputIdentifierCount = (null == _inputIdentifiers ? 0 : _inputIdentifiers.Count);
var otherInputIdentifierCount = (null == other._inputIdentifiers ? 0 : other._inputIdentifiers.Count);
result = thisInputIdentifierCount - otherInputIdentifierCount;
if (0 != result)
{
    return result;
}
for (var i = 0; i < thisInputIdentifierCount; i++)
{
    var thisParameter = _inputIdentifiers[i].Value;
    var otherParameter = other._inputIdentifiers[i].Value;
    result = ByValueComparer.Default.Compare(thisParameter.Value, otherParameter.Value);
    if (0 != result)
    {
        return result;
    }
}
  • To:
// order by key value count
var thisInputIdentifierCount = (null == _inputIdentifiers ? 0 : _inputIdentifiers.Count);
var otherInputIdentifierCount = (null == other._inputIdentifiers ? 0 : other._inputIdentifiers.Count);
result = thisInputIdentifierCount - otherInputIdentifierCount;
if (0 != result)
{
    return result;
}

// order by key types/values
for (var i = 0; i < thisInputIdentifierCount; i++)
{
       var thisParameter = _inputIdentifiers[i].Value;
       var otherParameter = other._inputIdentifiers[i].Value;

       // order by key types
       result = (int)thisParameter.DbType - (int)otherParameter.DbType;
       if (0 != result)
       {
              return result;
       }

       // order by key values
       result = ByValueComparer.Default.Compare(thisParameter.Value, otherParameter.Value);
       if (0 != result)
       {
              return result;
       }
}

The above changes addressed the issue I was having, and the SQL commands were subsequently successfully generated and executed by the framework.

Does this fix make sense, and, if so, could it be incorporated into the official version? We're otherwise working with the official (6.2) version.

Further technical details

EF version: 6.13, 6.2
Database Provider: EntityFramework.SqlServer
Operating system: Windows 7
IDE: Visual Studio 2015 (14.0 Update 3)

brian-vn added a commit to brian-vn/EntityFramework6 that referenced this issue Oct 24, 2018
Addresses the Issue#654 reported here:  dotnet#654
@ajcvickers ajcvickers self-assigned this Mar 18, 2019
@ajcvickers ajcvickers added this to the 6.3.0 milestone Mar 18, 2019
@ajcvickers ajcvickers modified the milestones: 6.3.0, Backlog Aug 6, 2019
@ajcvickers
Copy link
Member

This issue has been closed because EF6 is no longer being actively developed. We are instead focusing on stability of the codebase, which means we will only make changes to address security issues. See the repo README for more information.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants