From b07b1eb093f83afcd695004c815b7edf48fc139f Mon Sep 17 00:00:00 2001 From: Pawel Gerr Date: Sun, 4 Dec 2022 16:03:17 +0100 Subject: [PATCH] Fixed BulkUpdate/BulkInsertOrUpdate for entities with auto increment. --- .../SqlServerBulkInsertOrUpdateOptions.cs | 135 +++++++++--------- .../SqlServerBulkUpdateOptions.cs | 7 +- .../BulkInsertOrUpdateContext.cs | 13 +- .../BulkOperations/BulkUpdateContext.cs | 13 +- .../BulkOperations/SqliteBulkInsertOptions.cs | 8 +- .../SqliteBulkInsertOrUpdateOptions.cs | 21 ++- .../SqliteBulkOperationExecutor.cs | 11 +- .../BulkOperations/SqliteBulkUpdateOptions.cs | 21 ++- .../BulkOperations/SqliteCommandBuilder.cs | 2 +- .../BulkInsertOrUpdateAsync.cs | 54 +++++-- .../BulkUpdateAsync.cs | 58 ++++++++ .../BulkInsertOrUpdateAsync.cs | 37 +++-- .../BulkUpdateAsync.cs | 58 ++++++++ 13 files changed, 317 insertions(+), 121 deletions(-) diff --git a/src/Thinktecture.EntityFrameworkCore.SqlServer/EntityFrameworkCore/BulkOperations/SqlServerBulkInsertOrUpdateOptions.cs b/src/Thinktecture.EntityFrameworkCore.SqlServer/EntityFrameworkCore/BulkOperations/SqlServerBulkInsertOrUpdateOptions.cs index bfd9590e..638fd580 100644 --- a/src/Thinktecture.EntityFrameworkCore.SqlServer/EntityFrameworkCore/BulkOperations/SqlServerBulkInsertOrUpdateOptions.cs +++ b/src/Thinktecture.EntityFrameworkCore.SqlServer/EntityFrameworkCore/BulkOperations/SqlServerBulkInsertOrUpdateOptions.cs @@ -1,65 +1,70 @@ -namespace Thinktecture.EntityFrameworkCore.BulkOperations; - -/// -/// Bulk insert or update options for SQL Server. -/// -public sealed class SqlServerBulkInsertOrUpdateOptions : ISqlServerMergeOperationOptions, IBulkInsertOrUpdateOptions -{ - /// - public IEntityPropertiesProvider? PropertiesToInsert { get; set; } - - /// - public IEntityPropertiesProvider? PropertiesToUpdate { get; set; } - - /// - public IEntityPropertiesProvider? KeyProperties { get; set; } - - /// - public List MergeTableHints { get; } - - /// - public SqlServerBulkOperationTempTableOptions TempTableOptions { get; } - - /// - /// Initializes new instance of . - /// - /// Options to initialize from. - public SqlServerBulkInsertOrUpdateOptions(IBulkInsertOrUpdateOptions? optionsToInitializeFrom = null) - { - if (optionsToInitializeFrom is not null) - { - PropertiesToInsert = optionsToInitializeFrom.PropertiesToInsert; - PropertiesToUpdate = optionsToInitializeFrom.PropertiesToUpdate; - KeyProperties = optionsToInitializeFrom.KeyProperties; - } - - if (optionsToInitializeFrom is ISqlServerMergeOperationOptions mergeOptions) - { - TempTableOptions = new SqlServerBulkOperationTempTableOptions(mergeOptions.TempTableOptions); - MergeTableHints = mergeOptions.MergeTableHints.ToList(); - } - else - { - TempTableOptions = new SqlServerBulkOperationTempTableOptions(); - MergeTableHints = new List { SqlServerTableHintLimited.HoldLock }; - } - } - - /// - /// Gets the options for bulk insert into a temp table. - /// - public SqlServerTempTableBulkOperationOptions GetTempTableBulkInsertOptions() - { - var options = new SqlServerTempTableBulkInsertOptions - { - PropertiesToInsert = PropertiesToInsert is null || PropertiesToUpdate is null - ? null - : CompositeTempTableEntityPropertiesProvider.CreateForInsertOrUpdate(PropertiesToInsert, PropertiesToUpdate, KeyProperties), - Advanced = { UsePropertiesToInsertForTempTableCreation = true } - }; - - TempTableOptions.Populate(options); - - return options; - } -} +using Microsoft.Data.SqlClient; + +namespace Thinktecture.EntityFrameworkCore.BulkOperations; + +/// +/// Bulk insert or update options for SQL Server. +/// +public sealed class SqlServerBulkInsertOrUpdateOptions : ISqlServerMergeOperationOptions, IBulkInsertOrUpdateOptions +{ + /// + public IEntityPropertiesProvider? PropertiesToInsert { get; set; } + + /// + public IEntityPropertiesProvider? PropertiesToUpdate { get; set; } + + /// + public IEntityPropertiesProvider? KeyProperties { get; set; } + + /// + public List MergeTableHints { get; } + + /// + public SqlServerBulkOperationTempTableOptions TempTableOptions { get; } + + /// + /// Initializes new instance of . + /// + /// Options to initialize from. + public SqlServerBulkInsertOrUpdateOptions(IBulkInsertOrUpdateOptions? optionsToInitializeFrom = null) + { + if (optionsToInitializeFrom is not null) + { + PropertiesToInsert = optionsToInitializeFrom.PropertiesToInsert; + PropertiesToUpdate = optionsToInitializeFrom.PropertiesToUpdate; + KeyProperties = optionsToInitializeFrom.KeyProperties; + } + + if (optionsToInitializeFrom is ISqlServerMergeOperationOptions mergeOptions) + { + TempTableOptions = new SqlServerBulkOperationTempTableOptions(mergeOptions.TempTableOptions); + MergeTableHints = mergeOptions.MergeTableHints.ToList(); + } + else + { + TempTableOptions = new SqlServerBulkOperationTempTableOptions + { + SqlBulkCopyOptions = SqlBulkCopyOptions.KeepIdentity + }; + MergeTableHints = new List { SqlServerTableHintLimited.HoldLock }; + } + } + + /// + /// Gets the options for bulk insert into a temp table. + /// + public SqlServerTempTableBulkOperationOptions GetTempTableBulkInsertOptions() + { + var options = new SqlServerTempTableBulkInsertOptions + { + PropertiesToInsert = PropertiesToInsert is null || PropertiesToUpdate is null + ? null + : CompositeTempTableEntityPropertiesProvider.CreateForInsertOrUpdate(PropertiesToInsert, PropertiesToUpdate, KeyProperties), + Advanced = { UsePropertiesToInsertForTempTableCreation = true } + }; + + TempTableOptions.Populate(options); + + return options; + } +} diff --git a/src/Thinktecture.EntityFrameworkCore.SqlServer/EntityFrameworkCore/BulkOperations/SqlServerBulkUpdateOptions.cs b/src/Thinktecture.EntityFrameworkCore.SqlServer/EntityFrameworkCore/BulkOperations/SqlServerBulkUpdateOptions.cs index 328b8ef1..4c20df4f 100644 --- a/src/Thinktecture.EntityFrameworkCore.SqlServer/EntityFrameworkCore/BulkOperations/SqlServerBulkUpdateOptions.cs +++ b/src/Thinktecture.EntityFrameworkCore.SqlServer/EntityFrameworkCore/BulkOperations/SqlServerBulkUpdateOptions.cs @@ -1,3 +1,5 @@ +using Microsoft.Data.SqlClient; + namespace Thinktecture.EntityFrameworkCore.BulkOperations; /// @@ -36,7 +38,10 @@ public SqlServerBulkUpdateOptions(IBulkUpdateOptions? optionsToInitializeFrom = } else { - TempTableOptions = new SqlServerBulkOperationTempTableOptions(); + TempTableOptions = new SqlServerBulkOperationTempTableOptions + { + SqlBulkCopyOptions = SqlBulkCopyOptions.KeepIdentity + }; MergeTableHints = new List { SqlServerTableHintLimited.HoldLock }; } } diff --git a/src/Thinktecture.EntityFrameworkCore.Sqlite/EntityFrameworkCore/BulkOperations/BulkInsertOrUpdateContext.cs b/src/Thinktecture.EntityFrameworkCore.Sqlite/EntityFrameworkCore/BulkOperations/BulkInsertOrUpdateContext.cs index 36214623..5ec63767 100644 --- a/src/Thinktecture.EntityFrameworkCore.Sqlite/EntityFrameworkCore/BulkOperations/BulkInsertOrUpdateContext.cs +++ b/src/Thinktecture.EntityFrameworkCore.Sqlite/EntityFrameworkCore/BulkOperations/BulkInsertOrUpdateContext.cs @@ -23,7 +23,7 @@ public IEntityDataReader CreateReader(IEnumerable entities) return _readerFactory.Create(_ctx, entities, Properties, HasExternalProperties); } - public SqliteAutoIncrementBehavior AutoIncrementBehavior => SqliteAutoIncrementBehavior.KeepValueAsIs; + public SqliteAutoIncrementBehavior AutoIncrementBehavior { get; } public SqliteConnection Connection { get; } public BulkInsertOrUpdateContext( @@ -32,12 +32,14 @@ public BulkInsertOrUpdateContext( SqliteConnection connection, IReadOnlyList keyProperties, IReadOnlyList propertiesToInsert, - IReadOnlyList propertiesForUpdate) + IReadOnlyList propertiesForUpdate, + SqliteAutoIncrementBehavior autoIncrementBehavior) { _ctx = ctx; _readerFactory = factory; Connection = connection; _keyProperties = keyProperties; + AutoIncrementBehavior = autoIncrementBehavior; var (ownPropertiesToInsert, externalPropertiesToInsert) = propertiesToInsert.SeparateProperties(); _propertiesToInsert = ownPropertiesToInsert; @@ -75,7 +77,7 @@ public IReadOnlyList GetChildren(IReadOnly propertiesToUpdateData.Remove(propertiesToUpdateTuple); - var ownedTypeCtx = new OwnedTypeBulkInsertOrUpdateContext(_ctx, _readerFactory, Connection, propertiesToInsert, propertiesToUpdate, navigation.TargetEntityType, ownedEntities); + var ownedTypeCtx = new OwnedTypeBulkInsertOrUpdateContext(_ctx, _readerFactory, Connection, propertiesToInsert, propertiesToUpdate, navigation.TargetEntityType, ownedEntities, AutoIncrementBehavior); childCtx.Add(ownedTypeCtx); } @@ -100,8 +102,9 @@ public OwnedTypeBulkInsertOrUpdateContext( IReadOnlyList propertiesToInsert, IReadOnlyList propertiesToUpdate, IEntityType entityType, - IEnumerable entities) - : base(ctx, factory, sqlCon, GetKeyProperties(entityType), propertiesToInsert, propertiesToUpdate) + IEnumerable entities, + SqliteAutoIncrementBehavior autoIncrementBehavior) + : base(ctx, factory, sqlCon, GetKeyProperties(entityType), propertiesToInsert, propertiesToUpdate, autoIncrementBehavior) { EntityType = entityType; Entities = entities; diff --git a/src/Thinktecture.EntityFrameworkCore.Sqlite/EntityFrameworkCore/BulkOperations/BulkUpdateContext.cs b/src/Thinktecture.EntityFrameworkCore.Sqlite/EntityFrameworkCore/BulkOperations/BulkUpdateContext.cs index 66c9ebf0..bd95fa57 100644 --- a/src/Thinktecture.EntityFrameworkCore.Sqlite/EntityFrameworkCore/BulkOperations/BulkUpdateContext.cs +++ b/src/Thinktecture.EntityFrameworkCore.Sqlite/EntityFrameworkCore/BulkOperations/BulkUpdateContext.cs @@ -23,14 +23,15 @@ public IEntityDataReader CreateReader(IEnumerable entities) return _readerFactory.Create(_ctx, entities, Properties, HasExternalProperties); } - public SqliteAutoIncrementBehavior AutoIncrementBehavior => SqliteAutoIncrementBehavior.KeepValueAsIs; + public SqliteAutoIncrementBehavior AutoIncrementBehavior { get; } public BulkUpdateContext( DbContext ctx, IEntityDataReaderFactory factory, SqliteConnection connection, IReadOnlyList keyProperties, - IReadOnlyList propertiesForUpdate) + IReadOnlyList propertiesForUpdate, + SqliteAutoIncrementBehavior autoIncrementBehavior) { _ctx = ctx; _readerFactory = factory; @@ -40,6 +41,7 @@ public BulkUpdateContext( _propertiesToUpdate = ownProperties; _externalProperties = externalProperties; Properties = ownProperties.Union(keyProperties).ToList(); + AutoIncrementBehavior = autoIncrementBehavior; } /// @@ -58,7 +60,7 @@ public IReadOnlyList GetChildren(IReadOnly foreach (var (navigation, ownedEntities, properties) in _externalProperties.GroupExternalProperties(entities)) { - var ownedTypeCtx = new OwnedTypeBulkUpdateContext(_ctx, _readerFactory, Connection, properties, navigation.TargetEntityType, ownedEntities); + var ownedTypeCtx = new OwnedTypeBulkUpdateContext(_ctx, _readerFactory, Connection, properties, navigation.TargetEntityType, ownedEntities, AutoIncrementBehavior); childCtx.Add(ownedTypeCtx); } @@ -76,8 +78,9 @@ public OwnedTypeBulkUpdateContext( SqliteConnection sqlCon, IReadOnlyList properties, IEntityType entityType, - IEnumerable entities) - : base(ctx, factory, sqlCon, GetKeyProperties(entityType), properties) + IEnumerable entities, + SqliteAutoIncrementBehavior autoIncrementBehavior) + : base(ctx, factory, sqlCon, GetKeyProperties(entityType), properties, autoIncrementBehavior) { EntityType = entityType; Entities = entities; diff --git a/src/Thinktecture.EntityFrameworkCore.Sqlite/EntityFrameworkCore/BulkOperations/SqliteBulkInsertOptions.cs b/src/Thinktecture.EntityFrameworkCore.Sqlite/EntityFrameworkCore/BulkOperations/SqliteBulkInsertOptions.cs index c0f233f6..0e7a73df 100644 --- a/src/Thinktecture.EntityFrameworkCore.Sqlite/EntityFrameworkCore/BulkOperations/SqliteBulkInsertOptions.cs +++ b/src/Thinktecture.EntityFrameworkCore.Sqlite/EntityFrameworkCore/BulkOperations/SqliteBulkInsertOptions.cs @@ -20,11 +20,9 @@ public sealed class SqliteBulkInsertOptions : IBulkInsertOptions /// Options to initialize from. public SqliteBulkInsertOptions(IBulkInsertOptions? optionsToInitializeFrom = null) { - if (optionsToInitializeFrom is null) - { - AutoIncrementBehavior = SqliteAutoIncrementBehavior.SetZeroToNull; - } - else + AutoIncrementBehavior = SqliteAutoIncrementBehavior.SetZeroToNull; + + if (optionsToInitializeFrom is not null) { PropertiesToInsert = optionsToInitializeFrom.PropertiesToInsert; diff --git a/src/Thinktecture.EntityFrameworkCore.Sqlite/EntityFrameworkCore/BulkOperations/SqliteBulkInsertOrUpdateOptions.cs b/src/Thinktecture.EntityFrameworkCore.Sqlite/EntityFrameworkCore/BulkOperations/SqliteBulkInsertOrUpdateOptions.cs index d84032e7..0964d1e4 100644 --- a/src/Thinktecture.EntityFrameworkCore.Sqlite/EntityFrameworkCore/BulkOperations/SqliteBulkInsertOrUpdateOptions.cs +++ b/src/Thinktecture.EntityFrameworkCore.Sqlite/EntityFrameworkCore/BulkOperations/SqliteBulkInsertOrUpdateOptions.cs @@ -23,17 +23,16 @@ public class SqliteBulkInsertOrUpdateOptions : ISqliteBulkInsertOrUpdateOptions /// Options to initialize from. public SqliteBulkInsertOrUpdateOptions(IBulkInsertOrUpdateOptions? optionsToInitializeFrom = null) { - if (optionsToInitializeFrom is null) - { - AutoIncrementBehavior = SqliteAutoIncrementBehavior.KeepValueAsIs; - return; - } + AutoIncrementBehavior = SqliteAutoIncrementBehavior.SetZeroToNull; - PropertiesToInsert = optionsToInitializeFrom.PropertiesToInsert; - PropertiesToUpdate = optionsToInitializeFrom.PropertiesToUpdate; - KeyProperties = optionsToInitializeFrom.KeyProperties; + if (optionsToInitializeFrom is not null) + { + PropertiesToInsert = optionsToInitializeFrom.PropertiesToInsert; + PropertiesToUpdate = optionsToInitializeFrom.PropertiesToUpdate; + KeyProperties = optionsToInitializeFrom.KeyProperties; - if (optionsToInitializeFrom is ISqliteBulkInsertOrUpdateOptions sqliteOptions) - AutoIncrementBehavior = sqliteOptions.AutoIncrementBehavior; + if (optionsToInitializeFrom is ISqliteBulkInsertOrUpdateOptions sqliteOptions) + AutoIncrementBehavior = sqliteOptions.AutoIncrementBehavior; + } } -} \ No newline at end of file +} diff --git a/src/Thinktecture.EntityFrameworkCore.Sqlite/EntityFrameworkCore/BulkOperations/SqliteBulkOperationExecutor.cs b/src/Thinktecture.EntityFrameworkCore.Sqlite/EntityFrameworkCore/BulkOperations/SqliteBulkOperationExecutor.cs index bec253a1..926dce5d 100644 --- a/src/Thinktecture.EntityFrameworkCore.Sqlite/EntityFrameworkCore/BulkOperations/SqliteBulkOperationExecutor.cs +++ b/src/Thinktecture.EntityFrameworkCore.Sqlite/EntityFrameworkCore/BulkOperations/SqliteBulkOperationExecutor.cs @@ -143,11 +143,15 @@ public async Task BulkUpdateAsync( var entityType = _ctx.Model.GetEntityType(typeof(T)); + if (options is not SqliteBulkUpdateOptions sqliteOptions) + sqliteOptions = new SqliteBulkUpdateOptions(options); + var ctx = new BulkUpdateContext(_ctx, _ctx.GetService(), (SqliteConnection)_ctx.Database.GetDbConnection(), - options.KeyProperties.DetermineKeyProperties(entityType, true), - options.PropertiesToUpdate.DeterminePropertiesForUpdate(entityType, null)); + sqliteOptions.KeyProperties.DetermineKeyProperties(entityType, true), + sqliteOptions.PropertiesToUpdate.DeterminePropertiesForUpdate(entityType, null), + sqliteOptions.AutoIncrementBehavior); var tableName = entityType.GetTableName() ?? throw new Exception($"The entity '{entityType.Name}' has no table name."); @@ -173,7 +177,8 @@ public async Task BulkInsertOrUpdateAsync( (SqliteConnection)_ctx.Database.GetDbConnection(), sqliteOptions.KeyProperties.DetermineKeyProperties(entityType, true), sqliteOptions.PropertiesToInsert.DeterminePropertiesForInsert(entityType, null), - sqliteOptions.PropertiesToUpdate.DeterminePropertiesForUpdate(entityType, true)); + sqliteOptions.PropertiesToUpdate.DeterminePropertiesForUpdate(entityType, true), + sqliteOptions.AutoIncrementBehavior); var tableName = entityType.GetTableName() ?? throw new Exception($"The entity '{entityType.Name}' has no table name."); diff --git a/src/Thinktecture.EntityFrameworkCore.Sqlite/EntityFrameworkCore/BulkOperations/SqliteBulkUpdateOptions.cs b/src/Thinktecture.EntityFrameworkCore.Sqlite/EntityFrameworkCore/BulkOperations/SqliteBulkUpdateOptions.cs index 9d765794..74061594 100644 --- a/src/Thinktecture.EntityFrameworkCore.Sqlite/EntityFrameworkCore/BulkOperations/SqliteBulkUpdateOptions.cs +++ b/src/Thinktecture.EntityFrameworkCore.Sqlite/EntityFrameworkCore/BulkOperations/SqliteBulkUpdateOptions.cs @@ -11,16 +11,27 @@ public class SqliteBulkUpdateOptions : IBulkUpdateOptions /// public IEntityPropertiesProvider? KeyProperties { get; set; } + /// + /// Behavior for auto-increment columns. + /// Default is + /// + public SqliteAutoIncrementBehavior AutoIncrementBehavior { get; set; } + /// /// Initializes new instance of . /// /// Options to initialize from. public SqliteBulkUpdateOptions(IBulkUpdateOptions? optionsToInitializeFrom = null) { - if (optionsToInitializeFrom is null) - return; + AutoIncrementBehavior = SqliteAutoIncrementBehavior.SetZeroToNull; + + if (optionsToInitializeFrom is not null) + { + PropertiesToUpdate = optionsToInitializeFrom.PropertiesToUpdate; + KeyProperties = optionsToInitializeFrom.KeyProperties; - PropertiesToUpdate = optionsToInitializeFrom.PropertiesToUpdate; - KeyProperties = optionsToInitializeFrom.KeyProperties; + if (optionsToInitializeFrom is SqliteBulkUpdateOptions sqliteOptions) + AutoIncrementBehavior = sqliteOptions.AutoIncrementBehavior; + } } -} \ No newline at end of file +} diff --git a/src/Thinktecture.EntityFrameworkCore.Sqlite/EntityFrameworkCore/BulkOperations/SqliteCommandBuilder.cs b/src/Thinktecture.EntityFrameworkCore.Sqlite/EntityFrameworkCore/BulkOperations/SqliteCommandBuilder.cs index f93f6f59..16888481 100644 --- a/src/Thinktecture.EntityFrameworkCore.Sqlite/EntityFrameworkCore/BulkOperations/SqliteCommandBuilder.cs +++ b/src/Thinktecture.EntityFrameworkCore.Sqlite/EntityFrameworkCore/BulkOperations/SqliteCommandBuilder.cs @@ -222,7 +222,7 @@ public override string GetStatement( try { - GenerateInsertStatement(sb, sqlGenerationHelper, reader, tableIdentifier, _propertiesToInsert); + GenerateInsertStatement(sb, sqlGenerationHelper, reader, tableIdentifier, _propertiesToInsert.Union(_keyProperties).ToList()); sb.AppendLine() .Append("\tON CONFLICT("); diff --git a/tests/Thinktecture.EntityFrameworkCore.SqlServer.Tests/EntityFrameworkCore/BulkOperations/SqlServerBulkOperationExecutorTests/BulkInsertOrUpdateAsync.cs b/tests/Thinktecture.EntityFrameworkCore.SqlServer.Tests/EntityFrameworkCore/BulkOperations/SqlServerBulkOperationExecutorTests/BulkInsertOrUpdateAsync.cs index 7a1e9e66..69e540d1 100644 --- a/tests/Thinktecture.EntityFrameworkCore.SqlServer.Tests/EntityFrameworkCore/BulkOperations/SqlServerBulkOperationExecutorTests/BulkInsertOrUpdateAsync.cs +++ b/tests/Thinktecture.EntityFrameworkCore.SqlServer.Tests/EntityFrameworkCore/BulkOperations/SqlServerBulkOperationExecutorTests/BulkInsertOrUpdateAsync.cs @@ -1,3 +1,4 @@ +using Microsoft.Data.SqlClient; using Microsoft.EntityFrameworkCore.Infrastructure; using Thinktecture.EntityFrameworkCore.Testing; using Thinktecture.TestDatabaseContext; @@ -163,19 +164,52 @@ public async Task Should_throw_because_sqlbulkcopy_dont_support_null_for_NOT_NUL } [Fact] - public async Task Should_insert_entity_with_auto_increment_column() + public async Task Should_throw_when_trying_to_insert_entities_with_auto_increment_column_without_excluding_auto_increment_column() { - var testEntity = new TestEntityWithAutoIncrement { Name = "Name" }; - var testEntities = new[] { testEntity }; + var existingEntity = new TestEntityWithAutoIncrement { Name = "original value" }; + ArrangeDbContext.Add(existingEntity); + await ArrangeDbContext.SaveChangesAsync(); + + existingEntity.Name = "Name"; + + var newEntity = new TestEntityWithAutoIncrement { Name = "New Entity" }; + var testEntities = new[] { existingEntity, newEntity }; + + await SUT.Awaiting(sut => sut.BulkInsertOrUpdateAsync(testEntities, new SqlServerBulkInsertOrUpdateOptions())) + .Should().ThrowAsync().WithMessage("Cannot insert explicit value for identity column in table 'TestEntitiesWithAutoIncrement' when IDENTITY_INSERT is set to OFF."); + } + + [Fact] + public async Task Should_insert_and_update_entities_with_auto_increment_column_having_property_selector() + { + var existingEntity = new TestEntityWithAutoIncrement { Name = "original value" }; + ArrangeDbContext.Add(existingEntity); + await ArrangeDbContext.SaveChangesAsync(); + + existingEntity.Name = "Name"; + + var newEntity = new TestEntityWithAutoIncrement { Name = "New Entity" }; + var testEntities = new[] { existingEntity, newEntity }; - await SUT.BulkInsertOrUpdateAsync(testEntities, new SqlServerBulkInsertOrUpdateOptions - { - PropertiesToInsert = IEntityPropertiesProvider.Include(entity => entity.Name) - }); + var affectedRows = await SUT.BulkInsertOrUpdateAsync(testEntities, new SqlServerBulkInsertOrUpdateOptions + { + PropertiesToInsert = IEntityPropertiesProvider.Include(entity => entity.Name) + }); - var loadedEntity = await AssertDbContext.TestEntitiesWithAutoIncrement.FirstOrDefaultAsync(); - loadedEntity.Should().NotBeNull(); - loadedEntity!.Id.Should().NotBe(0); + affectedRows.Should().Be(2); + + var loadedEntities = await AssertDbContext.TestEntitiesWithAutoIncrement.ToListAsync(); + loadedEntities.Should().HaveCount(2); + loadedEntities.Should().AllSatisfy(e => e.Id.Should().NotBe(0)); + loadedEntities.Should().BeEquivalentTo(new[] + { + existingEntity, + new TestEntityWithAutoIncrement + { + Id = loadedEntities.Single(e => e.Id != existingEntity.Id).Id, + Name = "New Entity" + } + }); } [Fact] diff --git a/tests/Thinktecture.EntityFrameworkCore.SqlServer.Tests/EntityFrameworkCore/BulkOperations/SqlServerBulkOperationExecutorTests/BulkUpdateAsync.cs b/tests/Thinktecture.EntityFrameworkCore.SqlServer.Tests/EntityFrameworkCore/BulkOperations/SqlServerBulkOperationExecutorTests/BulkUpdateAsync.cs index 12211efa..0f6e2e3e 100644 --- a/tests/Thinktecture.EntityFrameworkCore.SqlServer.Tests/EntityFrameworkCore/BulkOperations/SqlServerBulkOperationExecutorTests/BulkUpdateAsync.cs +++ b/tests/Thinktecture.EntityFrameworkCore.SqlServer.Tests/EntityFrameworkCore/BulkOperations/SqlServerBulkOperationExecutorTests/BulkUpdateAsync.cs @@ -267,4 +267,62 @@ public async Task Should_update_specified_properties_only() }); loadedEntity.GetPrivateField().Should().Be(3); } + + [Fact] + public async Task Should_update_entity_with_auto_increment() + { + var entity = new TestEntityWithAutoIncrement { Name = "original value" }; + ArrangeDbContext.Add(entity); + await ArrangeDbContext.SaveChangesAsync(); + + entity.Name = "Name"; + + var affectedRows = await SUT.BulkUpdateAsync(new[] { entity }, + new SqlServerBulkUpdateOptions()); + + affectedRows.Should().Be(1); + + var loadedEntities = await AssertDbContext.TestEntitiesWithAutoIncrement.ToListAsync(); + loadedEntities.Should().HaveCount(1); + var loadedEntity = loadedEntities[0]; + loadedEntity.Id.Should().NotBe(0); + loadedEntity.Should().BeEquivalentTo(new TestEntityWithAutoIncrement + { + Id = loadedEntity.Id, + Name = "Name" + }); + } + + [Fact] + public async Task Should_update_entity_with_auto_increment_having_custom_property_selector() + { + var entity = new TestEntityWithAutoIncrement { Name = "original value" }; + ArrangeDbContext.Add(entity); + await ArrangeDbContext.SaveChangesAsync(); + + entity.Name = "Name"; + + var affectedRows = await SUT.BulkUpdateAsync(new[] { entity }, + new SqlServerBulkUpdateOptions + { + KeyProperties = IEntityPropertiesProvider.Include(e => e.Id), + PropertiesToUpdate = IEntityPropertiesProvider.Include(e => new + { + e.Id, + e.Name + }) + }); + + affectedRows.Should().Be(1); + + var loadedEntities = await AssertDbContext.TestEntitiesWithAutoIncrement.ToListAsync(); + loadedEntities.Should().HaveCount(1); + var loadedEntity = loadedEntities[0]; + loadedEntity.Id.Should().NotBe(0); + loadedEntity.Should().BeEquivalentTo(new TestEntityWithAutoIncrement + { + Id = entity.Id, + Name = "Name" + }); + } } diff --git a/tests/Thinktecture.EntityFrameworkCore.Sqlite.Tests/EntityFrameworkCore/BulkOperations/SqliteBulkOperationExecutorTests/BulkInsertOrUpdateAsync.cs b/tests/Thinktecture.EntityFrameworkCore.Sqlite.Tests/EntityFrameworkCore/BulkOperations/SqliteBulkOperationExecutorTests/BulkInsertOrUpdateAsync.cs index 54ea482a..7266a9b5 100644 --- a/tests/Thinktecture.EntityFrameworkCore.Sqlite.Tests/EntityFrameworkCore/BulkOperations/SqliteBulkOperationExecutorTests/BulkInsertOrUpdateAsync.cs +++ b/tests/Thinktecture.EntityFrameworkCore.Sqlite.Tests/EntityFrameworkCore/BulkOperations/SqliteBulkOperationExecutorTests/BulkInsertOrUpdateAsync.cs @@ -141,19 +141,36 @@ public async Task Should_insert_shadow_properties() } [Fact] - public async Task Should_insert_entity_with_auto_increment_column() + public async Task Should_insert_and_update_entities_with_auto_increment_column_having_property_selector() { - var testEntity = new TestEntityWithAutoIncrement { Name = "Name" }; - var testEntities = new[] { testEntity }; + var existingEntity = new TestEntityWithAutoIncrement { Name = "original value" }; + ArrangeDbContext.Add(existingEntity); + await ArrangeDbContext.SaveChangesAsync(); + + existingEntity.Name = "Name"; - await SUT.BulkInsertOrUpdateAsync(testEntities, new SqliteBulkInsertOrUpdateOptions - { - PropertiesToInsert = IEntityPropertiesProvider.Include(entity => entity.Name) - }); + var newEntity = new TestEntityWithAutoIncrement { Name = "New Entity" }; + var testEntities = new[] { existingEntity, newEntity }; + + var affectedRows = await SUT.BulkInsertOrUpdateAsync(testEntities, new SqliteBulkInsertOrUpdateOptions + { + PropertiesToInsert = IEntityPropertiesProvider.Include(entity => entity.Name) + }); + + affectedRows.Should().Be(2); - var loadedEntity = await AssertDbContext.TestEntitiesWithAutoIncrement.FirstOrDefaultAsync(); - loadedEntity.Should().NotBeNull(); - loadedEntity!.Id.Should().NotBe(0); + var loadedEntities = await AssertDbContext.TestEntitiesWithAutoIncrement.ToListAsync(); + loadedEntities.Should().HaveCount(2); + loadedEntities.Should().AllSatisfy(e => e.Id.Should().NotBe(0)); + loadedEntities.Should().BeEquivalentTo(new[] + { + existingEntity, + new TestEntityWithAutoIncrement + { + Id = loadedEntities.Single(e => e.Id != existingEntity.Id).Id, + Name = "New Entity" + } + }); } [Fact] diff --git a/tests/Thinktecture.EntityFrameworkCore.Sqlite.Tests/EntityFrameworkCore/BulkOperations/SqliteBulkOperationExecutorTests/BulkUpdateAsync.cs b/tests/Thinktecture.EntityFrameworkCore.Sqlite.Tests/EntityFrameworkCore/BulkOperations/SqliteBulkOperationExecutorTests/BulkUpdateAsync.cs index ec91172c..d3c27e00 100644 --- a/tests/Thinktecture.EntityFrameworkCore.Sqlite.Tests/EntityFrameworkCore/BulkOperations/SqliteBulkOperationExecutorTests/BulkUpdateAsync.cs +++ b/tests/Thinktecture.EntityFrameworkCore.Sqlite.Tests/EntityFrameworkCore/BulkOperations/SqliteBulkOperationExecutorTests/BulkUpdateAsync.cs @@ -276,4 +276,62 @@ public async Task Should_update_entity_without_required_fields_if_excluded() RequiredName = "RequiredName" }); } + + [Fact] + public async Task Should_update_entity_with_auto_increment() + { + var entity = new TestEntityWithAutoIncrement { Name = "original value" }; + ArrangeDbContext.Add(entity); + await ArrangeDbContext.SaveChangesAsync(); + + entity.Name = "Name"; + + var affectedRows = await SUT.BulkUpdateAsync(new[] { entity }, + new SqliteBulkUpdateOptions()); + + affectedRows.Should().Be(1); + + var loadedEntities = await AssertDbContext.TestEntitiesWithAutoIncrement.ToListAsync(); + loadedEntities.Should().HaveCount(1); + var loadedEntity = loadedEntities[0]; + loadedEntity.Id.Should().NotBe(0); + loadedEntity.Should().BeEquivalentTo(new TestEntityWithAutoIncrement + { + Id = loadedEntity.Id, + Name = "Name" + }); + } + + [Fact] + public async Task Should_update_entity_with_auto_increment_having_custom_property_selector() + { + var entity = new TestEntityWithAutoIncrement { Name = "original value" }; + ArrangeDbContext.Add(entity); + await ArrangeDbContext.SaveChangesAsync(); + + entity.Name = "Name"; + + var affectedRows = await SUT.BulkUpdateAsync(new[] { entity }, + new SqliteBulkUpdateOptions + { + KeyProperties = IEntityPropertiesProvider.Include(e => e.Id), + PropertiesToUpdate = IEntityPropertiesProvider.Include(e => new + { + e.Id, + e.Name + }) + }); + + affectedRows.Should().Be(1); + + var loadedEntities = await AssertDbContext.TestEntitiesWithAutoIncrement.ToListAsync(); + loadedEntities.Should().HaveCount(1); + var loadedEntity = loadedEntities[0]; + loadedEntity.Id.Should().NotBe(0); + loadedEntity.Should().BeEquivalentTo(new TestEntityWithAutoIncrement + { + Id = entity.Id, + Name = "Name" + }); + } }