diff --git a/src/Microsoft.EntityFrameworkCore.Relational/Update/Internal/CommandBatchPreparer.cs b/src/Microsoft.EntityFrameworkCore.Relational/Update/Internal/CommandBatchPreparer.cs index 751b5dbbcfa..e1141deca47 100644 --- a/src/Microsoft.EntityFrameworkCore.Relational/Update/Internal/CommandBatchPreparer.cs +++ b/src/Microsoft.EntityFrameworkCore.Relational/Update/Internal/CommandBatchPreparer.cs @@ -76,7 +76,7 @@ protected virtual IEnumerable CreateModificationCommands( command.AddEntry(e); return command; - }); + }).Where(c => c.EntityState != EntityState.Modified || c.ColumnModifications.Any(m => m.IsWrite)); } // To avoid violating store constraints the modification commands must be sorted diff --git a/src/Microsoft.EntityFrameworkCore.Relational/Update/UpdateSqlGenerator.cs b/src/Microsoft.EntityFrameworkCore.Relational/Update/UpdateSqlGenerator.cs index 3a52f0a0619..43044d09e2b 100644 --- a/src/Microsoft.EntityFrameworkCore.Relational/Update/UpdateSqlGenerator.cs +++ b/src/Microsoft.EntityFrameworkCore.Relational/Update/UpdateSqlGenerator.cs @@ -108,7 +108,7 @@ protected virtual void AppendUpdateCommand( { Check.NotNull(commandStringBuilder, nameof(commandStringBuilder)); Check.NotEmpty(name, nameof(name)); - Check.NotNull(writeOperations, nameof(writeOperations)); + Check.NotEmpty(writeOperations, nameof(writeOperations)); Check.NotNull(conditionOperations, nameof(conditionOperations)); AppendUpdateCommandHeader(commandStringBuilder, name, schema, writeOperations); diff --git a/test/Microsoft.EntityFrameworkCore.FunctionalTests/OptimisticConcurrencyTestBase.cs b/test/Microsoft.EntityFrameworkCore.FunctionalTests/OptimisticConcurrencyTestBase.cs index d8bf582e6ff..7fe58670cd5 100644 --- a/test/Microsoft.EntityFrameworkCore.FunctionalTests/OptimisticConcurrencyTestBase.cs +++ b/test/Microsoft.EntityFrameworkCore.FunctionalTests/OptimisticConcurrencyTestBase.cs @@ -10,7 +10,6 @@ using Microsoft.EntityFrameworkCore.FunctionalTests.TestModels.ConcurrencyModel; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Update; using Xunit; namespace Microsoft.EntityFrameworkCore.FunctionalTests @@ -125,6 +124,39 @@ public abstract class OptimisticConcurrencyTestBase : ICla where TTestStore : TestStore where TFixture : F1FixtureBase, new() { + [Fact] + public virtual async Task Modifying_concurrency_token_only_is_noop() + { + byte[] firstVersion; + using (var context = CreateF1Context()) + { + var driver = context.Drivers.Single(d => d.CarNumber == 1); + Assert.NotEqual(1, driver.Version[0]); + driver.Podiums = StorePodiums; + firstVersion = driver.Version; + await context.SaveChangesAsync(); + } + + byte[] secondVersion; + using (var context = CreateF1Context()) + { + var driver = context.Drivers.Single(d => d.CarNumber == 1); + Assert.NotEqual(firstVersion, driver.Version); + Assert.Equal(StorePodiums, driver.Podiums); + + secondVersion = driver.Version; + driver.Version = firstVersion; + await context.SaveChangesAsync(); + } + + using (var validationContext = CreateF1Context()) + { + var driver = validationContext.Drivers.Single(d => d.CarNumber == 1); + Assert.Equal(secondVersion, driver.Version); + Assert.Equal(StorePodiums, driver.Podiums); + } + } + #region Concurrency resolution with FK associations [Fact] diff --git a/test/Microsoft.EntityFrameworkCore.Relational.Tests/Update/CommandBatchPreparerTest.cs b/test/Microsoft.EntityFrameworkCore.Relational.Tests/Update/CommandBatchPreparerTest.cs index 682101cfb8f..a5ca8806a25 100644 --- a/test/Microsoft.EntityFrameworkCore.Relational.Tests/Update/CommandBatchPreparerTest.cs +++ b/test/Microsoft.EntityFrameworkCore.Relational.Tests/Update/CommandBatchPreparerTest.cs @@ -148,12 +148,14 @@ public void BatchCommands_sorts_added_and_related_modified_entities() { var configuration = CreateContextServices(CreateSimpleFKModel()); var stateManager = configuration.GetRequiredService(); + var model = configuration.GetRequiredService(); var entry = stateManager.GetOrCreateEntry(new FakeEntity { Id = 42, Value = "Test" }); entry.SetEntityState(EntityState.Added); var relatedentry = stateManager.GetOrCreateEntry(new RelatedFakeEntity { Id = 42 }); relatedentry.SetEntityState(EntityState.Modified); + relatedentry.SetPropertyModified(relatedentry.EntityType.FindProperty(nameof(RelatedFakeEntity.RelatedId))); var commandBatches = CreateCommandBatchPreparer().BatchCommands(new[] { relatedentry, entry }).ToArray(); @@ -381,6 +383,7 @@ private static IModel CreateSimpleFKModel() b.HasOne() .WithOne() .HasForeignKey(c => c.Id); + b.Property(c => c.RelatedId); }); return modelBuilder.Model; diff --git a/test/Microsoft.EntityFrameworkCore.Sqlite.FunctionalTests/OptimisticConcurrencySqliteTest.cs b/test/Microsoft.EntityFrameworkCore.Sqlite.FunctionalTests/OptimisticConcurrencySqliteTest.cs index 89bd516ecc6..b8a4f0ac538 100644 --- a/test/Microsoft.EntityFrameworkCore.Sqlite.FunctionalTests/OptimisticConcurrencySqliteTest.cs +++ b/test/Microsoft.EntityFrameworkCore.Sqlite.FunctionalTests/OptimisticConcurrencySqliteTest.cs @@ -16,6 +16,7 @@ public OptimisticConcurrencySqliteTest(F1SqliteFixture fixture) // Override failing tests because SQLite does not allow store-generated row versions. // Row version behavior could be imitated on SQLite. See Issue #2195 // TODO move these tests into the testing just for SqlServer since they don't apply to SQLite + public override Task Modifying_concurrency_token_only_is_noop() => Task.FromResult(true); public override Task Simple_concurrency_exception_can_be_resolved_with_store_values() => Task.FromResult(true); public override Task Simple_concurrency_exception_can_be_resolved_with_client_values() => Task.FromResult(true); public override Task Simple_concurrency_exception_can_be_resolved_with_new_values() => Task.FromResult(true);