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

InvalidOperationException on save after upgrading from 5 to 6 #27455

Closed
csmager opened this issue Feb 16, 2022 · 2 comments
Closed

InvalidOperationException on save after upgrading from 5 to 6 #27455

csmager opened this issue Feb 16, 2022 · 2 comments
Labels
area-save-changes closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. customer-reported regression Servicing-approved type-bug
Milestone

Comments

@csmager
Copy link

csmager commented Feb 16, 2022

On attempting to upgrade our project from EF Core 5 to 6, most of our integration tests fail. I've narrowed this down to a small repro below. An entity Foo has many FooPeriods. One of these is 'current'. So we have a 1:many and a 1:1 relationship:

public class Foo
{
    public int Id { get; set; }
    public int? CurrentPeriodNumber { get; set; }
    public FooPeriod? CurrentPeriod { get; set; }
    public ICollection<FooPeriod> Periods { get; } = new HashSet<FooPeriod>();
}

public class FooPeriod
{
    public int FooId { get; set; }    
    public int PeriodNumber { get; set; }    
    public Foo? Foo { get; set; }
}

This is configured via the fluent builder as below:

public class Context : DbContext
{
    public Context(DbContextOptions<Context> options)
        :base(options)
    {
        
    }

    public DbSet<Foo> Foos => Set<Foo>();
    
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Foo>(entity =>
        {
            entity.HasKey(x => x.Id);
            entity.Property(x => x.Id)
                .ValueGeneratedOnAdd();
            entity.HasOne(x => x.CurrentPeriod)
                .WithOne()
                .HasForeignKey<Foo>(x => new { x.Id, x.CurrentPeriodNumber });
        });

        modelBuilder.Entity<FooPeriod>(entity =>
        {
            entity.HasKey(x => new { x.FooId, x.PeriodNumber });
            entity.HasOne(x => x.Foo)
                .WithMany(x => x.Periods)
                .HasForeignKey(x => x.FooId);
        });
    }
}

If I insert a new period as below:

var period = new FooPeriod
{
    PeriodNumber = 1,
    Foo = new Foo()
};

context.Add(period);
context.SaveChanges();

Then I get this exception:

Unhandled exception. Microsoft.EntityFrameworkCore.DbUpdateException: An error occurred while saving the entity changes. See the inner exception for details.
 ---> System.InvalidOperationException: The property 'FooPeriod.FooId' cannot be assigned a value generated by the database. Store-generated values can only be assigned to properties configured to use store-generated values.
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.SetStoreGeneratedValue(IProperty property, Object value)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.PropagateValue(InternalEntityEntry principalEntry, IProperty principalProperty, IProperty dependentProperty, Boolean isMaterialization, Boolean setModified)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.NavigationFixer.SetForeignKeyProperties(InternalEntityEntry dependentEntry, InternalEntityEntry principalEntry, IForeignKey foreignKey, Boolean setModified, Boolean fromQuery)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.NavigationFixer.KeyPropertyChanged(InternalEntityEntry entry, IProperty property, IEnumerable`1 containingPrincipalKeys, IEnumerable`1 containingForeignKeys, Object oldValue, Object newValue)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntryNotifier.KeyPropertyChanged(InternalEntityEntry entry, IProperty property, IEnumerable`1 keys, IEnumerable`1 foreignKeys, Object oldValue, Object newValue)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.ChangeDetector.DetectKeyChange(InternalEntityEntry entry, IProperty property)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.ChangeDetector.PropertyChanged(InternalEntityEntry entry, IPropertyBase propertyBase, Boolean setModified)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntryNotifier.PropertyChanged(InternalEntityEntry entry, IPropertyBase property, Boolean setModified)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.SetProperty(IPropertyBase propertyBase, Object value, Boolean isMaterialization, Boolean setModified, Boolean isCascadeDelete, CurrentValueType valueType)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.SetStoreGeneratedValue(IProperty property, Object value)
   at Microsoft.EntityFrameworkCore.Update.ColumnModification.set_Value(Object value)
   at Microsoft.EntityFrameworkCore.Update.ModificationCommand.PropagateResults(ValueBuffer valueBuffer)
   at Microsoft.EntityFrameworkCore.Update.AffectedCountModificationCommandBatch.ConsumeResultSetWithPropagation(Int32 commandIndex, RelationalDataReader reader)
   at Microsoft.EntityFrameworkCore.Update.AffectedCountModificationCommandBatch.Consume(RelationalDataReader reader)
   --- End of inner exception stack trace ---
   at Microsoft.EntityFrameworkCore.Update.AffectedCountModificationCommandBatch.Consume(RelationalDataReader reader)
   at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.Execute(IRelationalConnection connection)
   at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.Execute(IEnumerable`1 commandBatches, IRelationalConnection connection)
   at Microsoft.EntityFrameworkCore.Storage.RelationalDatabase.SaveChanges(IList`1 entries)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChanges(IList`1 entriesToSave)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChanges(StateManager stateManager, Boolean acceptAllChangesOnSuccess)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.<>c.<SaveChanges>b__104_0(DbContext _, ValueTuple`2 t)
   at Microsoft.EntityFrameworkCore.Storage.NonRetryingExecutionStrategy.Execute[TState,TResult](TState state, Func`3 operation, Func`3 verifySucceeded)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChanges(Boolean acceptAllChangesOnSuccess)
   at Microsoft.EntityFrameworkCore.DbContext.SaveChanges(Boolean acceptAllChangesOnSuccess)
   at Microsoft.EntityFrameworkCore.DbContext.SaveChanges()

I've dug around to see what might have changed between 5 and 6 and am pretty confident the change in behaviour was caused by the fix for #22603. I also note that this is the same sort of model as #26629 (which prevented us updating to 6.0.0).

EF Core version: 6.0.2
Database provider: I've tried Microsoft.EntityFrameworkCore.SqlServer and Microsoft.EntityFrameworkCore.Sqlite
Target framework: .NET 6.0
Operating system: macOS

@ajcvickers
Copy link
Member

@csmager I can't reproduce this--see my code below. Please attach a small, runnable project or post a small, runnable code listing that reproduces what you are seeing so that we can investigate.

@AndriySvyryd With the code below I get the following model and change tracker state. The update pipeline then attempts to insert null. Any idea what is happening here?

Model: 
  EntityType: Foo
    Properties: 
      Id (int) Required PK FK Index AfterSave:Throw ValueGenerated.OnAdd
      CurrentPeriodNumber (int?) FK Index
    Navigations: 
      CurrentPeriod (FooPeriod) ToPrincipal FooPeriod
      Periods (ICollection<FooPeriod>) Collection ToDependent FooPeriod Inverse: Foo
    Keys: 
      Id PK
    Foreign keys: 
      Foo {'Id', 'CurrentPeriodNumber'} -> FooPeriod {'FooId', 'PeriodNumber'} Unique ToPrincipal: CurrentPeriod ClientSetNull
    Indexes: 
      Id, CurrentPeriodNumber  Unique
  EntityType: FooPeriod
    Properties: 
      FooId (int) Required PK FK AfterSave:Throw
      PeriodNumber (int) Required PK AfterSave:Throw
    Navigations: 
      Foo (Foo) ToPrincipal Foo Inverse: Periods
    Keys: 
      FooId, PeriodNumber PK
    Foreign keys: 
      FooPeriod {'FooId'} -> Foo {'Id'} ToDependent: Periods ToPrincipal: Foo Cascade
Foo {Id: 0} Added
  Id: 0 PK FK
  CurrentPeriodNumber: <null> FK
  CurrentPeriod: <null>
  Periods: [{FooId: 0, PeriodNumber: 1}]
FooPeriod {FooId: 0, PeriodNumber: 1} Added
  FooId: 0 PK FK
  PeriodNumber: 1 PK
  Foo: {Id: 0}
#nullable enable

public class Foo
{
    public int Id { get; set; }
    public int? CurrentPeriodNumber { get; set; }
    public FooPeriod? CurrentPeriod { get; set; }
    public ICollection<FooPeriod> Periods { get; } = new HashSet<FooPeriod>();
}

public class FooPeriod
{
    public int FooId { get; set; }
    public int PeriodNumber { get; set; }
    public Foo? Foo { get; set; }
}

public class SomeDbContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder
            .UseSqlServer(Your.ConnectionString)
            .LogTo(Console.WriteLine, LogLevel.Information)
            .EnableSensitiveDataLogging();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Foo>(entity =>
        {
            entity.HasKey(x => x.Id);
            entity.Property(x => x.Id)
                .ValueGeneratedOnAdd();
            entity.HasOne(x => x.CurrentPeriod)
                .WithOne()
                .HasForeignKey<Foo>(x => new { x.Id, x.CurrentPeriodNumber });
        });

        modelBuilder.Entity<FooPeriod>(entity =>
        {
            entity.HasKey(x => new { x.FooId, x.PeriodNumber });
            entity.HasOne(x => x.Foo)
                .WithMany(x => x.Periods)
                .HasForeignKey(x => x.FooId);
        });
    }
}

public class Program
{
    public static void Main()
    {
        using (var context = new SomeDbContext())
        {
            context.Database.EnsureDeleted();
            context.Database.EnsureCreated();

            var period = new FooPeriod
            {
                PeriodNumber = 1,
                Foo = new Foo()
            };

            context.Add(period);
            context.SaveChanges();
        }
    }
}
fail: 2/21/2022 11:56:46.224 RelationalEventId.CommandError[20102] (Microsoft.EntityFrameworkCore.Database.Command)
      Failed executing DbCommand (24ms) [Parameters=[@p0=NULL (DbType = Int32)], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      DECLARE @inserted0 TABLE ([Id] int);
      INSERT INTO [Foo] ([CurrentPeriodNumber])
      OUTPUT INSERTED.[Id]
      INTO @inserted0
      VALUES (@p0);
      SELECT [i].[Id] FROM @inserted0 i;
fail: 2/21/2022 11:56:46.245 CoreEventId.SaveChangesFailed[10000] (Microsoft.EntityFrameworkCore.Update)
      An exception occurred in the database while saving changes for context type 'SomeDbContext'.
      Microsoft.EntityFrameworkCore.DbUpdateException: An error occurred while saving the entity changes. See the inner exception for details.
       ---> Microsoft.Data.SqlClient.SqlException (0x80131904): Cannot insert the value NULL into column 'Id', table 'SixOh.dbo.Foo'; column does not allow nulls. INSERT fails.
      The statement has been terminated.
         at Microsoft.Data.SqlClient.SqlConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)
         at Microsoft.Data.SqlClient.SqlInternalConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)
         at Microsoft.Data.SqlClient.TdsParser.ThrowExceptionAndWarning(TdsParserStateObject stateObj, Boolean callerHasConnectionLock, Boolean asyncClose)
         at Microsoft.Data.SqlClient.TdsParser.TryRun(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj, Boolean& dataReady)
         at Microsoft.Data.SqlClient.SqlDataReader.TryConsumeMetaData()
         at Microsoft.Data.SqlClient.SqlDataReader.get_MetaData()
         at Microsoft.Data.SqlClient.SqlCommand.FinishExecuteReader(SqlDataReader ds, RunBehavior runBehavior, String resetOptionsString, Boolean isInternal, Boolean forDescribeParameterEncryption, Boolean shouldCacheForAlwaysEncrypted)
         at Microsoft.Data.SqlClient.SqlCommand.RunExecuteReaderTds(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, Boolean isAsync, Int32 timeout, Task& task, Boolean asyncWrite, Boolean inRetry, SqlDataReader ds, Boolean describeParameterEncryptionRequest)
         at Microsoft.Data.SqlClient.SqlCommand.RunExecuteReader(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, TaskCompletionSource`1 completion, Int32 timeout, Task& task, Boolean& usedCache, Boolean asyncWrite, Boolean inRetry, String method)
         at Microsoft.Data.SqlClient.SqlCommand.RunExecuteReader(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, String method)
         at Microsoft.Data.SqlClient.SqlCommand.ExecuteReader(CommandBehavior behavior)
         at Microsoft.Data.SqlClient.SqlCommand.ExecuteDbDataReader(CommandBehavior behavior)
         at System.Data.Common.DbCommand.ExecuteReader()
         at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReader(RelationalCommandParameterObject parameterObject)
         at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.Execute(IRelationalConnection connection)
      ClientConnectionId:1c4cbf7d-e825-4b2e-b16c-ccfe8932e760
      Error Number:515,State:2,Class:16
         --- End of inner exception stack trace ---
         at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.Execute(IRelationalConnection connection)
         at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.Execute(IEnumerable`1 commandBatches, IRelationalConnection connection)
         at Microsoft.EntityFrameworkCore.Storage.RelationalDatabase.SaveChanges(IList`1 entries)
         at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChanges(IList`1 entriesToSave)
         at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChanges(StateManager stateManager, Boolean acceptAllChangesOnSuccess)
         at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.<>c.<SaveChanges>b__104_0(DbContext _, ValueTuple`2 t)
         at Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal.SqlServerExecutionStrategy.Execute[TState,TResult](TState state, Func`3 operation, Func`3 verifySucceeded)
         at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChanges(Boolean acceptAllChangesOnSuccess)
         at Microsoft.EntityFrameworkCore.DbContext.SaveChanges(Boolean acceptAllChangesOnSuccess)
Unhandled exception. Microsoft.EntityFrameworkCore.DbUpdateException: An error occurred while saving the entity changes. See the inner exception for details.
 ---> Microsoft.Data.SqlClient.SqlException (0x80131904): Cannot insert the value NULL into column 'Id', table 'SixOh.dbo.Foo'; column does not allow nulls. INSERT fails.
The statement has been terminated.
   at Microsoft.Data.SqlClient.SqlConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)
   at Microsoft.Data.SqlClient.SqlInternalConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)
   at Microsoft.Data.SqlClient.TdsParser.ThrowExceptionAndWarning(TdsParserStateObject stateObj, Boolean callerHasConnectionLock, Boolean asyncClose)
   at Microsoft.Data.SqlClient.TdsParser.TryRun(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj, Boolean& dataReady)
   at Microsoft.Data.SqlClient.SqlDataReader.TryConsumeMetaData()
   at Microsoft.Data.SqlClient.SqlDataReader.get_MetaData()
   at Microsoft.Data.SqlClient.SqlCommand.FinishExecuteReader(SqlDataReader ds, RunBehavior runBehavior, String resetOptionsString, Boolean isInternal, Boolean forDescribeParameterEncryption, Boolean shouldCacheForAlwaysEncrypted)
   at Microsoft.Data.SqlClient.SqlCommand.RunExecuteReaderTds(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, Boolean isAsync, Int32 timeout, Task& task, Boolean asyncWrite, Boolean inRetry, SqlDataReader ds, Boolean describeParameterEncryptionRequest)
   at Microsoft.Data.SqlClient.SqlCommand.RunExecuteReader(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, TaskCompletionSource`1 completion, Int32 timeout, Task& task, Boolean& usedCache, Boolean asyncWrite, Boolean inRetry, String method)
   at Microsoft.Data.SqlClient.SqlCommand.RunExecuteReader(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, String method)
   at Microsoft.Data.SqlClient.SqlCommand.ExecuteReader(CommandBehavior behavior)
   at Microsoft.Data.SqlClient.SqlCommand.ExecuteDbDataReader(CommandBehavior behavior)
   at System.Data.Common.DbCommand.ExecuteReader()
   at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReader(RelationalCommandParameterObject parameterObject)
   at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.Execute(IRelationalConnection connection)
ClientConnectionId:1c4cbf7d-e825-4b2e-b16c-ccfe8932e760
Error Number:515,State:2,Class:16
   --- End of inner exception stack trace ---
   at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.Execute(IRelationalConnection connection)
   at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.Execute(IEnumerable`1 commandBatches, IRelationalConnection connection)
   at Microsoft.EntityFrameworkCore.Storage.RelationalDatabase.SaveChanges(IList`1 entries)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChanges(IList`1 entriesToSave)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChanges(StateManager stateManager, Boolean acceptAllChangesOnSuccess)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.<>c.<SaveChanges>b__104_0(DbContext _, ValueTuple`2 t)
   at Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal.SqlServerExecutionStrategy.Execute[TState,TResult](TState state, Func`3 operation, Func`3 verifySucceeded)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChanges(Boolean acceptAllChangesOnSuccess)
   at Microsoft.EntityFrameworkCore.DbContext.SaveChanges(Boolean acceptAllChangesOnSuccess)
   at Microsoft.EntityFrameworkCore.DbContext.SaveChanges()
   at Program.Main() in C:\local\code\AllTogetherNow\SixOh\Program.cs:line 134

@csmager
Copy link
Author

csmager commented Feb 21, 2022

Hi @ajcvickers - apologies, the above generates the exception I pasted when I use SQLite. If I swap to SQL Server I do get the same error you've pasted above - in my brief test before logging I hadn't noticed the exception was different.

To get SQL Server to generate the error I referred to, the configuration for Foo.Id has to include UseIdentityColumn:

entity.Property(x => x.Id)
    .ValueGeneratedOnAdd()
    .UseIdentityColumn();

@AndriySvyryd AndriySvyryd self-assigned this Feb 22, 2022
@AndriySvyryd AndriySvyryd added this to the 6.0.x milestone Feb 22, 2022
AndriySvyryd added a commit that referenced this issue Mar 6, 2022
Don't throw for unknown values that won't be sent to the database

Fixes #27455
@AndriySvyryd AndriySvyryd modified the milestones: 6.0.x, 6.0.4 Mar 8, 2022
@AndriySvyryd AndriySvyryd added the closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. label Mar 8, 2022
@AndriySvyryd AndriySvyryd removed their assignment Mar 8, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-save-changes closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. customer-reported regression Servicing-approved type-bug
Projects
None yet
Development

No branches or pull requests

3 participants