From bae981a8e8a0067fc4bf5d6e602cbacee8999ac6 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Wed, 12 Oct 2022 20:54:22 +0200 Subject: [PATCH] Redo sproc tests as NonSharedModelTestBase (#29331) --- test/Directory.Build.props | 2 +- .../StoredProcedureUpdateModel/Entity.cs | 10 - .../EntityWithAdditionalProperty.cs | 11 - .../EntityWithTwoAdditionalProperties.cs | 12 - .../StoredProcedureUpdateContext.cs | 63 -- .../StoredProcedureUpdateModel/TpcChild.cs | 9 - .../StoredProcedureUpdateModel/TpcParent.cs | 10 - .../StoredProcedureUpdateModel/TphChild1.cs | 9 - .../StoredProcedureUpdateModel/TphChild2.cs | 11 - .../StoredProcedureUpdateModel/TphParent.cs | 10 - .../StoredProcedureUpdateModel/TptChild.cs | 9 - .../TptMixedChild.cs | 9 - .../TptMixedParent.cs | 10 - .../StoredProcedureUpdateModel/TptParent.cs | 10 - .../StoredProcedureUpdateFixtureBase.cs | 290 ------- .../Update/StoredProcedureUpdateTestBase.cs | 804 ++++++++++++++---- .../TestUtilities/TestHelpers.cs | 6 +- .../StoredProcedureUpdateSqlServerTest.cs | 603 ++++++------- .../SqliteComplianceTest.cs | 2 +- 19 files changed, 904 insertions(+), 986 deletions(-) delete mode 100644 test/EFCore.Relational.Specification.Tests/TestModels/StoredProcedureUpdateModel/Entity.cs delete mode 100644 test/EFCore.Relational.Specification.Tests/TestModels/StoredProcedureUpdateModel/EntityWithAdditionalProperty.cs delete mode 100644 test/EFCore.Relational.Specification.Tests/TestModels/StoredProcedureUpdateModel/EntityWithTwoAdditionalProperties.cs delete mode 100644 test/EFCore.Relational.Specification.Tests/TestModels/StoredProcedureUpdateModel/StoredProcedureUpdateContext.cs delete mode 100644 test/EFCore.Relational.Specification.Tests/TestModels/StoredProcedureUpdateModel/TpcChild.cs delete mode 100644 test/EFCore.Relational.Specification.Tests/TestModels/StoredProcedureUpdateModel/TpcParent.cs delete mode 100644 test/EFCore.Relational.Specification.Tests/TestModels/StoredProcedureUpdateModel/TphChild1.cs delete mode 100644 test/EFCore.Relational.Specification.Tests/TestModels/StoredProcedureUpdateModel/TphChild2.cs delete mode 100644 test/EFCore.Relational.Specification.Tests/TestModels/StoredProcedureUpdateModel/TphParent.cs delete mode 100644 test/EFCore.Relational.Specification.Tests/TestModels/StoredProcedureUpdateModel/TptChild.cs delete mode 100644 test/EFCore.Relational.Specification.Tests/TestModels/StoredProcedureUpdateModel/TptMixedChild.cs delete mode 100644 test/EFCore.Relational.Specification.Tests/TestModels/StoredProcedureUpdateModel/TptMixedParent.cs delete mode 100644 test/EFCore.Relational.Specification.Tests/TestModels/StoredProcedureUpdateModel/TptParent.cs delete mode 100644 test/EFCore.Relational.Specification.Tests/Update/StoredProcedureUpdateFixtureBase.cs diff --git a/test/Directory.Build.props b/test/Directory.Build.props index d964d5ceeb2..b6beb5ada16 100644 --- a/test/Directory.Build.props +++ b/test/Directory.Build.props @@ -2,7 +2,7 @@ - $(NoWarn);CA1707;1591;xUnit1000;xUnit1003;xUnit1004;xUnit1010;xUnit1013;xUnit1026;xUnit2013 + $(NoWarn);CA1707;1591;xUnit1000;xUnit1003;xUnit1004;xUnit1010;xUnit1013;xUnit1026;xUnit2013;xUnit1024 $(MSBuildThisFileDirectory)..\rulesets\EFCore.test.ruleset diff --git a/test/EFCore.Relational.Specification.Tests/TestModels/StoredProcedureUpdateModel/Entity.cs b/test/EFCore.Relational.Specification.Tests/TestModels/StoredProcedureUpdateModel/Entity.cs deleted file mode 100644 index 6868071dbef..00000000000 --- a/test/EFCore.Relational.Specification.Tests/TestModels/StoredProcedureUpdateModel/Entity.cs +++ /dev/null @@ -1,10 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.EntityFrameworkCore.TestModels.StoredProcedureUpdateModel; - -public class Entity -{ - public int Id { get; set; } - public string Name { get; set; } -} diff --git a/test/EFCore.Relational.Specification.Tests/TestModels/StoredProcedureUpdateModel/EntityWithAdditionalProperty.cs b/test/EFCore.Relational.Specification.Tests/TestModels/StoredProcedureUpdateModel/EntityWithAdditionalProperty.cs deleted file mode 100644 index 3cc4a830b0f..00000000000 --- a/test/EFCore.Relational.Specification.Tests/TestModels/StoredProcedureUpdateModel/EntityWithAdditionalProperty.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.EntityFrameworkCore.TestModels.StoredProcedureUpdateModel; - -public class EntityWithAdditionalProperty -{ - public int Id { get; set; } - public string Name { get; set; } - public int AdditionalProperty { get; set; } -} diff --git a/test/EFCore.Relational.Specification.Tests/TestModels/StoredProcedureUpdateModel/EntityWithTwoAdditionalProperties.cs b/test/EFCore.Relational.Specification.Tests/TestModels/StoredProcedureUpdateModel/EntityWithTwoAdditionalProperties.cs deleted file mode 100644 index b29334dd227..00000000000 --- a/test/EFCore.Relational.Specification.Tests/TestModels/StoredProcedureUpdateModel/EntityWithTwoAdditionalProperties.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.EntityFrameworkCore.TestModels.StoredProcedureUpdateModel; - -public class EntityWithTwoAdditionalProperties -{ - public int Id { get; set; } - public string Name { get; set; } - public int AdditionalProperty1 { get; set; } - public int AdditionalProperty2 { get; set; } -} diff --git a/test/EFCore.Relational.Specification.Tests/TestModels/StoredProcedureUpdateModel/StoredProcedureUpdateContext.cs b/test/EFCore.Relational.Specification.Tests/TestModels/StoredProcedureUpdateModel/StoredProcedureUpdateContext.cs deleted file mode 100644 index 4e976c1af93..00000000000 --- a/test/EFCore.Relational.Specification.Tests/TestModels/StoredProcedureUpdateModel/StoredProcedureUpdateContext.cs +++ /dev/null @@ -1,63 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.EntityFrameworkCore.TestModels.StoredProcedureUpdateModel; - -public class StoredProcedureUpdateContext : PoolableDbContext -{ - public StoredProcedureUpdateContext(DbContextOptions options) - : base(options) - { - } - - public DbSet WithOutputParameter - => Set(nameof(WithOutputParameter)); - - public DbSet WithResultColumn - => Set(nameof(WithResultColumn)); - - public DbSet WithTwoResultColumns - => Set(nameof(WithTwoResultColumns)); - - public DbSet WithOutputParameterAndResultColumn - => Set(nameof(WithOutputParameterAndResultColumn)); - - public DbSet WithOutputParameterAndRowsAffectedResultColumn - => Set(nameof(WithOutputParameterAndRowsAffectedResultColumn)); - - public DbSet WithTwoInputParameters - => Set(nameof(WithTwoInputParameters)); - - public DbSet WithRowsAffectedParameter - => Set(nameof(WithRowsAffectedParameter)); - - public DbSet WithRowsAffectedResultColumn - => Set(nameof(WithRowsAffectedResultColumn)); - - public DbSet WithRowsAffectedReturnValue - => Set(nameof(WithRowsAffectedReturnValue)); - - public DbSet WithStoreGeneratedConcurrencyTokenAsInOutParameter - => Set(nameof(WithStoreGeneratedConcurrencyTokenAsInOutParameter)); - - public DbSet WithStoreGeneratedConcurrencyTokenAsTwoParameters - => Set(nameof(WithStoreGeneratedConcurrencyTokenAsTwoParameters)); - - public DbSet WithUserManagedConcurrencyToken - => Set(nameof(WithUserManagedConcurrencyToken)); - - public DbSet WithOriginalAndCurrentValueOnNonConcurrencyToken - => Set(nameof(WithOriginalAndCurrentValueOnNonConcurrencyToken)); - - public DbSet WithInputOrOutputParameter - => Set(nameof(WithInputOrOutputParameter)); - - public DbSet TphParent { get; set; } - public DbSet TphChild { get; set; } - public DbSet TptParent { get; set; } - public DbSet TptChild { get; set; } - public DbSet TptMixedParent { get; set; } - public DbSet TptMixedChild { get; set; } - public DbSet TpcParent { get; set; } - public DbSet TpcChild { get; set; } -} diff --git a/test/EFCore.Relational.Specification.Tests/TestModels/StoredProcedureUpdateModel/TpcChild.cs b/test/EFCore.Relational.Specification.Tests/TestModels/StoredProcedureUpdateModel/TpcChild.cs deleted file mode 100644 index dc68342567f..00000000000 --- a/test/EFCore.Relational.Specification.Tests/TestModels/StoredProcedureUpdateModel/TpcChild.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.EntityFrameworkCore.TestModels.StoredProcedureUpdateModel; - -public class TpcChild : TpcParent -{ - public int ChildProperty { get; set; } -} diff --git a/test/EFCore.Relational.Specification.Tests/TestModels/StoredProcedureUpdateModel/TpcParent.cs b/test/EFCore.Relational.Specification.Tests/TestModels/StoredProcedureUpdateModel/TpcParent.cs deleted file mode 100644 index 801a45b95d2..00000000000 --- a/test/EFCore.Relational.Specification.Tests/TestModels/StoredProcedureUpdateModel/TpcParent.cs +++ /dev/null @@ -1,10 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.EntityFrameworkCore.TestModels.StoredProcedureUpdateModel; - -public class TpcParent -{ - public int Id { get; set; } - public string Name { get; set; } -} diff --git a/test/EFCore.Relational.Specification.Tests/TestModels/StoredProcedureUpdateModel/TphChild1.cs b/test/EFCore.Relational.Specification.Tests/TestModels/StoredProcedureUpdateModel/TphChild1.cs deleted file mode 100644 index 1278bb97aca..00000000000 --- a/test/EFCore.Relational.Specification.Tests/TestModels/StoredProcedureUpdateModel/TphChild1.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.EntityFrameworkCore.TestModels.StoredProcedureUpdateModel; - -public class TphChild1 : TphParent -{ - public int Child1Property { get; set; } -} diff --git a/test/EFCore.Relational.Specification.Tests/TestModels/StoredProcedureUpdateModel/TphChild2.cs b/test/EFCore.Relational.Specification.Tests/TestModels/StoredProcedureUpdateModel/TphChild2.cs deleted file mode 100644 index 3fdf4059531..00000000000 --- a/test/EFCore.Relational.Specification.Tests/TestModels/StoredProcedureUpdateModel/TphChild2.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.EntityFrameworkCore.TestModels.StoredProcedureUpdateModel; - -public class TphChild2 : TphParent -{ - public int Child2InputProperty { get; set; } - public int Child2OutputParameterProperty { get; set; } - public int Child2ResultColumnProperty { get; set; } -} diff --git a/test/EFCore.Relational.Specification.Tests/TestModels/StoredProcedureUpdateModel/TphParent.cs b/test/EFCore.Relational.Specification.Tests/TestModels/StoredProcedureUpdateModel/TphParent.cs deleted file mode 100644 index b8583fe1525..00000000000 --- a/test/EFCore.Relational.Specification.Tests/TestModels/StoredProcedureUpdateModel/TphParent.cs +++ /dev/null @@ -1,10 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.EntityFrameworkCore.TestModels.StoredProcedureUpdateModel; - -public class TphParent -{ - public int Id { get; set; } - public string Name { get; set; } -} diff --git a/test/EFCore.Relational.Specification.Tests/TestModels/StoredProcedureUpdateModel/TptChild.cs b/test/EFCore.Relational.Specification.Tests/TestModels/StoredProcedureUpdateModel/TptChild.cs deleted file mode 100644 index 3d3d2e8266c..00000000000 --- a/test/EFCore.Relational.Specification.Tests/TestModels/StoredProcedureUpdateModel/TptChild.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.EntityFrameworkCore.TestModels.StoredProcedureUpdateModel; - -public class TptChild : TptParent -{ - public int ChildProperty { get; set; } -} diff --git a/test/EFCore.Relational.Specification.Tests/TestModels/StoredProcedureUpdateModel/TptMixedChild.cs b/test/EFCore.Relational.Specification.Tests/TestModels/StoredProcedureUpdateModel/TptMixedChild.cs deleted file mode 100644 index b37fc9fc324..00000000000 --- a/test/EFCore.Relational.Specification.Tests/TestModels/StoredProcedureUpdateModel/TptMixedChild.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.EntityFrameworkCore.TestModels.StoredProcedureUpdateModel; - -public class TptMixedChild : TptMixedParent -{ - public int ChildProperty { get; set; } -} diff --git a/test/EFCore.Relational.Specification.Tests/TestModels/StoredProcedureUpdateModel/TptMixedParent.cs b/test/EFCore.Relational.Specification.Tests/TestModels/StoredProcedureUpdateModel/TptMixedParent.cs deleted file mode 100644 index 209f056bee1..00000000000 --- a/test/EFCore.Relational.Specification.Tests/TestModels/StoredProcedureUpdateModel/TptMixedParent.cs +++ /dev/null @@ -1,10 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.EntityFrameworkCore.TestModels.StoredProcedureUpdateModel; - -public class TptMixedParent -{ - public int Id { get; set; } - public string Name { get; set; } -} diff --git a/test/EFCore.Relational.Specification.Tests/TestModels/StoredProcedureUpdateModel/TptParent.cs b/test/EFCore.Relational.Specification.Tests/TestModels/StoredProcedureUpdateModel/TptParent.cs deleted file mode 100644 index fd5646fe143..00000000000 --- a/test/EFCore.Relational.Specification.Tests/TestModels/StoredProcedureUpdateModel/TptParent.cs +++ /dev/null @@ -1,10 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.EntityFrameworkCore.TestModels.StoredProcedureUpdateModel; - -public class TptParent -{ - public int Id { get; set; } - public string Name { get; set; } -} diff --git a/test/EFCore.Relational.Specification.Tests/Update/StoredProcedureUpdateFixtureBase.cs b/test/EFCore.Relational.Specification.Tests/Update/StoredProcedureUpdateFixtureBase.cs deleted file mode 100644 index dfa204c5d1e..00000000000 --- a/test/EFCore.Relational.Specification.Tests/Update/StoredProcedureUpdateFixtureBase.cs +++ /dev/null @@ -1,290 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.EntityFrameworkCore.TestModels.StoredProcedureUpdateModel; - -namespace Microsoft.EntityFrameworkCore.Update; - -#nullable enable - -public abstract class StoredProcedureUpdateFixtureBase : SharedStoreFixtureBase -{ - protected override string StoreName - => "StoredProcedureUpdateTest"; - - protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) - { - modelBuilder.SharedTypeEntity(nameof(StoredProcedureUpdateContext.WithOutputParameter)) - .InsertUsingStoredProcedure( - nameof(StoredProcedureUpdateContext.WithOutputParameter) + "_Insert", - spb => spb - .HasParameter(w => w.Name) - .HasParameter(w => w.Id, pb => pb.IsOutput())) - .UpdateUsingStoredProcedure( - nameof(StoredProcedureUpdateContext.WithOutputParameter) + "_Update", - spb => spb - .HasOriginalValueParameter(w => w.Id) - .HasParameter(w => w.Name)) - .DeleteUsingStoredProcedure( - nameof(StoredProcedureUpdateContext.WithOutputParameter) + "_Delete", - spb => spb.HasOriginalValueParameter(w => w.Id)); - - modelBuilder.SharedTypeEntity(nameof(StoredProcedureUpdateContext.WithResultColumn)) - .InsertUsingStoredProcedure( - nameof(StoredProcedureUpdateContext.WithResultColumn) + "_Insert", spb => spb - .HasParameter(w => w.Name) - .HasResultColumn(w => w.Id)); - - modelBuilder.SharedTypeEntity( - nameof(StoredProcedureUpdateContext.WithTwoResultColumns), - b => - { - b.Property(w => w.AdditionalProperty).HasComputedColumnSql("8"); - - b.InsertUsingStoredProcedure( - "WithTwoResultColumns_Insert", spb => spb - .HasParameter(w => w.Name) - .HasResultColumn(w => w.AdditionalProperty) - .HasResultColumn(w => w.Id)); - }); - - modelBuilder.SharedTypeEntity( - nameof(StoredProcedureUpdateContext.WithOutputParameterAndResultColumn), - b => - { - b.Property(w => w.AdditionalProperty).HasComputedColumnSql("8"); - - b.InsertUsingStoredProcedure( - "WithOutputParameterAndResultColumn_Insert", spb => spb - .HasParameter(w => w.Id, pb => pb.IsOutput()) - .HasParameter(w => w.Name) - .HasResultColumn(w => w.AdditionalProperty)); - }); - - modelBuilder.SharedTypeEntity( - nameof(StoredProcedureUpdateContext.WithOutputParameterAndRowsAffectedResultColumn), - b => - { - b.Property(w => w.AdditionalProperty).HasComputedColumnSql("8"); - - b.UpdateUsingStoredProcedure( - nameof(StoredProcedureUpdateContext.WithOutputParameterAndRowsAffectedResultColumn) + "_Update", - spb => spb - .HasOriginalValueParameter(w => w.Id) - .HasParameter(w => w.Name) - .HasParameter(w => w.AdditionalProperty, pb => pb.IsOutput()) - .HasRowsAffectedResultColumn()); - }); - - modelBuilder.SharedTypeEntity(nameof(StoredProcedureUpdateContext.WithTwoInputParameters)) - .UpdateUsingStoredProcedure( - nameof(StoredProcedureUpdateContext.WithTwoInputParameters) + "_Update", spb => spb - .HasOriginalValueParameter(w => w.Id) - .HasParameter(w => w.Name) - .HasParameter(w => w.AdditionalProperty)); - - modelBuilder.SharedTypeEntity(nameof(StoredProcedureUpdateContext.WithRowsAffectedParameter)) - .UpdateUsingStoredProcedure( - nameof(StoredProcedureUpdateContext.WithRowsAffectedParameter) + "_Update", - spb => spb - .HasOriginalValueParameter(w => w.Id) - .HasParameter(w => w.Name) - .HasRowsAffectedParameter()); - - modelBuilder.SharedTypeEntity(nameof(StoredProcedureUpdateContext.WithRowsAffectedResultColumn)) - .UpdateUsingStoredProcedure( - nameof(StoredProcedureUpdateContext.WithRowsAffectedResultColumn) + "_Update", - spb => spb - .HasOriginalValueParameter(w => w.Id) - .HasParameter(w => w.Name) - .HasRowsAffectedResultColumn()); - - modelBuilder.SharedTypeEntity(nameof(StoredProcedureUpdateContext.WithRowsAffectedReturnValue)) - .UpdateUsingStoredProcedure( - nameof(StoredProcedureUpdateContext.WithRowsAffectedReturnValue) + "_Update", - spb => spb - .HasOriginalValueParameter(w => w.Id) - .HasParameter(w => w.Name) - .HasRowsAffectedReturnValue()); - - modelBuilder.SharedTypeEntity( - nameof(StoredProcedureUpdateContext.WithStoreGeneratedConcurrencyTokenAsInOutParameter), - b => - { - ConfigureStoreGeneratedConcurrencyToken(b, "ConcurrencyToken"); - - b.UpdateUsingStoredProcedure( - nameof(StoredProcedureUpdateContext.WithStoreGeneratedConcurrencyTokenAsInOutParameter) + "_Update", - spb => spb - .HasOriginalValueParameter(w => w.Id) - .HasOriginalValueParameter("ConcurrencyToken", pb => pb.IsInputOutput()) - .HasParameter(w => w.Name) - .HasRowsAffectedParameter()); - }); - - modelBuilder.SharedTypeEntity( - nameof(StoredProcedureUpdateContext.WithStoreGeneratedConcurrencyTokenAsTwoParameters), - b => - { - ConfigureStoreGeneratedConcurrencyToken(b, "ConcurrencyToken"); - - b.UpdateUsingStoredProcedure( - nameof(StoredProcedureUpdateContext.WithStoreGeneratedConcurrencyTokenAsTwoParameters) + "_Update", - spb => spb - .HasOriginalValueParameter(w => w.Id) - .HasOriginalValueParameter("ConcurrencyToken", pb => pb.HasName("ConcurrencyTokenIn")) - .HasParameter(w => w.Name) - .HasParameter( - "ConcurrencyToken", pb => pb - .HasName("ConcurrencyTokenOut") - .IsOutput()) - .HasRowsAffectedParameter()); - }); - - modelBuilder.SharedTypeEntity( - nameof(StoredProcedureUpdateContext.WithUserManagedConcurrencyToken), - b => - { - b.Property(e => e.AdditionalProperty).IsConcurrencyToken(); - - b.UpdateUsingStoredProcedure( - nameof(StoredProcedureUpdateContext.WithUserManagedConcurrencyToken) + "_Update", - spb => spb - .HasOriginalValueParameter(w => w.Id) - .HasOriginalValueParameter(w => w.AdditionalProperty, pb => pb.HasName("ConcurrencyTokenOriginal")) - .HasParameter(w => w.Name) - .HasParameter(w => w.AdditionalProperty, pb => pb.HasName("ConcurrencyTokenCurrent")) - .HasRowsAffectedParameter()); - }); - - modelBuilder.SharedTypeEntity(nameof(StoredProcedureUpdateContext.WithOriginalAndCurrentValueOnNonConcurrencyToken)) - .UpdateUsingStoredProcedure( - nameof(StoredProcedureUpdateContext.WithOriginalAndCurrentValueOnNonConcurrencyToken) + "_Update", - spb => spb - .HasOriginalValueParameter(w => w.Id) - .HasParameter(w => w.Name, pb => pb.HasName("NameCurrent")) - .HasOriginalValueParameter(w => w.Name, pb => pb.HasName("NameOriginal"))); - - modelBuilder.SharedTypeEntity( - nameof(StoredProcedureUpdateContext.WithInputOrOutputParameter), - b => - { - b.Property(w => w.Name).IsRequired().ValueGeneratedOnAdd(); - - b.InsertUsingStoredProcedure( - nameof(StoredProcedureUpdateContext.WithInputOrOutputParameter) + "_Insert", - spb => spb - .HasParameter(w => w.Id, pb => pb.IsOutput()) - .HasParameter(w => w.Name, pb => pb.IsInputOutput())); - }); - - modelBuilder.Entity(); - - modelBuilder.Entity( - b => - { - b.Property(w => w.Child2OutputParameterProperty).HasDefaultValue(8); - b.Property(w => w.Child2ResultColumnProperty).HasDefaultValue(9); - }); - - modelBuilder.Entity( - b => - { - b.ToTable("Tph"); - - b.InsertUsingStoredProcedure( - "Tph_Insert", - spb => spb - .HasParameter(w => w.Id, pb => pb.IsOutput()) - .HasParameter("Discriminator") - .HasParameter(w => w.Name) - .HasParameter(nameof(TphChild2.Child2InputProperty)) - .HasParameter(nameof(TphChild2.Child2OutputParameterProperty), o => o.IsOutput()) - .HasParameter(nameof(TphChild1.Child1Property)) - .HasResultColumn(nameof(TphChild2.Child2ResultColumnProperty))); - }); - - modelBuilder.Entity( - b => - { - b.UseTptMappingStrategy(); - - b.InsertUsingStoredProcedure( - "TptParent_Insert", - spb => spb - .HasParameter(w => w.Id, pb => pb.IsOutput()) - .HasParameter(w => w.Name)); - }); - - // TODO: The following fails validation: - // The entity type 'TptChild' is mapped to the stored procedure 'TptChild_Insert', however the store-generated properties {'Id'} are not mapped to any output parameter or result column. - modelBuilder.Entity() - .InsertUsingStoredProcedure( - "TptChild_Insert", - spb => spb - .HasParameter(w => w.Id) - .HasParameter(w => w.ChildProperty)); - - modelBuilder.Entity( - b => - { - b.UseTptMappingStrategy(); - - b.InsertUsingStoredProcedure( - "TptMixedParent_Insert", - spb => spb - .HasParameter(w => w.Id, pb => pb.IsOutput()) - .HasParameter(w => w.Name)); - }); - - // No sproc mapping for TptMixedChild, use regular SQL - - modelBuilder.Entity().UseTpcMappingStrategy(); - - modelBuilder.Entity() - .UseTpcMappingStrategy() - .InsertUsingStoredProcedure( - "TpcChild_Insert", - spb => spb - .HasParameter(w => w.Id, pb => pb.IsOutput()) - .HasParameter(w => w.Name) - .HasParameter(w => w.ChildProperty)); - } - - /// - /// A method to be implement by the provider, to set up a store-generated concurrency token shadow property with the given name. - /// - protected abstract void ConfigureStoreGeneratedConcurrencyToken(EntityTypeBuilder entityTypeBuilder, string propertyName); - - public virtual void CleanData() - { - using var context = CreateContext(); - - context.WithOutputParameter.RemoveRange(context.WithOutputParameter); - context.WithResultColumn.RemoveRange(context.WithResultColumn); - context.WithTwoResultColumns.RemoveRange(context.WithTwoResultColumns); - context.WithOutputParameterAndResultColumn.RemoveRange(context.WithOutputParameterAndResultColumn); - 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.WithStoreGeneratedConcurrencyTokenAsTwoParameters.RemoveRange(context.WithStoreGeneratedConcurrencyTokenAsTwoParameters); - context.WithUserManagedConcurrencyToken.RemoveRange(context.WithUserManagedConcurrencyToken); - context.WithOriginalAndCurrentValueOnNonConcurrencyToken.RemoveRange(context.WithOriginalAndCurrentValueOnNonConcurrencyToken); - context.WithInputOrOutputParameter.RemoveRange(context.WithInputOrOutputParameter); - context.TphParent.RemoveRange(context.TphParent); - context.TphChild.RemoveRange(context.TphChild); - context.TptParent.RemoveRange(context.TptParent); - context.TptChild.RemoveRange(context.TptChild); - context.TptMixedParent.RemoveRange(context.TptMixedParent); - context.TptMixedChild.RemoveRange(context.TptMixedChild); - context.TpcParent.RemoveRange(context.TpcParent); - context.TpcChild.RemoveRange(context.TpcChild); - - context.SaveChanges(); - } - - public TestSqlLoggerFactory TestSqlLoggerFactory - => (TestSqlLoggerFactory)ListLoggerFactory; -} diff --git a/test/EFCore.Relational.Specification.Tests/Update/StoredProcedureUpdateTestBase.cs b/test/EFCore.Relational.Specification.Tests/Update/StoredProcedureUpdateTestBase.cs index 6a7abce9896..2f7be4ce72d 100644 --- a/test/EFCore.Relational.Specification.Tests/Update/StoredProcedureUpdateTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Update/StoredProcedureUpdateTestBase.cs @@ -1,119 +1,183 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.EntityFrameworkCore.TestModels.StoredProcedureUpdateModel; +using System.Text.RegularExpressions; namespace Microsoft.EntityFrameworkCore.Update; -#nullable enable - -public class StoredProcedureUpdateTestBase : IClassFixture - where TFixture : StoredProcedureUpdateFixtureBase +public abstract class StoredProcedureUpdateTestBase : NonSharedModelTestBase { - protected StoredProcedureUpdateTestBase(TFixture fixture) - { - Fixture = fixture; - - fixture.CleanData(); - - ClearLog(); - } + protected override string StoreName + => "StoredProcedureUpdateTest"; [ConditionalTheory] [MemberData(nameof(IsAsyncData))] - public virtual async Task Insert_with_output_parameter(bool async) + public abstract Task Insert_with_output_parameter(bool async); + + protected async Task Insert_with_output_parameter(bool async, string createSprocSql) { - await using var context = CreateContext(); + var contextFactory = await InitializeAsync( + modelBuilder => modelBuilder.Entity() + .InsertUsingStoredProcedure( + nameof(Entity) + "_Insert", + spb => spb + .HasParameter(w => w.Name) + .HasParameter(w => w.Id, pb => pb.IsOutput())), + seed: ctx => CreateStoredProcedures(ctx, createSprocSql)); + + await using var context = contextFactory.CreateContext(); var newEntity1 = new Entity { Name = "New" }; - context.WithOutputParameter.Add(newEntity1); + context.Set().Add(newEntity1); await SaveChanges(context, async); - using (Fixture.TestSqlLoggerFactory.SuspendRecordingEvents()) + using (TestSqlLoggerFactory.SuspendRecordingEvents()) { - Assert.Equal("New", context.WithOutputParameter.Single(b => b.Id == newEntity1.Id).Name); + Assert.Equal("New", context.Set().Single(b => b.Id == newEntity1.Id).Name); } } [ConditionalTheory] [MemberData(nameof(IsAsyncData))] - public virtual async Task Insert_twice_with_output_parameter(bool async) + public abstract Task Insert_twice_with_output_parameter(bool async); + + protected async Task Insert_twice_with_output_parameter(bool async, string createSprocSql) { - await using var context = CreateContext(); + var contextFactory = await InitializeAsync( + modelBuilder => modelBuilder.Entity() + .InsertUsingStoredProcedure( + nameof(Entity) + "_Insert", + spb => spb + .HasParameter(w => w.Name) + .HasParameter(w => w.Id, pb => pb.IsOutput())), + seed: ctx => CreateStoredProcedures(ctx, createSprocSql)); + + await using var context = contextFactory.CreateContext(); var (newEntity1, newEntity2) = (new Entity { Name = "New1" }, new Entity { Name = "New2" }); - context.WithOutputParameter.AddRange(newEntity1, newEntity2); + context.Set().AddRange(newEntity1, newEntity2); await SaveChanges(context, async); - using (Fixture.TestSqlLoggerFactory.SuspendRecordingEvents()) + using (TestSqlLoggerFactory.SuspendRecordingEvents()) { - Assert.Equal("New1", context.WithOutputParameter.Single(b => b.Id == newEntity1.Id).Name); - Assert.Equal("New2", context.WithOutputParameter.Single(b => b.Id == newEntity2.Id).Name); + Assert.Equal("New1", context.Set().Single(b => b.Id == newEntity1.Id).Name); + Assert.Equal("New2", context.Set().Single(b => b.Id == newEntity2.Id).Name); } } [ConditionalTheory] [MemberData(nameof(IsAsyncData))] - public virtual async Task Insert_with_result_column(bool async) + public abstract Task Insert_with_result_column(bool async); + + protected async Task Insert_with_result_column(bool async, string createSprocSql) { - await using var context = CreateContext(); + var contextFactory = await InitializeAsync( + modelBuilder => modelBuilder.Entity().InsertUsingStoredProcedure( + nameof(Entity) + "_Insert", spb => spb + .HasParameter(w => w.Name) + .HasResultColumn(w => w.Id)), + seed: ctx => CreateStoredProcedures(ctx, createSprocSql)); + + await using var context = contextFactory.CreateContext(); var entity = new Entity { Name = "Foo" }; - context.WithResultColumn.Add(entity); + context.Set().Add(entity); await SaveChanges(context, async); - using (Fixture.TestSqlLoggerFactory.SuspendRecordingEvents()) + using (TestSqlLoggerFactory.SuspendRecordingEvents()) { - Assert.Equal("Foo", context.WithResultColumn.Single(b => b.Id == entity.Id).Name); + Assert.Equal("Foo", context.Set().Single(b => b.Id == entity.Id).Name); } } [ConditionalTheory] [MemberData(nameof(IsAsyncData))] - public virtual async Task Insert_with_two_result_columns(bool async) + public abstract Task Insert_with_two_result_columns(bool async); + + protected async Task Insert_with_two_result_columns(bool async, string createSprocSql) { - await using var context = CreateContext(); + var contextFactory = await InitializeAsync( + modelBuilder => modelBuilder.Entity( + b => + { + b.Property(w => w.AdditionalProperty).HasComputedColumnSql("8"); + + b.InsertUsingStoredProcedure( + nameof(EntityWithAdditionalProperty) + "_Insert", spb => spb + .HasParameter(w => w.Name) + .HasResultColumn(w => w.AdditionalProperty) + .HasResultColumn(w => w.Id)); + }), + seed: ctx => CreateStoredProcedures(ctx, createSprocSql)); + + await using var context = contextFactory.CreateContext(); var entity = new EntityWithAdditionalProperty { Name = "Foo" }; - context.WithTwoResultColumns.Add(entity); + context.Set().Add(entity); await SaveChanges(context, async); Assert.Equal(1, entity.Id); Assert.Equal(8, entity.AdditionalProperty); - using (Fixture.TestSqlLoggerFactory.SuspendRecordingEvents()) + using (TestSqlLoggerFactory.SuspendRecordingEvents()) { - Assert.Equal("Foo", context.WithTwoResultColumns.Single(b => b.Id == entity.Id).Name); + Assert.Equal("Foo", context.Set().Single(b => b.Id == entity.Id).Name); } } [ConditionalTheory] [MemberData(nameof(IsAsyncData))] - public virtual async Task Insert_with_output_parameter_and_result_column(bool async) + public abstract Task Insert_with_output_parameter_and_result_column(bool async); + + protected async Task Insert_with_output_parameter_and_result_column(bool async, string createSprocSql) { - await using var context = CreateContext(); + var contextFactory = await InitializeAsync( + modelBuilder => modelBuilder.Entity( + b => + { + b.Property(w => w.AdditionalProperty).HasComputedColumnSql("8"); + + b.InsertUsingStoredProcedure( + nameof(EntityWithAdditionalProperty) + "_Insert", spb => spb + .HasParameter(w => w.Id, pb => pb.IsOutput()) + .HasParameter(w => w.Name) + .HasResultColumn(w => w.AdditionalProperty)); + }), + seed: ctx => CreateStoredProcedures(ctx, createSprocSql)); + + await using var context = contextFactory.CreateContext(); var entity = new EntityWithAdditionalProperty { Name = "Foo" }; - context.WithOutputParameterAndResultColumn.Add(entity); + context.Set().Add(entity); await SaveChanges(context, async); Assert.Equal(8, entity.AdditionalProperty); - using (Fixture.TestSqlLoggerFactory.SuspendRecordingEvents()) + using (TestSqlLoggerFactory.SuspendRecordingEvents()) { - Assert.Equal("Foo", context.WithOutputParameterAndResultColumn.Single(b => b.Id == entity.Id).Name); + Assert.Equal("Foo", context.Set().Single(b => b.Id == entity.Id).Name); } } [ConditionalTheory] [MemberData(nameof(IsAsyncData))] - public virtual async Task Update(bool async) + public abstract Task Update(bool async); + + protected async Task Update(bool async, string createSprocSql) { - await using var context = CreateContext(); + var contextFactory = await InitializeAsync( + modelBuilder => modelBuilder.Entity().UpdateUsingStoredProcedure( + nameof(Entity) + "_Update", + spb => spb + .HasOriginalValueParameter(w => w.Id) + .HasParameter(w => w.Name)), + seed: ctx => CreateStoredProcedures(ctx, createSprocSql)); + + await using var context = contextFactory.CreateContext(); var entity = new Entity { Name = "Initial" }; - context.WithOutputParameter.Add(entity); + context.Set().Add(entity); await SaveChanges(context, async); ClearLog(); @@ -121,20 +185,30 @@ public virtual async Task Update(bool async) entity.Name = "Updated"; await SaveChanges(context, async); - using (Fixture.TestSqlLoggerFactory.SuspendRecordingEvents()) + using (TestSqlLoggerFactory.SuspendRecordingEvents()) { - Assert.Equal("Updated", (await context.WithOutputParameter.SingleAsync(w => w.Id == entity.Id)).Name); + Assert.Equal("Updated", (await context.Set().SingleAsync(w => w.Id == entity.Id)).Name); } } [ConditionalTheory] [MemberData(nameof(IsAsyncData))] - public virtual async Task Update_partial(bool async) + public abstract Task Update_partial(bool async); + + protected async Task Update_partial(bool async, string createSprocSql) { - await using var context = CreateContext(); + var contextFactory = await InitializeAsync( + modelBuilder => modelBuilder.Entity().UpdateUsingStoredProcedure( + nameof(EntityWithAdditionalProperty) + "_Update", spb => spb + .HasOriginalValueParameter(w => w.Id) + .HasParameter(w => w.Name) + .HasParameter(w => w.AdditionalProperty)), + seed: ctx => CreateStoredProcedures(ctx, createSprocSql)); + + await using var context = contextFactory.CreateContext(); var entity = new EntityWithAdditionalProperty { Name = "Foo", AdditionalProperty = 8 }; - context.WithTwoInputParameters.Add(entity); + context.Set().Add(entity); await context.SaveChangesAsync(); entity.Name = "Updated"; @@ -143,9 +217,9 @@ public virtual async Task Update_partial(bool async) await SaveChanges(context, async); - using (Fixture.TestSqlLoggerFactory.SuspendRecordingEvents()) + using (TestSqlLoggerFactory.SuspendRecordingEvents()) { - var actual = await context.WithTwoInputParameters.SingleAsync(w => w.Id == entity.Id); + var actual = await context.Set().SingleAsync(w => w.Id == entity.Id); Assert.Equal("Updated", actual.Name); Assert.Equal(8, actual.AdditionalProperty); @@ -154,12 +228,30 @@ public virtual async Task Update_partial(bool async) [ConditionalTheory] [MemberData(nameof(IsAsyncData))] - public virtual async Task Update_with_output_parameter_and_rows_affected_result_column(bool async) + public abstract Task Update_with_output_parameter_and_rows_affected_result_column(bool async); + + protected async Task Update_with_output_parameter_and_rows_affected_result_column(bool async, string createSprocSql) { - await using var context = CreateContext(); + var contextFactory = await InitializeAsync( + modelBuilder => modelBuilder.Entity( + b => + { + b.Property(w => w.AdditionalProperty).HasComputedColumnSql("8"); + + b.UpdateUsingStoredProcedure( + nameof(EntityWithAdditionalProperty) + "_Update", + spb => spb + .HasOriginalValueParameter(w => w.Id) + .HasParameter(w => w.Name) + .HasParameter(w => w.AdditionalProperty, pb => pb.IsOutput()) + .HasRowsAffectedResultColumn()); + }), + seed: ctx => CreateStoredProcedures(ctx, createSprocSql)); + + await using var context = contextFactory.CreateContext(); var entity = new EntityWithAdditionalProperty { Name = "Foo" }; - context.WithOutputParameterAndRowsAffectedResultColumn.Add(entity); + context.Set().Add(entity); await context.SaveChangesAsync(); entity.Name = "Updated"; @@ -168,9 +260,9 @@ public virtual async Task Update_with_output_parameter_and_rows_affected_result_ await SaveChanges(context, async); - using (Fixture.TestSqlLoggerFactory.SuspendRecordingEvents()) + using (TestSqlLoggerFactory.SuspendRecordingEvents()) { - var actual = await context.WithOutputParameterAndRowsAffectedResultColumn.SingleAsync(w => w.Id == entity.Id); + var actual = await context.Set().SingleAsync(w => w.Id == entity.Id); Assert.Equal("Updated", actual.Name); Assert.Equal(8, actual.AdditionalProperty); @@ -179,18 +271,36 @@ public virtual async Task Update_with_output_parameter_and_rows_affected_result_ [ConditionalTheory] [MemberData(nameof(IsAsyncData))] - public virtual async Task Update_with_output_parameter_and_rows_affected_result_column_concurrency_failure(bool async) + public abstract Task Update_with_output_parameter_and_rows_affected_result_column_concurrency_failure(bool async); + + protected async Task Update_with_output_parameter_and_rows_affected_result_column_concurrency_failure(bool async, string createSprocSql) { - await using var context1 = CreateContext(); + var contextFactory = await InitializeAsync( + modelBuilder => modelBuilder.Entity( + b => + { + b.Property(w => w.AdditionalProperty).HasComputedColumnSql("8"); + + b.UpdateUsingStoredProcedure( + nameof(EntityWithAdditionalProperty) + "_Update", + spb => spb + .HasOriginalValueParameter(w => w.Id) + .HasParameter(w => w.Name) + .HasParameter(w => w.AdditionalProperty, pb => pb.IsOutput()) + .HasRowsAffectedResultColumn()); + }), + seed: ctx => CreateStoredProcedures(ctx, createSprocSql)); + + await using var context1 = contextFactory.CreateContext(); var entity1 = new EntityWithAdditionalProperty { Name = "Initial" }; - context1.WithOutputParameterAndRowsAffectedResultColumn.Add(entity1); + context1.Set().Add(entity1); await context1.SaveChangesAsync(); - await using (var context2 = CreateContext()) + await using (var context2 = contextFactory.CreateContext()) { - var entity2 = await context2.WithOutputParameterAndRowsAffectedResultColumn.SingleAsync(w => w.Name == "Initial"); - context2.WithOutputParameterAndRowsAffectedResultColumn.Remove(entity2); + var entity2 = await context2.Set().SingleAsync(w => w.Name == "Initial"); + context2.Set().Remove(entity2); await context2.SaveChangesAsync(); } @@ -205,56 +315,91 @@ public virtual async Task Update_with_output_parameter_and_rows_affected_result_ [ConditionalTheory] [MemberData(nameof(IsAsyncData))] - public virtual async Task Delete(bool async) + public abstract Task Delete(bool async); + + protected async Task Delete(bool async, string createSprocSql) { - await using var context = CreateContext(); + var contextFactory = await InitializeAsync( + modelBuilder => modelBuilder.Entity() + .DeleteUsingStoredProcedure( + nameof(Entity) + "_Delete", + spb => spb.HasOriginalValueParameter(w => w.Id)), + seed: ctx => CreateStoredProcedures(ctx, createSprocSql)); + + await using var context = contextFactory.CreateContext(); var entity = new Entity { Name = "Initial" }; - context.WithOutputParameter.Add(entity); + context.Set().Add(entity); await context.SaveChangesAsync(); ClearLog(); - context.WithOutputParameter.Remove(entity); + context.Set().Remove(entity); await SaveChanges(context, async); - using (Fixture.TestSqlLoggerFactory.SuspendRecordingEvents()) + using (TestSqlLoggerFactory.SuspendRecordingEvents()) { - Assert.Equal(0, await context.WithOutputParameter.CountAsync(b => b.Name == "Initial")); + Assert.Equal(0, await context.Set().CountAsync(b => b.Name == "Initial")); } } [ConditionalTheory] [MemberData(nameof(IsAsyncData))] - public virtual async Task Delete_and_insert(bool async) + public abstract Task Delete_and_insert(bool async); + + protected async Task Delete_and_insert(bool async, string createSprocSql) { - await using var context = CreateContext(); + var contextFactory = await InitializeAsync( + modelBuilder => modelBuilder.Entity() + .InsertUsingStoredProcedure( + nameof(Entity) + "_Insert", + spb => spb + .HasParameter(w => w.Name) + .HasParameter(w => w.Id, pb => pb.IsOutput())) + .DeleteUsingStoredProcedure( + nameof(Entity) + "_Delete", + spb => spb.HasOriginalValueParameter(w => w.Id)), + seed: ctx => CreateStoredProcedures(ctx, createSprocSql)); + + await using var context = contextFactory.CreateContext(); var entity1 = new Entity { Name = "Entity1" }; - context.WithOutputParameter.Add(entity1); + context.Set().Add(entity1); await context.SaveChangesAsync(); ClearLog(); - context.WithOutputParameter.Remove(entity1); - context.WithOutputParameter.Add(new Entity { Name = "Entity2" }); + context.Set().Remove(entity1); + context.Set().Add(new Entity { Name = "Entity2" }); await SaveChanges(context, async); - using (Fixture.TestSqlLoggerFactory.SuspendRecordingEvents()) + using (TestSqlLoggerFactory.SuspendRecordingEvents()) { - Assert.Equal(0, await context.WithOutputParameter.CountAsync(b => b.Name == "Entity1")); - Assert.Equal(1, await context.WithOutputParameter.CountAsync(b => b.Name == "Entity2")); + Assert.Equal(0, await context.Set().CountAsync(b => b.Name == "Entity1")); + Assert.Equal(1, await context.Set().CountAsync(b => b.Name == "Entity2")); } } [ConditionalTheory] [MemberData(nameof(IsAsyncData))] - public virtual async Task Rows_affected_parameter(bool async) + public abstract Task Rows_affected_parameter(bool async); + + protected async Task Rows_affected_parameter(bool async, string createSprocSql) { - await using var context = CreateContext(); + var contextFactory = await InitializeAsync( + modelBuilder => modelBuilder.Entity() + .UpdateUsingStoredProcedure( + nameof(Entity) + "_Update", + spb => spb + .HasOriginalValueParameter(w => w.Id) + .HasParameter(w => w.Name) + .HasRowsAffectedParameter()), + seed: ctx => CreateStoredProcedures(ctx, createSprocSql)); + + await using var context = contextFactory.CreateContext(); var entity = new Entity { Name = "Initial" }; - context.WithRowsAffectedParameter.Add(entity); + context.Set().Add(entity); await context.SaveChangesAsync(); ClearLog(); @@ -263,26 +408,38 @@ public virtual async Task Rows_affected_parameter(bool async) await SaveChanges(context, async); - using (Fixture.TestSqlLoggerFactory.SuspendRecordingEvents()) + using (TestSqlLoggerFactory.SuspendRecordingEvents()) { - Assert.Equal("Updated", (await context.WithRowsAffectedParameter.SingleAsync(w => w.Id == entity.Id)).Name); + Assert.Equal("Updated", (await context.Set().SingleAsync(w => w.Id == entity.Id)).Name); } } [ConditionalTheory] [MemberData(nameof(IsAsyncData))] - public virtual async Task Rows_affected_parameter_and_concurrency_failure(bool async) + public abstract Task Rows_affected_parameter_and_concurrency_failure(bool async); + + protected async Task Rows_affected_parameter_and_concurrency_failure(bool async, string createSprocSql) { - await using var context1 = CreateContext(); + var contextFactory = await InitializeAsync( + modelBuilder => modelBuilder.Entity() + .UpdateUsingStoredProcedure( + nameof(Entity) + "_Update", + spb => spb + .HasOriginalValueParameter(w => w.Id) + .HasParameter(w => w.Name) + .HasRowsAffectedParameter()), + seed: ctx => CreateStoredProcedures(ctx, createSprocSql)); + + await using var context1 = contextFactory.CreateContext(); var entity1 = new Entity { Name = "Initial" }; - context1.WithRowsAffectedParameter.Add(entity1); + context1.Set().Add(entity1); await context1.SaveChangesAsync(); - await using (var context2 = CreateContext()) + await using (var context2 = contextFactory.CreateContext()) { - var entity2 = await context2.WithRowsAffectedParameter.SingleAsync(w => w.Name == "Initial"); - context2.WithRowsAffectedParameter.Remove(entity2); + var entity2 = await context2.Set().SingleAsync(w => w.Name == "Initial"); + context2.Set().Remove(entity2); await context2.SaveChangesAsync(); } @@ -297,12 +454,24 @@ public virtual async Task Rows_affected_parameter_and_concurrency_failure(bool a [ConditionalTheory] [MemberData(nameof(IsAsyncData))] - public virtual async Task Rows_affected_result_column(bool async) + public abstract Task Rows_affected_result_column(bool async); + + protected async Task Rows_affected_result_column(bool async, string createSprocSql) { - await using var context = CreateContext(); + var contextFactory = await InitializeAsync( + modelBuilder => modelBuilder.Entity() + .UpdateUsingStoredProcedure( + nameof(Entity) + "_Update", + spb => spb + .HasOriginalValueParameter(w => w.Id) + .HasParameter(w => w.Name) + .HasRowsAffectedResultColumn()), + seed: ctx => CreateStoredProcedures(ctx, createSprocSql)); + + await using var context = contextFactory.CreateContext(); var entity = new Entity { Name = "Initial" }; - context.WithRowsAffectedResultColumn.Add(entity); + context.Set().Add(entity); await context.SaveChangesAsync(); ClearLog(); @@ -311,26 +480,38 @@ public virtual async Task Rows_affected_result_column(bool async) await SaveChanges(context, async); - using (Fixture.TestSqlLoggerFactory.SuspendRecordingEvents()) + using (TestSqlLoggerFactory.SuspendRecordingEvents()) { - Assert.Equal("Updated", (await context.WithRowsAffectedResultColumn.SingleAsync(w => w.Id == entity.Id)).Name); + Assert.Equal("Updated", (await context.Set().SingleAsync(w => w.Id == entity.Id)).Name); } } [ConditionalTheory] [MemberData(nameof(IsAsyncData))] - public virtual async Task Rows_affected_result_column_and_concurrency_failure(bool async) + public abstract Task Rows_affected_result_column_and_concurrency_failure(bool async); + + protected async Task Rows_affected_result_column_and_concurrency_failure(bool async, string createSprocSql) { - await using var context1 = CreateContext(); + var contextFactory = await InitializeAsync( + modelBuilder => modelBuilder.Entity() + .UpdateUsingStoredProcedure( + nameof(Entity) + "_Update", + spb => spb + .HasOriginalValueParameter(w => w.Id) + .HasParameter(w => w.Name) + .HasRowsAffectedResultColumn()), + seed: ctx => CreateStoredProcedures(ctx, createSprocSql)); + + await using var context1 = contextFactory.CreateContext(); var entity1 = new Entity { Name = "Initial" }; - context1.WithRowsAffectedResultColumn.Add(entity1); + context1.Set().Add(entity1); await context1.SaveChangesAsync(); - await using (var context2 = CreateContext()) + await using (var context2 = contextFactory.CreateContext()) { - var entity2 = await context2.WithRowsAffectedResultColumn.SingleAsync(w => w.Name == "Initial"); - context2.WithRowsAffectedResultColumn.Remove(entity2); + var entity2 = await context2.Set().SingleAsync(w => w.Name == "Initial"); + context2.Set().Remove(entity2); await context2.SaveChangesAsync(); } @@ -345,12 +526,24 @@ public virtual async Task Rows_affected_result_column_and_concurrency_failure(bo [ConditionalTheory] [MemberData(nameof(IsAsyncData))] - public virtual async Task Rows_affected_return_value(bool async) + public abstract Task Rows_affected_return_value(bool async); + + protected async Task Rows_affected_return_value(bool async, string createSprocSql) { - await using var context = CreateContext(); + var contextFactory = await InitializeAsync( + modelBuilder => modelBuilder.Entity() + .UpdateUsingStoredProcedure( + nameof(Entity) + "_Update", + spb => spb + .HasOriginalValueParameter(w => w.Id) + .HasParameter(w => w.Name) + .HasRowsAffectedReturnValue()), + seed: ctx => CreateStoredProcedures(ctx, createSprocSql)); + + await using var context = contextFactory.CreateContext(); var entity = new Entity { Name = "Initial" }; - context.WithRowsAffectedReturnValue.Add(entity); + context.Set().Add(entity); await context.SaveChangesAsync(); ClearLog(); @@ -359,26 +552,38 @@ public virtual async Task Rows_affected_return_value(bool async) await SaveChanges(context, async); - using (Fixture.TestSqlLoggerFactory.SuspendRecordingEvents()) + using (TestSqlLoggerFactory.SuspendRecordingEvents()) { - Assert.Equal("Updated", (await context.WithRowsAffectedReturnValue.SingleAsync(w => w.Id == entity.Id)).Name); + Assert.Equal("Updated", (await context.Set().SingleAsync(w => w.Id == entity.Id)).Name); } } [ConditionalTheory] [MemberData(nameof(IsAsyncData))] - public virtual async Task Rows_affected_return_value_and_concurrency_failure(bool async) + public abstract Task Rows_affected_return_value_and_concurrency_failure(bool async); + + protected async Task Rows_affected_return_value_and_concurrency_failure(bool async, string createSprocSql) { - await using var context1 = CreateContext(); + var contextFactory = await InitializeAsync( + modelBuilder => modelBuilder.Entity() + .UpdateUsingStoredProcedure( + nameof(Entity) + "_Update", + spb => spb + .HasOriginalValueParameter(w => w.Id) + .HasParameter(w => w.Name) + .HasRowsAffectedReturnValue()), + seed: ctx => CreateStoredProcedures(ctx, createSprocSql)); + + await using var context1 = contextFactory.CreateContext(); var entity1 = new Entity { Name = "Initial" }; - context1.WithRowsAffectedReturnValue.Add(entity1); + context1.Set().Add(entity1); await context1.SaveChangesAsync(); - await using (var context2 = CreateContext()) + await using (var context2 = contextFactory.CreateContext()) { - var entity2 = await context2.WithRowsAffectedReturnValue.SingleAsync(w => w.Name == "Initial"); - context2.WithRowsAffectedReturnValue.Remove(entity2); + var entity2 = await context2.Set().SingleAsync(w => w.Name == "Initial"); + context2.Set().Remove(entity2); await SaveChanges(context2, async); } @@ -393,17 +598,35 @@ 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_in_out_parameter(bool async) + public abstract Task Store_generated_concurrency_token_as_in_out_parameter(bool async); + + protected async Task Store_generated_concurrency_token_as_in_out_parameter(bool async, string createSprocSql) { - await using var context1 = CreateContext(); + var contextFactory = await InitializeAsync( + modelBuilder => modelBuilder.Entity( + b => + { + ConfigureStoreGeneratedConcurrencyToken(b, "ConcurrencyToken"); + + b.UpdateUsingStoredProcedure( + nameof(Entity) + "_Update", + spb => spb + .HasOriginalValueParameter(w => w.Id) + .HasOriginalValueParameter("ConcurrencyToken", pb => pb.IsInputOutput()) + .HasParameter(w => w.Name) + .HasRowsAffectedParameter()); + }), + seed: ctx => CreateStoredProcedures(ctx, createSprocSql)); + + await using var context1 = contextFactory.CreateContext(); var entity1 = new Entity { Name = "Initial" }; - context1.WithStoreGeneratedConcurrencyTokenAsInOutParameter.Add(entity1); + context1.Set().Add(entity1); await context1.SaveChangesAsync(); - await using (var context2 = CreateContext()) + await using (var context2 = contextFactory.CreateContext()) { - var entity2 = await context2.WithStoreGeneratedConcurrencyTokenAsInOutParameter.SingleAsync(w => w.Name == "Initial"); + var entity2 = await context2.Set().SingleAsync(w => w.Name == "Initial"); entity2.Name = "Preempted"; await SaveChanges(context2, async); } @@ -419,17 +642,39 @@ public virtual async Task Store_generated_concurrency_token_as_in_out_parameter( [ConditionalTheory] [MemberData(nameof(IsAsyncData))] - public virtual async Task Store_generated_concurrency_token_as_two_parameters(bool async) + public abstract Task Store_generated_concurrency_token_as_two_parameters(bool async); + + protected async Task Store_generated_concurrency_token_as_two_parameters(bool async, string createSprocSql) { - await using var context1 = CreateContext(); + var contextFactory = await InitializeAsync( + modelBuilder => modelBuilder.Entity( + b => + { + ConfigureStoreGeneratedConcurrencyToken(b, "ConcurrencyToken"); + + b.UpdateUsingStoredProcedure( + nameof(Entity) + "_Update", + spb => spb + .HasOriginalValueParameter(w => w.Id) + .HasOriginalValueParameter("ConcurrencyToken", pb => pb.HasName("ConcurrencyTokenIn")) + .HasParameter(w => w.Name) + .HasParameter( + "ConcurrencyToken", pb => pb + .HasName("ConcurrencyTokenOut") + .IsOutput()) + .HasRowsAffectedParameter()); + }), + seed: ctx => CreateStoredProcedures(ctx, createSprocSql)); + + await using var context1 = contextFactory.CreateContext(); var entity1 = new Entity { Name = "Initial" }; - context1.WithStoreGeneratedConcurrencyTokenAsTwoParameters.Add(entity1); + context1.Set().Add(entity1); await context1.SaveChangesAsync(); - await using (var context2 = CreateContext()) + await using (var context2 = contextFactory.CreateContext()) { - var entity2 = await context2.WithStoreGeneratedConcurrencyTokenAsTwoParameters.SingleAsync(w => w.Name == "Initial"); + var entity2 = await context2.Set().SingleAsync(w => w.Name == "Initial"); entity2.Name = "Preempted"; await SaveChanges(context2, async); } @@ -445,24 +690,43 @@ public virtual async Task Store_generated_concurrency_token_as_two_parameters(bo [ConditionalTheory] [MemberData(nameof(IsAsyncData))] - public virtual async Task User_managed_concurrency_token(bool async) + public abstract Task User_managed_concurrency_token(bool async); + + protected async Task User_managed_concurrency_token(bool async, string createSprocSql) { - await using var context1 = CreateContext(); + var contextFactory = await InitializeAsync( + modelBuilder => modelBuilder.Entity( + b => + { + b.Property(e => e.AdditionalProperty).IsConcurrencyToken(); + + b.UpdateUsingStoredProcedure( + nameof(EntityWithAdditionalProperty) + "_Update", + spb => spb + .HasOriginalValueParameter(w => w.Id) + .HasOriginalValueParameter(w => w.AdditionalProperty, pb => pb.HasName("ConcurrencyTokenOriginal")) + .HasParameter(w => w.Name) + .HasParameter(w => w.AdditionalProperty, pb => pb.HasName("ConcurrencyTokenCurrent")) + .HasRowsAffectedParameter()); + }), + seed: ctx => CreateStoredProcedures(ctx, createSprocSql)); + + await using var context1 = contextFactory.CreateContext(); var entity1 = new EntityWithAdditionalProperty { Name = "Initial", AdditionalProperty = 8 // The concurrency token }; - context1.WithUserManagedConcurrencyToken.Add(entity1); + context1.Set().Add(entity1); await context1.SaveChangesAsync(); entity1.Name = "Updated"; entity1.AdditionalProperty = 9; - await using (var context2 = CreateContext()) + await using (var context2 = contextFactory.CreateContext()) { - var entity2 = await context2.WithUserManagedConcurrencyToken.SingleAsync(w => w.Name == "Initial"); + var entity2 = await context2.Set().SingleAsync(w => w.Name == "Initial"); entity2.Name = "Preempted"; entity2.AdditionalProperty = 999; await SaveChanges(context2, async); @@ -476,13 +740,25 @@ public virtual async Task User_managed_concurrency_token(bool async) [ConditionalTheory] [MemberData(nameof(IsAsyncData))] - public virtual async Task Original_and_current_value_on_non_concurrency_token(bool async) + public abstract Task Original_and_current_value_on_non_concurrency_token(bool async); + + protected async Task Original_and_current_value_on_non_concurrency_token(bool async, string createSprocSql) { - await using var context = CreateContext(); + var contextFactory = await InitializeAsync( + modelBuilder => modelBuilder.Entity() + .UpdateUsingStoredProcedure( + nameof(Entity) + "_Update", + spb => spb + .HasOriginalValueParameter(w => w.Id) + .HasParameter(w => w.Name, pb => pb.HasName("NameCurrent")) + .HasOriginalValueParameter(w => w.Name, pb => pb.HasName("NameOriginal"))), + seed: ctx => CreateStoredProcedures(ctx, createSprocSql)); + + await using var context = contextFactory.CreateContext(); var entity = new Entity { Name = "Initial" }; - context.WithOriginalAndCurrentValueOnNonConcurrencyToken.Add(entity); + context.Set().Add(entity); await context.SaveChangesAsync(); entity.Name = "Updated"; @@ -491,67 +767,132 @@ public virtual async Task Original_and_current_value_on_non_concurrency_token(bo await SaveChanges(context, async); - using (Fixture.TestSqlLoggerFactory.SuspendRecordingEvents()) + using (TestSqlLoggerFactory.SuspendRecordingEvents()) { Assert.Equal( "Updated", - (await context.WithOriginalAndCurrentValueOnNonConcurrencyToken.SingleAsync(w => w.Id == entity.Id)).Name); + (await context.Set().SingleAsync(w => w.Id == entity.Id)).Name); } } [ConditionalTheory] [MemberData(nameof(IsAsyncData))] - public virtual async Task Input_or_output_parameter_with_input(bool async) + public abstract Task Input_or_output_parameter_with_input(bool async); + + protected async Task Input_or_output_parameter_with_input(bool async, string createSprocSql) { - await using var context = CreateContext(); + var contextFactory = await InitializeAsync( + modelBuilder => modelBuilder.Entity( + b => + { + b.Property(w => w.Name).IsRequired().ValueGeneratedOnAdd(); + + b.InsertUsingStoredProcedure( + nameof(Entity) + "_Insert", + spb => spb + .HasParameter(w => w.Id, pb => pb.IsOutput()) + .HasParameter(w => w.Name, pb => pb.IsInputOutput())); + }), + seed: ctx => CreateStoredProcedures(ctx, createSprocSql)); + + await using var context = contextFactory.CreateContext(); var entity = new Entity { Name = "Initial" }; - context.WithInputOrOutputParameter.Add(entity); + context.Set().Add(entity); await SaveChanges(context, async); Assert.Equal("Initial", entity.Name); - using (Fixture.TestSqlLoggerFactory.SuspendRecordingEvents()) + using (TestSqlLoggerFactory.SuspendRecordingEvents()) { Assert.Same( - entity, await context.WithInputOrOutputParameter.SingleAsync(w => w.Id == entity.Id && w.Name == "Initial")); + entity, await context.Set().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) + public abstract Task Input_or_output_parameter_with_output(bool async); + + protected async Task Input_or_output_parameter_with_output(bool async, string createSprocSql) { - await using var context = CreateContext(); + var contextFactory = await InitializeAsync( + modelBuilder => modelBuilder.Entity( + b => + { + b.Property(w => w.Name).IsRequired().ValueGeneratedOnAdd(); + + b.InsertUsingStoredProcedure( + nameof(Entity) + "_Insert", + spb => spb + .HasParameter(w => w.Id, pb => pb.IsOutput()) + .HasParameter(w => w.Name, pb => pb.IsInputOutput())); + }), + seed: ctx => CreateStoredProcedures(ctx, createSprocSql)); + + await using var context = contextFactory.CreateContext(); var entity = new Entity(); - context.WithInputOrOutputParameter.Add(entity); + context.Set().Add(entity); await SaveChanges(context, async); Assert.Equal("Some default value", entity.Name); - using (Fixture.TestSqlLoggerFactory.SuspendRecordingEvents()) + using (TestSqlLoggerFactory.SuspendRecordingEvents()) { Assert.Same( - entity, await context.WithInputOrOutputParameter.SingleAsync(w => w.Id == entity.Id && w.Name == "Some default value")); + entity, await context.Set().SingleAsync(w => w.Id == entity.Id && w.Name == "Some default value")); } } [ConditionalTheory] [MemberData(nameof(IsAsyncData))] - public virtual async Task Tph(bool async) - { - await using var context = CreateContext(); + public abstract Task Tph(bool async); - var entity1 = new TphChild1 { Name = "Child", Child1Property = 8 }; - context.TphChild.Add(entity1); + protected async Task Tph(bool async, string createSprocSql) + { + var contextFactory = await InitializeAsync( + modelBuilder => + { + modelBuilder.Entity(); + + modelBuilder.Entity( + b => + { + b.Property(w => w.Child2OutputParameterProperty).HasDefaultValue(8); + b.Property(w => w.Child2ResultColumnProperty).HasDefaultValue(9); + }); + + modelBuilder.Entity( + b => + { + b.ToTable("Tph"); + + b.InsertUsingStoredProcedure( + "Tph_Insert", + spb => spb + .HasParameter(w => w.Id, pb => pb.IsOutput()) + .HasParameter("Discriminator") + .HasParameter(w => w.Name) + .HasParameter(nameof(Child2.Child2InputProperty)) + .HasParameter(nameof(Child2.Child2OutputParameterProperty), o => o.IsOutput()) + .HasParameter(nameof(Child1.Child1Property)) + .HasResultColumn(nameof(Child2.Child2ResultColumnProperty))); + }); + }, + seed: ctx => CreateStoredProcedures(ctx, createSprocSql)); + + await using var context = contextFactory.CreateContext(); + + var entity1 = new Child1 { Name = "Child", Child1Property = 8 }; + context.Set().Add(entity1); await SaveChanges(context, async); context.ChangeTracker.Clear(); - using (Fixture.TestSqlLoggerFactory.SuspendRecordingEvents()) + using (TestSqlLoggerFactory.SuspendRecordingEvents()) { - var entity2 = context.TphChild.Single(b => b.Id == entity1.Id); + var entity2 = context.Set().Single(b => b.Id == entity1.Id); Assert.Equal("Child", entity2.Name); Assert.Equal(8, entity2.Child1Property); @@ -560,68 +901,170 @@ public virtual async Task Tph(bool async) [ConditionalTheory] [MemberData(nameof(IsAsyncData))] - public virtual async Task Tpt(bool async) - { - await using var context = CreateContext(); + public abstract Task Tpt(bool async); - var entity1 = new TptChild { Name = "Child", ChildProperty = 8 }; - context.TptChild.Add(entity1); + protected async Task Tpt(bool async, string createSprocSql) + { + var contextFactory = await InitializeAsync( + modelBuilder => + { + modelBuilder.Entity( + b => + { + b.UseTptMappingStrategy(); + + b.InsertUsingStoredProcedure( + "Parent_Insert", + spb => spb + .HasParameter(w => w.Id, pb => pb.IsOutput()) + .HasParameter(w => w.Name)); + }); + + modelBuilder.Entity() + .InsertUsingStoredProcedure( + nameof(Child1) + "_Insert", + spb => spb + .HasParameter(w => w.Id) + .HasParameter(w => w.Child1Property)); + }, + seed: ctx => CreateStoredProcedures(ctx, createSprocSql)); + + await using var context = contextFactory.CreateContext(); + + var entity1 = new Child1 { Name = "Child", Child1Property = 8 }; + context.Set().Add(entity1); await SaveChanges(context, async); context.ChangeTracker.Clear(); - using (Fixture.TestSqlLoggerFactory.SuspendRecordingEvents()) + using (TestSqlLoggerFactory.SuspendRecordingEvents()) { - var entity2 = context.TptChild.Single(b => b.Id == entity1.Id); + var entity2 = context.Set().Single(b => b.Id == entity1.Id); Assert.Equal("Child", entity2.Name); - Assert.Equal(8, entity2.ChildProperty); + Assert.Equal(8, entity2.Child1Property); } } [ConditionalTheory] [MemberData(nameof(IsAsyncData))] - public virtual async Task Tpt_mixed_sproc_and_non_sproc(bool async) - { - await using var context = CreateContext(); + public abstract Task Tpt_mixed_sproc_and_non_sproc(bool async); - var entity1 = new TptMixedChild { Name = "Child", ChildProperty = 8 }; - context.TptMixedChild.Add(entity1); + protected async Task Tpt_mixed_sproc_and_non_sproc(bool async, string createSprocSql) + { + var contextFactory = await InitializeAsync( + modelBuilder => + { + modelBuilder.Entity( + b => + { + b.UseTptMappingStrategy(); + + b.InsertUsingStoredProcedure( + nameof(Parent) + "_Insert", + spb => spb + .HasParameter(w => w.Id, pb => pb.IsOutput()) + .HasParameter(w => w.Name)); + }); + + // No sproc mapping for Child1, use regular SQL + modelBuilder.Entity(); + }, + seed: ctx => CreateStoredProcedures(ctx, createSprocSql)); + + await using var context = contextFactory.CreateContext(); + + var entity1 = new Child1 { Name = "Child", Child1Property = 8 }; + context.Set().Add(entity1); await SaveChanges(context, async); context.ChangeTracker.Clear(); - using (Fixture.TestSqlLoggerFactory.SuspendRecordingEvents()) + using (TestSqlLoggerFactory.SuspendRecordingEvents()) { - var entity2 = context.TptMixedChild.Single(b => b.Id == entity1.Id); + var entity2 = context.Set().Single(b => b.Id == entity1.Id); Assert.Equal("Child", entity2.Name); - Assert.Equal(8, entity2.ChildProperty); + Assert.Equal(8, entity2.Child1Property); } } [ConditionalTheory] [MemberData(nameof(IsAsyncData))] - public virtual async Task Tpc(bool async) - { - await using var context = CreateContext(); + public abstract Task Tpc(bool async); - var entity1 = new TpcChild { Name = "Child", ChildProperty = 8 }; - context.TpcChild.Add(entity1); + protected async Task Tpc(bool async, string createSprocSql) + { + var contextFactory = await InitializeAsync( + modelBuilder => + { + modelBuilder.Entity().UseTpcMappingStrategy(); + + modelBuilder.Entity() + .UseTpcMappingStrategy() + .InsertUsingStoredProcedure( + nameof(Child1) + "_Insert", + spb => spb + .HasParameter(w => w.Id, pb => pb.IsOutput()) + .HasParameter(w => w.Name) + .HasParameter(w => w.Child1Property)); + }, + seed: ctx => CreateStoredProcedures(ctx, createSprocSql)); + + await using var context = contextFactory.CreateContext(); + + var entity1 = new Child1 { Name = "Child", Child1Property = 8 }; + context.Set().Add(entity1); await SaveChanges(context, async); context.ChangeTracker.Clear(); - using (Fixture.TestSqlLoggerFactory.SuspendRecordingEvents()) + using (TestSqlLoggerFactory.SuspendRecordingEvents()) { - var entity2 = context.TpcChild.Single(b => b.Id == entity1.Id); + var entity2 = context.Set().Single(b => b.Id == entity1.Id); Assert.Equal("Child", entity2.Name); - Assert.Equal(8, entity2.ChildProperty); + Assert.Equal(8, entity2.Child1Property); } } - private async Task SaveChanges(StoredProcedureUpdateContext context, bool async) + /// + /// A method to be implement by the provider, to set up a store-generated concurrency token shadow property with the given name. + /// + protected abstract void ConfigureStoreGeneratedConcurrencyToken(EntityTypeBuilder entityTypeBuilder, string propertyName); + + protected class Entity + { + public int Id { get; set; } + public string Name { get; set; } + } + + protected class EntityWithAdditionalProperty + { + public int Id { get; set; } + public string Name { get; set; } + public int AdditionalProperty { get; set; } + } + + protected class Child1 : Parent + { + public int Child1Property { get; set; } + } + + protected class Child2 : Parent + { + public int Child2InputProperty { get; set; } + public int Child2OutputParameterProperty { get; set; } + public int Child2ResultColumnProperty { get; set; } + } + + protected class Parent + { + public int Id { get; set; } + public string Name { get; set; } + } + + private async Task SaveChanges(DbContext context, bool async) { if (async) { @@ -629,20 +1072,27 @@ private async Task SaveChanges(StoredProcedureUpdateContext context, bool async) } else { + // ReSharper disable once MethodHasAsyncOverload context.SaveChanges(); } } - protected StoredProcedureUpdateContext CreateContext() - => Fixture.CreateContext(); - - public static IEnumerable IsAsyncData = new[] { new object[] { false }, new object[] { true } }; + protected TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ListLoggerFactory; - protected virtual void AssertSql(params string[] expected) - => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + protected void AssertSql(params string[] expected) + => TestSqlLoggerFactory.AssertBaseline(expected); protected virtual void ClearLog() - => Fixture.TestSqlLoggerFactory.Clear(); + => TestSqlLoggerFactory.Clear(); - protected TFixture Fixture { get; } + protected virtual void CreateStoredProcedures(DbContext context, string createSprocSql) + { + foreach (var batch in + new Regex("^GO", RegexOptions.IgnoreCase | RegexOptions.Multiline, TimeSpan.FromMilliseconds(1000.0)) + .Split(createSprocSql).Where(b => !string.IsNullOrEmpty(b))) + { + context.Database.ExecuteSqlRaw(batch); + } + } } diff --git a/test/EFCore.Specification.Tests/TestUtilities/TestHelpers.cs b/test/EFCore.Specification.Tests/TestUtilities/TestHelpers.cs index d6071eb7659..56afddf5ed8 100644 --- a/test/EFCore.Specification.Tests/TestUtilities/TestHelpers.cs +++ b/test/EFCore.Specification.Tests/TestUtilities/TestHelpers.cs @@ -238,10 +238,8 @@ public static void AssertAllMethodsOverridden(Type testClass) .GetRuntimeMethods() .Where( m => m.DeclaringType != testClass - && m.GetCustomAttributes() - .Any( - a => a is ConditionalFactAttribute - || a is ConditionalTheoryAttribute)) + && (Attribute.IsDefined(m, typeof(ConditionalFactAttribute)) + || Attribute.IsDefined(m, typeof(ConditionalTheoryAttribute)))) .ToList(); var methodCalls = new StringBuilder(); diff --git a/test/EFCore.SqlServer.FunctionalTests/Update/StoredProcedureUpdateSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Update/StoredProcedureUpdateSqlServerTest.cs index 4a67e71ef99..20f6ff61c8a 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Update/StoredProcedureUpdateSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Update/StoredProcedureUpdateSqlServerTest.cs @@ -3,33 +3,35 @@ namespace Microsoft.EntityFrameworkCore.Update; -#nullable enable - -public class StoredProcedureUpdateSqlServerTest - : StoredProcedureUpdateTestBase +public class StoredProcedureUpdateSqlServerTest : StoredProcedureUpdateTestBase { - public StoredProcedureUpdateSqlServerTest(StoredProcedureUpdateSqlServerFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - // Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } - public override async Task Insert_with_output_parameter(bool async) { - await base.Insert_with_output_parameter(async); + await base.Insert_with_output_parameter( + async, @" +CREATE PROCEDURE Entity_Insert(@Name varchar(max), @Id int OUT) +AS BEGIN + INSERT INTO [Entity] ([Name]) VALUES (@Name); + SET @Id = SCOPE_IDENTITY(); +END"); AssertSql( @"@p0='New' (Size = 4000) @p1='1' (Direction = Output) SET NOCOUNT ON; -EXEC [WithOutputParameter_Insert] @p0, @p1 OUTPUT;"); +EXEC [Entity_Insert] @p0, @p1 OUTPUT;"); } public override async Task Insert_twice_with_output_parameter(bool async) { - await base.Insert_twice_with_output_parameter(async); + await base.Insert_twice_with_output_parameter( + async, @" +CREATE PROCEDURE Entity_Insert(@Name varchar(max), @Id int OUT) +AS BEGIN + INSERT INTO [Entity] ([Name]) VALUES (@Name); + SET @Id = SCOPE_IDENTITY(); +END"); AssertSql( @"@p0='New1' (Size = 4000) @@ -38,59 +40,78 @@ public override async Task Insert_twice_with_output_parameter(bool async) @p3='2' (Direction = Output) SET NOCOUNT ON; -EXEC [WithOutputParameter_Insert] @p0, @p1 OUTPUT; -EXEC [WithOutputParameter_Insert] @p2, @p3 OUTPUT;"); +EXEC [Entity_Insert] @p0, @p1 OUTPUT; +EXEC [Entity_Insert] @p2, @p3 OUTPUT;"); } public override async Task Insert_with_result_column(bool async) { - await base.Insert_with_result_column(async); + await base.Insert_with_result_column( + async, @" +CREATE PROCEDURE Entity_Insert(@Name varchar(max)) +AS INSERT INTO [Entity] ([Name]) OUTPUT [Inserted].[Id] VALUES (@Name)"); AssertSql( @"@p0='Foo' (Size = 4000) SET NOCOUNT ON; -EXEC [WithResultColumn_Insert] @p0;"); +EXEC [Entity_Insert] @p0;"); } public override async Task Insert_with_two_result_columns(bool async) { - await base.Insert_with_two_result_columns(async); + await base.Insert_with_two_result_columns( + async, @" +CREATE PROCEDURE EntityWithAdditionalProperty_Insert(@Name varchar(max)) +AS INSERT INTO [EntityWithAdditionalProperty] ([Name]) OUTPUT [Inserted].[AdditionalProperty], [Inserted].[Id] VALUES (@Name)"); AssertSql( @"@p0='Foo' (Size = 4000) SET NOCOUNT ON; -EXEC [WithTwoResultColumns_Insert] @p0;"); +EXEC [EntityWithAdditionalProperty_Insert] @p0;"); } public override async Task Insert_with_output_parameter_and_result_column(bool async) { - await base.Insert_with_output_parameter_and_result_column(async); + await base.Insert_with_output_parameter_and_result_column( + async, @" +CREATE PROCEDURE EntityWithAdditionalProperty_Insert(@Id int OUT, @Name varchar(max)) +AS BEGIN + INSERT INTO [EntityWithAdditionalProperty] ([Name]) VALUES (@Name); + SET @Id = SCOPE_IDENTITY(); + SELECT [AdditionalProperty] FROM [EntityWithAdditionalProperty] WHERE [Id] = @Id +END"); AssertSql( @"@p0=NULL (Nullable = false) (Direction = Output) (DbType = Int32) @p1='Foo' (Size = 4000) SET NOCOUNT ON; -EXEC [WithOutputParameterAndResultColumn_Insert] @p0 OUTPUT, @p1;"); +EXEC [EntityWithAdditionalProperty_Insert] @p0 OUTPUT, @p1;"); } public override async Task Update(bool async) { - await base.Update(async); + await base.Update( + async, @" +CREATE PROCEDURE Entity_Update(@Id int, @Name varchar(max)) +AS UPDATE [Entity] SET [Name] = @Name WHERE [Id] = @id"); AssertSql( @"@p0='1' @p1='Updated' (Size = 4000) SET NOCOUNT ON; -EXEC [WithOutputParameter_Update] @p0, @p1;"); +EXEC [Entity_Update] @p0, @p1;"); } public override async Task Update_partial(bool async) { - await base.Update_partial(async); + await base.Update_partial( + async, @" +CREATE PROCEDURE EntityWithAdditionalProperty_Update(@Id int, @Name varchar(max), @AdditionalProperty int) +AS UPDATE [EntityWithAdditionalProperty] SET [Name] = @Name, [AdditionalProperty] = @AdditionalProperty WHERE [Id] = @id"); AssertSql( @"@p0='1' @@ -98,12 +119,18 @@ public override async Task Update_partial(bool async) @p2='8' SET NOCOUNT ON; -EXEC [WithTwoInputParameters_Update] @p0, @p1, @p2;"); +EXEC [EntityWithAdditionalProperty_Update] @p0, @p1, @p2;"); } public override async Task Update_with_output_parameter_and_rows_affected_result_column(bool async) { - await base.Update_with_output_parameter_and_rows_affected_result_column(async); + await base.Update_with_output_parameter_and_rows_affected_result_column( + async, @" +CREATE PROCEDURE EntityWithAdditionalProperty_Update(@Id int, @Name varchar(max), @AdditionalProperty int OUT) +AS BEGIN + UPDATE [EntityWithAdditionalProperty] SET [Name] = @Name, @AdditionalProperty = [AdditionalProperty] WHERE [Id] = @Id; + SELECT @@ROWCOUNT; +END"); AssertSql( @"@p0='1' @@ -111,12 +138,18 @@ public override async Task Update_with_output_parameter_and_rows_affected_result @p2=NULL (Nullable = false) (Direction = Output) (DbType = Int32) SET NOCOUNT ON; -EXEC [WithOutputParameterAndRowsAffectedResultColumn_Update] @p0, @p1, @p2 OUTPUT;"); +EXEC [EntityWithAdditionalProperty_Update] @p0, @p1, @p2 OUTPUT;"); } public override async Task Update_with_output_parameter_and_rows_affected_result_column_concurrency_failure(bool async) { - await base.Update_with_output_parameter_and_rows_affected_result_column_concurrency_failure(async); + await base.Update_with_output_parameter_and_rows_affected_result_column_concurrency_failure( + async, @" +CREATE PROCEDURE EntityWithAdditionalProperty_Update(@Id int, @Name varchar(max), @AdditionalProperty int OUT) +AS BEGIN + UPDATE [EntityWithAdditionalProperty] SET [Name] = @Name, @AdditionalProperty = [AdditionalProperty] WHERE [Id] = @Id; + SELECT @@ROWCOUNT; +END"); AssertSql( @"@p0='1' @@ -124,23 +157,37 @@ public override async Task Update_with_output_parameter_and_rows_affected_result @p2=NULL (Nullable = false) (Direction = Output) (DbType = Int32) SET NOCOUNT ON; -EXEC [WithOutputParameterAndRowsAffectedResultColumn_Update] @p0, @p1, @p2 OUTPUT;"); +EXEC [EntityWithAdditionalProperty_Update] @p0, @p1, @p2 OUTPUT;"); } public override async Task Delete(bool async) { - await base.Delete(async); + await base.Delete( + async, @" +CREATE PROCEDURE Entity_Delete(@Id int) +AS DELETE FROM [Entity] WHERE [Id] = @Id"); AssertSql( @"@p0='1' SET NOCOUNT ON; -EXEC [WithOutputParameter_Delete] @p0;"); +EXEC [Entity_Delete] @p0;"); } public override async Task Delete_and_insert(bool async) { - await base.Delete_and_insert(async); + await base.Delete_and_insert( + async, @" +CREATE PROCEDURE Entity_Insert(@Name varchar(max), @Id int OUT) +AS BEGIN + INSERT INTO [Entity] ([Name]) VALUES (@Name); + SET @Id = SCOPE_IDENTITY(); +END; + +GO; + +CREATE PROCEDURE Entity_Delete(@Id int) +AS DELETE FROM [Entity] WHERE [Id] = @Id;"); AssertSql( @"@p0='1' @@ -148,13 +195,19 @@ public override async Task Delete_and_insert(bool async) @p2='2' (Direction = Output) SET NOCOUNT ON; -EXEC [WithOutputParameter_Delete] @p0; -EXEC [WithOutputParameter_Insert] @p1, @p2 OUTPUT;"); +EXEC [Entity_Delete] @p0; +EXEC [Entity_Insert] @p1, @p2 OUTPUT;"); } public override async Task Rows_affected_parameter(bool async) { - await base.Rows_affected_parameter(async); + await base.Rows_affected_parameter( + async, @" +CREATE PROCEDURE Entity_Update(@Id int, @Name varchar(max), @RowsAffected int OUT) +AS BEGIN + UPDATE [Entity] SET [Name] = @Name WHERE [Id] = @Id; + SET @RowsAffected = @@ROWCOUNT; +END"); AssertSql( @"@p0='1' @@ -162,12 +215,18 @@ public override async Task Rows_affected_parameter(bool async) @p2='1' (Direction = Output) SET NOCOUNT ON; -EXEC [WithRowsAffectedParameter_Update] @p0, @p1, @p2 OUTPUT;"); +EXEC [Entity_Update] @p0, @p1, @p2 OUTPUT;"); } public override async Task Rows_affected_parameter_and_concurrency_failure(bool async) { - await base.Rows_affected_parameter_and_concurrency_failure(async); + await base.Rows_affected_parameter_and_concurrency_failure( + async, @" +CREATE PROCEDURE Entity_Update(@Id int, @Name varchar(max), @RowsAffected int OUT) +AS BEGIN + UPDATE [Entity] SET [Name] = @Name WHERE [Id] = @Id; + SET @RowsAffected = @@ROWCOUNT; +END"); AssertSql( @"@p0='1' @@ -175,36 +234,54 @@ public override async Task Rows_affected_parameter_and_concurrency_failure(bool @p2='0' (Direction = Output) SET NOCOUNT ON; -EXEC [WithRowsAffectedParameter_Update] @p0, @p1, @p2 OUTPUT;"); +EXEC [Entity_Update] @p0, @p1, @p2 OUTPUT;"); } public override async Task Rows_affected_result_column(bool async) { - await base.Rows_affected_result_column(async); + await base.Rows_affected_result_column( + async, @" +CREATE PROCEDURE Entity_Update(@Id int, @Name varchar(max)) +AS BEGIN + UPDATE [Entity] SET [Name] = @Name WHERE [Id] = @Id; + SELECT @@ROWCOUNT; +END"); AssertSql( @"@p0='1' @p1='Updated' (Size = 4000) SET NOCOUNT ON; -EXEC [WithRowsAffectedResultColumn_Update] @p0, @p1;"); +EXEC [Entity_Update] @p0, @p1;"); } public override async Task Rows_affected_result_column_and_concurrency_failure(bool async) { - await base.Rows_affected_result_column_and_concurrency_failure(async); + await base.Rows_affected_result_column_and_concurrency_failure( + async, @" +CREATE PROCEDURE Entity_Update(@Id int, @Name varchar(max)) +AS BEGIN + UPDATE [Entity] SET [Name] = @Name WHERE [Id] = @Id; + SELECT @@ROWCOUNT; +END"); AssertSql( @"@p0='1' @p1='Updated' (Size = 4000) SET NOCOUNT ON; -EXEC [WithRowsAffectedResultColumn_Update] @p0, @p1;"); +EXEC [Entity_Update] @p0, @p1;"); } public override async Task Rows_affected_return_value(bool async) { - await base.Rows_affected_return_value(async); + await base.Rows_affected_return_value( + async, @" +CREATE PROCEDURE Entity_Update(@Id int, @Name varchar(max)) +AS BEGIN + UPDATE [Entity] SET [Name] = @Name WHERE [Id] = @Id; + RETURN @@ROWCOUNT; +END"); AssertSql( @"@p0='1' (Direction = Output) @@ -212,12 +289,18 @@ public override async Task Rows_affected_return_value(bool async) @p2='Updated' (Size = 4000) SET NOCOUNT ON; -EXEC @p0 = [WithRowsAffectedReturnValue_Update] @p1, @p2;"); +EXEC @p0 = [Entity_Update] @p1, @p2;"); } public override async Task Rows_affected_return_value_and_concurrency_failure(bool async) { - await base.Rows_affected_return_value_and_concurrency_failure(async); + await base.Rows_affected_return_value_and_concurrency_failure( + async, @" +CREATE PROCEDURE Entity_Update(@Id int, @Name varchar(max)) +AS BEGIN + UPDATE [Entity] SET [Name] = @Name WHERE [Id] = @Id; + RETURN @@ROWCOUNT; +END"); AssertSql( @"@p0='0' (Direction = Output) @@ -225,38 +308,41 @@ public override async Task Rows_affected_return_value_and_concurrency_failure(bo @p2='Updated' (Size = 4000) SET NOCOUNT ON; -EXEC @p0 = [WithRowsAffectedReturnValue_Update] @p1, @p2;"); +EXEC @p0 = [Entity_Update] @p1, @p2;"); } public override async Task Store_generated_concurrency_token_as_in_out_parameter(bool async) { - await base.Store_generated_concurrency_token_as_in_out_parameter(async); + await base.Store_generated_concurrency_token_as_in_out_parameter( + async, @" +CREATE PROCEDURE Entity_Update(@Id int, @ConcurrencyToken rowversion OUT, @Name varchar(max), @RowsAffected int OUT) +AS BEGIN + UPDATE [Entity] SET [Name] = @Name WHERE [Id] = @Id AND [ConcurrencyToken] = @ConcurrencyToken; + SET @RowsAffected = @@ROWCOUNT; +END"); // Can't assert SQL baseline as usual because the concurrency token changes - Assert.Contains("(Size = 8) (Direction = InputOutput)", Fixture.TestSqlLoggerFactory.Sql); - - Assert.Equal( - @"@p2='Updated' (Size = 4000) -@p3='0' (Direction = Output) - -SET NOCOUNT ON; -EXEC [WithStoreGeneratedConcurrencyTokenAsInOutParameter_Update] @p0, @p1 OUTPUT, @p2, @p3 OUTPUT;", - Fixture.TestSqlLoggerFactory.Sql.Substring(Fixture.TestSqlLoggerFactory.Sql.IndexOf("@p2", StringComparison.Ordinal)), - ignoreLineEndingDifferences: true); + Assert.Contains("(Size = 8) (Direction = InputOutput)", TestSqlLoggerFactory.Sql); Assert.Equal( @"@p2='Updated' (Size = 4000) @p3='0' (Direction = Output) SET NOCOUNT ON; -EXEC [WithStoreGeneratedConcurrencyTokenAsInOutParameter_Update] @p0, @p1 OUTPUT, @p2, @p3 OUTPUT;", - Fixture.TestSqlLoggerFactory.Sql.Substring(Fixture.TestSqlLoggerFactory.Sql.IndexOf("@p2", StringComparison.Ordinal)), +EXEC [Entity_Update] @p0, @p1 OUTPUT, @p2, @p3 OUTPUT;", + TestSqlLoggerFactory.Sql.Substring(TestSqlLoggerFactory.Sql.IndexOf("@p2", StringComparison.Ordinal)), ignoreLineEndingDifferences: true); } public override async Task Store_generated_concurrency_token_as_two_parameters(bool async) { - await base.Store_generated_concurrency_token_as_two_parameters(async); + await base.Store_generated_concurrency_token_as_two_parameters( + async, @" +CREATE PROCEDURE Entity_Update(@Id int, @ConcurrencyTokenIn rowversion, @Name varchar(max), @ConcurrencyTokenOut rowversion OUT, @RowsAffected int OUT) +AS BEGIN + UPDATE [Entity] SET [Name] = @Name, @ConcurrencyTokenOut = [ConcurrencyToken] WHERE [Id] = @Id AND [ConcurrencyToken] = @ConcurrencyTokenIn; + SET @RowsAffected = @@ROWCOUNT; +END"); // Can't assert SQL baseline as usual because the concurrency token changes Assert.Equal( @@ -265,14 +351,20 @@ public override async Task Store_generated_concurrency_token_as_two_parameters(b @p4='0' (Direction = Output) SET NOCOUNT ON; -EXEC [WithStoreGeneratedConcurrencyTokenAsTwoParameters_Update] @p0, @p1, @p2, @p3 OUTPUT, @p4 OUTPUT;", - Fixture.TestSqlLoggerFactory.Sql.Substring(Fixture.TestSqlLoggerFactory.Sql.IndexOf("@p2", StringComparison.Ordinal)), +EXEC [Entity_Update] @p0, @p1, @p2, @p3 OUTPUT, @p4 OUTPUT;", + TestSqlLoggerFactory.Sql.Substring(TestSqlLoggerFactory.Sql.IndexOf("@p2", StringComparison.Ordinal)), ignoreLineEndingDifferences: true); } public override async Task User_managed_concurrency_token(bool async) { - await base.User_managed_concurrency_token(async); + await base.User_managed_concurrency_token( + async, @" +CREATE PROCEDURE EntityWithAdditionalProperty_Update(@Id int, @ConcurrencyTokenOriginal int, @Name varchar(max), @ConcurrencyTokenCurrent int, @RowsAffected int OUT) +AS BEGIN + UPDATE [EntityWithAdditionalProperty] SET [Name] = @Name, [AdditionalProperty] = @ConcurrencyTokenCurrent WHERE [Id] = @Id AND [AdditionalProperty] = @ConcurrencyTokenOriginal; + SET @RowsAffected = @@ROWCOUNT; +END"); AssertSql( @"@p0='1' @@ -282,12 +374,20 @@ public override async Task User_managed_concurrency_token(bool async) @p4='0' (Direction = Output) SET NOCOUNT ON; -EXEC [WithUserManagedConcurrencyToken_Update] @p0, @p1, @p2, @p3, @p4 OUTPUT;"); +EXEC [EntityWithAdditionalProperty_Update] @p0, @p1, @p2, @p3, @p4 OUTPUT;"); } public override async Task Original_and_current_value_on_non_concurrency_token(bool async) { - await base.Original_and_current_value_on_non_concurrency_token(async); + await base.Original_and_current_value_on_non_concurrency_token( + async, @" +CREATE PROCEDURE Entity_Update(@Id int, @NameCurrent varchar(max), @NameOriginal varchar(max)) +AS BEGIN + IF @NameCurrent <> @NameOriginal + BEGIN + UPDATE [Entity] SET [Name] = @NameCurrent WHERE [Id] = @Id; + END +END"); AssertSql( @"@p0='1' @@ -295,16 +395,81 @@ public override async Task Original_and_current_value_on_non_concurrency_token(b @p2='Initial' (Size = 4000) SET NOCOUNT ON; -EXEC [WithOriginalAndCurrentValueOnNonConcurrencyToken_Update] @p0, @p1, @p2;"); +EXEC [Entity_Update] @p0, @p1, @p2;"); + } + + public override async Task Input_or_output_parameter_with_input(bool async) + { + await base.Input_or_output_parameter_with_input( + async, @" +CREATE PROCEDURE Entity_Insert(@Id int OUT, @Name varchar(max) OUT) +AS BEGIN + IF @Name IS NULL + BEGIN + INSERT INTO [Entity] ([Name]) VALUES ('Some default value'); + SET @Name = 'Some default value'; + END + ELSE + BEGIN + INSERT INTO [Entity] ([Name]) VALUES (@Name); + SET @Name = NULL; + END + + SET @Id = SCOPE_IDENTITY(); +END"); + + AssertSql( + @"@p0='1' (Direction = Output) +@p1=NULL (Nullable = false) (Size = 4000) (Direction = InputOutput) + +SET NOCOUNT ON; +EXEC [Entity_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, @" +CREATE PROCEDURE Entity_Insert(@Id int OUT, @Name varchar(max) OUT) +AS BEGIN + IF @Name IS NULL + BEGIN + INSERT INTO [Entity] ([Name]) VALUES ('Some default value'); + SET @Name = 'Some default value'; + END + ELSE + BEGIN + INSERT INTO [Entity] ([Name]) VALUES (@Name); + SET @Name = NULL; + END + + SET @Id = SCOPE_IDENTITY(); +END"); + + AssertSql( + @"@p0='1' (Direction = Output) +@p1='Some default value' (Nullable = false) (Size = 4000) (Direction = InputOutput) + +SET NOCOUNT ON; +EXEC [Entity_Insert] @p0 OUTPUT, @p1 OUTPUT;"); } public override async Task Tph(bool async) { - await base.Tph(async); + await base.Tph( + async, @" +CREATE PROCEDURE Tph_Insert(@Id int OUT, @Discriminator varchar(max), @Name varchar(max), @Child2InputProperty int, @Child2OutputParameterProperty int OUT, @Child1Property int) +AS BEGIN + DECLARE @Table table ([Child2OutputParameterProperty] int); + INSERT INTO [Tph] ([Discriminator], [Name], [Child1Property], [Child2InputProperty]) OUTPUT [Inserted].[Child2OutputParameterProperty] INTO @Table VALUES (@Discriminator, @Name, @Child1Property, @Child2InputProperty); + SET @Id = SCOPE_IDENTITY(); + SELECT @Child2OutputParameterProperty = [Child2OutputParameterProperty] FROM @Table; + SELECT [Child2ResultColumnProperty] FROM [Tph] WHERE [Id] = @Id +END"); AssertSql( @"@p0=NULL (Nullable = false) (Direction = Output) (DbType = Int32) -@p1='TphChild1' (Nullable = false) (Size = 4000) +@p1='Child1' (Nullable = false) (Size = 4000) @p2='Child' (Size = 4000) @p3=NULL (DbType = Int32) @p4=NULL (Direction = Output) (DbType = Int32) @@ -316,45 +481,72 @@ public override async Task Tph(bool async) public override async Task Tpt(bool async) { - await base.Tpt(async); + await base.Tpt( + async, @" +CREATE PROCEDURE Parent_Insert(@Id int OUT, @Name varchar(max)) +AS BEGIN + INSERT INTO [Parent] ([Name]) VALUES (@Name); + SET @Id = SCOPE_IDENTITY(); +END; + +GO + +CREATE PROCEDURE Child1_Insert(@Id int, @Child1Property int) +AS BEGIN + INSERT INTO [Child1] ([Id], [Child1Property]) VALUES (@Id, @Child1Property); + SET @Id = SCOPE_IDENTITY(); +END;"); AssertSql( @"@p0='1' (Direction = Output) @p1='Child' (Size = 4000) SET NOCOUNT ON; -EXEC [TptParent_Insert] @p0 OUTPUT, @p1;", +EXEC [Parent_Insert] @p0 OUTPUT, @p1;", // @"@p2='1' @p3='8' SET NOCOUNT ON; -EXEC [TptChild_Insert] @p2, @p3;"); +EXEC [Child1_Insert] @p2, @p3;"); } public override async Task Tpt_mixed_sproc_and_non_sproc(bool async) { - await base.Tpt_mixed_sproc_and_non_sproc(async); + await base.Tpt_mixed_sproc_and_non_sproc( + async, @" +CREATE PROCEDURE Parent_Insert(@Id int OUT, @Name varchar(max)) +AS BEGIN + INSERT INTO [Parent] ([Name]) VALUES (@Name); + SET @Id = SCOPE_IDENTITY(); +END"); AssertSql( @"@p0='1' (Direction = Output) @p1='Child' (Size = 4000) SET NOCOUNT ON; -EXEC [TptMixedParent_Insert] @p0 OUTPUT, @p1;", +EXEC [Parent_Insert] @p0 OUTPUT, @p1;", // @"@p2='1' @p3='8' SET IMPLICIT_TRANSACTIONS OFF; SET NOCOUNT ON; -INSERT INTO [TptMixedChild] ([Id], [ChildProperty]) +INSERT INTO [Child1] ([Id], [Child1Property]) VALUES (@p2, @p3);"); } public override async Task Tpc(bool async) { - await base.Tpc(async); + await base.Tpc( + async, @" +CREATE PROCEDURE Child1_Insert(@Id int OUT, @Name varchar(max), @Child1Property int) +AS BEGIN + DECLARE @Table table ([Id] int); + INSERT INTO [Child1] ([Name], [Child1Property]) OUTPUT [Inserted].[Id] INTO @Table VALUES (@Name, @Child1Property); + SELECT @Id = [Id] FROM @Table; +END"); AssertSql( @"@p0='1' (Direction = Output) @@ -362,261 +554,12 @@ public override async Task Tpc(bool async) @p2='8' SET NOCOUNT ON; -EXEC [TpcChild_Insert] @p0 OUTPUT, @p1, @p2;"); - } - - public override async Task Input_or_output_parameter_with_input(bool async) - { - await base.Input_or_output_parameter_with_input(async); - - 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;"); +EXEC [Child1_Insert] @p0 OUTPUT, @p1, @p2;"); } - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); - - public class StoredProcedureUpdateSqlServerFixture : StoredProcedureUpdateFixtureBase - { - protected override ITestStoreFactory TestStoreFactory - => StoredProcedureTestStoryFactory.Instance; - - protected override void ConfigureStoreGeneratedConcurrencyToken(EntityTypeBuilder entityTypeBuilder, string propertyName) - => entityTypeBuilder.Property(propertyName).IsRowVersion(); - - public override void CleanData() - { - using var context = CreateContext(); - context.Database.ExecuteSqlRaw(CleanDataSql); - } - - private const string CleanDataSql = @" --- Regular tables without foreign keys -TRUNCATE TABLE [WithInputOrOutputParameter]; -TRUNCATE TABLE [WithOriginalAndCurrentValueOnNonConcurrencyToken]; -TRUNCATE TABLE [WithOutputParameter]; -TRUNCATE TABLE [WithOutputParameterAndResultColumn]; -TRUNCATE TABLE [WithOutputParameterAndRowsAffectedResultColumn]; -TRUNCATE TABLE [WithResultColumn]; -TRUNCATE TABLE [WithTwoResultColumns]; -TRUNCATE TABLE [WithRowsAffectedParameter]; -TRUNCATE TABLE [WithRowsAffectedResultColumn]; -TRUNCATE TABLE [WithRowsAffectedReturnValue]; -TRUNCATE TABLE [WithStoreGeneratedConcurrencyTokenAsInOutParameter]; -TRUNCATE TABLE [WithStoreGeneratedConcurrencyTokenAsTwoParameters]; -TRUNCATE TABLE [WithTwoInputParameters]; -TRUNCATE TABLE [WithUserManagedConcurrencyToken]; -TRUNCATE TABLE [Tph]; -TRUNCATE TABLE [TpcChild]; -TRUNCATE TABLE [TpcParent]; - -ALTER SEQUENCE [TpcParentSequence] RESTART WITH 1; - --- We can't use TRUNCATE on tables with foreign keys, so we DELETE and reset IDENTITY manually. --- DBCC CHECKIDENT resets IDENTITY, but behaves differently based on whether whether rows were ever inserted (seed+1) or not (seed). --- So we insert a dummy row before deleting everything to make sure we get the seed value 1. -INSERT INTO [TptMixedParent] DEFAULT VALUES; -DELETE FROM [TptMixedChild]; -DELETE FROM [TptMixedParent]; -DBCC CHECKIDENT ('[TptMixedParent]', RESEED, 0); - -INSERT INTO [TptParent] DEFAULT VALUES; -DELETE FROM [TptChild]; -DELETE FROM [TptParent]; -DBCC CHECKIDENT ('[TptParent]', RESEED, 0);"; - - private class StoredProcedureTestStoryFactory : SqlServerTestStoreFactory - { - public static new StoredProcedureTestStoryFactory Instance { get; } = new(); - - public override TestStore GetOrCreate(string storeName) - => SqlServerTestStore.GetOrCreateWithInitScript(storeName, InitScript); - - private const string InitScript = @" -CREATE PROCEDURE WithOutputParameter_Insert(@Name varchar(max), @Id int OUT) -AS BEGIN - INSERT INTO [WithOutputParameter] ([Name]) VALUES (@Name); - SET @Id = SCOPE_IDENTITY(); -END; - -GO - -CREATE PROCEDURE WithOutputParameter_Update(@Id int, @Name varchar(max)) -AS UPDATE [WithOutputParameter] SET [Name] = @Name WHERE [Id] = @id; - -GO - -CREATE PROCEDURE WithOutputParameter_Delete(@Id int) -AS DELETE FROM [WithOutputParameter] WHERE [Id] = @Id; - -GO - -CREATE PROCEDURE WithResultColumn_Insert(@Name varchar(max)) -AS INSERT INTO [WithResultColumn] ([Name]) OUTPUT [Inserted].[Id] VALUES (@Name); - -GO - -CREATE PROCEDURE WithTwoResultColumns_Insert(@Name varchar(max)) -AS INSERT INTO [WithTwoResultColumns] ([Name]) OUTPUT [Inserted].[AdditionalProperty], [Inserted].[Id] VALUES (@Name); - -GO - -CREATE PROCEDURE WithOutputParameterAndResultColumn_Insert(@Id int OUT, @Name varchar(max)) -AS BEGIN - INSERT INTO [WithOutputParameterAndResultColumn] ([Name]) VALUES (@Name); - SET @Id = SCOPE_IDENTITY(); - SELECT [AdditionalProperty] FROM [WithOutputParameterAndResultColumn] WHERE [Id] = @Id -END; - -GO - -CREATE PROCEDURE WithOutputParameterAndRowsAffectedResultColumn_Update(@Id int, @Name varchar(max), @AdditionalProperty int OUT) -AS BEGIN - UPDATE [WithOutputParameterAndRowsAffectedResultColumn] SET [Name] = @Name, @AdditionalProperty = [AdditionalProperty] WHERE [Id] = @Id; - SELECT @@ROWCOUNT; -END; - -GO - -CREATE PROCEDURE WithTwoInputParameters_Update(@Id int, @Name varchar(max), @AdditionalProperty int) -AS UPDATE [WithTwoInputParameters] SET [Name] = @Name, [AdditionalProperty] = @AdditionalProperty WHERE [Id] = @id; - -GO - -CREATE PROCEDURE WithRowsAffectedParameter_Update(@Id int, @Name varchar(max), @RowsAffected int OUT) -AS BEGIN - UPDATE [WithRowsAffectedParameter] SET [Name] = @Name WHERE [Id] = @Id; - SET @RowsAffected = @@ROWCOUNT; -END; - -GO - -CREATE PROCEDURE WithRowsAffectedResultColumn_Update(@Id int, @Name varchar(max)) -AS BEGIN - UPDATE [WithRowsAffectedResultColumn] SET [Name] = @Name WHERE [Id] = @Id; - SELECT @@ROWCOUNT; -END; - -GO - -CREATE PROCEDURE WithRowsAffectedReturnValue_Update(@Id int, @Name varchar(max)) -AS BEGIN - UPDATE [WithRowsAffectedReturnValue] SET [Name] = @Name WHERE [Id] = @Id; - RETURN @@ROWCOUNT; -END; - -GO - -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; - SET @RowsAffected = @@ROWCOUNT; -END; - -GO - -CREATE PROCEDURE WithStoreGeneratedConcurrencyTokenAsTwoParameters_Update(@Id int, @ConcurrencyTokenIn rowversion, @Name varchar(max), @ConcurrencyTokenOut rowversion OUT, @RowsAffected int OUT) -AS BEGIN - UPDATE [WithStoreGeneratedConcurrencyTokenAsTwoParameters] SET [Name] = @Name, @ConcurrencyTokenOut = [ConcurrencyToken] WHERE [Id] = @Id AND [ConcurrencyToken] = @ConcurrencyTokenIn; - SET @RowsAffected = @@ROWCOUNT; -END; - -GO - -CREATE PROCEDURE WithUserManagedConcurrencyToken_Update(@Id int, @ConcurrencyTokenOriginal int, @Name varchar(max), @ConcurrencyTokenCurrent int, @RowsAffected int OUT) -AS BEGIN - UPDATE [WithUserManagedConcurrencyToken] SET [Name] = @Name, [AdditionalProperty] = @ConcurrencyTokenCurrent WHERE [Id] = @Id AND [AdditionalProperty] = @ConcurrencyTokenOriginal; - SET @RowsAffected = @@ROWCOUNT; -END; - -GO - -CREATE PROCEDURE WithOriginalAndCurrentValueOnNonConcurrencyToken_Update(@Id int, @NameCurrent varchar(max), @NameOriginal varchar(max)) -AS BEGIN - IF @NameCurrent <> @NameOriginal - BEGIN - UPDATE [WithOriginalAndCurrentValueOnNonConcurrencyToken] SET [Name] = @NameCurrent WHERE [Id] = @Id; - END -END; - -GO - -CREATE PROCEDURE WithInputOrOutputParameter_Insert(@Id int OUT, @Name varchar(max) OUT) -AS BEGIN - 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 - -CREATE PROCEDURE Tph_Insert(@Id int OUT, @Discriminator varchar(max), @Name varchar(max), @Child2InputProperty int, @Child2OutputParameterProperty int OUT, @Child1Property int) -AS BEGIN - DECLARE @Table table ([Child2OutputParameterProperty] int); - INSERT INTO [Tph] ([Discriminator], [Name], [Child1Property], [Child2InputProperty]) OUTPUT [Inserted].[Child2OutputParameterProperty] INTO @Table VALUES (@Discriminator, @Name, @Child1Property, @Child2InputProperty); - SET @Id = SCOPE_IDENTITY(); - SELECT @Child2OutputParameterProperty = [Child2OutputParameterProperty] FROM @Table; - SELECT [Child2ResultColumnProperty] FROM [Tph] WHERE [Id] = @Id -END; - -GO - -CREATE PROCEDURE TptParent_Insert(@Id int OUT, @Name varchar(max)) -AS BEGIN - INSERT INTO [TptParent] ([Name]) VALUES (@Name); - SET @Id = SCOPE_IDENTITY(); -END; - -GO + protected override void ConfigureStoreGeneratedConcurrencyToken(EntityTypeBuilder entityTypeBuilder, string propertyName) + => entityTypeBuilder.Property(propertyName).IsRowVersion(); -CREATE PROCEDURE TptChild_Insert(@Id int, @ChildProperty int) -AS BEGIN - INSERT INTO [TptChild] ([Id], [ChildProperty]) VALUES (@Id, @ChildProperty); - SET @Id = SCOPE_IDENTITY(); -END; - -GO - -CREATE PROCEDURE TptMixedParent_Insert(@Id int OUT, @Name varchar(max)) -AS BEGIN - INSERT INTO [TptMixedParent] ([Name]) VALUES (@Name); - SET @Id = SCOPE_IDENTITY(); -END; - -GO - -CREATE PROCEDURE TpcChild_Insert(@Id int OUT, @Name varchar(max), @ChildProperty int) -AS BEGIN - DECLARE @Table table ([Id] int); - INSERT INTO [TpcChild] ([Name], [ChildProperty]) OUTPUT [Inserted].[Id] INTO @Table VALUES (@Name, @ChildProperty); - SELECT @Id = [Id] FROM @Table; -END;"; - } - } + protected override ITestStoreFactory TestStoreFactory + => SqlServerTestStoreFactory.Instance; } diff --git a/test/EFCore.Sqlite.FunctionalTests/SqliteComplianceTest.cs b/test/EFCore.Sqlite.FunctionalTests/SqliteComplianceTest.cs index 70b6b4ee80f..ddaf5dc308e 100644 --- a/test/EFCore.Sqlite.FunctionalTests/SqliteComplianceTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/SqliteComplianceTest.cs @@ -14,7 +14,7 @@ public class SqliteComplianceTest : RelationalComplianceTestBase typeof(SqlExecutorTestBase<>), typeof(UdfDbFunctionTestBase<>), typeof(TPCRelationshipsQueryTestBase<>), // internal class is added - typeof(StoredProcedureUpdateTestBase<>) // SQLite doesn't support stored procedures + typeof(StoredProcedureUpdateTestBase) // SQLite doesn't support stored procedures }; protected override Assembly TargetAssembly { get; } = typeof(SqliteComplianceTest).Assembly;