AutoMapper/AutoMapper.Collection.EFCore

Swapping property values between elements with a unique constraint results in unique constraint violation

conreaux opened this issue · 0 comments

When entities in a collection contain a property with a unique constraint, and a "swap" is attempted between those property values, the result is a unique constraint error.

I created a test case to demonstrate. It works without issue when no unique constraint is present.
But when the unique constraint is placed on Title, the error occurs. I assume this is because the
first row to be updated violates the unique constraint due to the second row's values.

Is there a recommended solution for this situation?

Also, it was necessary to modify the AutoMapper.Collection.EntityFrameworkCore.Tests to use SQLite in-memory database
to add the following constraint to DbContextBase:

protected override void OnModelCreating(ModelBuilder builder)
{
    builder.Entity<Thing>()
        .HasIndex(e => e.Title)
        .IsUnique();
}

The test case and exception message are shown below:

[Fact]
public async Task Persist_InsertOrUpdateAsync_WhenTitlesSwapped_ThenEntitiesShouldHaveExpectedTitles()
{
    // Arrange [mapper and db fields set in ctor]
    db.Things.Add(new Thing { Title = "Test1" });
    db.Things.Add(new Thing { Title = "Test2" });
    await db.SaveChangesAsync();

    var item1 = await db.Things.Where(x => x.Title == "Test1").FirstAsync();
    var item2 = await db.Things.Where(x => x.Title == "Test2").FirstAsync();

    // Act
    var p = db.Things.Persist(mapper);
    var update1 = await p.InsertOrUpdateAsync(new ThingDto { ID = item1.ID, Title = "Test2" });  // Test1 => Test2
    var update2 = await p.InsertOrUpdateAsync(new ThingDto { ID = item2.ID, Title = "Test1" });  // Test2 => Test1
    await db.SaveChangesAsync();

    // Assert
    (await db.Things.CountAsync()).Should().Be(2);
    (await db.Things.FirstOrDefaultAsync(x => x.ID == item1.ID))?.Title.Should().Be("Test2");
    (await db.Things.FirstOrDefaultAsync(x => x.ID == item2.ID))?.Title.Should().Be("Test1");
}
AutoMapper.Collection.EntityFrameworkCore.Tests.EntityFrameworkCoreUsingCtorTests.Persist_InsertOrUpdateAsync_NonUniqueThings
   Source: EntityFrameworkCoreTestsBase.cs line 448
   Duration: 968 ms

  Message: 
Microsoft.EntityFrameworkCore.DbUpdateException : An error occurred while saving the entity changes. See the inner exception for details.
---- Microsoft.Data.Sqlite.SqliteException : SQLite Error 19: 'UNIQUE constraint failed: Things.Title'.

  Stack Trace: 
ReaderModificationCommandBatch.ExecuteAsync(IRelationalConnection connection, CancellationToken cancellationToken)
BatchExecutor.ExecuteAsync(IEnumerable`1 commandBatches, IRelationalConnection connection, CancellationToken cancellationToken)
BatchExecutor.ExecuteAsync(IEnumerable`1 commandBatches, IRelationalConnection connection, CancellationToken cancellationToken)
BatchExecutor.ExecuteAsync(IEnumerable`1 commandBatches, IRelationalConnection connection, CancellationToken cancellationToken)
StateManager.SaveChangesAsync(IList`1 entriesToSave, CancellationToken cancellationToken)
StateManager.SaveChangesAsync(StateManager stateManager, Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
DbContext.SaveChangesAsync(Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
DbContext.SaveChangesAsync(Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
EntityFramworkCoreTestsBase.Persist_InsertOrUpdateAsync_NonUniqueThings() line 453
--- End of stack trace from previous location ---
----- Inner Stack Trace -----
SqliteException.ThrowExceptionForRC(Int32 rc, sqlite3 db)
SqliteDataReader.NextResult()
SqliteCommand.ExecuteReader(CommandBehavior behavior)
SqliteCommand.ExecuteReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken)
SqliteCommand.ExecuteDbDataReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken)
RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
ReaderModificationCommandBatch.ExecuteAsync(IRelationalConnection connection, CancellationToken cancellationToken)