diff --git a/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs b/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs
index cffd450b5a5..a23d121017d 100644
--- a/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs
+++ b/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs
@@ -436,12 +436,69 @@ private static void ValidateSproc(IStoredProcedure sproc, string mappingStrategy
RelationalStrings.StoredProcedureParameterNotFound(
parameter.PropertyName, entityType.DisplayName(), storeObjectIdentifier.DisplayName()));
}
+
+ if (storeObjectIdentifier.StoreObjectType == StoreObjectType.InsertStoredProcedure)
+ {
+ throw new InvalidOperationException(
+ RelationalStrings.StoredProcedureOriginalValueParameterOnInsert(
+ parameter.Name, storeObjectIdentifier.DisplayName()));
+ }
}
- else if (!properties.TryGetAndRemove(parameter.PropertyName, out property))
+ else
{
- throw new InvalidOperationException(
- RelationalStrings.StoredProcedureParameterNotFound(
- parameter.PropertyName, entityType.DisplayName(), storeObjectIdentifier.DisplayName()));
+ if (!properties.TryGetAndRemove(parameter.PropertyName, out property))
+ {
+ throw new InvalidOperationException(
+ RelationalStrings.StoredProcedureParameterNotFound(
+ parameter.PropertyName, entityType.DisplayName(), storeObjectIdentifier.DisplayName()));
+ }
+
+ if (storeObjectIdentifier.StoreObjectType == StoreObjectType.DeleteStoredProcedure)
+ {
+ throw new InvalidOperationException(
+ RelationalStrings.StoredProcedureCurrentValueParameterOnDelete(
+ parameter.Name, storeObjectIdentifier.DisplayName()));
+ }
+
+ if (parameter.Direction.HasFlag(ParameterDirection.Input))
+ {
+ switch (storeObjectIdentifier.StoreObjectType)
+ {
+ case StoreObjectType.InsertStoredProcedure:
+ if (property.GetBeforeSaveBehavior() != PropertySaveBehavior.Save)
+ {
+ throw new InvalidOperationException(
+ RelationalStrings.StoredProcedureInputParameterForInsertNonSaveProperty(
+ parameter.Name,
+ storeObjectIdentifier.DisplayName(),
+ parameter.PropertyName,
+ entityType.DisplayName(),
+ property.GetBeforeSaveBehavior()));
+ }
+ break;
+
+ case StoreObjectType.UpdateStoredProcedure:
+ if (property.GetAfterSaveBehavior() != PropertySaveBehavior.Save)
+ {
+ throw new InvalidOperationException(
+ RelationalStrings.StoredProcedureInputParameterForUpdateNonSaveProperty(
+ parameter.Name,
+ storeObjectIdentifier.DisplayName(),
+ parameter.PropertyName,
+ entityType.DisplayName(),
+ property.GetAfterSaveBehavior()));
+ }
+
+ break;
+
+ case StoreObjectType.DeleteStoredProcedure:
+ break;
+
+ default:
+ Check.DebugFail("Unexpected stored procedure type: " + storeObjectIdentifier.StoreObjectType);
+ break;
+ }
+ }
}
}
@@ -578,8 +635,14 @@ private static void ValidateSproc(IStoredProcedure sproc, string mappingStrategy
|| sproc.FindRowsAffectedParameter() != null
|| sproc.FindRowsAffectedResultColumn() != null)
{
- if (originalValueProperties.Values.FirstOrDefault(p => p.IsConcurrencyToken) is { } missedConcurrencyToken
- && storeObjectIdentifier.StoreObjectType != StoreObjectType.InsertStoredProcedure)
+ if (storeObjectIdentifier.StoreObjectType == StoreObjectType.InsertStoredProcedure)
+ {
+ throw new InvalidOperationException(
+ RelationalStrings.StoredProcedureRowsAffectedForInsert(
+ storeObjectIdentifier.DisplayName()));
+ }
+
+ if (originalValueProperties.Values.FirstOrDefault(p => p.IsConcurrencyToken) is { } missedConcurrencyToken)
{
throw new InvalidOperationException(
RelationalStrings.StoredProcedureConcurrencyTokenNotMapped(
diff --git a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs
index 37fdf1976fc..d7713ebfc95 100644
--- a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs
+++ b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs
@@ -1483,6 +1483,14 @@ public static string StoredProcedureConcurrencyTokenNotMapped(object? entityType
GetString("StoredProcedureConcurrencyTokenNotMapped", nameof(entityType), nameof(sproc), nameof(token)),
entityType, sproc, token);
+ ///
+ /// Current value parameter '{parameter}' is not allowed on delete stored procedure '{sproc}'. Use HasOriginalValueParameter() instead.
+ ///
+ public static string StoredProcedureCurrentValueParameterOnDelete(object? parameter, object? sproc)
+ => string.Format(
+ GetString("StoredProcedureCurrentValueParameterOnDelete", nameof(parameter), nameof(sproc)),
+ parameter, sproc);
+
///
/// The property '{entityType}.{property}' is mapped to a parameter of the stored procedure '{sproc}', but only concurrency token and key properties are supported for Delete stored procedures.
///
@@ -1555,6 +1563,22 @@ public static string StoredProcedureGeneratedPropertiesNotMapped(object? entityT
GetString("StoredProcedureGeneratedPropertiesNotMapped", nameof(entityType), nameof(sproc), nameof(properties)),
entityType, sproc, properties);
+ ///
+ /// Input parameter '{parameter}' of insert stored procedure '{sproc}' is mapped to property '{property}' of entity type '{entityType}', but that property is configured with BeforeSaveBehavior '{behavior}', and so cannot be saved on insert.
+ ///
+ public static string StoredProcedureInputParameterForInsertNonSaveProperty(object? parameter, object? sproc, object? property, object? entityType, object? behavior)
+ => string.Format(
+ GetString("StoredProcedureInputParameterForInsertNonSaveProperty", nameof(parameter), nameof(sproc), nameof(property), nameof(entityType), nameof(behavior)),
+ parameter, sproc, property, entityType, behavior);
+
+ ///
+ /// Input parameter '{parameter}' of update stored procedure '{sproc}' is mapped to property '{property}' of entity type '{entityType}', but that property is configured with AfterSaveBehavior '{behavior}', and so cannot be saved on update.
+ ///
+ public static string StoredProcedureInputParameterForUpdateNonSaveProperty(object? parameter, object? sproc, object? property, object? entityType, object? behavior)
+ => string.Format(
+ GetString("StoredProcedureInputParameterForUpdateNonSaveProperty", nameof(parameter), nameof(sproc), nameof(property), nameof(entityType), nameof(behavior)),
+ parameter, sproc, property, entityType, behavior);
+
///
/// The keyless entity type '{entityType}' was configured to use '{sproc}'. An entity type requires a primary key to be able to be mapped to a stored procedure.
///
@@ -1571,6 +1595,14 @@ public static string StoredProcedureNoName(object? entityType, object? sproc)
GetString("StoredProcedureNoName", nameof(entityType), nameof(sproc)),
entityType, sproc);
+ ///
+ /// Original value parameter '{parameter}' is not allowed on insert stored procedure '{sproc}'. Use HasParameter() instead.
+ ///
+ public static string StoredProcedureOriginalValueParameterOnInsert(object? parameter, object? sproc)
+ => string.Format(
+ GetString("StoredProcedureOriginalValueParameterOnInsert", nameof(parameter), nameof(sproc)),
+ parameter, sproc);
+
///
/// The property '{entityType}.{property}' is mapped to an output parameter of the stored procedure '{sproc}', but it is also mapped to an output original value output parameter. A store-generated property can only be mapped to one output parameter.
///
@@ -1667,6 +1699,14 @@ public static string StoredProcedureRowsAffectedNotPopulated(object? sproc)
GetString("StoredProcedureRowsAffectedNotPopulated", nameof(sproc)),
sproc);
+ ///
+ /// A rows affected parameter, result column or return value cannot be configured on stored procedure '{sproc}' because it is used for insertion. Rows affected values are only allowed on stored procedures performing updating or deletion.
+ ///
+ public static string StoredProcedureRowsAffectedForInsert(object? sproc)
+ => string.Format(
+ GetString("StoredProcedureRowsAffectedForInsert", nameof(sproc)),
+ sproc);
+
///
/// The stored procedure '{sproc}' cannot be configured to return the rows affected because a rows affected parameter or a rows affected result column for this stored procedure already exists.
///
diff --git a/src/EFCore.Relational/Properties/RelationalStrings.resx b/src/EFCore.Relational/Properties/RelationalStrings.resx
index f49dc141908..c9fd958c4dd 100644
--- a/src/EFCore.Relational/Properties/RelationalStrings.resx
+++ b/src/EFCore.Relational/Properties/RelationalStrings.resx
@@ -979,6 +979,9 @@
The entity type '{entityType}' is mapped to the stored procedure '{sproc}', but the concurrency token '{token}' is not mapped to any original value parameter.
+
+ Current value parameter '{parameter}' is not allowed on delete stored procedure '{sproc}'. Use HasOriginalValueParameter() instead.
+
The property '{entityType}.{property}' is mapped to a parameter of the stored procedure '{sproc}', but only concurrency token and key properties are supported for Delete stored procedures.
@@ -1006,12 +1009,21 @@
The entity type '{entityType}' is mapped to the stored procedure '{sproc}', however the store-generated properties {properties} are not mapped to any output parameter or result column.
+
+ Input parameter '{parameter}' of insert stored procedure '{sproc}' is mapped to property '{property}' of entity type '{entityType}', but that property is configured with BeforeSaveBehavior '{behavior}', and so cannot be saved on insert.
+
+
+ Input parameter '{parameter}' of update stored procedure '{sproc}' is mapped to property '{property}' of entity type '{entityType}', but that property is configured with AfterSaveBehavior '{behavior}', and so cannot be saved on update.
+
The keyless entity type '{entityType}' was configured to use '{sproc}'. An entity type requires a primary key to be able to be mapped to a stored procedure.
The entity type '{entityType}' was configured to use '{sproc}', but the store name was not specified. Configure the stored procedure name explicitly.
+
+ Original value parameter '{parameter}' is not allowed on insert stored procedure '{sproc}'. Use HasParameter() instead.
+
The property '{entityType}.{property}' is mapped to an output parameter of the stored procedure '{sproc}', but it is also mapped to an output original value output parameter. A store-generated property can only be mapped to one output parameter.
@@ -1048,6 +1060,9 @@
Stored procedure '{sproc}' was configured with a rows affected output parameter or return value, but a valid value was not found when executing the procedure.
+
+ A rows affected parameter, result column or return value cannot be configured on stored procedure '{sproc}' because it is used for insertion. Rows affected values are only allowed on stored procedures performing updating or deletion.
+
The stored procedure '{sproc}' cannot be configured to return the rows affected because a rows affected parameter or a rows affected result column for this stored procedure already exists.
diff --git a/src/EFCore.Relational/Update/AffectedCountModificationCommandBatch.cs b/src/EFCore.Relational/Update/AffectedCountModificationCommandBatch.cs
index d5e6ece0848..0f32062e3e6 100644
--- a/src/EFCore.Relational/Update/AffectedCountModificationCommandBatch.cs
+++ b/src/EFCore.Relational/Update/AffectedCountModificationCommandBatch.cs
@@ -44,7 +44,7 @@ protected override void Consume(RelationalDataReader reader)
bool? onResultSet = null;
var hasOutputParameters = false;
- for (; commandIndex < ResultSetMappings.Count; commandIndex++)
+ while (commandIndex < ResultSetMappings.Count)
{
var resultSetMapping = ResultSetMappings[commandIndex];
@@ -63,10 +63,14 @@ protected override void Consume(RelationalDataReader reader)
? lastHandledCommandIndex == commandIndex
: lastHandledCommandIndex > commandIndex, "Bad handling of ResultSetMapping and command indexing");
- commandIndex = lastHandledCommandIndex;
+ commandIndex = lastHandledCommandIndex + 1;
onResultSet = reader.DbDataReader.NextResult();
}
+ else
+ {
+ commandIndex++;
+ }
if (resultSetMapping.HasFlag(ResultSetMapping.HasOutputParameters))
{
@@ -158,7 +162,7 @@ protected override async Task ConsumeAsync(
bool? onResultSet = null;
var hasOutputParameters = false;
- for (; commandIndex < ResultSetMappings.Count; commandIndex++)
+ while (commandIndex < ResultSetMappings.Count)
{
var resultSetMapping = ResultSetMappings[commandIndex];
@@ -177,10 +181,14 @@ protected override async Task ConsumeAsync(
? lastHandledCommandIndex == commandIndex
: lastHandledCommandIndex > commandIndex, "Bad handling of ResultSetMapping and command indexing");
- commandIndex = lastHandledCommandIndex;
+ commandIndex = lastHandledCommandIndex + 1;
onResultSet = await reader.DbDataReader.NextResultAsync(cancellationToken).ConfigureAwait(false);
}
+ else
+ {
+ commandIndex++;
+ }
if (resultSetMapping.HasFlag(ResultSetMapping.HasOutputParameters))
{
diff --git a/src/EFCore.Relational/Update/ModificationCommand.cs b/src/EFCore.Relational/Update/ModificationCommand.cs
index 1372330ca08..78480b9c1f9 100644
--- a/src/EFCore.Relational/Update/ModificationCommand.cs
+++ b/src/EFCore.Relational/Update/ModificationCommand.cs
@@ -523,9 +523,12 @@ void HandleColumnModification(IColumnMappingBase columnMapping)
&& (isKey
|| storedProcedureParameter is { ForOriginalValue: true }
|| (property.IsConcurrencyToken && storedProcedureParameter is null));
+
+ // Store-generated properties generally need to be read back (unless we're deleting).
+ // One exception is if the property is mapped to a non-output parameter.
var readValue = state != EntityState.Deleted
&& entry.IsStoreGenerated(property)
- && storedProcedureParameter is null or { ForOriginalValue: false };
+ && (storedProcedureParameter is null || storedProcedureParameter.Direction.HasFlag(ParameterDirection.Output));
ColumnValuePropagator? columnPropagator = null;
sharedTableColumnMap?.TryGetValue(column.Name, out columnPropagator);
diff --git a/src/EFCore.Relational/Update/ReaderModificationCommandBatch.cs b/src/EFCore.Relational/Update/ReaderModificationCommandBatch.cs
index 6fc8474f3e0..d574f30b427 100644
--- a/src/EFCore.Relational/Update/ReaderModificationCommandBatch.cs
+++ b/src/EFCore.Relational/Update/ReaderModificationCommandBatch.cs
@@ -269,7 +269,13 @@ protected virtual void AddParameters(IReadOnlyModificationCommand modificationCo
Check.DebugAssert(!modificationCommand.ColumnModifications.Any(m => m.Column is IStoreStoredProcedureReturnValue)
|| modificationCommand.ColumnModifications[0].Column is IStoreStoredProcedureReturnValue,
"ResultValue column modification in non-first position");
- foreach (var columnModification in modificationCommand.ColumnModifications)
+
+ var modifications = modificationCommand.StoreStoredProcedure is null
+ ? modificationCommand.ColumnModifications
+ : modificationCommand.ColumnModifications.Where(
+ c => c.Column is IStoreStoredProcedureParameter or IStoreStoredProcedureReturnValue);
+
+ foreach (var columnModification in modifications)
{
AddParameter(columnModification);
}
@@ -288,13 +294,17 @@ protected virtual void AddParameter(IColumnModification columnModification)
_ => ParameterDirection.Input
};
- // For in/out parameters, both UseCurrentValueParameter and UseOriginalValueParameter are true, but we only want to add a single
- // parameter. This will happen below.
- if (columnModification.UseCurrentValueParameter && direction != ParameterDirection.InputOutput)
+ // For the case where the same modification has both current and original value parameters, and corresponds to an in/out parameter,
+ // we only want to add a single parameter. This will happen below.
+ if (columnModification.UseCurrentValueParameter
+ && !(columnModification.UseOriginalValueParameter && direction == ParameterDirection.InputOutput))
{
- AddParameterCore(columnModification.ParameterName, direction == ParameterDirection.Output
- ? null
- : columnModification.Value);
+ AddParameterCore(
+ columnModification.ParameterName, columnModification.UseCurrentValue
+ ? columnModification.Value
+ : direction == ParameterDirection.InputOutput
+ ? DBNull.Value
+ : null);
}
if (columnModification.UseOriginalValueParameter)
@@ -313,8 +323,6 @@ void AddParameterCore(string name, object? value)
columnModification.IsNullable,
direction);
- // TODO: As an alternative, don't add output-only parameters to ParameterValues at all.
- // But that means we can't check values exist for input parameters in RelationalParameterBase.AddDbParameter
ParameterValues.Add(name, value);
_pendingParameters++;
diff --git a/test/EFCore.Relational.Specification.Tests/TestModels/StoredProcedureUpdateModel/StoredProcedureUpdateContext.cs b/test/EFCore.Relational.Specification.Tests/TestModels/StoredProcedureUpdateModel/StoredProcedureUpdateContext.cs
index 03de6ed0761..4e976c1af93 100644
--- a/test/EFCore.Relational.Specification.Tests/TestModels/StoredProcedureUpdateModel/StoredProcedureUpdateContext.cs
+++ b/test/EFCore.Relational.Specification.Tests/TestModels/StoredProcedureUpdateModel/StoredProcedureUpdateContext.cs
@@ -25,8 +25,8 @@ public DbSet WithOutputParameterAndResultColumn
public DbSet WithOutputParameterAndRowsAffectedResultColumn
=> Set(nameof(WithOutputParameterAndRowsAffectedResultColumn));
- public DbSet WithTwoOutputParameters
- => Set(nameof(WithTwoOutputParameters));
+ public DbSet WithTwoInputParameters
+ => Set(nameof(WithTwoInputParameters));
public DbSet WithRowsAffectedParameter
=> Set(nameof(WithRowsAffectedParameter));
@@ -37,8 +37,8 @@ public DbSet WithRowsAffectedResultColumn
public DbSet WithRowsAffectedReturnValue
=> Set(nameof(WithRowsAffectedReturnValue));
- public DbSet WithStoreGeneratedConcurrencyTokenAsInoutParameter
- => Set(nameof(WithStoreGeneratedConcurrencyTokenAsInoutParameter));
+ public DbSet WithStoreGeneratedConcurrencyTokenAsInOutParameter
+ => Set(nameof(WithStoreGeneratedConcurrencyTokenAsInOutParameter));
public DbSet WithStoreGeneratedConcurrencyTokenAsTwoParameters
=> Set(nameof(WithStoreGeneratedConcurrencyTokenAsTwoParameters));
@@ -49,8 +49,8 @@ public DbSet WithUserManagedConcurrencyToken
public DbSet WithOriginalAndCurrentValueOnNonConcurrencyToken
=> Set(nameof(WithOriginalAndCurrentValueOnNonConcurrencyToken));
- public DbSet WithInputOutputParameterOnNonConcurrencyToken
- => Set(nameof(WithInputOutputParameterOnNonConcurrencyToken));
+ public DbSet WithInputOrOutputParameter
+ => Set(nameof(WithInputOrOutputParameter));
public DbSet TphParent { get; set; }
public DbSet TphChild { get; set; }
diff --git a/test/EFCore.Relational.Specification.Tests/Update/StoredProcedureUpdateFixtureBase.cs b/test/EFCore.Relational.Specification.Tests/Update/StoredProcedureUpdateFixtureBase.cs
index d5a9ec54f3b..15909628c88 100644
--- a/test/EFCore.Relational.Specification.Tests/Update/StoredProcedureUpdateFixtureBase.cs
+++ b/test/EFCore.Relational.Specification.Tests/Update/StoredProcedureUpdateFixtureBase.cs
@@ -23,11 +23,11 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con
.UpdateUsingStoredProcedure(
nameof(StoredProcedureUpdateContext.WithOutputParameter) + "_Update",
spb => spb
- .HasParameter(w => w.Id)
+ .HasOriginalValueParameter(w => w.Id)
.HasParameter(w => w.Name))
.DeleteUsingStoredProcedure(
nameof(StoredProcedureUpdateContext.WithOutputParameter) + "_Delete",
- spb => spb.HasParameter(w => w.Id));
+ spb => spb.HasOriginalValueParameter(w => w.Id));
modelBuilder.SharedTypeEntity(nameof(StoredProcedureUpdateContext.WithResultColumn))
.InsertUsingStoredProcedure(
@@ -70,16 +70,16 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con
b.UpdateUsingStoredProcedure(
nameof(StoredProcedureUpdateContext.WithOutputParameterAndRowsAffectedResultColumn) + "_Update",
spb => spb
- .HasParameter(w => w.Id)
+ .HasOriginalValueParameter(w => w.Id)
.HasParameter(w => w.Name)
.HasParameter(w => w.AdditionalProperty, pb => pb.IsOutput())
.HasRowsAffectedResultColumn());
});
- modelBuilder.SharedTypeEntity(nameof(StoredProcedureUpdateContext.WithTwoOutputParameters))
+ modelBuilder.SharedTypeEntity(nameof(StoredProcedureUpdateContext.WithTwoInputParameters))
.UpdateUsingStoredProcedure(
- nameof(StoredProcedureUpdateContext.WithTwoOutputParameters) + "_Update", spb => spb
- .HasParameter(w => w.Id)
+ nameof(StoredProcedureUpdateContext.WithTwoInputParameters) + "_Update", spb => spb
+ .HasOriginalValueParameter(w => w.Id)
.HasParameter(w => w.Name)
.HasParameter(w => w.AdditionalProperty));
@@ -87,7 +87,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con
.UpdateUsingStoredProcedure(
nameof(StoredProcedureUpdateContext.WithRowsAffectedParameter) + "_Update",
spb => spb
- .HasParameter(w => w.Id)
+ .HasOriginalValueParameter(w => w.Id)
.HasParameter(w => w.Name)
.HasRowsAffectedParameter());
@@ -95,7 +95,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con
.UpdateUsingStoredProcedure(
nameof(StoredProcedureUpdateContext.WithRowsAffectedResultColumn) + "_Update",
spb => spb
- .HasParameter(w => w.Id)
+ .HasOriginalValueParameter(w => w.Id)
.HasParameter(w => w.Name)
.HasRowsAffectedResultColumn());
@@ -103,20 +103,20 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con
.UpdateUsingStoredProcedure(
nameof(StoredProcedureUpdateContext.WithRowsAffectedReturnValue) + "_Update",
spb => spb
- .HasParameter(w => w.Id)
+ .HasOriginalValueParameter(w => w.Id)
.HasParameter(w => w.Name)
.HasRowsAffectedReturnValue());
modelBuilder.SharedTypeEntity(
- nameof(StoredProcedureUpdateContext.WithStoreGeneratedConcurrencyTokenAsInoutParameter),
+ nameof(StoredProcedureUpdateContext.WithStoreGeneratedConcurrencyTokenAsInOutParameter),
b =>
{
ConfigureStoreGeneratedConcurrencyToken(b, "ConcurrencyToken");
b.UpdateUsingStoredProcedure(
- nameof(StoredProcedureUpdateContext.WithStoreGeneratedConcurrencyTokenAsInoutParameter) + "_Update",
+ nameof(StoredProcedureUpdateContext.WithStoreGeneratedConcurrencyTokenAsInOutParameter) + "_Update",
spb => spb
- .HasParameter(w => w.Id)
+ .HasOriginalValueParameter(w => w.Id)
.HasOriginalValueParameter("ConcurrencyToken", pb => pb.IsInputOutput())
.HasParameter(w => w.Name)
.HasRowsAffectedParameter());
@@ -131,7 +131,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con
b.UpdateUsingStoredProcedure(
nameof(StoredProcedureUpdateContext.WithStoreGeneratedConcurrencyTokenAsTwoParameters) + "_Update",
spb => spb
- .HasParameter(w => w.Id)
+ .HasOriginalValueParameter(w => w.Id)
.HasOriginalValueParameter("ConcurrencyToken", pb => pb.HasName("ConcurrencyTokenIn"))
.HasParameter(w => w.Name)
.HasParameter(
@@ -150,7 +150,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con
b.UpdateUsingStoredProcedure(
nameof(StoredProcedureUpdateContext.WithUserManagedConcurrencyToken) + "_Update",
spb => spb
- .HasParameter(w => w.Id)
+ .HasOriginalValueParameter(w => w.Id)
.HasOriginalValueParameter(w => w.AdditionalProperty, pb => pb.HasName("ConcurrencyTokenOriginal"))
.HasParameter(w => w.Name)
.HasParameter(w => w.AdditionalProperty, pb => pb.HasName("ConcurrencyTokenCurrent"))
@@ -161,20 +161,20 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con
.UpdateUsingStoredProcedure(
nameof(StoredProcedureUpdateContext.WithOriginalAndCurrentValueOnNonConcurrencyToken) + "_Update",
spb => spb
- .HasParameter(w => w.Id)
+ .HasOriginalValueParameter(w => w.Id)
.HasParameter(w => w.Name, pb => pb.HasName("NameCurrent"))
.HasOriginalValueParameter(w => w.Name, pb => pb.HasName("NameOriginal")));
modelBuilder.SharedTypeEntity(
- nameof(StoredProcedureUpdateContext.WithInputOutputParameterOnNonConcurrencyToken),
+ nameof(StoredProcedureUpdateContext.WithInputOrOutputParameter),
b =>
{
- b.Property(w => w.Name).ValueGeneratedOnAddOrUpdate();
+ b.Property(w => w.Name).IsRequired().ValueGeneratedOnAdd();
- b.UpdateUsingStoredProcedure(
- nameof(StoredProcedureUpdateContext.WithInputOutputParameterOnNonConcurrencyToken) + "_Update",
+ b.InsertUsingStoredProcedure(
+ nameof(StoredProcedureUpdateContext.WithInputOrOutputParameter) + "_Insert",
spb => spb
- .HasParameter(w => w.Id)
+ .HasParameter(w => w.Id, pb => pb.IsOutput())
.HasParameter(w => w.Name, pb => pb.IsInputOutput()));
});
@@ -264,15 +264,15 @@ public virtual void CleanData()
context.WithResultColumn.RemoveRange(context.WithResultColumn);
context.WithTwoResultColumns.RemoveRange(context.WithTwoResultColumns);
context.WithOutputParameterAndResultColumn.RemoveRange(context.WithOutputParameterAndResultColumn);
- context.WithTwoOutputParameters.RemoveRange(context.WithTwoOutputParameters);
+ context.WithTwoInputParameters.RemoveRange(context.WithTwoInputParameters);
context.WithRowsAffectedParameter.RemoveRange(context.WithRowsAffectedParameter);
context.WithRowsAffectedResultColumn.RemoveRange(context.WithRowsAffectedResultColumn);
context.WithRowsAffectedReturnValue.RemoveRange(context.WithRowsAffectedReturnValue);
- context.WithStoreGeneratedConcurrencyTokenAsInoutParameter.RemoveRange(context.WithStoreGeneratedConcurrencyTokenAsInoutParameter);
+ context.WithStoreGeneratedConcurrencyTokenAsInOutParameter.RemoveRange(context.WithStoreGeneratedConcurrencyTokenAsInOutParameter);
context.WithStoreGeneratedConcurrencyTokenAsTwoParameters.RemoveRange(context.WithStoreGeneratedConcurrencyTokenAsTwoParameters);
context.WithUserManagedConcurrencyToken.RemoveRange(context.WithUserManagedConcurrencyToken);
context.WithOriginalAndCurrentValueOnNonConcurrencyToken.RemoveRange(context.WithOriginalAndCurrentValueOnNonConcurrencyToken);
- context.WithInputOutputParameterOnNonConcurrencyToken.RemoveRange(context.WithInputOutputParameterOnNonConcurrencyToken);
+ context.WithInputOrOutputParameter.RemoveRange(context.WithInputOrOutputParameter);
context.TphParent.RemoveRange(context.TphParent);
context.TphChild.RemoveRange(context.TphChild);
context.TptParent.RemoveRange(context.TptParent);
diff --git a/test/EFCore.Relational.Specification.Tests/Update/StoredProcedureUpdateTestBase.cs b/test/EFCore.Relational.Specification.Tests/Update/StoredProcedureUpdateTestBase.cs
index 3a01d33ac10..fd542ce294d 100644
--- a/test/EFCore.Relational.Specification.Tests/Update/StoredProcedureUpdateTestBase.cs
+++ b/test/EFCore.Relational.Specification.Tests/Update/StoredProcedureUpdateTestBase.cs
@@ -134,7 +134,7 @@ public virtual async Task Update_partial(bool async)
await using var context = CreateContext();
var entity = new EntityWithAdditionalProperty { Name = "Foo", AdditionalProperty = 8 };
- context.WithTwoOutputParameters.Add(entity);
+ context.WithTwoInputParameters.Add(entity);
await context.SaveChangesAsync();
entity.Name = "Updated";
@@ -145,7 +145,7 @@ public virtual async Task Update_partial(bool async)
using (Fixture.TestSqlLoggerFactory.SuspendRecordingEvents())
{
- var actual = await context.WithTwoOutputParameters.SingleAsync(w => w.Id == entity.Id);
+ var actual = await context.WithTwoInputParameters.SingleAsync(w => w.Id == entity.Id);
Assert.Equal("Updated", actual.Name);
Assert.Equal(8, actual.AdditionalProperty);
@@ -393,17 +393,17 @@ public virtual async Task Rows_affected_return_value_and_concurrency_failure(boo
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual async Task Store_generated_concurrency_token_as_inout_parameter(bool async)
+ public virtual async Task Store_generated_concurrency_token_as_in_out_parameter(bool async)
{
await using var context1 = CreateContext();
var entity1 = new Entity { Name = "Initial" };
- context1.WithStoreGeneratedConcurrencyTokenAsInoutParameter.Add(entity1);
+ context1.WithStoreGeneratedConcurrencyTokenAsInOutParameter.Add(entity1);
await context1.SaveChangesAsync();
await using (var context2 = CreateContext())
{
- var entity2 = await context2.WithStoreGeneratedConcurrencyTokenAsInoutParameter.SingleAsync(w => w.Name == "Initial");
+ var entity2 = await context2.WithStoreGeneratedConcurrencyTokenAsInOutParameter.SingleAsync(w => w.Name == "Initial");
entity2.Name = "Preempted";
await SaveChanges(context2, async);
}
@@ -505,27 +505,39 @@ public virtual async Task Original_and_current_value_on_non_concurrency_token(bo
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual async Task Input_output_parameter_on_non_concurrency_token(bool async)
+ public virtual async Task Input_or_output_parameter_with_input(bool async)
{
await using var context = CreateContext();
- var entity = new Entity { Name = "Initial", };
- context.WithInputOutputParameterOnNonConcurrencyToken.Add(entity);
- await context.SaveChangesAsync();
+ var entity = new Entity { Name = "Initial" };
+ context.WithInputOrOutputParameter.Add(entity);
+ await SaveChanges(context, async);
- entity.Name = "Updated";
+ Assert.Equal("Initial", entity.Name);
- ClearLog();
+ using (Fixture.TestSqlLoggerFactory.SuspendRecordingEvents())
+ {
+ Assert.Same(
+ entity, await context.WithInputOrOutputParameter.SingleAsync(w => w.Id == entity.Id && w.Name == "Initial"));
+ }
+ }
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual async Task Input_or_output_parameter_with_output(bool async)
+ {
+ await using var context = CreateContext();
+
+ var entity = new Entity();
+ context.WithInputOrOutputParameter.Add(entity);
await SaveChanges(context, async);
- // TODO: This (and below) should be UpdatedWithSuffix. Reference issue tracking this.
- Assert.Equal("Updated", entity.Name);
+ Assert.Equal("Some default value", entity.Name);
using (Fixture.TestSqlLoggerFactory.SuspendRecordingEvents())
{
- Assert.Equal(
- "Updated", (await context.WithInputOutputParameterOnNonConcurrencyToken.SingleAsync(w => w.Id == entity.Id)).Name);
+ Assert.Same(
+ entity, await context.WithInputOrOutputParameter.SingleAsync(w => w.Id == entity.Id && w.Name == "Some default value"));
}
}
diff --git a/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs b/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs
index 8f20fb55884..16a0f36f3f9 100644
--- a/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs
+++ b/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs
@@ -2631,9 +2631,9 @@ public void Detects_multiple_entity_types_mapped_to_the_same_stored_procedure()
{
db.HasBaseType((string)null);
db.OwnsOne(d => d.SomeTestMethods).DeleteUsingStoredProcedure("Delete",
- s => s.HasParameter("DerivedTestMethodsId"));
+ s => s.HasOriginalValueParameter("DerivedTestMethodsId"));
db.OwnsOne(d => d.OtherTestMethods).DeleteUsingStoredProcedure("Delete",
- s => s.HasParameter("DerivedTestMethodsId"));
+ s => s.HasOriginalValueParameter("DerivedTestMethodsId"));
});
VerifyError(
@@ -2668,7 +2668,7 @@ public virtual void Detects_tableless_entity_type_mapped_to_some_stored_procedur
.Ignore(a => a.FavoritePerson)
.ToTable((string)null)
.InsertUsingStoredProcedure(s => s.HasParameter(c => c.Id).HasParameter(c => c.Name))
- .UpdateUsingStoredProcedure(s => s.HasParameter(c => c.Id).HasParameter(c => c.Name))
+ .UpdateUsingStoredProcedure(s => s.HasOriginalValueParameter(c => c.Id).HasParameter(c => c.Name))
.Property(a => a.Id).ValueGeneratedNever();
VerifyError(
@@ -2714,16 +2714,16 @@ public virtual void Detects_derived_entity_type_mapped_to_a_different_stored_pro
}
[ConditionalFact]
- public virtual void Detects_missing_stored_procedure_parameters_in_TPH()
+ public virtual void Detects_missing_generated_stored_procedure_parameters_in_TPH()
{
var modelBuilder = CreateConventionModelBuilder();
modelBuilder.Entity()
.UpdateUsingStoredProcedure("Update", s => s
- .HasParameter(a => a.Id, p => p.HasName("MyId"))
+ .HasOriginalValueParameter(a => a.Id, p => p.HasName("MyId"))
.HasParameter(a => a.Name)
.HasParameter((Cat c) => c.Breed)
.HasResultColumn(a => a.Name))
- .Property(a => a.Name).ValueGeneratedOnUpdate();
+ .Property(a => a.Name).ValueGeneratedOnUpdate().Metadata.SetAfterSaveBehavior(PropertySaveBehavior.Save);
modelBuilder.Entity();
VerifyError(
@@ -2749,8 +2749,9 @@ public virtual void Detects_non_key_delete_stored_procedure_params_in_TPH()
{
var modelBuilder = CreateConventionModelBuilder();
modelBuilder.Entity()
- .DeleteUsingStoredProcedure(s => s.HasParameter(a => a.Id)
- .HasParameter(a => a.Name))
+ .DeleteUsingStoredProcedure(s => s
+ .HasOriginalValueParameter(a => a.Id)
+ .HasOriginalValueParameter(a => a.Name))
.Property(a => a.Name).ValueGeneratedOnUpdate();
VerifyError(
@@ -2819,7 +2820,9 @@ public virtual void Detects_non_generated_update_stored_procedure_result_columns
var modelBuilder = CreateConventionModelBuilder();
modelBuilder.Entity()
.Ignore(a => a.FavoritePerson)
- .UpdateUsingStoredProcedure(s => s.HasParameter(a => a.Id).HasParameter(a => a.Name))
+ .UpdateUsingStoredProcedure(s => s
+ .HasOriginalValueParameter(a => a.Id)
+ .HasParameter(a => a.Name))
.UseTptMappingStrategy();
modelBuilder.Entity()
.UpdateUsingStoredProcedure("Update", s => s.HasResultColumn(c => c.Breed));
@@ -2849,7 +2852,9 @@ public virtual void Detects_non_generated_update_stored_procedure_input_output_p
var modelBuilder = CreateConventionModelBuilder();
modelBuilder.Entity()
.Ignore(a => a.FavoritePerson)
- .UpdateUsingStoredProcedure(s => s.HasParameter(a => a.Id).HasParameter(a => a.Name, p => p.IsInputOutput()));
+ .UpdateUsingStoredProcedure(s => s
+ .HasOriginalValueParameter(a => a.Id)
+ .HasParameter(a => a.Name, p => p.IsInputOutput()));
VerifyError(
RelationalStrings.StoredProcedureOutputParameterNotGenerated(nameof(Animal), nameof(Animal.Name), "Animal_Update"),
@@ -2875,10 +2880,10 @@ public virtual void Detects_generated_properties_mapped_to_result_and_parameter(
var modelBuilder = CreateConventionModelBuilder();
modelBuilder.Entity()
.UpdateUsingStoredProcedure(s => s
- .HasParameter(a => a.Id)
+ .HasOriginalValueParameter(a => a.Id)
.HasParameter(a => a.Name, p => p.IsInputOutput())
.HasResultColumn(a => a.Name))
- .Property(a => a.Name).ValueGeneratedOnUpdate();
+ .Property(a => a.Name).ValueGeneratedOnUpdate().Metadata.SetAfterSaveBehavior(PropertySaveBehavior.Save);;
VerifyError(
RelationalStrings.StoredProcedureResultColumnParameterConflict(nameof(Animal), nameof(Animal.Name), "Animal_Update"),
@@ -2891,7 +2896,7 @@ public virtual void Detects_generated_properties_mapped_to_original_and_current_
var modelBuilder = CreateConventionModelBuilder();
modelBuilder.Entity()
.UpdateUsingStoredProcedure(s => s
- .HasParameter(a => a.Id)
+ .HasOriginalValueParameter(a => a.Id)
.HasParameter(a => a.Name, p => p.IsOutput())
.HasOriginalValueParameter(a => a.Name, p => p.IsInputOutput().HasName("OriginalName")))
.Property(a => a.Name).ValueGeneratedOnUpdate();
@@ -2901,13 +2906,39 @@ public virtual void Detects_generated_properties_mapped_to_original_and_current_
modelBuilder);
}
+ [ConditionalFact]
+ public virtual void Detects_original_value_parameter_on_insert_stored_procedure()
+ {
+ var modelBuilder = CreateConventionModelBuilder();
+ modelBuilder.Entity()
+ .InsertUsingStoredProcedure(s => s
+ .HasParameter(a => a.Id)
+ .HasOriginalValueParameter(a => a.Name));
+
+ VerifyError(
+ RelationalStrings.StoredProcedureOriginalValueParameterOnInsert(nameof(Animal.Name) + "_Original", "Animal_Insert"),
+ modelBuilder);
+ }
+
+ [ConditionalFact]
+ public virtual void Detects_current_value_parameter_on_delete_stored_procedure()
+ {
+ var modelBuilder = CreateConventionModelBuilder();
+ modelBuilder.Entity()
+ .DeleteUsingStoredProcedure(s => s.HasParameter(a => a.Id));
+
+ VerifyError(
+ RelationalStrings.StoredProcedureCurrentValueParameterOnDelete(nameof(Animal.Id), "Animal_Delete"),
+ modelBuilder);
+ }
+
[ConditionalFact]
public virtual void Detects_unmapped_concurrency_token()
{
var modelBuilder = CreateConventionModelBuilder();
modelBuilder.Entity()
.UpdateUsingStoredProcedure(s => s
- .HasParameter(a => a.Id)
+ .HasOriginalValueParameter(a => a.Id)
.HasParameter("FavoritePersonId")
.HasParameter(a => a.Name, p => p.IsOutput())
.HasRowsAffectedReturnValue())
@@ -2925,7 +2956,7 @@ public virtual void Detects_rows_affected_with_result_columns()
modelBuilder.Entity()
.UpdateUsingStoredProcedure(
s => s
- .HasParameter(a => a.Id)
+ .HasOriginalValueParameter(a => a.Id)
.HasParameter("FavoritePersonId")
.HasResultColumn(a => a.Name)
.HasRowsAffectedReturnValue())
@@ -2936,16 +2967,101 @@ public virtual void Detects_rows_affected_with_result_columns()
modelBuilder);
}
+ [ConditionalFact]
+ public virtual void Detects_rows_affected_on_insert_stored_procedure()
+ {
+ var modelBuilder = CreateConventionModelBuilder();
+ modelBuilder.Entity()
+ .InsertUsingStoredProcedure(
+ s => s
+ .HasParameter(a => a.Id, pb => pb.IsOutput())
+ .HasParameter("FavoritePersonId")
+ .HasParameter(a => a.Name)
+ .HasRowsAffectedReturnValue());
+
+ VerifyError(
+ RelationalStrings.StoredProcedureRowsAffectedForInsert("Animal_Insert"),
+ modelBuilder);
+ }
+
+ [ConditionalFact]
+ public virtual void Detects_stored_procedure_input_parameter_for_insert_non_save_property()
+ {
+ var modelBuilder = CreateConventionModelBuilder();
+ modelBuilder.Entity()
+ .InsertUsingStoredProcedure(
+ "Insert",
+ s => s
+ .HasParameter(a => a.Id, pb => pb.IsOutput())
+ .HasParameter("FavoritePersonId")
+ .HasParameter(a => a.Name))
+ .Property(b => b.Name).Metadata.SetBeforeSaveBehavior(PropertySaveBehavior.Ignore);
+
+ VerifyError(
+ RelationalStrings.StoredProcedureInputParameterForInsertNonSaveProperty(
+ nameof(Animal.Name), "Insert", nameof(Animal.Name), nameof(Animal), nameof(PropertySaveBehavior.Ignore)),
+ modelBuilder);
+ }
+
+ [ConditionalFact]
+ public virtual void Detects_stored_procedure_input_parameter_for_update_non_save_property()
+ {
+ var modelBuilder = CreateConventionModelBuilder();
+ modelBuilder.Entity()
+ .UpdateUsingStoredProcedure(
+ "Update",
+ s => s
+ .HasOriginalValueParameter(a => a.Id)
+ .HasParameter("FavoritePersonId")
+ .HasParameter(a => a.Name))
+ .Property(b => b.Name).Metadata.SetAfterSaveBehavior(PropertySaveBehavior.Ignore);
+
+ VerifyError(
+ RelationalStrings.StoredProcedureInputParameterForUpdateNonSaveProperty(
+ nameof(Animal.Name), "Update", nameof(Animal.Name), nameof(Animal), nameof(PropertySaveBehavior.Ignore)),
+ modelBuilder);
+ }
+
+ [ConditionalFact]
+ public virtual void Passes_for_stored_procedure_without_parameter_for_insert_non_save_property()
+ {
+ var modelBuilder = CreateConventionModelBuilder();
+ modelBuilder.Entity()
+ .InsertUsingStoredProcedure(
+ "Insert",
+ s => s
+ .HasParameter(a => a.Id, pb => pb.IsOutput())
+ .HasParameter("FavoritePersonId"))
+ .Property(b => b.Name).Metadata.SetBeforeSaveBehavior(PropertySaveBehavior.Ignore);
+
+ Validate(modelBuilder);
+ }
+
+ [ConditionalFact]
+ public virtual void Passes_for_stored_procedure_without_parameter_for_update_non_save_property()
+ {
+ var modelBuilder = CreateConventionModelBuilder();
+ modelBuilder.Entity()
+ .UpdateUsingStoredProcedure(
+ "Update",
+ s => s
+ .HasOriginalValueParameter(a => a.Id)
+ .HasParameter("FavoritePersonId"))
+ .Property(b => b.Name).Metadata.SetAfterSaveBehavior(PropertySaveBehavior.Ignore);
+
+ Validate(modelBuilder);
+ }
+
[ConditionalFact]
public virtual void Passes_on_valid_UsingDeleteStoredProcedure_in_TPT()
{
var modelBuilder = CreateConventionModelBuilder();
modelBuilder.Entity()
.UseTptMappingStrategy()
- .DeleteUsingStoredProcedure("Delete", s => s.HasParameter(a => a.Id))
+ .DeleteUsingStoredProcedure("Delete", s => s.HasOriginalValueParameter(a => a.Id))
.Property(a => a.Name).ValueGeneratedOnUpdate();
modelBuilder.Entity()
- .DeleteUsingStoredProcedure(s => s.HasParameter(a => a.Id));
+ .DeleteUsingStoredProcedure(s => s.HasOriginalValueParameter(a => a.Id));
Validate(modelBuilder);
}
@@ -2956,7 +3072,7 @@ public virtual void Passes_on_derived_entity_type_mapped_to_a_stored_procedure_i
var modelBuilder = CreateConventionModelBuilder();
modelBuilder.Entity().UseTptMappingStrategy();
modelBuilder.Entity().UpdateUsingStoredProcedure("Update", s => s
- .HasParameter(c => c.Id)
+ .HasOriginalValueParameter(c => c.Id)
.HasParameter(c => c.Breed)
.HasParameter(c => c.Identity));
@@ -2970,28 +3086,28 @@ public virtual void Passes_on_derived_entity_type_not_mapped_to_a_stored_procedu
modelBuilder.Entity()
.UseTptMappingStrategy()
.UpdateUsingStoredProcedure("Update", s => s
- .HasParameter(a => a.Id, p => p.HasName("MyId"))
+ .HasOriginalValueParameter(a => a.Id, p => p.HasName("MyId"))
.HasParameter(a => a.Name)
.HasParameter("FavoritePersonId")
.HasResultColumn(a => a.Name))
- .Property(a => a.Name).ValueGeneratedOnUpdate();
+ .Property(a => a.Name).ValueGeneratedOnUpdate().Metadata.SetAfterSaveBehavior(PropertySaveBehavior.Save);;
modelBuilder.Entity();
Validate(modelBuilder);
}
[ConditionalFact]
- public virtual void Detects_missing_stored_procedure_parameters_in_TPT()
+ public virtual void Detects_missing_generated_stored_procedure_parameters_in_TPT()
{
var modelBuilder = CreateConventionModelBuilder();
modelBuilder.Entity()
.UseTptMappingStrategy()
.UpdateUsingStoredProcedure("Update", s => s
- .HasParameter(a => a.Id, p => p.HasName("MyId"))
+ .HasOriginalValueParameter(a => a.Id, p => p.HasName("MyId"))
.HasParameter(a => a.Name)
.HasParameter("FavoritePersonId")
.HasResultColumn(a => a.Name))
- .Property(a => a.Name).ValueGeneratedOnUpdate();
+ .Property(a => a.Name).ValueGeneratedOnUpdate().Metadata.SetAfterSaveBehavior(PropertySaveBehavior.Save);;
modelBuilder.Entity()
.UpdateUsingStoredProcedure(s => s.HasParameter(c => c.Breed));
@@ -3008,7 +3124,7 @@ public virtual void Detects_missing_stored_procedure_parameters_for_abstract_pro
modelBuilder.Entity().UseTptMappingStrategy();
modelBuilder.Entity>()
.UpdateUsingStoredProcedure("Update", s => s
- .HasParameter(a => a.Id));
+ .HasOriginalValueParameter(a => a.Id));
VerifyError(
RelationalStrings.StoredProcedurePropertiesNotMapped("Generic", "Update", "{'P0', 'P1', 'P2', 'P3'}"),
@@ -3062,11 +3178,11 @@ public virtual void Detects_missing_generated_stored_procedure_parameters()
var modelBuilder = CreateConventionModelBuilder();
modelBuilder.Entity()
.UpdateUsingStoredProcedure("Update", s => s
- .HasParameter(a => a.Id, p => p.HasName("MyId"))
+ .HasOriginalValueParameter(a => a.Id, p => p.HasName("MyId"))
.HasParameter(a => a.Name)
.HasParameter("FavoritePersonId")
.HasParameter(a => a.Name))
- .Property(a => a.Name).ValueGeneratedOnUpdate();
+ .Property(a => a.Name).ValueGeneratedOnUpdate().Metadata.SetAfterSaveBehavior(PropertySaveBehavior.Save);
VerifyError(
RelationalStrings.StoredProcedureGeneratedPropertiesNotMapped(nameof(Animal),
@@ -3075,17 +3191,17 @@ public virtual void Detects_missing_generated_stored_procedure_parameters()
}
[ConditionalFact]
- public virtual void Detects_missing_stored_procedure_parameters_in_TPC()
+ public virtual void Detects_missing_generated_stored_procedure_parameters_in_TPC()
{
var modelBuilder = CreateConventionModelBuilder();
modelBuilder.Entity()
.UseTpcMappingStrategy()
.UpdateUsingStoredProcedure("Update", s => s
- .HasParameter(a => a.Id, p => p.HasName("MyId"))
+ .HasOriginalValueParameter(a => a.Id, p => p.HasName("MyId"))
.HasParameter(a => a.Name)
.HasParameter("FavoritePersonId")
.HasResultColumn(a => a.Name))
- .Property(a => a.Name).ValueGeneratedOnUpdate();
+ .Property(a => a.Name).ValueGeneratedOnUpdate().Metadata.SetAfterSaveBehavior(PropertySaveBehavior.Save);
modelBuilder.Entity()
.UpdateUsingStoredProcedure(s => s
.HasResultColumn(a => a.Name)
diff --git a/test/EFCore.SqlServer.FunctionalTests/Update/StoredProcedureUpdateSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Update/StoredProcedureUpdateSqlServerTest.cs
index 8a5a0352e76..89a11a91daa 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Update/StoredProcedureUpdateSqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Update/StoredProcedureUpdateSqlServerTest.cs
@@ -98,7 +98,7 @@ public override async Task Update_partial(bool async)
@p2='8'
SET NOCOUNT ON;
-EXEC [WithTwoOutputParameters_Update] @p0, @p1, @p2;");
+EXEC [WithTwoInputParameters_Update] @p0, @p1, @p2;");
}
public override async Task Update_with_output_parameter_and_rows_affected_result_column(bool async)
@@ -228,9 +228,9 @@ public override async Task Rows_affected_return_value_and_concurrency_failure(bo
EXEC @p0 = [WithRowsAffectedReturnValue_Update] @p1, @p2;");
}
- public override async Task Store_generated_concurrency_token_as_inout_parameter(bool async)
+ public override async Task Store_generated_concurrency_token_as_in_out_parameter(bool async)
{
- await base.Store_generated_concurrency_token_as_inout_parameter(async);
+ await base.Store_generated_concurrency_token_as_in_out_parameter(async);
// Can't assert SQL baseline as usual because the concurrency token changes
Assert.Contains("(Size = 8) (Direction = InputOutput)", Fixture.TestSqlLoggerFactory.Sql);
@@ -240,7 +240,7 @@ public override async Task Store_generated_concurrency_token_as_inout_parameter(
@p3='0' (Direction = Output)
SET NOCOUNT ON;
-EXEC [WithStoreGeneratedConcurrencyTokenAsInoutParameter_Update] @p0, @p1 OUTPUT, @p2, @p3 OUTPUT;",
+EXEC [WithStoreGeneratedConcurrencyTokenAsInOutParameter_Update] @p0, @p1 OUTPUT, @p2, @p3 OUTPUT;",
Fixture.TestSqlLoggerFactory.Sql.Substring(Fixture.TestSqlLoggerFactory.Sql.IndexOf("@p2", StringComparison.Ordinal)),
ignoreLineEndingDifferences: true);
@@ -249,7 +249,7 @@ public override async Task Store_generated_concurrency_token_as_inout_parameter(
@p3='0' (Direction = Output)
SET NOCOUNT ON;
-EXEC [WithStoreGeneratedConcurrencyTokenAsInoutParameter_Update] @p0, @p1 OUTPUT, @p2, @p3 OUTPUT;",
+EXEC [WithStoreGeneratedConcurrencyTokenAsInOutParameter_Update] @p0, @p1 OUTPUT, @p2, @p3 OUTPUT;",
Fixture.TestSqlLoggerFactory.Sql.Substring(Fixture.TestSqlLoggerFactory.Sql.IndexOf("@p2", StringComparison.Ordinal)),
ignoreLineEndingDifferences: true);
}
@@ -365,11 +365,28 @@ public override async Task Tpc(bool async)
EXEC [TpcChild_Insert] @p0 OUTPUT, @p1, @p2;");
}
- public override async Task Input_output_parameter_on_non_concurrency_token(bool async)
+ public override async Task Input_or_output_parameter_with_input(bool async)
{
- await base.Input_output_parameter_on_non_concurrency_token(async);
+ await base.Input_or_output_parameter_with_input(async);
- AssertSql();
+ AssertSql(
+ @"@p0='1' (Direction = Output)
+@p1=NULL (Nullable = false) (Size = 4000) (Direction = InputOutput)
+
+SET NOCOUNT ON;
+EXEC [WithInputOrOutputParameter_Insert] @p0 OUTPUT, @p1 OUTPUT;");
+ }
+
+ public override async Task Input_or_output_parameter_with_output(bool async)
+ {
+ await base.Input_or_output_parameter_with_output(async);
+
+ AssertSql(
+ @"@p0='1' (Direction = Output)
+@p1='Some default value' (Nullable = false) (Size = 4000) (Direction = InputOutput)
+
+SET NOCOUNT ON;
+EXEC [WithInputOrOutputParameter_Insert] @p0 OUTPUT, @p1 OUTPUT;");
}
[ConditionalFact]
@@ -392,7 +409,7 @@ public override void CleanData()
private const string CleanDataSql = @"
-- Regular tables without foreign keys
-TRUNCATE TABLE [WithInputOutputParameterOnNonConcurrencyToken];
+TRUNCATE TABLE [WithInputOrOutputParameter];
TRUNCATE TABLE [WithOriginalAndCurrentValueOnNonConcurrencyToken];
TRUNCATE TABLE [WithOutputParameter];
TRUNCATE TABLE [WithOutputParameterAndResultColumn];
@@ -402,9 +419,9 @@ public override void CleanData()
TRUNCATE TABLE [WithRowsAffectedParameter];
TRUNCATE TABLE [WithRowsAffectedResultColumn];
TRUNCATE TABLE [WithRowsAffectedReturnValue];
-TRUNCATE TABLE [WithStoreGeneratedConcurrencyTokenAsInoutParameter];
+TRUNCATE TABLE [WithStoreGeneratedConcurrencyTokenAsInOutParameter];
TRUNCATE TABLE [WithStoreGeneratedConcurrencyTokenAsTwoParameters];
-TRUNCATE TABLE [WithTwoOutputParameters];
+TRUNCATE TABLE [WithTwoInputParameters];
TRUNCATE TABLE [WithUserManagedConcurrencyToken];
TRUNCATE TABLE [Tph];
TRUNCATE TABLE [TpcChild];
@@ -478,8 +495,8 @@ AS BEGIN
GO
-CREATE PROCEDURE WithTwoOutputParameters_Update(@Id int, @Name varchar(max), @AdditionalProperty int)
-AS UPDATE [WithTwoOutputParameters] SET [Name] = @Name, [AdditionalProperty] = @AdditionalProperty WHERE [Id] = @id;
+CREATE PROCEDURE WithTwoInputParameters_Update(@Id int, @Name varchar(max), @AdditionalProperty int)
+AS UPDATE [WithTwoInputParameters] SET [Name] = @Name, [AdditionalProperty] = @AdditionalProperty WHERE [Id] = @id;
GO
@@ -507,9 +524,9 @@ AS BEGIN
GO
-CREATE PROCEDURE WithStoreGeneratedConcurrencyTokenAsInoutParameter_Update(@Id int, @ConcurrencyToken rowversion OUT, @Name varchar(max), @RowsAffected int OUT)
+CREATE PROCEDURE WithStoreGeneratedConcurrencyTokenAsInOutParameter_Update(@Id int, @ConcurrencyToken rowversion OUT, @Name varchar(max), @RowsAffected int OUT)
AS BEGIN
- UPDATE [WithStoreGeneratedConcurrencyTokenAsInoutParameter] SET [Name] = @Name WHERE [Id] = @Id AND [ConcurrencyToken] = @ConcurrencyToken;
+ UPDATE [WithStoreGeneratedConcurrencyTokenAsInOutParameter] SET [Name] = @Name WHERE [Id] = @Id AND [ConcurrencyToken] = @ConcurrencyToken;
SET @RowsAffected = @@ROWCOUNT;
END;
@@ -541,10 +558,20 @@ IF @NameCurrent <> @NameOriginal
GO
-CREATE PROCEDURE WithInputOutputParameterOnNonConcurrencyToken_Update(@Id int, @Name varchar(max) OUT)
+CREATE PROCEDURE WithInputOrOutputParameter_Insert(@Id int OUT, @Name varchar(max) OUT)
AS BEGIN
- SET @Name = @Name + 'WithSuffix';
- UPDATE [WithInputOutputParameterOnNonConcurrencyToken] SET [Name] = @Name WHERE [Id] = @Id;
+ IF @Name IS NULL
+ BEGIN
+ INSERT INTO [WithInputOrOutputParameter] ([Name]) VALUES ('Some default value');
+ SET @Name = 'Some default value';
+ END
+ ELSE
+ BEGIN
+ INSERT INTO [WithInputOrOutputParameter] ([Name]) VALUES (@Name);
+ SET @Name = NULL;
+ END
+
+ SET @Id = SCOPE_IDENTITY();
END;
GO