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