diff --git a/samples/core/Miscellaneous/NewInEFCore7/BlogsContext.cs b/samples/core/Miscellaneous/NewInEFCore7/BlogsContext.cs index fbdcb7b6a5..4a923dae3b 100644 --- a/samples/core/Miscellaneous/NewInEFCore7/BlogsContext.cs +++ b/samples/core/Miscellaneous/NewInEFCore7/BlogsContext.cs @@ -70,7 +70,7 @@ public Author(string name) public class ContactDetails { public Address Address { get; init; } = null!; - public string? Phone { get; init; } + public string? Phone { get; set; } } public class Address @@ -83,10 +83,10 @@ public Address(string street, string city, string postcode, string country) Country = country; } - public string Street { get; init; } - public string City { get; init; } - public string Postcode { get; init; } - public string Country { get; init; } + public string Street { get; set; } + public string City { get; set; } + public string Postcode { get; set; } + public string Country { get; set; } } #endregion diff --git a/samples/core/Miscellaneous/NewInEFCore7/DocumentsContext.cs b/samples/core/Miscellaneous/NewInEFCore7/DocumentsContext.cs new file mode 100644 index 0000000000..2ebe1726d2 --- /dev/null +++ b/samples/core/Miscellaneous/NewInEFCore7/DocumentsContext.cs @@ -0,0 +1,595 @@ +namespace NewInEfCore7; + +public abstract class Document +{ + protected Document(string title, int numberOfPages, DateTime publicationDate, byte[]? coverArt) + { + Title = title; + NumberOfPages = numberOfPages; + PublicationDate = publicationDate; + CoverArt = coverArt; + } + + public int Id { get; private set; } + + [Timestamp] + public byte[]? RowVersion { get; set; } + + public string Title { get; set; } + public int NumberOfPages { get; set; } + public DateTime PublicationDate { get; set; } + public byte[]? CoverArt { get; set; } + + public DateTime FirstRecordedOn { get; private set; } + public DateTime RetrievedOn { get; private set; } +} + +public class Book : Document +{ + public Book(string title, int numberOfPages, DateTime publicationDate, byte[]? coverArt) + : base(title, numberOfPages, publicationDate, coverArt) + { + } + + public string? Isbn { get; set; } + + public List Authors { get; } = new(); +} + +public class Magazine : Document +{ + public Magazine(string title, int numberOfPages, DateTime publicationDate, byte[]? coverArt, int issueNumber) + : base(title, numberOfPages, publicationDate, coverArt) + { + IssueNumber = issueNumber; + } + + public int IssueNumber { get; set; } + public decimal? CoverPrice { get; set; } + public Person Editor { get; set; } = null!; +} + +public class Person +{ + public Person(string name) + { + Name = name; + } + + public int Id { get; private set; } + + [ConcurrencyCheck] + public string Name { get; set; } + + public ContactDetails Contact { get; set; } = null!; + + public List PublishedWorks { get; } = new(); + public List Edited { get; } = new(); +} + +public abstract class DocumentsContext : DbContext +{ + public bool LoggingEnabled { get; set; } + public abstract MappingStrategy MappingStrategy { get; } + public virtual bool UseStoredProcedures => true; + + public DbSet Documents => Set(); + public DbSet Books => Set(); + public DbSet Magazines => Set(); + public DbSet People => Set(); + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder.UseSqlServer(@$"Server=(localdb)\mssqllocaldb;Database={GetType().Name}") + .EnableSensitiveDataLogging() + .LogTo( + s => + { + if (LoggingEnabled) + { + Console.WriteLine(s); + } + }, LogLevel.Information); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity( + entityTypeBuilder => + { + entityTypeBuilder.Property(document => document.FirstRecordedOn).HasDefaultValueSql("getutcdate()"); + entityTypeBuilder.Property(document => document.RetrievedOn).HasComputedColumnSql("getutcdate()"); + + if (UseStoredProcedures) + { + switch (MappingStrategy) + { + case MappingStrategy.Tph: + entityTypeBuilder.InsertUsingStoredProcedure( + "Document_Insert", + storedProcedureBuilder => + { + storedProcedureBuilder.HasParameter("Discriminator"); + storedProcedureBuilder.HasParameter(document => document.Title); + storedProcedureBuilder.HasParameter(document => document.NumberOfPages); + storedProcedureBuilder.HasParameter(document => document.PublicationDate); + storedProcedureBuilder.HasParameter(document => document.CoverArt); + storedProcedureBuilder.HasResultColumn(document => document.Id); + storedProcedureBuilder.HasParameter(document => ((Book)document).Isbn); + storedProcedureBuilder.HasParameter(document => ((Magazine)document).CoverPrice); + storedProcedureBuilder.HasParameter(document => ((Magazine)document).IssueNumber); + storedProcedureBuilder.HasParameter("EditorId"); + storedProcedureBuilder.HasResultColumn(document => document.FirstRecordedOn); + storedProcedureBuilder.HasResultColumn(document => document.RetrievedOn); + storedProcedureBuilder.HasResultColumn(document => document.RowVersion); + }); + + entityTypeBuilder.UpdateUsingStoredProcedure( + "Document_Update", + storedProcedureBuilder => + { + storedProcedureBuilder.HasParameter(document => document.Id); + storedProcedureBuilder.HasParameter("Discriminator"); + storedProcedureBuilder.HasOriginalValueParameter( + document => document.RowVersion, + parameterBuilder => parameterBuilder.HasName("RowVersion_Original")); + storedProcedureBuilder.HasParameter(document => document.Title); + storedProcedureBuilder.HasParameter(document => document.NumberOfPages); + storedProcedureBuilder.HasParameter(document => document.PublicationDate); + storedProcedureBuilder.HasParameter(document => document.CoverArt); + storedProcedureBuilder.HasParameter(document => ((Book)document).Isbn); + storedProcedureBuilder.HasParameter(document => ((Magazine)document).CoverPrice); + storedProcedureBuilder.HasParameter(document => ((Magazine)document).IssueNumber); + storedProcedureBuilder.HasParameter("EditorId"); + storedProcedureBuilder.HasParameter(document => document.FirstRecordedOn); + storedProcedureBuilder.HasParameter( + document => document.RetrievedOn, parameterBuilder => parameterBuilder.IsOutput()); + storedProcedureBuilder.HasParameter( + document => document.RowVersion, parameterBuilder => parameterBuilder.IsOutput()); + }); + + entityTypeBuilder.DeleteUsingStoredProcedure( + "Document_Delete", + storedProcedureBuilder => + { + storedProcedureBuilder.HasParameter(document => document.Id); + storedProcedureBuilder.HasOriginalValueParameter(document => document.RowVersion); + }); + break; + case MappingStrategy.Tpt: + entityTypeBuilder.InsertUsingStoredProcedure( + "Document_Insert", + storedProcedureBuilder => + { + storedProcedureBuilder.HasParameter(document => document.Title); + storedProcedureBuilder.HasParameter(document => document.NumberOfPages); + storedProcedureBuilder.HasParameter(document => document.PublicationDate); + storedProcedureBuilder.HasParameter(document => document.CoverArt); + storedProcedureBuilder.HasResultColumn(document => document.Id); + storedProcedureBuilder.HasResultColumn(document => document.FirstRecordedOn); + storedProcedureBuilder.HasResultColumn(document => document.RetrievedOn); + storedProcedureBuilder.HasResultColumn(document => document.RowVersion); + }); + + entityTypeBuilder.UpdateUsingStoredProcedure( + "Document_Update", + storedProcedureBuilder => + { + storedProcedureBuilder.HasParameter(document => document.Id); + storedProcedureBuilder.HasOriginalValueParameter( + magazine => magazine.RowVersion, + parameterBuilder => parameterBuilder.HasName("RowVersion_Original")); + storedProcedureBuilder.HasParameter(document => document.Title); + storedProcedureBuilder.HasParameter(document => document.NumberOfPages); + storedProcedureBuilder.HasParameter(document => document.PublicationDate); + storedProcedureBuilder.HasParameter(document => document.CoverArt); + storedProcedureBuilder.HasParameter(document => document.FirstRecordedOn); + storedProcedureBuilder.HasParameter( + magazine => magazine.RetrievedOn, parameterBuilder => parameterBuilder.IsOutput()); + storedProcedureBuilder.HasParameter( + magazine => magazine.RowVersion, parameterBuilder => parameterBuilder.IsOutput()); + }); + + entityTypeBuilder.DeleteUsingStoredProcedure( + "Document_Delete", + storedProcedureBuilder => + { + storedProcedureBuilder.HasParameter(document => document.Id); + storedProcedureBuilder.HasOriginalValueParameter(document => document.RowVersion); + }); + break; + case MappingStrategy.Tpc: + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + }); + + modelBuilder.Entity( + entityTypeBuilder => + { + entityTypeBuilder + .HasMany(document => document.Authors) + .WithMany(author => author.PublishedWorks) + .UsingEntity>( + "BookPerson", + builder => builder.HasOne().WithMany().OnDelete(DeleteBehavior.Cascade), + builder => builder.HasOne().WithMany().OnDelete(DeleteBehavior.ClientCascade), + joinTypeBuilder => + { + if (UseStoredProcedures) + { + joinTypeBuilder.InsertUsingStoredProcedure( + "BookPerson_Insert", + storedProcedureBuilder => + { + storedProcedureBuilder.HasParameter("AuthorsId"); + storedProcedureBuilder.HasParameter("PublishedWorksId"); + }); + + joinTypeBuilder.DeleteUsingStoredProcedure( + "BookPerson_Delete", + storedProcedureBuilder => + { + storedProcedureBuilder.HasParameter("AuthorsId"); + storedProcedureBuilder.HasParameter("PublishedWorksId"); + }); + } + }); + + if (UseStoredProcedures) + { + switch (MappingStrategy) + { + case MappingStrategy.Tph: + break; + case MappingStrategy.Tpt: + entityTypeBuilder.InsertUsingStoredProcedure( + "Book_Insert", + storedProcedureBuilder => + { + storedProcedureBuilder.HasParameter(book => book.Id); + storedProcedureBuilder.HasParameter(book => book.Isbn); + }); + + entityTypeBuilder.UpdateUsingStoredProcedure( + "Book_Update", + storedProcedureBuilder => + { + storedProcedureBuilder.HasParameter(book => book.Id); + storedProcedureBuilder.HasParameter(book => book.Isbn); + }); + + entityTypeBuilder.DeleteUsingStoredProcedure( + "Book_Delete", + storedProcedureBuilder => + { + storedProcedureBuilder.HasParameter(book => book.Id); + }); + break; + case MappingStrategy.Tpc: + entityTypeBuilder.InsertUsingStoredProcedure( + "Book_Insert", + storedProcedureBuilder => + { + storedProcedureBuilder.HasParameter(book => book.Title); + storedProcedureBuilder.HasParameter(book => book.NumberOfPages); + storedProcedureBuilder.HasParameter(book => book.PublicationDate); + storedProcedureBuilder.HasParameter(book => book.CoverArt); + storedProcedureBuilder.HasParameter(book => book.Isbn); + storedProcedureBuilder.HasResultColumn(book => book.Id); + storedProcedureBuilder.HasResultColumn(book => book.FirstRecordedOn); + storedProcedureBuilder.HasResultColumn(book => book.RetrievedOn); + storedProcedureBuilder.HasResultColumn(book => book.RowVersion); + }); + + entityTypeBuilder.UpdateUsingStoredProcedure( + "Book_Update", + storedProcedureBuilder => + { + storedProcedureBuilder.HasParameter(book => book.Id); + storedProcedureBuilder.HasOriginalValueParameter( + magazine => magazine.RowVersion, + parameterBuilder => parameterBuilder.HasName("RowVersion_Original")); + storedProcedureBuilder.HasParameter(book => book.Title); + storedProcedureBuilder.HasParameter(book => book.NumberOfPages); + storedProcedureBuilder.HasParameter(book => book.PublicationDate); + storedProcedureBuilder.HasParameter(book => book.CoverArt); + storedProcedureBuilder.HasParameter(book => book.FirstRecordedOn); + storedProcedureBuilder.HasParameter(book => book.Isbn); + storedProcedureBuilder.HasParameter( + magazine => magazine.RetrievedOn, parameterBuilder => parameterBuilder.IsOutput()); + storedProcedureBuilder.HasParameter( + magazine => magazine.RowVersion, parameterBuilder => parameterBuilder.IsOutput()); + }); + + entityTypeBuilder.DeleteUsingStoredProcedure( + "Book_Delete", + storedProcedureBuilder => + { + storedProcedureBuilder.HasParameter(book => book.Id); + storedProcedureBuilder.HasOriginalValueParameter(book => book.RowVersion); + }); + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + }); + + modelBuilder.Entity( + entityTypeBuilder => + { + if (UseStoredProcedures) + { + switch (MappingStrategy) + { + case MappingStrategy.Tph: + break; + case MappingStrategy.Tpt: + entityTypeBuilder.InsertUsingStoredProcedure( + "Magazine_Insert", + storedProcedureBuilder => + { + storedProcedureBuilder.HasParameter(magazine => magazine.Id); + storedProcedureBuilder.HasParameter(magazine => magazine.CoverPrice); + storedProcedureBuilder.HasParameter(magazine => magazine.IssueNumber); + storedProcedureBuilder.HasParameter("EditorId"); + }); + + entityTypeBuilder.UpdateUsingStoredProcedure( + "Magazine_Update", + storedProcedureBuilder => + { + storedProcedureBuilder.HasParameter(magazine => magazine.Id); + storedProcedureBuilder.HasParameter(magazine => magazine.CoverPrice); + storedProcedureBuilder.HasParameter(magazine => magazine.IssueNumber); + storedProcedureBuilder.HasParameter("EditorId"); + }); + + entityTypeBuilder.DeleteUsingStoredProcedure( + "Magazine_Delete", + storedProcedureBuilder => + { + storedProcedureBuilder.HasParameter(magazine => magazine.Id); + }); + break; + case MappingStrategy.Tpc: + entityTypeBuilder.InsertUsingStoredProcedure( + "Magazine_Insert", + storedProcedureBuilder => + { + storedProcedureBuilder.HasParameter(magazine => magazine.Title); + storedProcedureBuilder.HasParameter(magazine => magazine.NumberOfPages); + storedProcedureBuilder.HasParameter(magazine => magazine.PublicationDate); + storedProcedureBuilder.HasParameter(magazine => magazine.CoverArt); + storedProcedureBuilder.HasResultColumn(magazine => magazine.Id); + storedProcedureBuilder.HasParameter(magazine => magazine.CoverPrice); + storedProcedureBuilder.HasParameter(magazine => magazine.IssueNumber); + storedProcedureBuilder.HasParameter("EditorId"); + storedProcedureBuilder.HasResultColumn(magazine => magazine.FirstRecordedOn); + storedProcedureBuilder.HasResultColumn(magazine => magazine.RetrievedOn); + storedProcedureBuilder.HasResultColumn(magazine => magazine.RowVersion); + }); + + entityTypeBuilder.UpdateUsingStoredProcedure( + "Magazine_Update", + storedProcedureBuilder => + { + storedProcedureBuilder.HasParameter(magazine => magazine.Id); + storedProcedureBuilder.HasOriginalValueParameter( + magazine => magazine.RowVersion, + parameterBuilder => parameterBuilder.HasName("RowVersion_Original")); + storedProcedureBuilder.HasParameter(magazine => magazine.Title); + storedProcedureBuilder.HasParameter(magazine => magazine.NumberOfPages); + storedProcedureBuilder.HasParameter(magazine => magazine.PublicationDate); + storedProcedureBuilder.HasParameter(magazine => magazine.CoverArt); + storedProcedureBuilder.HasParameter(magazine => magazine.CoverPrice); + storedProcedureBuilder.HasParameter(magazine => magazine.IssueNumber); + storedProcedureBuilder.HasParameter("EditorId"); + storedProcedureBuilder.HasParameter(magazine => magazine.FirstRecordedOn); + storedProcedureBuilder.HasParameter( + magazine => magazine.RetrievedOn, parameterBuilder => parameterBuilder.IsOutput()); + storedProcedureBuilder.HasParameter( + magazine => magazine.RowVersion, parameterBuilder => parameterBuilder.IsOutput()); + }); + + entityTypeBuilder.DeleteUsingStoredProcedure( + "Magazine_Delete", + storedProcedureBuilder => + { + storedProcedureBuilder.HasParameter(magazine => magazine.Id); + storedProcedureBuilder.HasOriginalValueParameter(magazine => magazine.RowVersion); + }); + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + }); + + modelBuilder.Entity( + entityTypeBuilder => + { + if (UseStoredProcedures) + { + + entityTypeBuilder.InsertUsingStoredProcedure( + "Person_Insert", storedProcedureBuilder => + { + storedProcedureBuilder.HasParameter(a => a.Name); + storedProcedureBuilder.HasResultColumn(a => a.Id); + }); + + entityTypeBuilder.UpdateUsingStoredProcedure( + "Person_Update", + storedProcedureBuilder => + { + storedProcedureBuilder.HasParameter(person => person.Id); + storedProcedureBuilder.HasOriginalValueParameter( + person => person.Name, parameterBuilder => parameterBuilder.HasName("Name_Original")); + storedProcedureBuilder.HasParameter(person => person.Name); + storedProcedureBuilder.HasRowsAffectedResultColumn(); + }); + + entityTypeBuilder.DeleteUsingStoredProcedure( + "Person_Delete", + storedProcedureBuilder => + { + storedProcedureBuilder.HasParameter(person => person.Id); + storedProcedureBuilder.HasOriginalValueParameter(person => person.Name); + }); + } + + entityTypeBuilder.OwnsOne( + author => author.Contact, + ownedNavigationBuilder => + { + ownedNavigationBuilder.ToTable("Contacts"); + + if (UseStoredProcedures) + { + ownedNavigationBuilder.InsertUsingStoredProcedure( + "Contacts_Insert", storedProcedureBuilder => + { + storedProcedureBuilder.HasParameter("PersonId"); + storedProcedureBuilder.HasParameter(contactDetails => contactDetails.Phone); + }); + + ownedNavigationBuilder.UpdateUsingStoredProcedure( + "Contacts_Update", + storedProcedureBuilder => + { + storedProcedureBuilder.HasParameter("PersonId"); + storedProcedureBuilder.HasParameter(contactDetails => contactDetails.Phone); + storedProcedureBuilder.HasRowsAffectedResultColumn(); + }); + + ownedNavigationBuilder.DeleteUsingStoredProcedure( + "Contacts_Delete", storedProcedureBuilder => + { + storedProcedureBuilder.HasParameter("PersonId"); + }); + } + + ownedNavigationBuilder.OwnsOne( + contactDetails => contactDetails.Address, + ownedOwnedNavigationBuilder => + { + ownedOwnedNavigationBuilder.ToTable("Addresses"); + + if (UseStoredProcedures) + { + ownedOwnedNavigationBuilder.InsertUsingStoredProcedure( + "Addresses_Insert", storedProcedureBuilder => + { + storedProcedureBuilder.HasParameter("ContactDetailsPersonId"); + storedProcedureBuilder.HasParameter(address => address.Street); + storedProcedureBuilder.HasParameter(address => address.City); + storedProcedureBuilder.HasParameter(address => address.Postcode); + storedProcedureBuilder.HasParameter(address => address.Country); + }); + + ownedOwnedNavigationBuilder.UpdateUsingStoredProcedure( + "Addresses_Update", + storedProcedureBuilder => + { + storedProcedureBuilder.HasParameter("ContactDetailsPersonId"); + storedProcedureBuilder.HasParameter(address => address.Street); + storedProcedureBuilder.HasParameter(address => address.City); + storedProcedureBuilder.HasParameter(address => address.Postcode); + storedProcedureBuilder.HasParameter(address => address.Country); + storedProcedureBuilder.HasRowsAffectedResultColumn(); + }); + + ownedOwnedNavigationBuilder.DeleteUsingStoredProcedure( + "Addresses_Delete", storedProcedureBuilder => + { + storedProcedureBuilder.HasParameter("ContactDetailsPersonId"); + }); + } + }); + + }); + }); + } + + public void Seed() + { + var kentBeck = new Person("Kent Beck") + { + Contact = new() { Address = new Address("1 Smalltalk Ave", "Camberwick Green", "CW1 5ZH", "UK"), Phone = "01632 12346" } + }; + + var joshuaBloch = new Person("Joshua Bloch") + { + Contact = new() { Address = new Address("1 AFS Walk", "Chigley", "CW1 5ZH", "UK"), Phone = "01632 12347" } + }; + + var nealGafter = new Person("Neal Gafter") + { + Contact = new() { Address = new Address("1 Merlin Closure", "Chigley", "CW1 5ZH", "UK"), Phone = "01632 12348" } + }; + + var simonRockman = new Person("Simon Rockman") + { + Contact = new() { Address = new Address("1 Copper Run", "Camberwick Green", "CW1 5ZH", "UK"), Phone = "01632 12349" } + }; + + var documents = new List + { + new Book("Extreme Programming Explained", 190, new DateTime(2000, 1, 1), null) + { + Isbn = "201-61641-6", Authors = { kentBeck } + }, + new Book("Java Puzzlers", 283, new DateTime(2005, 1, 1), null) + { + Isbn = "0-321-33678-X", Authors = { joshuaBloch, nealGafter } + }, + new Book("Effective Java", 252, new DateTime(2001, 1, 1), new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }) + { + Isbn = "0-201-31005-8", Authors = { joshuaBloch } + }, + new Book("Test-Driven Development By Example", 220, new DateTime(2003, 1, 1), null) + { + Isbn = "0-321-14653-0", Authors = { kentBeck } + }, + new Magazine("Amstrad Computer User", 95, new DateTime(1986, 1, 12), new byte[] { 1, 2, 3 }, 15) + { + CoverPrice = 0.95m, Editor = simonRockman + }, + new Magazine("Amiga Computing", 90, new DateTime(1988, 5, 16), null, 1) { CoverPrice = 1.95m, Editor = simonRockman } + }; + + AddRange(documents); + SaveChanges(); + } +} + +public class TphDocumentsContext : DocumentsContext +{ + public override MappingStrategy MappingStrategy => MappingStrategy.Tph; +} + +public class TptDocumentsContext : DocumentsContext +{ + public override MappingStrategy MappingStrategy => MappingStrategy.Tpt; + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().UseTptMappingStrategy(); + + base.OnModelCreating(modelBuilder); + } +} + +public class TpcDocumentsContext : DocumentsContext +{ + public override MappingStrategy MappingStrategy => MappingStrategy.Tpc; + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().UseTpcMappingStrategy(); + + base.OnModelCreating(modelBuilder); + } +} diff --git a/samples/core/Miscellaneous/NewInEFCore7/DocumentsContextStoredProcedures.cs b/samples/core/Miscellaneous/NewInEFCore7/DocumentsContextStoredProcedures.cs new file mode 100644 index 0000000000..7ba9857f28 --- /dev/null +++ b/samples/core/Miscellaneous/NewInEFCore7/DocumentsContextStoredProcedures.cs @@ -0,0 +1,476 @@ +namespace NewInEfCore7; + +public static class DocumentsContextStoredProcedures +{ + public static void CreateStoredProcedures(this DocumentsContext context) + { + switch (context.MappingStrategy) + { + case MappingStrategy.Tph: + // Document + context.Database.ExecuteSqlRaw( + @" +CREATE PROCEDURE [dbo].[Document_Insert] + @Discriminator [nvarchar](max), + @Title [nvarchar](max), + @NumberOfPages [int], + @PublicationDate [datetime2], + @CoverArt [varbinary](max), + @Isbn [nvarchar](max), + @CoverPrice [decimal](18,2), + @IssueNumber [int], + @EditorId [int] +AS +BEGIN + INSERT INTO [Documents] ([Discriminator], [CoverArt], [NumberOfPages], [PublicationDate], [Title], [Isbn], [CoverPrice], [IssueNumber], [EditorId]) + OUTPUT INSERTED.[Id], INSERTED.[FirstRecordedOn], INSERTED.[RetrievedOn], INSERTED.[RowVersion] + VALUES (@Discriminator, @CoverArt, @NumberOfPages, @PublicationDate, @Title, @Isbn, @CoverPrice, @IssueNumber, @EditorId); +END"); + + context.Database.ExecuteSqlRaw( + @" +CREATE PROCEDURE [dbo].[Document_Update] + @Id [int], + @Discriminator [nvarchar](max), + @RowVersion_Original [rowversion], + @Title [nvarchar](max), + @NumberOfPages [int], + @PublicationDate [datetime2], + @CoverArt [varbinary](max), + @Isbn [nvarchar](max), + @CoverPrice [decimal](18,2), + @IssueNumber [int], + @EditorId [int], + @FirstRecordedOn [datetime2], + @RetrievedOn [datetime2] OUT, + @RowVersion [rowversion] OUT +AS +BEGIN + UPDATE [Documents] SET + [Discriminator] = @Discriminator, + [Title] = @Title, + [NumberOfPages] = @NumberOfPages, + [PublicationDate] = @PublicationDate, + [CoverArt] = @CoverArt, + [FirstRecordedOn] = @FirstRecordedOn, + [Isbn] = @Isbn, + [CoverPrice] = @CoverPrice, + [IssueNumber] = @IssueNumber, + [EditorId] = @EditorId, + @RetrievedOn = [RetrievedOn], + @RowVersion = [RowVersion] + WHERE [Id] = @Id AND [RowVersion] = @RowVersion_Original + SELECT @@ROWCOUNT; +END"); + + context.Database.ExecuteSqlRaw( + @" +CREATE PROCEDURE [dbo].[Document_Delete] + @Id [int], + @RowVersion_Original [rowversion] +AS +BEGIN + DELETE FROM [Documents] + OUTPUT 1 + WHERE [Id] = @Id AND [RowVersion] = @RowVersion_Original; +END"); + break; + case MappingStrategy.Tpt: + // Document + context.Database.ExecuteSqlRaw( + @" +CREATE PROCEDURE [dbo].[Document_Insert] + @Title [nvarchar](max), + @NumberOfPages [int], + @PublicationDate [datetime2], + @CoverArt [varbinary](max) +AS +BEGIN + INSERT INTO [Documents] ([CoverArt], [NumberOfPages], [PublicationDate], [Title]) + OUTPUT INSERTED.[Id], INSERTED.[FirstRecordedOn], INSERTED.[RetrievedOn], INSERTED.[RowVersion] + VALUES (@CoverArt, @NumberOfPages, @PublicationDate, @Title); +END"); + + context.Database.ExecuteSqlRaw( + @" +CREATE PROCEDURE [dbo].[Document_Update] + @Id [int], + @RowVersion_Original [rowversion], + @Title [nvarchar](max), + @NumberOfPages [int], + @PublicationDate [datetime2], + @CoverArt [varbinary](max), + @FirstRecordedOn [datetime2], + @RetrievedOn [datetime2] OUT, + @RowVersion [rowversion] OUT +AS +BEGIN + UPDATE [Documents] SET + [Title] = @Title, + [NumberOfPages] = @NumberOfPages, + [PublicationDate] = @PublicationDate, + [CoverArt] = @CoverArt, + [FirstRecordedOn] = @FirstRecordedOn, + @RetrievedOn = [RetrievedOn], + @RowVersion = [RowVersion] + WHERE [Id] = @Id AND [RowVersion] = @RowVersion_Original + SELECT @@ROWCOUNT; +END"); + + context.Database.ExecuteSqlRaw( + @" +CREATE PROCEDURE [dbo].[Document_Delete] + @Id [int], + @RowVersion_Original [rowversion] +AS +BEGIN + DELETE FROM [Documents] + OUTPUT 1 + WHERE [Id] = @Id AND [RowVersion] = @RowVersion_Original; +END"); + + // Book + context.Database.ExecuteSqlRaw( + @" +CREATE PROCEDURE [dbo].[Book_Insert] + @Id [int], + @Isbn [nvarchar](max) +AS +BEGIN + INSERT INTO [Books] ([Id], [Isbn]) + VALUES (@Id, @Isbn); +END"); + + context.Database.ExecuteSqlRaw( + @" +CREATE PROCEDURE [dbo].[Book_Update] + @Id [int], + @Isbn [nvarchar](max) +AS +BEGIN + UPDATE [Books] SET + [Isbn] = @Isbn + WHERE [Id] = @Id + SELECT @@ROWCOUNT; +END"); + + context.Database.ExecuteSqlRaw( + @" +CREATE PROCEDURE [dbo].[Book_Delete] + @Id [int] +AS +BEGIN + DELETE FROM [Books] + OUTPUT 1 + WHERE [Id] = @Id; +END"); + + // Magazine + context.Database.ExecuteSqlRaw( + @" +CREATE PROCEDURE [dbo].[Magazine_Insert] + @Id [int], + @CoverPrice [decimal](18,2), + @IssueNumber [int], + @EditorId [int] +AS +BEGIN + INSERT INTO [Magazines] ([Id], [CoverPrice], [IssueNumber], [EditorId]) + VALUES (@Id, @CoverPrice, @IssueNumber, @EditorId); +END"); + + context.Database.ExecuteSqlRaw( + @" +CREATE PROCEDURE [dbo].[Magazine_Update] + @Id [int], + @CoverPrice [decimal](18,2), + @IssueNumber [int], + @EditorId [int] +AS +BEGIN + UPDATE [Magazines] SET + [CoverPrice] = @CoverPrice, + [IssueNumber] = @IssueNumber, + [EditorId] = @EditorId + WHERE [Id] = @Id + SELECT @@ROWCOUNT; +END"); + + context.Database.ExecuteSqlRaw( + @" +CREATE PROCEDURE [dbo].[Magazine_Delete] + @Id [int] +AS +BEGIN + DELETE FROM [Magazines] + OUTPUT 1 + WHERE [Id] = @Id; +END"); + break; + case MappingStrategy.Tpc: + // Book + context.Database.ExecuteSqlRaw( + @" +CREATE PROCEDURE [dbo].[Book_Insert] + @Title [nvarchar](max), + @NumberOfPages [int], + @PublicationDate [datetime2], + @CoverArt [varbinary](max), + @Isbn [nvarchar](max) +AS +BEGIN + INSERT INTO [Books] ([CoverArt], [Isbn], [NumberOfPages], [PublicationDate], [Title]) + OUTPUT INSERTED.[Id], INSERTED.[FirstRecordedOn], INSERTED.[RetrievedOn], INSERTED.[RowVersion] + VALUES (@CoverArt, @Isbn, @NumberOfPages, @PublicationDate, @Title); +END"); + + context.Database.ExecuteSqlRaw( + @" +CREATE PROCEDURE [dbo].[Book_Update] + @Id [int], + @RowVersion_Original [rowversion], + @Title [nvarchar](max), + @NumberOfPages [int], + @PublicationDate [datetime2], + @CoverArt [varbinary](max), + @FirstRecordedOn [datetime2], + @Isbn [nvarchar](max), + @RetrievedOn [datetime2] OUT, + @RowVersion [rowversion] OUT +AS +BEGIN + UPDATE [Books] SET + [Title] = @Title, + [NumberOfPages] = @NumberOfPages, + [PublicationDate] = @PublicationDate, + [CoverArt] = @CoverArt, + [FirstRecordedOn] = @FirstRecordedOn, + [Isbn] = @Isbn, + @RetrievedOn = [RetrievedOn], + @RowVersion = [RowVersion] + WHERE [Id] = @Id AND [RowVersion] = @RowVersion_Original + SELECT @@ROWCOUNT; +END"); + + context.Database.ExecuteSqlRaw( + @" +CREATE PROCEDURE [dbo].[Book_Delete] + @Id [int], + @RowVersion_Original [rowversion] +AS +BEGIN + DELETE FROM [Books] + OUTPUT 1 + WHERE [Id] = @Id AND [RowVersion] = @RowVersion_Original; +END"); + + // Magazine + context.Database.ExecuteSqlRaw( + @" +CREATE PROCEDURE [dbo].[Magazine_Insert] + @Title [nvarchar](max), + @NumberOfPages [int], + @PublicationDate [datetime2], + @CoverArt [varbinary](max), + @CoverPrice [decimal](18,2), + @IssueNumber [int], + @EditorId [int] +AS +BEGIN + INSERT INTO [Magazines] ([CoverArt], [NumberOfPages], [PublicationDate], [Title], [CoverPrice], [IssueNumber], [EditorId]) + OUTPUT INSERTED.[Id], INSERTED.[FirstRecordedOn], INSERTED.[RetrievedOn], INSERTED.[RowVersion] + VALUES (@CoverArt, @NumberOfPages, @PublicationDate, @Title, @CoverPrice, @IssueNumber, @EditorId); +END"); + + context.Database.ExecuteSqlRaw( + @" +CREATE PROCEDURE [dbo].[Magazine_Update] + @Id [int], + @RowVersion_Original [rowversion], + @Title [nvarchar](max), + @NumberOfPages [int], + @PublicationDate [datetime2], + @CoverArt [varbinary](max), + @CoverPrice [decimal](18,2), + @IssueNumber [int], + @EditorId [int], + @FirstRecordedOn [datetime2], + @RetrievedOn [datetime2] OUT, + @RowVersion [rowversion] OUT +AS +BEGIN + UPDATE [Magazines] SET + [Title] = @Title, + [NumberOfPages] = @NumberOfPages, + [PublicationDate] = @PublicationDate, + [CoverArt] = @CoverArt, + [FirstRecordedOn] = @FirstRecordedOn, + [CoverPrice] = @CoverPrice, + [IssueNumber] = @IssueNumber, + [EditorId] = @EditorId, + @RetrievedOn = [RetrievedOn], + @RowVersion = [RowVersion] + WHERE [Id] = @Id AND [RowVersion] = @RowVersion_Original + SELECT @@ROWCOUNT; +END"); + + context.Database.ExecuteSqlRaw( + @" +CREATE PROCEDURE [dbo].[Magazine_Delete] + @Id [int], + @RowVersion_Original [rowversion] +AS +BEGIN + DELETE FROM [Magazines] + OUTPUT 1 + WHERE [Id] = @Id AND [RowVersion] = @RowVersion_Original; +END"); + break; + default: + throw new ArgumentOutOfRangeException(); + } + + // Person + context.Database.ExecuteSqlRaw( + @" +CREATE PROCEDURE [dbo].[Person_Insert] + @Name [nvarchar](max) +AS +BEGIN + INSERT INTO [People] ([Name]) + OUTPUT INSERTED.[Id] + VALUES (@Name); +END"); + + context.Database.ExecuteSqlRaw( + @" +CREATE PROCEDURE [dbo].[Person_Update] + @Id [int], + @Name_Original [nvarchar](max), + @Name [nvarchar](max) +AS +BEGIN + UPDATE [People] SET + [Name] = @Name + WHERE [Id] = @Id AND [Name] = @Name_Original + SELECT @@ROWCOUNT +END"); + + context.Database.ExecuteSqlRaw( + @" +CREATE PROCEDURE [dbo].[Person_Delete] + @Id [int], + @Name_Original [nvarchar](max) +AS +BEGIN + DELETE FROM [People] + OUTPUT 1 + WHERE [Id] = @Id AND [Name] = @Name_Original; +END"); + + // Contacts + context.Database.ExecuteSqlRaw( + @" +CREATE PROCEDURE [dbo].[Contacts_Insert] + @PersonId [int], + @Phone [nvarchar](max) +AS +BEGIN + INSERT INTO [Contacts] ([PersonId], [Phone]) + VALUES (@PersonId, @Phone); +END"); + + context.Database.ExecuteSqlRaw( + @" +CREATE PROCEDURE [dbo].[Contacts_Update] + @PersonId [int], + @Phone [nvarchar](max) +AS +BEGIN + UPDATE [Contacts] SET + [Phone] = @Phone + WHERE [PersonId] = @PersonId + SELECT @@ROWCOUNT; +END"); + + context.Database.ExecuteSqlRaw( + @" +CREATE PROCEDURE [dbo].[Contacts_Delete] + @PersonId [int] +AS +BEGIN + DELETE FROM [Contacts] + OUTPUT 1 + WHERE [PersonId] = @PersonId; +END"); + + // Addresses + context.Database.ExecuteSqlRaw( + @" +CREATE PROCEDURE [dbo].[Addresses_Insert] + @ContactDetailsPersonId [int], + @Street [nvarchar](max), + @City [nvarchar](max), + @Postcode [nvarchar](max), + @Country [nvarchar](max) +AS +BEGIN + INSERT INTO [Addresses] ([ContactDetailsPersonId], [City], [Country], [Postcode], [Street]) + VALUES (@ContactDetailsPersonId, @City, @Country, @Postcode, @Street); +END"); + + context.Database.ExecuteSqlRaw( + @" +CREATE PROCEDURE [dbo].[Addresses_Update] + @ContactDetailsPersonId [int], + @Street [nvarchar](max), + @City [nvarchar](max), + @Postcode [nvarchar](max), + @Country [nvarchar](max) +AS +BEGIN + UPDATE [Addresses] SET + [Street] = @Street, + [City] = @City, + [Postcode] = @Postcode, + [Country] = @Country + WHERE [ContactDetailsPersonId] = @ContactDetailsPersonId + SELECT @@ROWCOUNT; +END"); + + context.Database.ExecuteSqlRaw( + @" +CREATE PROCEDURE [dbo].[Addresses_Delete] + @ContactDetailsPersonId [int] +AS +BEGIN + DELETE FROM [Addresses] + OUTPUT 1 + WHERE [ContactDetailsPersonId] = @ContactDetailsPersonId; +END"); + + // BookPerson join table + context.Database.ExecuteSqlRaw( + @" +CREATE PROCEDURE [dbo].[BookPerson_Insert] + @AuthorsId [int], + @PublishedWorksId [int] +AS +BEGIN + INSERT INTO [BookPerson] ([AuthorsId], [PublishedWorksId]) + VALUES (@AuthorsId, @PublishedWorksId); +END"); + + context.Database.ExecuteSqlRaw( + @" +CREATE PROCEDURE [dbo].[BookPerson_Delete] + @AuthorsId [int], + @PublishedWorksId [int] +AS +BEGIN + DELETE FROM [BookPerson] + OUTPUT 1 + WHERE [AuthorsId] = @AuthorsId AND [PublishedWorksId] = @PublishedWorksId; +END"); + } +} diff --git a/samples/core/Miscellaneous/NewInEFCore7/NewInEFCore7.csproj b/samples/core/Miscellaneous/NewInEFCore7/NewInEFCore7.csproj index e41335a352..d41b7940ba 100644 --- a/samples/core/Miscellaneous/NewInEFCore7/NewInEFCore7.csproj +++ b/samples/core/Miscellaneous/NewInEFCore7/NewInEFCore7.csproj @@ -9,13 +9,14 @@ - - - - + + + + + diff --git a/samples/core/Miscellaneous/NewInEFCore7/Program.cs b/samples/core/Miscellaneous/NewInEFCore7/Program.cs index 342f39eb44..64e88f765b 100644 --- a/samples/core/Miscellaneous/NewInEFCore7/Program.cs +++ b/samples/core/Miscellaneous/NewInEFCore7/Program.cs @@ -2,24 +2,28 @@ public class Program { - public static async Task Main() + public static void Main() { - await TpcInheritanceSample.Inheritance_with_TPH(); - await TpcInheritanceSample.Inheritance_with_TPT(); - await TpcInheritanceSample.Inheritance_with_TPC(); - await TpcInheritanceSample.Inheritance_with_TPC_using_HiLo(); + // await TpcInheritanceSample.Inheritance_with_TPH(); + // await TpcInheritanceSample.Inheritance_with_TPT(); + // await TpcInheritanceSample.Inheritance_with_TPC(); + // await TpcInheritanceSample.Inheritance_with_TPC_using_HiLo(); + // + // // Currently not working: see https://github.com/dotnet/efcore/issues/28195 + // // await TpcInheritanceSample.Inheritance_with_TPC_using_Identity(); + // + // await ExecuteDeleteSample.ExecuteDelete(); + // await ExecuteDeleteSample.ExecuteDeleteTpt(); + // await ExecuteDeleteSample.ExecuteDeleteTpc(); + // await ExecuteDeleteSample.ExecuteDeleteSqlite(); + // + // await ExecuteUpdateSample.ExecuteUpdate(); + // await ExecuteUpdateSample.ExecuteUpdateTpt(); + // await ExecuteUpdateSample.ExecuteUpdateTpc(); + // await ExecuteUpdateSample.ExecuteUpdateSqlite(); - // Currently not working: see https://github.com/dotnet/efcore/issues/28195 - // await TpcInheritanceSample.Inheritance_with_TPC_using_Identity(); - - await ExecuteDeleteSample.ExecuteDelete(); - await ExecuteDeleteSample.ExecuteDeleteTpt(); - await ExecuteDeleteSample.ExecuteDeleteTpc(); - await ExecuteDeleteSample.ExecuteDeleteSqlite(); - - await ExecuteUpdateSample.ExecuteUpdate(); - await ExecuteUpdateSample.ExecuteUpdateTpt(); - await ExecuteUpdateSample.ExecuteUpdateTpc(); - await ExecuteUpdateSample.ExecuteUpdateSqlite(); + //StoredProcedureMappingSample.Inset_Update_and_Delete_using_stored_procedures_with_TPH(); + //StoredProcedureMappingSample.Inset_Update_and_Delete_using_stored_procedures_with_TPT(); + StoredProcedureMappingSample.Inset_Update_and_Delete_using_stored_procedures_with_TPC(); } } diff --git a/samples/core/Miscellaneous/NewInEFCore7/StoredProcedureMappingSample.cs b/samples/core/Miscellaneous/NewInEFCore7/StoredProcedureMappingSample.cs new file mode 100644 index 0000000000..d3a0eed996 --- /dev/null +++ b/samples/core/Miscellaneous/NewInEFCore7/StoredProcedureMappingSample.cs @@ -0,0 +1,111 @@ +namespace NewInEfCore7; + +public static class StoredProcedureMappingSample +{ + public static void Inset_Update_and_Delete_using_stored_procedures_with_TPH() + { + Console.WriteLine($">>>> Sample: {nameof(Inset_Update_and_Delete_using_stored_procedures_with_TPH)}"); + Console.WriteLine(); + + SprocMappingTest(); + } + + public static void Inset_Update_and_Delete_using_stored_procedures_with_TPT() + { + Console.WriteLine($">>>> Sample: {nameof(Inset_Update_and_Delete_using_stored_procedures_with_TPT)}"); + Console.WriteLine(); + + SprocMappingTest(); + } + + public static void Inset_Update_and_Delete_using_stored_procedures_with_TPC() + { + Console.WriteLine($">>>> Sample: {nameof(Inset_Update_and_Delete_using_stored_procedures_with_TPC)}"); + Console.WriteLine(); + + SprocMappingTest(); + } + + private static void SprocMappingTest() + where TContext : DocumentsContext, new() + { + using var context = new TContext(); + context.Database.EnsureDeleted(); + context.LoggingEnabled = true; + context.Database.EnsureCreated(); + context.CreateStoredProcedures(); + + context.LoggingEnabled = true; + + context.Seed(); + context.ChangeTracker.Clear(); + + context.Documents + .Include(document => ((Book)document).Authors) + .Include(document => ((Magazine)document).Editor) + .Load(); + + context.RemoveRange(context.People.Local.Where(person => person.Contact.Address.City == "Chigley")); + context.RemoveRange(context.Magazines.Local.Where(magazine => magazine.Title.Contains("Amstrad"))); + context.RemoveRange(context.Books.Local.Where(book => book.NumberOfPages < 200)); + + // Comment this out for issue + context.SaveChanges(); + + foreach (var magazine in context.Magazines.Local) + { + magazine.CoverPrice += 1.0m; + } + + foreach (var book in context.Books.Local) + { + book.Title += " (New Edition!)"; + } + + context.SaveChanges(); + + foreach (var person in context.People.Local.Where(person => person.Contact.Address.Country == "UK")) + { + person.Name = "Dr. " + person.Name; + person.Contact.Phone = "+44 " + person.Contact.Phone!.Substring(1); + person.Contact.Address.Country = "United Kingdom"; + } + + context.SaveChanges(); + + // try + // { + // using var context2 = new TContext(); + // context2.Books.Single(book => book.Title.StartsWith("Test")).Isbn = "Mod1"; + // + // context.Books.Local.Single(book => book.Title.StartsWith("Test", StringComparison.Ordinal)).Isbn = null; + // context.SaveChanges(); + // + // context2.SaveChanges(); + // + // } + // catch (DbUpdateConcurrencyException exception) + // { + // Console.WriteLine($"Caught expected: " + exception.Message); + // } + + try + { + using var context2 = new TContext(); + context2.People.Single(person => person.Name.StartsWith("Dr. Kent")).Name += ": Legend!"; + + context.People.Local.Single(person => person.Name.StartsWith("Dr. Kent", StringComparison.Ordinal)).Name += ": Hero!"; + context.SaveChanges(); + + context2.SaveChanges(); + + } + catch (DbUpdateConcurrencyException exception) + { + Console.WriteLine($"Caught expected: " + exception.Message); + } + + Console.WriteLine(); + } + +}