diff --git a/All.sln.DotSettings b/All.sln.DotSettings
index 4e7bf4fbf52..cb5862cb1a8 100644
--- a/All.sln.DotSettings
+++ b/All.sln.DotSettings
@@ -320,6 +320,7 @@ The .NET Foundation licenses this file to you under the MIT license.
True
True
True
+ True
True
True
True
diff --git a/src/EFCore.SqlServer/Update/Internal/SqlServerModificationCommandBatch.cs b/src/EFCore.SqlServer/Update/Internal/SqlServerModificationCommandBatch.cs
index e26533d030b..d1056037bb0 100644
--- a/src/EFCore.SqlServer/Update/Internal/SqlServerModificationCommandBatch.cs
+++ b/src/EFCore.SqlServer/Update/Internal/SqlServerModificationCommandBatch.cs
@@ -1,7 +1,6 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using System.Collections;
using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore.SqlServer.Internal;
@@ -15,9 +14,15 @@ namespace Microsoft.EntityFrameworkCore.SqlServer.Update.Internal;
///
public class SqlServerModificationCommandBatch : AffectedCountModificationCommandBatch
{
+ // https://docs.microsoft.com/sql/sql-server/maximum-capacity-specifications-for-sql-server
private const int DefaultNetworkPacketSizeBytes = 4096;
private const int MaxScriptLength = 65536 * DefaultNetworkPacketSizeBytes / 2;
- private const int MaxParameterCount = 2100;
+
+ ///
+ /// The SQL Server limit on parameters, including two extra parameters to sp_executesql (@stmt and @params).
+ ///
+ private const int MaxParameterCount = 2100 - 2;
+
private readonly List _pendingBulkInsertCommands = new();
///
@@ -60,7 +65,6 @@ protected override void RollbackLastCommand()
if (_pendingBulkInsertCommands.Count > 0)
{
_pendingBulkInsertCommands.RemoveAt(_pendingBulkInsertCommands.Count - 1);
- return;
}
base.RollbackLastCommand();
@@ -73,9 +77,30 @@ protected override void RollbackLastCommand()
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
protected override bool IsValid()
- => SqlBuilder.Length < MaxScriptLength
- // A single implicit parameter for the command text itself
- && ParameterValues.Count + 1 < MaxParameterCount;
+ {
+ if (ParameterValues.Count > MaxParameterCount)
+ {
+ return false;
+ }
+
+ var sqlLength = SqlBuilder.Length;
+
+ if (_pendingBulkInsertCommands.Count > 0)
+ {
+ // Conservative heuristic for the length of the pending bulk insert commands.
+ // See EXEC sp_server_info.
+ var numColumns = _pendingBulkInsertCommands[0].ColumnModifications.Count;
+
+ sqlLength +=
+ numColumns * 128 // column name lengths
+ + 128 // schema name length
+ + 128 // table name length
+ + _pendingBulkInsertCommands.Count * numColumns * 6 // column parameter placeholders
+ + 300; // some extra fixed overhead
+ }
+
+ return sqlLength < MaxScriptLength;
+ }
private void ApplyPendingBulkInsertCommands()
{
@@ -93,10 +118,8 @@ private void ApplyPendingBulkInsertCommands()
SetRequiresTransaction(!wasCachedCommandTextEmpty || requiresTransaction);
- foreach (var pendingCommand in _pendingBulkInsertCommands)
+ for (var i = 0; i < _pendingBulkInsertCommands.Count; i++)
{
- AddParameters(pendingCommand);
-
CommandResultSet.Add(resultSetMapping);
}
@@ -126,6 +149,7 @@ protected override void AddCommand(IReadOnlyModificationCommand modificationComm
}
_pendingBulkInsertCommands.Add(modificationCommand);
+ AddParameters(modificationCommand);
}
else
{
diff --git a/test/EFCore.SqlServer.Tests/Update/SqlServerModificationCommandBatchTest.cs b/test/EFCore.SqlServer.Tests/Update/SqlServerModificationCommandBatchTest.cs
index 6b9ee9b6b43..873a6e4b4f8 100644
--- a/test/EFCore.SqlServer.Tests/Update/SqlServerModificationCommandBatchTest.cs
+++ b/test/EFCore.SqlServer.Tests/Update/SqlServerModificationCommandBatchTest.cs
@@ -11,20 +11,33 @@ namespace Microsoft.EntityFrameworkCore.Update;
public class SqlServerModificationCommandBatchTest
{
- [ConditionalFact]
- public void AddCommand_returns_false_when_max_batch_size_is_reached()
+ [ConditionalTheory]
+ [InlineData(EntityState.Added)]
+ [InlineData(EntityState.Deleted)]
+ [InlineData(EntityState.Modified)]
+ public void AddCommand_returns_false_when_max_batch_size_is_reached(EntityState entityState)
{
var batch = CreateBatch(maxBatchSize: 1);
var firstCommand = CreateModificationCommand("T1", null, false);
+ firstCommand.EntityState = entityState;
+ var secondCommand = CreateModificationCommand("T1", null, false);
+ secondCommand.EntityState = entityState;
+
Assert.True(batch.TryAddCommand(firstCommand));
- Assert.False(batch.TryAddCommand(CreateModificationCommand("T1", null, false)));
+ Assert.False(batch.TryAddCommand(secondCommand));
Assert.Same(firstCommand, Assert.Single(batch.ModificationCommands));
}
- [ConditionalFact]
- public void AddCommand_returns_false_when_max_parameters_are_reached()
+ [ConditionalTheory]
+ [InlineData(EntityState.Added, true)]
+ [InlineData(EntityState.Added, false)]
+ [InlineData(EntityState.Deleted, true)]
+ [InlineData(EntityState.Deleted, false)]
+ [InlineData(EntityState.Modified, true)]
+ [InlineData(EntityState.Modified, false)]
+ public void AddCommand_returns_false_when_max_parameters_are_reached(EntityState entityState, bool withSameTable)
{
var typeMapper = CreateTypeMappingSource();
var intMapping = typeMapper.FindMapping(typeof(int));
@@ -33,6 +46,7 @@ public void AddCommand_returns_false_when_max_parameters_are_reached()
var batch = CreateBatch();
var command = CreateModificationCommand("T1", null, false);
+ command.EntityState = entityState;
for (var i = 0; i < 2098; i++)
{
command.AddColumnModification(CreateModificationParameters("col" + i));
@@ -40,6 +54,7 @@ public void AddCommand_returns_false_when_max_parameters_are_reached()
Assert.True(batch.TryAddCommand(command));
var secondCommand = CreateModificationCommand("T2", null, false);
+ secondCommand.EntityState = entityState;
secondCommand.AddColumnModification(CreateModificationParameters("col"));
Assert.False(batch.TryAddCommand(secondCommand));
Assert.Same(command, Assert.Single(batch.ModificationCommands));