diff --git a/src/EFCore.SqlServer/Update/Internal/SqlServerModificationCommandBatch.cs b/src/EFCore.SqlServer/Update/Internal/SqlServerModificationCommandBatch.cs
index 82fb31dd1e2..aaa5feab34d 100644
--- a/src/EFCore.SqlServer/Update/Internal/SqlServerModificationCommandBatch.cs
+++ b/src/EFCore.SqlServer/Update/Internal/SqlServerModificationCommandBatch.cs
@@ -27,6 +27,8 @@ public class SqlServerModificationCommandBatch : AffectedCountModificationComman
private static readonly bool QuirkEnabled29502
= AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue29502", out var enabled) && enabled;
+ private static readonly bool QuirkEnabled29539
+ = AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue29539", out var enabled) && enabled;
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -143,31 +145,64 @@ private void ApplyPendingBulkInsertCommands()
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
- protected override void AddCommand(IReadOnlyModificationCommand modificationCommand)
+ public override bool TryAddCommand(IReadOnlyModificationCommand modificationCommand)
{
- if (modificationCommand.EntityState == EntityState.Added && modificationCommand.StoreStoredProcedure is null)
+ if (!QuirkEnabled29539)
{
+ // If there are any pending bulk insert commands and the new command is incompatible with them (not an insert, insert into a
+ // separate table..), apply the pending commands.
if (_pendingBulkInsertCommands.Count > 0
- && !CanBeInsertedInSameStatement(_pendingBulkInsertCommands[0], modificationCommand))
+ && (modificationCommand.EntityState != EntityState.Added
+ || modificationCommand.StoreStoredProcedure is not null
+ || !CanBeInsertedInSameStatement(_pendingBulkInsertCommands[0], modificationCommand)))
{
- // The new Add command cannot be added to the pending bulk insert commands (e.g. different table).
- // Write out the pending commands before starting a new pending chain.
ApplyPendingBulkInsertCommands();
_pendingBulkInsertCommands.Clear();
}
+ }
+
+ return base.TryAddCommand(modificationCommand);
+ }
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ protected override void AddCommand(IReadOnlyModificationCommand modificationCommand)
+ {
+ // TryAddCommand above already applied any pending commands if the new command is incompatible with them.
+ // So if the new command is an insert, just append it to pending, otherwise do the regular add logic.
+ if (modificationCommand.EntityState == EntityState.Added && modificationCommand.StoreStoredProcedure is null)
+ {
+ if (QuirkEnabled29539)
+ {
+ if (_pendingBulkInsertCommands.Count > 0
+ && !CanBeInsertedInSameStatement(_pendingBulkInsertCommands[0], modificationCommand))
+ {
+ // The new Add command cannot be added to the pending bulk insert commands (e.g. different table).
+ // Write out the pending commands before starting a new pending chain.
+ ApplyPendingBulkInsertCommands();
+ _pendingBulkInsertCommands.Clear();
+ }
+ }
_pendingBulkInsertCommands.Add(modificationCommand);
AddParameters(modificationCommand);
}
else
{
- // If we have any pending bulk insert commands, write them out before the next non-Add command
- if (_pendingBulkInsertCommands.Count > 0)
+ if (QuirkEnabled29539)
{
- // Note that we don't care about the transactionality of the bulk insert SQL, since there's the additional non-Add
- // command coming right afterwards, and so a transaction is required in any case.
- ApplyPendingBulkInsertCommands();
- _pendingBulkInsertCommands.Clear();
+ // If we have any pending bulk insert commands, write them out before the next non-Add command
+ if (_pendingBulkInsertCommands.Count > 0)
+ {
+ // Note that we don't care about the transactionality of the bulk insert SQL, since there's the additional non-Add
+ // command coming right afterwards, and so a transaction is required in any case.
+ ApplyPendingBulkInsertCommands();
+ _pendingBulkInsertCommands.Clear();
+ }
}
base.AddCommand(modificationCommand);
diff --git a/test/EFCore.SqlServer.Tests/Update/SqlServerModificationCommandBatchTest.cs b/test/EFCore.SqlServer.Tests/Update/SqlServerModificationCommandBatchTest.cs
index 2739516fa82..0c62bab1051 100644
--- a/test/EFCore.SqlServer.Tests/Update/SqlServerModificationCommandBatchTest.cs
+++ b/test/EFCore.SqlServer.Tests/Update/SqlServerModificationCommandBatchTest.cs
@@ -73,6 +73,58 @@ ColumnModificationParameters CreateModificationParameters(string columnName)
};
}
+ [ConditionalTheory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void AddCommand_when_max_parameters_are_reached_with_pending_commands(bool lastCommandPending)
+ {
+ var typeMapper = CreateTypeMappingSource();
+ var intMapping = typeMapper.FindMapping(typeof(int));
+ var paramIndex = 0;
+
+ var batch = CreateBatch();
+
+ for (var i = 0; i < 20; i++)
+ {
+ var pendingCommand = CreateModificationCommand("T1", null, false);
+ pendingCommand.EntityState = EntityState.Added;
+ for (var j = 0; j < 100; j++)
+ {
+ pendingCommand.AddColumnModification(CreateModificationParameters("col" + j));
+ }
+
+ Assert.True(batch.TryAddCommand(pendingCommand));
+ }
+
+ // We now have 20 pending commands with a total of 2000 parameters.
+ // Add another command - either compatible with the pending ones or not - and which also gets us past the 2098 parameter limit.
+ var command = CreateModificationCommand(lastCommandPending ? "T1" : "T2", null, false);
+ command.EntityState = EntityState.Added;
+ for (var i = 0; i < 100; i++)
+ {
+ command.AddColumnModification(CreateModificationParameters("col" + i));
+ }
+
+ Assert.False(batch.TryAddCommand(command));
+
+ batch.Complete(moreBatchesExpected: false);
+
+ Assert.Equal(2000, batch.ParameterValues.Count);
+ Assert.Contains("INSERT", batch.StoreCommand.RelationalCommand.CommandText);
+ Assert.Equal(20, batch.ResultSetMappings.Count);
+
+ ColumnModificationParameters CreateModificationParameters(string columnName)
+ => new()
+ {
+ ColumnName = columnName,
+ ColumnType = "integer",
+ TypeMapping = intMapping,
+ IsWrite = true,
+ OriginalValue = 8,
+ GenerateParameterName = () => "p" + paramIndex++
+ };
+ }
+
private class FakeDbContext : DbContext
{
}
@@ -121,5 +173,11 @@ public TestSqlServerModificationCommandBatch(ModificationCommandBatchFactoryDepe
public new Dictionary ParameterValues
=> base.ParameterValues;
+
+ public new RawSqlCommand StoreCommand
+ => base.StoreCommand;
+
+ public new IList ResultSetMappings
+ => base.ResultSetMappings;
}
}