diff --git a/entity-framework/core/what-is-new/ef-core-7.0/whatsnew.md b/entity-framework/core/what-is-new/ef-core-7.0/whatsnew.md index 6d60e5a4a9..0a71c2a8c5 100644 --- a/entity-framework/core/what-is-new/ef-core-7.0/whatsnew.md +++ b/entity-framework/core/what-is-new/ef-core-7.0/whatsnew.md @@ -19,6 +19,315 @@ EF7 is also available as [daily builds](https://github.com/dotnet/efcore/blob/ma EF7 targets .NET 6, and so can be used with either [.NET 6 (LTS)](https://dotnet.microsoft.com/download/dotnet/6.0) or [.NET 7](https://dotnet.microsoft.com/download/dotnet/7.0). +## ExecuteUpdate and ExecuteDelete (Bulk updates) + +By default, EF Core [tracks changes to entities](xref:core/change-tracking/index), and then [sends updates to the database](xref:core/saving/index) when one of the `SaveChanges` methods is called. Changes are only sent for properties and relationships that have actually changed. Also, the tracked entities remain in sync with the changes sent to the database. This mechanism is an efficient and convenient way to send general-purpose inserts, updates, and deletes to the database. These changes are also batched to reduce the number of database round-trips. + +However, it is sometimes useful to execute update or delete commands on the database without involving the change tracker. EF7 enables this with the new `ExecuteUpdate` and `ExecuteDelete` methods. These methods are applied to a LINQ query and will update or delete entities in the database based on the results of that query. Many entities can be updated with a single command and the entities are not loaded into memory, which means this can result in more efficient updates and deletes. + +However, keep in mind that: + +- The specific changes to make must be specified explicitly; they are not automatically detected by EF Core. +- Any tracked entities will not be kept in sync. +- Additional commands may need to be sent in the correct order so as not to violate database constraints. For example deleting dependents before a principal can be deleted. + +All of this means that the `ExecuteUpdate` and `ExecuteDelete` methods complement, rather than replace, the existing `SaveChanges` mechanism. + +### Sample model + +The examples below use a simple model with blogs, posts, tags, and authors: + + +[!code-csharp[BlogsModel](../../../../samples/core/Miscellaneous/NewInEFCore7/BlogsContext.cs?name=BlogsModel)] + +> [!TIP] +> The sample model can be found in [BlogsContext.cs](https://github.com/dotnet/EntityFramework.Docs/tree/main/samples/core/Miscellaneous/NewInEFCore7/BlogsContext.cs). + +### Basic `ExecuteDelete` examples + +> [!TIP] +> The code shown here comes from [ExecuteDeleteSample.cs](https://github.com/dotnet/EntityFramework.Docs/tree/main/samples/core/Miscellaneous/NewInEFCore7/ExecuteDeleteSample.cs). + +Calling `ExecuteDelete` or `ExecuteDeleteAsync` on a `DbSet` immediately deletes all entities of that `DbSet` from the database. For example, to delete all `Tag` entities: + + +[!code-csharp[DeleteAllTags](../../../../samples/core/Miscellaneous/NewInEFCore7/ExecuteDeleteSample.cs?name=DeleteAllTags)] + +This executes the following SQL when using SQL Server: + +```sql +DELETE FROM [t] +FROM [Tags] AS [t] +``` + +More interestingly, the query can contain a filter. For example: + + +[!code-csharp[DeleteTagsContainingDotNet](../../../../samples/core/Miscellaneous/NewInEFCore7/ExecuteDeleteSample.cs?name=DeleteTagsContainingDotNet)] + +This executes the following SQL: + +```sql +DELETE FROM [t] +FROM [Tags] AS [t] +WHERE [t].[Text] LIKE N'%.NET%' +``` + +The query can also use more complex filters, including navigations to other types. For example, to delete tags only from old blog posts: + + +[!code-csharp[DeleteTagsFromOldPosts](../../../../samples/core/Miscellaneous/NewInEFCore7/ExecuteDeleteSample.cs?name=DeleteTagsFromOldPosts)] + +Which executes: + +```sql +DELETE FROM [t] +FROM [Tags] AS [t] +WHERE NOT EXISTS ( + SELECT 1 + FROM [PostTag] AS [p] + INNER JOIN [Posts] AS [p0] ON [p].[PostsId] = [p0].[Id] + WHERE [t].[Id] = [p].[TagsId] AND NOT (DATEPART(year, [p0].[PublishedOn]) < 2022)) +``` + +### Basic `ExecuteUpdate` examples + +> [!TIP] +> The code shown here comes from [ExecuteUpdateSample.cs](https://github.com/dotnet/EntityFramework.Docs/tree/main/samples/core/Miscellaneous/NewInEFCore7/ExecuteUpdateSample.cs). + +`ExecuteUpdate` and `ExecuteUpdateAsync` behave in a very similar way to the `ExecuteDelete` methods. The main difference is that an update requires knowing _which_ properties to update, and _how_ to update them. This is achieved using one or more calls to `SetProperty`. For example, to update the `Name` of every blog: + + +[!code-csharp[UpdateAllBlogs](../../../../samples/core/Miscellaneous/NewInEFCore7/ExecuteUpdateSample.cs?name=UpdateAllBlogs)] + +The first parameter of `SetProperty` specifies which property to update; in this case, `Blog.Name`. The second parameter specifies how the new value should be calculated; in this case, by taking the existing value and appending `"*Featured!*"`. The resulting SQL is: + +```sql +UPDATE [b] + SET [b].[Name] = [b].[Name] + N' *Featured!*' +FROM [Blogs] AS [b] +``` + +As with `ExecuteDelete`, the query can be used to filter which entities are updated. In addition, multiple calls to `SetProperty` can be used to update more than one property on the target entity. For example, to update the `Title` and `Content` of all posts published before 2022: + + +[!code-csharp[UpdateOldPosts](../../../../samples/core/Miscellaneous/NewInEFCore7/ExecuteUpdateSample.cs?name=UpdateOldPosts)] + +In this case the generated SQL is a bit more complicated: + +```sql +UPDATE [p] + SET [p].[Content] = (([p].[Content] + N' ( This content was published in ') + COALESCE(CAST(DATEPART(year, [p].[PublishedOn]) AS nvarchar(max)), N'')) + N')', + [p].[Title] = (([p].[Title] + N' (') + COALESCE(CAST(DATEPART(year, [p].[PublishedOn]) AS nvarchar(max)), N'')) + N')' +FROM [Posts] AS [p] +WHERE DATEPART(year, [p].[PublishedOn]) < 2022 +``` + +Finally, again as with `ExecuteDelete`, the filter can reference other tables. For example, to update all tags from old posts: + + +[!code-csharp[UpdateTagsOnOldPosts](../../../../samples/core/Miscellaneous/NewInEFCore7/ExecuteUpdateSample.cs?name=UpdateTagsOnOldPosts)] + +Which generates: + +```sql +UPDATE [t] + SET [t].[Text] = [t].[Text] + N' (old)' +FROM [Tags] AS [t] +WHERE NOT EXISTS ( + SELECT 1 + FROM [PostTag] AS [p] + INNER JOIN [Posts] AS [p0] ON [p].[PostsId] = [p0].[Id] + WHERE [t].[Id] = [p].[TagsId] AND NOT (DATEPART(year, [p0].[PublishedOn]) < 2022)) +``` + +### Inheritance and multiple tables + +`ExecuteUpdate` and `ExecuteDelete` can only act on a single table. This has implications when working with different [inheritance mapping strategies](xref:core/modeling/inheritance). Generally, there are no problems when using the TPH mapping strategy, since there is only one table to modify. For example, deleting all `FeaturedPost` entities: + + +[!code-csharp[DeleteFeaturedPosts](../../../../samples/core/Miscellaneous/NewInEFCore7/ExecuteDeleteSample.cs?name=DeleteFeaturedPosts)] + +Generates the following SQL when using TPH mapping: + +```sql +DELETE FROM [p] +FROM [Posts] AS [p] +WHERE [p].[Discriminator] = N'FeaturedPost' +``` + +There are also no issues for this case when using the TPC mapping strategy, since again only changes to a single table are needed: + +```sql +DELETE FROM [f] +FROM [FeaturedPosts] AS [f] +``` + +However, attempting this when using the TPT mapping strategy will fail since it would require deleting rows from two different tables. + +Adding a filter to the query often means the operation will fail with both the TPC and TPT strategies. This is again because the rows may need to be deleted from multiple tables. For example, this query: + + +[!code-csharp[DeletePostsForGivenAuthor](../../../../samples/core/Miscellaneous/NewInEFCore7/ExecuteDeleteSample.cs?name=DeletePostsForGivenAuthor)] + +Generates the following SQL when using TPH: + +```sql +DELETE FROM [p] +FROM [Posts] AS [p] +LEFT JOIN [Authors] AS [a] ON [p].[AuthorId] = [a].[Id] +WHERE [a].[Name] IS NOT NULL AND ([a].[Name] LIKE N'Arthur%') +``` + +But fails when using TPC or TPT. + +> [!TIP] +> [Issue #10879](https://github.com/dotnet/efcore/issues/28520) tracks adding support for automatically sending multiple commands in these scenarios. Vote for this issue if it's something you would like to see implemented. + +### `ExecuteDelete` and relationships + +As mentioned above, it may be necessary to delete or update dependent entities before the principal of a relationship can be deleted. For example, each `Post` is a dependent of its associated `Author`. This means that an author cannot be deleted if a post still references it; doing so will violate the foreign key constraint in the database. For example, attempting this: + +```csharp +await context.Authors.ExecuteDeleteAsync(); +``` + +Will result in the following exception on SQL Server: + +> Microsoft.Data.SqlClient.SqlException (0x80131904): The DELETE statement conflicted with the REFERENCE constraint "FK_Posts_Authors_AuthorId". The conflict occurred in database "TphBlogsContext", table "dbo.Posts", column 'AuthorId'. +The statement has been terminated. + +To fix this, we must first either delete the posts, or sever the relationship between each post and its author by setting `AuthorId` foreign key property to null. For example, using the delete option: + + +[!code-csharp[DeleteAllAuthors](../../../../samples/core/Miscellaneous/NewInEFCore7/ExecuteDeleteSample.cs?name=DeleteAllAuthors)] + +This results in two separate commands; the first to delete the dependents: + +```sql +DELETE FROM [p] +FROM [Posts] AS [p] +``` + +And the second to delete the principals: + +```sql +DELETE FROM [a] +FROM [Authors] AS [a] +``` + +> [!IMPORTANT] +> Multiple `ExecuteDelete` and `ExecuteUpdate` commands will not be contained in a single transaction by default. However, the [DbContext transaction APIs](xref:core/saving/transactions) can be used in the normal way to wrap these commands in a transaction. + +> [!TIP] +> Sending these commands in a single round-trip depends on [Issue #10879](https://github.com/dotnet/efcore/issues/10879). Vote for this issue if it's something you would like to see implemented. + +Configuring [cascade deletes](xref:core/saving/cascade-delete) in the database can be very useful here. In our model, the relationship between `Blog` and `Post` is required, which causes EF Core to configure a cascade delete by convention. This means when a blog is deleted in the database, then all its dependent posts will also be deleted. It then follows that to delete all blogs and posts we need only delete the blogs: + + +[!code-csharp[DeleteAllBlogsAndPosts](../../../../samples/core/Miscellaneous/NewInEFCore7/ExecuteDeleteSample.cs?name=DeleteAllBlogsAndPosts)] + +This results in the following SQL: + +```sql +DELETE FROM [b] +FROM [Blogs] AS [b] +``` + +Which, as it is deleting a blog, will also cause all related posts to be deleted by the configured cascade delete. + ## Table-per-concrete-type (TPC) inheritance mapping By default, EF Core maps an inheritance hierarchy of .NET types to a single database table. This is known as the [table-per-hierarchy (TPH)](xref:core/modeling/inheritance#table-per-hierarchy-and-discriminator-configuration) mapping strategy. EF Core 5.0 introduced the [table-per-type (TPT)](xref:core/modeling/inheritance#table-per-type-configuration) strategy, which supports mapping each .NET type to a different database table. EF7 introduces the table-per-concrete-type (TPC) strategy. TPC also maps .NET types to different tables, but in a way that addresses some common performance issues with the TPT strategy. diff --git a/samples/core/Miscellaneous/NewInEFCore7/BlogsContext.cs b/samples/core/Miscellaneous/NewInEFCore7/BlogsContext.cs new file mode 100644 index 0000000000..fbdcb7b6a5 --- /dev/null +++ b/samples/core/Miscellaneous/NewInEFCore7/BlogsContext.cs @@ -0,0 +1,334 @@ +namespace NewInEfCore7; + +#region BlogsModel +public class Blog +{ + public Blog(string name) + { + Name = name; + } + + public int Id { get; private set; } + public string Name { get; set; } + public List Posts { get; } = new(); +} + +public class Post +{ + public Post(string title, string content, DateTime publishedOn) + { + Title = title; + Content = content; + PublishedOn = publishedOn; + } + + public int Id { get; private set; } + public string Title { get; set; } + public string Content { get; set; } + public DateTime PublishedOn { get; set; } + public Blog Blog { get; set; } = null!; + public List Tags { get; } = new(); + public Author? Author { get; set; } +} + +public class FeaturedPost : Post +{ + public FeaturedPost(string title, string content, DateTime publishedOn, string promoText) + : base(title, content, publishedOn) + { + PromoText = promoText; + } + + public string PromoText { get; set; } +} + +public class Tag +{ + public Tag(string text) + { + Text = text; + } + + public int Id { get; private set; } + public string Text { get; set; } + public List Posts { get; } = new(); +} + +public class Author +{ + public Author(string name) + { + Name = name; + } + + public int Id { get; private set; } + public string Name { get; set; } + public ContactDetails Contact { get; set; } = null!; + public List Posts { get; } = new(); +} + +public class ContactDetails +{ + public Address Address { get; init; } = null!; + public string? Phone { get; init; } +} + +public class Address +{ + public Address(string street, string city, string postcode, string country) + { + Street = street; + City = city; + Postcode = postcode; + Country = country; + } + + public string Street { get; init; } + public string City { get; init; } + public string Postcode { get; init; } + public string Country { get; init; } +} +#endregion + +public abstract class BlogsContext : DbContext +{ + protected BlogsContext(bool useSqlite) + { + UseSqlite = useSqlite; + } + + public bool UseSqlite { get; } + public bool LoggingEnabled { get; set; } + public abstract MappingStrategy MappingStrategy { get; } + + public DbSet Blogs => Set(); + public DbSet Tags => Set(); + public DbSet Posts => Set(); + public DbSet Authors => Set(); + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => (UseSqlite + ? optionsBuilder.UseSqlite(@$"DataSource={GetType().Name}") + : 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(); + + // https://github.com/dotnet/efcore/issues/28671 + // modelBuilder.Entity().OwnsOne(e => e.Contact).OwnsOne(e => e.Address); + modelBuilder.Entity().Ignore(e => e.Contact); + } + + public async Task Seed() + { + var tagEntityFramework = new Tag("Entity Framework"); + var tagDotNet = new Tag(".NET"); + var tagDotNetMaui = new Tag(".NET MAUI"); + var tagAspDotNet = new Tag("ASP.NET"); + var tagAspDotNetCore = new Tag("ASP.NET Core"); + var tagDotNetCore = new Tag(".NET Core"); + var tagHacking = new Tag("Hacking"); + var tagLinux = new Tag("Linux"); + var tagSqlite = new Tag("SQLite"); + var tagVisualStudio = new Tag("Visual Studio"); + var tagGraphQl = new Tag("GraphQL"); + var tagCosmosDb = new Tag("CosmosDB"); + var tagBlazor = new Tag("Blazor"); + + var maddy = new Author("Maddy Montaquila") + { + Contact = new() { Address = new("1 Main St", "Camberwick Green", "CW1 5ZH", "UK"), Phone = "01632 12345" } + }; + var jeremy = new Author("Jeremy Likness") + { + Contact = new() { Address = new("2 Main St", "Camberwick Green", "CW1 5ZH", "UK"), Phone = "01632 12346" } + }; + var dan = new Author("Daniel Roth") + { + Contact = new() { Address = new("3 Main St", "Camberwick Green", "CW1 5ZH", "UK"), Phone = "01632 12347" } + }; + var arthur = new Author("Arthur Vickers") + { + Contact = new() { Address = new("15a Main St", "Camberwick Green", "CW1 5ZH", "UK"), Phone = "01632 12348" } + }; + var brice = new Author("Brice Lambson") + { + Contact = new() { Address = new("4 Main St", "Camberwick Green", "CW1 5ZH", "UK"), Phone = "01632 12349" } + }; + + var blogs = new List + { + new(".NET Blog") + { + Posts = + { + new Post( + "Productivity comes to .NET MAUI in Visual Studio 2022", + "Visual Studio 2022 17.3 is now available and...", + new DateTime(2022, 8, 9)) { Tags = { tagDotNetMaui, tagDotNet }, Author = maddy, }, + new Post( + "Announcing .NET 7 Preview 7", ".NET 7 Preview 7 is now available with improvements to System.LINQ, Unix...", + new DateTime(2022, 8, 9)) { Tags = { tagDotNet }, Author = jeremy, }, + new Post( + "ASP.NET Core updates in .NET 7 Preview 7", ".NET 7 Preview 7 is now available! Check out what's new in...", + new DateTime(2022, 8, 9)) { Tags = { tagDotNet, tagAspDotNet, tagAspDotNetCore }, Author = dan, }, + new FeaturedPost( + "Announcing Entity Framework 7 Preview 7: Interceptors!", + "Announcing EF7 Preview 7 with new and improved interceptors, and...", + new DateTime(2022, 8, 9), + "Loads of runnable code!") { Tags = { tagEntityFramework, tagDotNet, tagDotNetCore }, Author = arthur, } + }, + }, + new("1unicorn2") + { + Posts = + { + new Post( + "Hacking my Sixth Form College network in 1991", + "Back in 1991 I was a student at Franklin Sixth Form College...", + new DateTime(2020, 4, 10)) { Tags = { tagHacking }, Author = arthur, }, + new FeaturedPost( + "All your versions are belong to us", + "Totally made up conversations about choosing Entity Framework version numbers...", + new DateTime(2020, 3, 26), + "Way funny!") { Tags = { tagEntityFramework }, Author = arthur, }, + new Post( + "Moving to Linux", "A few weeks ago, I decided to move from Windows to Linux as...", + new DateTime(2020, 3, 7)) { Tags = { tagLinux }, Author = arthur, }, + new Post( + "Welcome to One Unicorn 2.0!", "I created my first blog back in 2011..", + new DateTime(2020, 2, 29)) { Tags = { tagEntityFramework }, Author = arthur, } + } + }, + new("Brice's Blog") + { + Posts = + { + new FeaturedPost( + "SQLite in Visual Studio 2022", "A couple of years ago, I was thinking of ways...", + new DateTime(2022, 7, 26), "Love for VS!") { Tags = { tagSqlite, tagVisualStudio }, Author = brice, }, + new Post( + "On .NET - Entity Framework Migrations Explained", + "This week, @JamesMontemagno invited me onto the On .NET show...", + new DateTime(2022, 5, 4)) { Tags = { tagEntityFramework, tagDotNet }, Author = brice, }, + new Post( + "Dear DBA: A silly idea", "We have fun on the Entity Framework team...", + new DateTime(2022, 3, 31)) { Tags = { tagEntityFramework }, Author = brice, }, + new Post( + "Microsoft.Data.Sqlite 6", "It’s that time of year again. Microsoft.Data.Sqlite version...", + new DateTime(2021, 11, 8)) { Tags = { tagSqlite, tagDotNet }, Author = brice, } + } + }, + new("Developer for Life") + { + Posts = + { + new Post( + "GraphQL for .NET Developers", "A comprehensive overview of GraphQL as...", + new DateTime(2021, 7, 1)) { Tags = { tagDotNet, tagGraphQl, tagAspDotNetCore }, Author = jeremy, }, + new FeaturedPost( + "Azure Cosmos DB With EF Core on Blazor Server", + "Learn how to build Azure Cosmos DB apps using Entity Framework Core...", + new DateTime(2021, 5, 16), + "Blazor FTW!") + { + Tags = + { + tagDotNet, + tagEntityFramework, + tagAspDotNetCore, + tagCosmosDb, + tagBlazor + }, + Author = jeremy, + }, + new Post( + "Multi-tenancy with EF Core in Blazor Server Apps", + "Learn several ways to implement multi-tenant databases in Blazor Server apps...", + new DateTime(2021, 4, 29)) + { + Tags = { tagDotNet, tagEntityFramework, tagAspDotNetCore, tagBlazor }, Author = jeremy, + }, + new Post( + "An Easier Blazor Debounce", "Where I propose a simple method to debounce input without...", + new DateTime(2021, 4, 12)) { Tags = { tagDotNet, tagAspDotNetCore, tagBlazor }, Author = jeremy, } + } + } + }; + + await AddRangeAsync(blogs); + await SaveChangesAsync(); + } +} + +public enum MappingStrategy +{ + Tph, + Tpt, + Tpc, +} + +public class TphBlogsContext : BlogsContext +{ + public TphBlogsContext() + : base(useSqlite: false) + { + } + + public override MappingStrategy MappingStrategy => MappingStrategy.Tph; +} + +public class TphSqliteBlogsContext : BlogsContext +{ + public TphSqliteBlogsContext() + : base(useSqlite: true) + { + } + + public override MappingStrategy MappingStrategy => MappingStrategy.Tph; +} + +public class TptBlogsContext : BlogsContext +{ + public TptBlogsContext() + : base(useSqlite: false) + { + } + + public override MappingStrategy MappingStrategy => MappingStrategy.Tpt; + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().ToTable("FeaturedPosts"); + + base.OnModelCreating(modelBuilder); + } +} + +public class TpcBlogsContext : BlogsContext +{ + public TpcBlogsContext() + : base(useSqlite: false) + { + } + + public override MappingStrategy MappingStrategy => MappingStrategy.Tpc; + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().UseTpcMappingStrategy(); + modelBuilder.Entity().ToTable("FeaturedPosts"); + + base.OnModelCreating(modelBuilder); + } +} diff --git a/samples/core/Miscellaneous/NewInEFCore7/ExecuteDeleteSample.cs b/samples/core/Miscellaneous/NewInEFCore7/ExecuteDeleteSample.cs new file mode 100644 index 0000000000..59303041f3 --- /dev/null +++ b/samples/core/Miscellaneous/NewInEFCore7/ExecuteDeleteSample.cs @@ -0,0 +1,277 @@ +namespace NewInEfCore7; + +public static class ExecuteDeleteSample +{ + public static Task ExecuteDelete() + { + Console.WriteLine($">>>> Sample: {nameof(ExecuteDelete)}"); + Console.WriteLine(); + + return ExecuteDeleteTest(); + } + + public static Task ExecuteDeleteTpt() + { + Console.WriteLine($">>>> Sample: {nameof(ExecuteDelete)}"); + Console.WriteLine(); + + return ExecuteDeleteTest(); + } + + public static Task ExecuteDeleteTpc() + { + Console.WriteLine($">>>> Sample: {nameof(ExecuteDelete)}"); + Console.WriteLine(); + + return ExecuteDeleteTest(); + } + + public static Task ExecuteDeleteSqlite() + { + Console.WriteLine($">>>> Sample: {nameof(ExecuteDelete)}"); + Console.WriteLine(); + + return ExecuteDeleteTest(); + } + + private static async Task ExecuteDeleteTest() + where TContext : BlogsContext, new() + { + await using var context = new TContext(); + await context.Database.EnsureDeletedAsync(); + await context.Database.EnsureCreatedAsync(); + await context.Seed(); + + await DeleteAllTags(); + await DeleteTagsContainingDotNet(); + await DeleteTagsFromOldPosts(); + + if (context.MappingStrategy == MappingStrategy.Tph) + { + await DeleteAllAuthors(); + await DeleteAuthorsWithOnePost(); + } + + if (context.MappingStrategy != MappingStrategy.Tpt) + { + await DeleteFeaturedPosts(); + } + + if (context.MappingStrategy == MappingStrategy.Tph + && !context.UseSqlite) + { + await DeletePostsForGivenAuthor(); + } + + if (context.MappingStrategy != MappingStrategy.Tpt) + { + // https://github.com/dotnet/efcore/issues/28532 + await DeleteAllBlogsAndPosts(); + } + + Console.WriteLine(); + } + + private static async Task DeleteAllTags() + where TContext : BlogsContext, new() + { + await using var context = new TContext(); + await context.Database.BeginTransactionAsync(); + + Console.WriteLine("Delete all tags..."); + Console.WriteLine( + $"Tags before delete: {string.Join(", ", await context.Tags.AsNoTracking().Select(e => "'" + e.Text + "'").ToListAsync())}"); + Console.WriteLine(); + + context.LoggingEnabled = true; + + #region DeleteAllTags + await context.Tags.ExecuteDeleteAsync(); + #endregion + + context.LoggingEnabled = false; + + Console.WriteLine(); + Console.WriteLine( + $"Tags after delete: {string.Join(", ", await context.Tags.AsNoTracking().Select(e => "'" + e.Text + "'").ToListAsync())}"); + Console.WriteLine(); + } + + private static async Task DeleteTagsContainingDotNet() + where TContext : BlogsContext, new() + { + await using var context = new TContext(); + await context.Database.BeginTransactionAsync(); + + Console.WriteLine("Delete tags containing '.NET'..."); + Console.WriteLine( + $"Tags before delete: {string.Join(", ", await context.Tags.AsNoTracking().Select(e => "'" + e.Text + "'").ToListAsync())}"); + Console.WriteLine(); + + context.LoggingEnabled = true; + + #region DeleteTagsContainingDotNet + await context.Tags.Where(t => t.Text.Contains(".NET")).ExecuteDeleteAsync(); + #endregion + + context.LoggingEnabled = false; + + Console.WriteLine(); + Console.WriteLine( + $"Tags after delete: {string.Join(", ", await context.Tags.AsNoTracking().Select(e => "'" + e.Text + "'").ToListAsync())}"); + Console.WriteLine(); + } + + private static async Task DeleteTagsFromOldPosts() + where TContext : BlogsContext, new() + { + await using var context = new TContext(); + await context.Database.BeginTransactionAsync(); + + Console.WriteLine("Delete tags from old posts..."); + Console.WriteLine( + $"Tags before delete: {string.Join(", ", await context.Tags.AsNoTracking().Select(e => "'" + e.Text + "'").ToListAsync())}"); + Console.WriteLine(); + + context.LoggingEnabled = true; + + #region DeleteTagsFromOldPosts + await context.Tags.Where(t => t.Posts.All(e => e.PublishedOn.Year < 2022)).ExecuteDeleteAsync(); + #endregion + + context.LoggingEnabled = false; + + Console.WriteLine(); + Console.WriteLine( + $"Tags after delete: {string.Join(", ", await context.Tags.AsNoTracking().Select(e => "'" + e.Text + "'").ToListAsync())}"); + Console.WriteLine(); + } + + private static async Task DeleteAllAuthors() + where TContext : BlogsContext, new() + { + await using var context = new TContext(); + await context.Database.BeginTransactionAsync(); + + Console.WriteLine("Delete all authors..."); + Console.WriteLine( + $"Authors before delete: {string.Join(", ", await context.Authors.AsNoTracking().Select(e => "'" + e.Name + "'").ToListAsync())}"); + Console.WriteLine(); + + context.LoggingEnabled = true; + + #region DeleteAllAuthors + await context.Posts.ExecuteDeleteAsync(); + await context.Authors.ExecuteDeleteAsync(); + #endregion + + context.LoggingEnabled = false; + + Console.WriteLine(); + Console.WriteLine( + $"Authors after delete: {string.Join(", ", await context.Authors.AsNoTracking().Select(e => "'" + e.Name + "'").ToListAsync())}"); + Console.WriteLine(); + } + + private static async Task DeleteFeaturedPosts() + where TContext : BlogsContext, new() + { + await using var context = new TContext(); + await context.Database.BeginTransactionAsync(); + + Console.WriteLine("Delete featured posts..."); + Console.WriteLine( + $"Posts before delete: {string.Join(", ", await context.Posts.AsNoTracking().Select(e => "'" + e.Id + "'").ToListAsync())}"); + Console.WriteLine(); + + context.LoggingEnabled = true; + + #region DeleteFeaturedPosts + await context.Set().ExecuteDeleteAsync(); + #endregion + + context.LoggingEnabled = false; + + Console.WriteLine(); + Console.WriteLine( + $"Posts after delete: {string.Join(", ", await context.Posts.AsNoTracking().Select(e => "'" + e.Id + "'").ToListAsync())}"); + Console.WriteLine(); + } + + private static async Task DeletePostsForGivenAuthor() + where TContext : BlogsContext, new() + { + await using var context = new TContext(); + await context.Database.BeginTransactionAsync(); + + Console.WriteLine("Delete posts for given author..."); + Console.WriteLine( + $"Posts before delete: {string.Join(", ", await context.Posts.AsNoTracking().Select(e => "'" + e.Id + "'").ToListAsync())}"); + Console.WriteLine(); + + context.LoggingEnabled = true; + + #region DeletePostsForGivenAuthor + await context.Posts.Where(p => p.Author!.Name.StartsWith("Arthur")).ExecuteDeleteAsync(); + #endregion + + context.LoggingEnabled = false; + + Console.WriteLine(); + Console.WriteLine( + $"Posts after delete: {string.Join(", ", await context.Posts.AsNoTracking().Select(e => "'" + e.Id + "'").ToListAsync())}"); + Console.WriteLine(); + } + + private static async Task DeleteAllBlogsAndPosts() + where TContext : BlogsContext, new() + { + await using var context = new TContext(); + await context.Database.BeginTransactionAsync(); + + Console.WriteLine("Delete all blogs and posts..."); + Console.WriteLine( + $"Blogs before delete: {string.Join(", ", await context.Blogs.AsNoTracking().Select(e => "'" + e.Id + "'").ToListAsync())}"); + Console.WriteLine( + $"Posts before delete: {string.Join(", ", await context.Posts.AsNoTracking().Select(e => "'" + e.Id + "'").ToListAsync())}"); + Console.WriteLine(); + + context.LoggingEnabled = true; + + #region DeleteAllBlogsAndPosts + await context.Blogs.ExecuteDeleteAsync(); + #endregion + + context.LoggingEnabled = false; + + Console.WriteLine(); + Console.WriteLine( + $"Blogs after delete: {string.Join(", ", await context.Blogs.AsNoTracking().Select(e => "'" + e.Id + "'").ToListAsync())}"); + Console.WriteLine( + $"Posts after delete: {string.Join(", ", await context.Posts.AsNoTracking().Select(e => "'" + e.Id + "'").ToListAsync())}"); + Console.WriteLine(); + } + + private static async Task DeleteAuthorsWithOnePost() + where TContext : BlogsContext, new() + { + await using var context = new TContext(); + await context.Database.BeginTransactionAsync(); + + Console.WriteLine("Delete authors with only one post..."); + Console.WriteLine( + $"Authors before delete: {string.Join(", ", await context.Authors.AsNoTracking().Select(e => "'" + e.Name + "'").ToListAsync())}"); + Console.WriteLine(); + + context.LoggingEnabled = true; + await context.Posts.Where(p => p.Author!.Posts.Count <= 1) + .ExecuteUpdateAsync(s => s.SetProperty(p => EF.Property(p, "AuthorId"), p => null)); + await context.Authors.Where(a => a.Posts.Count <= 1).ExecuteDeleteAsync(); + context.LoggingEnabled = false; + + Console.WriteLine(); + Console.WriteLine( + $"Authors after delete: {string.Join(", ", await context.Authors.AsNoTracking().Select(e => "'" + e.Name + "'").ToListAsync())}"); + Console.WriteLine(); + } +} diff --git a/samples/core/Miscellaneous/NewInEFCore7/ExecuteUpdateSample.cs b/samples/core/Miscellaneous/NewInEFCore7/ExecuteUpdateSample.cs new file mode 100644 index 0000000000..c223428cc6 --- /dev/null +++ b/samples/core/Miscellaneous/NewInEFCore7/ExecuteUpdateSample.cs @@ -0,0 +1,167 @@ +namespace NewInEfCore7; + +public static class ExecuteUpdateSample +{ + public static Task ExecuteUpdate() + { + Console.WriteLine($">>>> Sample: {nameof(ExecuteUpdate)}"); + Console.WriteLine(); + + return ExecuteUpdateTest(); + } + + public static Task ExecuteUpdateTpt() + { + Console.WriteLine($">>>> Sample: {nameof(ExecuteUpdate)}"); + Console.WriteLine(); + + return ExecuteUpdateTest(); + } + + public static Task ExecuteUpdateTpc() + { + Console.WriteLine($">>>> Sample: {nameof(ExecuteUpdate)}"); + Console.WriteLine(); + + return ExecuteUpdateTest(); + } + + public static Task ExecuteUpdateSqlite() + { + Console.WriteLine($">>>> Sample: {nameof(ExecuteUpdate)}"); + Console.WriteLine(); + + return ExecuteUpdateTest(); + } + + private static async Task ExecuteUpdateTest() + where TContext : BlogsContext, new() + { + await using var context = new TContext(); + await context.Database.EnsureDeletedAsync(); + await context.Database.EnsureCreatedAsync(); + await context.Seed(); + + await UpdateAllBlogs(); + + if (context.MappingStrategy == MappingStrategy.Tph) + { + await UpdateOldPosts(); + } + + if (context.MappingStrategy != MappingStrategy.Tpt) + { + await UpdateFeaturedPosts(); + } + + await UpdateTagsOnOldPosts(); + + Console.WriteLine(); + } + + private static async Task UpdateAllBlogs() + where TContext : BlogsContext, new() + { + await using var context = new TContext(); + await context.Database.BeginTransactionAsync(); + + Console.WriteLine("Update names for all blogs..."); + Console.WriteLine( + $"Blogs before update: {string.Join(", ", await context.Blogs.AsNoTracking().Select(e => "'" + e.Name + "'").ToListAsync())}"); + Console.WriteLine(); + + context.LoggingEnabled = true; + + #region UpdateAllBlogs + await context.Blogs.ExecuteUpdateAsync( + s => s.SetProperty(b => b.Name, b => b.Name + " *Featured!*")); + #endregion + + context.LoggingEnabled = false; + + Console.WriteLine(); + Console.WriteLine( + $"Blogs after update: {string.Join(", ", await context.Blogs.AsNoTracking().Select(e => "'" + e.Name + "'").ToListAsync())}"); + Console.WriteLine(); + } + + private static async Task UpdateOldPosts() + where TContext : BlogsContext, new() + { + await using var context = new TContext(); + await context.Database.BeginTransactionAsync(); + + Console.WriteLine("Update title and content for old posts..."); + Console.WriteLine( + $"Posts before update: {string.Join(", ", await context.Posts.AsNoTracking().Select(e => "'..." + e.Title.Substring(e.Title.Length - 12) + "'").ToListAsync())}"); + Console.WriteLine(); + + context.LoggingEnabled = true; + + #region UpdateOldPosts + await context.Posts + .Where(p => p.PublishedOn.Year < 2022) + .ExecuteUpdateAsync(s => s + .SetProperty(b => b.Title, b => b.Title + " (" + b.PublishedOn.Year + ")") + .SetProperty(b => b.Content, b => b.Content + " ( This content was published in " + b.PublishedOn.Year + ")")); + #endregion + + context.LoggingEnabled = false; + + Console.WriteLine(); + Console.WriteLine( + $"Posts after update: {string.Join(", ", await context.Posts.AsNoTracking().Select(e => "'..." + e.Title.Substring(e.Title.Length - 12) + "'").ToListAsync())}"); + Console.WriteLine(); + } + + private static async Task UpdateFeaturedPosts() + where TContext : BlogsContext, new() + { + await using var context = new TContext(); + await context.Database.BeginTransactionAsync(); + + Console.WriteLine("Update title and content for featured posts..."); + Console.WriteLine( + $"Posts before update: {string.Join(", ", await context.Posts.AsNoTracking().Select(e => "'..." + e.Title.Substring(e.Title.Length - 12) + "'").ToListAsync())}"); + Console.WriteLine(); + + context.LoggingEnabled = true; + await context.Set() + .ExecuteUpdateAsync( + s => s.SetProperty(b => b.Title, b => b.Title + " *Featured!*") + .SetProperty(b => b.Content, b => "Featured: " + b.Content)); + context.LoggingEnabled = false; + + Console.WriteLine(); + Console.WriteLine( + $"Posts after update: {string.Join(", ", await context.Posts.AsNoTracking().Select(e => "'..." + e.Title.Substring(e.Title.Length - 12) + "'").ToListAsync())}"); + Console.WriteLine(); + } + + private static async Task UpdateTagsOnOldPosts() + where TContext : BlogsContext, new() + { + await using var context = new TContext(); + await context.Database.BeginTransactionAsync(); + + Console.WriteLine("Update tags on old posts"); + Console.WriteLine( + $"Tags before update: {string.Join(", ", await context.Tags.AsNoTracking().Select(e => "'" + e.Text + "'").ToListAsync())}"); + Console.WriteLine(); + + context.LoggingEnabled = true; + + #region UpdateTagsOnOldPosts + await context.Tags + .Where(t => t.Posts.All(e => e.PublishedOn.Year < 2022)) + .ExecuteUpdateAsync(s => s.SetProperty(t => t.Text, t => t.Text + " (old)")); + #endregion + + context.LoggingEnabled = false; + + Console.WriteLine(); + Console.WriteLine( + $"Tags after update: {string.Join(", ", await context.Tags.AsNoTracking().Select(e => "'" + e.Text + "'").ToListAsync())}"); + Console.WriteLine(); + } +} diff --git a/samples/core/Miscellaneous/NewInEFCore7/NewInEFCore7.csproj b/samples/core/Miscellaneous/NewInEFCore7/NewInEFCore7.csproj index 4834ba8d86..e41335a352 100644 --- a/samples/core/Miscellaneous/NewInEFCore7/NewInEFCore7.csproj +++ b/samples/core/Miscellaneous/NewInEFCore7/NewInEFCore7.csproj @@ -5,14 +5,23 @@ net6.0 enable enable - + NewInEfCore7 - - - - + + + + + + + + + + + + + diff --git a/samples/core/Miscellaneous/NewInEFCore7/Program.cs b/samples/core/Miscellaneous/NewInEFCore7/Program.cs index 94a99446e2..342f39eb44 100644 --- a/samples/core/Miscellaneous/NewInEFCore7/Program.cs +++ b/samples/core/Miscellaneous/NewInEFCore7/Program.cs @@ -1,4 +1,6 @@ -public class Program +using NewInEfCore7; + +public class Program { public static async Task Main() { @@ -9,5 +11,15 @@ public static async Task Main() // 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(); } } diff --git a/samples/core/Miscellaneous/NewInEFCore7/TpcInheritanceSample.cs b/samples/core/Miscellaneous/NewInEFCore7/TpcInheritanceSample.cs index d3c9b86d77..eb7d2731ef 100644 --- a/samples/core/Miscellaneous/NewInEFCore7/TpcInheritanceSample.cs +++ b/samples/core/Miscellaneous/NewInEFCore7/TpcInheritanceSample.cs @@ -1,6 +1,4 @@ -using System.ComponentModel.DataAnnotations.Schema; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; +namespace NewInEfCore7; public static class TpcInheritanceSample {