Skip to content

Commit 4f6ca99

Browse files
authored
Fix bug where a non-sproc command comes before a sproc command (#29680) (#29722)
Fixes #29643 (cherry picked from commit 35e9e5a)
1 parent d0edfaa commit 4f6ca99

File tree

3 files changed

+119
-6
lines changed

3 files changed

+119
-6
lines changed

src/EFCore.Relational/Update/AffectedCountModificationCommandBatch.cs

+47-6
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ namespace Microsoft.EntityFrameworkCore.Update;
1717
/// </remarks>
1818
public abstract class AffectedCountModificationCommandBatch : ReaderModificationCommandBatch
1919
{
20+
private static readonly bool QuirkEnabled29643
21+
= AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue29643", out var enabled) && enabled;
22+
2023
/// <summary>
2124
/// Creates a new <see cref="AffectedCountModificationCommandBatch" /> instance.
2225
/// </summary>
@@ -91,12 +94,14 @@ protected override void Consume(RelationalDataReader reader)
9194
var parameterCounter = 0;
9295
IReadOnlyModificationCommand command;
9396

94-
for (commandIndex = 0;
95-
commandIndex < ResultSetMappings.Count;
96-
commandIndex++, parameterCounter += command.StoreStoredProcedure!.Parameters.Count)
97+
for (commandIndex = 0; commandIndex < ResultSetMappings.Count; commandIndex++, parameterCounter += ParameterCount(command))
9798
{
9899
command = ModificationCommands[commandIndex];
99100

101+
Check.DebugAssert(
102+
command.ColumnModifications.All(c => c.UseParameter),
103+
"This code assumes all column modifications involve a DbParameter (see counting above)");
104+
100105
if (!ResultSetMappings[commandIndex].HasFlag(ResultSetMapping.HasOutputParameters))
101106
{
102107
continue;
@@ -210,12 +215,14 @@ protected override async Task ConsumeAsync(
210215
var parameterCounter = 0;
211216
IReadOnlyModificationCommand command;
212217

213-
for (commandIndex = 0;
214-
commandIndex < ResultSetMappings.Count;
215-
commandIndex++, parameterCounter += command.StoreStoredProcedure!.Parameters.Count)
218+
for (commandIndex = 0; commandIndex < ResultSetMappings.Count; commandIndex++, parameterCounter += ParameterCount(command))
216219
{
217220
command = ModificationCommands[commandIndex];
218221

222+
Check.DebugAssert(
223+
command.ColumnModifications.All(c => c.UseParameter),
224+
"This code assumes all column modifications involve a DbParameter (see counting above)");
225+
219226
if (!ResultSetMappings[commandIndex].HasFlag(ResultSetMapping.HasOutputParameters))
220227
{
221228
continue;
@@ -448,6 +455,40 @@ await ThrowAggregateUpdateConcurrencyExceptionAsync(
448455
return commandIndex - 1;
449456
}
450457

458+
private static int ParameterCount(IReadOnlyModificationCommand command)
459+
{
460+
if (QuirkEnabled29643)
461+
{
462+
return command.StoreStoredProcedure!.Parameters.Count;
463+
}
464+
465+
// As a shortcut, if the command uses a stored procedure, return the number of parameters directly from it.
466+
if (command.StoreStoredProcedure is { } storedProcedure)
467+
{
468+
return storedProcedure.Parameters.Count;
469+
}
470+
471+
// Otherwise we need to count the total parameters used by all column modifications
472+
var parameterCount = 0;
473+
474+
for (var i = 0; i < command.ColumnModifications.Count; i++)
475+
{
476+
var columnModification = command.ColumnModifications[i];
477+
478+
if (columnModification.UseCurrentValueParameter)
479+
{
480+
parameterCount++;
481+
}
482+
483+
if (columnModification.UseOriginalValueParameter)
484+
{
485+
parameterCount++;
486+
}
487+
}
488+
489+
return parameterCount;
490+
}
491+
451492
private IReadOnlyList<IUpdateEntry> AggregateEntries(int endIndex, int commandCount)
452493
{
453494
var entries = new List<IUpdateEntry>();

test/EFCore.Relational.Specification.Tests/Update/StoredProcedureUpdateTestBase.cs

+42
Original file line numberDiff line numberDiff line change
@@ -1028,6 +1028,48 @@ protected async Task Tpc(bool async, string createSprocSql)
10281028
}
10291029
}
10301030

1031+
[ConditionalTheory]
1032+
[MemberData(nameof(IsAsyncData))]
1033+
public abstract Task Non_sproc_followed_by_sproc_commands_in_the_same_batch(bool async);
1034+
1035+
protected async Task Non_sproc_followed_by_sproc_commands_in_the_same_batch(bool async, string createSprocSql)
1036+
{
1037+
var contextFactory = await InitializeAsync<DbContext>(
1038+
modelBuilder => modelBuilder.Entity<EntityWithAdditionalProperty>()
1039+
.InsertUsingStoredProcedure(
1040+
nameof(EntityWithAdditionalProperty) + "_Insert",
1041+
spb => spb
1042+
.HasParameter(w => w.Name)
1043+
.HasParameter(w => w.Id, pb => pb.IsOutput())
1044+
.HasParameter(w => w.AdditionalProperty))
1045+
.Property(e => e.AdditionalProperty).IsConcurrencyToken(),
1046+
seed: ctx => CreateStoredProcedures(ctx, createSprocSql));
1047+
1048+
await using var context = contextFactory.CreateContext();
1049+
1050+
// Prepare by adding an entity
1051+
var entity1 = new EntityWithAdditionalProperty { Name = "Entity1", AdditionalProperty = 1 };
1052+
context.Set<EntityWithAdditionalProperty>().Add(entity1);
1053+
1054+
using (TestSqlLoggerFactory.SuspendRecordingEvents())
1055+
{
1056+
await SaveChanges(context, async);
1057+
}
1058+
1059+
// Now add a second entity and update the first one. The update gets ordered first, and doesn't use a sproc, and then the insertion
1060+
// does.
1061+
var entity2 = new EntityWithAdditionalProperty { Name = "Entity2" };
1062+
context.Set<EntityWithAdditionalProperty>().Add(entity2);
1063+
entity1.Name = "Entity1_Modified";
1064+
entity1.AdditionalProperty = 2;
1065+
await SaveChanges(context, async);
1066+
1067+
using (TestSqlLoggerFactory.SuspendRecordingEvents())
1068+
{
1069+
Assert.Equal("Entity2", context.Set<EntityWithAdditionalProperty>().Single(b => b.Id == entity2.Id).Name);
1070+
}
1071+
}
1072+
10311073
/// <summary>
10321074
/// A method to be implement by the provider, to set up a store-generated concurrency token shadow property with the given name.
10331075
/// </summary>

test/EFCore.SqlServer.FunctionalTests/Update/StoredProcedureUpdateSqlServerTest.cs

+30
Original file line numberDiff line numberDiff line change
@@ -665,6 +665,36 @@ AS BEGIN
665665
""");
666666
}
667667

668+
public override async Task Non_sproc_followed_by_sproc_commands_in_the_same_batch(bool async)
669+
{
670+
await base.Non_sproc_followed_by_sproc_commands_in_the_same_batch(
671+
async,
672+
"""
673+
CREATE PROCEDURE EntityWithAdditionalProperty_Insert(@Name varchar(max), @Id int OUT, @AdditionalProperty int)
674+
AS BEGIN
675+
INSERT INTO [EntityWithAdditionalProperty] ([Name], [AdditionalProperty]) VALUES (@Name, @AdditionalProperty);
676+
SET @Id = SCOPE_IDENTITY();
677+
END
678+
""");
679+
680+
AssertSql(
681+
"""
682+
@p2='1'
683+
@p0='2'
684+
@p3='1'
685+
@p1='Entity1_Modified' (Size = 4000)
686+
@p4='Entity2' (Size = 4000)
687+
@p5=NULL (Nullable = false) (Direction = Output) (DbType = Int32)
688+
@p6='0'
689+
690+
SET NOCOUNT ON;
691+
UPDATE [EntityWithAdditionalProperty] SET [AdditionalProperty] = @p0, [Name] = @p1
692+
OUTPUT 1
693+
WHERE [Id] = @p2 AND [AdditionalProperty] = @p3;
694+
EXEC [EntityWithAdditionalProperty_Insert] @p4, @p5 OUTPUT, @p6;
695+
""");
696+
}
697+
668698
protected override void ConfigureStoreGeneratedConcurrencyToken(EntityTypeBuilder entityTypeBuilder, string propertyName)
669699
=> entityTypeBuilder.Property<byte[]>(propertyName).IsRowVersion();
670700

0 commit comments

Comments
 (0)