diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/DeleteBehaviors/Relationships/Optional/Blog.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/DeleteBehaviors/Relationships/Optional/Blog.cs new file mode 100644 index 0000000000..352fc30ae3 --- /dev/null +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/DeleteBehaviors/Relationships/Optional/Blog.cs @@ -0,0 +1,19 @@ +using JetBrains.Annotations; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace JsonApiDotNetCoreTests.IntegrationTests.DeleteBehaviors.Relationships.Optional; + +[UsedImplicitly(ImplicitUseTargetFlags.Members)] +[Resource(ControllerNamespace = "JsonApiDotNetCoreTests.IntegrationTests.DeleteBehaviors.Relationships.Optional")] +public sealed class Blog : Identifiable +{ + [Attr] + public string Name { get; set; } = null!; + + [HasMany] + public IList Posts { get; } = new List(); + + [HasOne] + public Person? Owner { get; set; } +} diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/DeleteBehaviors/Relationships/Optional/BloggingDbContext.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/DeleteBehaviors/Relationships/Optional/BloggingDbContext.cs new file mode 100644 index 0000000000..6bdb7f777a --- /dev/null +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/DeleteBehaviors/Relationships/Optional/BloggingDbContext.cs @@ -0,0 +1,93 @@ +//#define HANDLE_CLIENT_SIDE + +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore; + +// @formatter:wrap_chained_method_calls chop_always + +namespace JsonApiDotNetCoreTests.IntegrationTests.DeleteBehaviors.Relationships.Optional; + +[UsedImplicitly(ImplicitUseTargetFlags.Members)] +public class BloggingDbContext : DbContext +{ + public DbSet Blogs => Set(); + public DbSet Posts => Set(); + public DbSet People => Set(); + + public BloggingDbContext(DbContextOptions options) + : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder builder) + { + builder.Entity() + .HasOne(post => post.Blog) + .WithMany(blog => blog.Posts) + .HasForeignKey("BlogId") +#if HANDLE_CLIENT_SIDE + .OnDelete(DeleteBehavior.ClientSetNull) +#else + .OnDelete(DeleteBehavior.SetNull) +#endif + ; + + builder.Entity() + .HasOne(post => post.Author) + .WithMany(person => person.Posts) + .HasForeignKey("AuthorId") +#if HANDLE_CLIENT_SIDE + .OnDelete(DeleteBehavior.ClientSetNull) +#else + .OnDelete(DeleteBehavior.SetNull) +#endif + ; + + builder.Entity() + .HasOne(blog => blog.Owner) + .WithOne(person => person.OwnedBlog) + .HasForeignKey("OwnerId") +#if HANDLE_CLIENT_SIDE + .OnDelete(DeleteBehavior.ClientSetNull) +#else + .OnDelete(DeleteBehavior.SetNull) +#endif + ; + + // Generated SQL: + /* + + CREATE TABLE "People" ( + "Id" integer GENERATED BY DEFAULT AS IDENTITY, + "Name" text NOT NULL, + CONSTRAINT "PK_People" PRIMARY KEY ("Id") + ); + + CREATE TABLE "Blogs" ( + "Id" integer GENERATED BY DEFAULT AS IDENTITY, + "Name" text NOT NULL, + "OwnerId" integer NULL, + CONSTRAINT "PK_Blogs" PRIMARY KEY ("Id"), + CONSTRAINT "FK_Blogs_People_OwnerId" FOREIGN KEY ("OwnerId") REFERENCES "People" ("Id") ON DELETE SET NULL + ); + + CREATE TABLE "Posts" ( + "Id" integer GENERATED BY DEFAULT AS IDENTITY, + "Title" text NOT NULL, + "Content" text NOT NULL, + "BlogId" integer NULL, + "AuthorId" integer NULL, + CONSTRAINT "PK_Posts" PRIMARY KEY ("Id"), + CONSTRAINT "FK_Posts_Blogs_BlogId" FOREIGN KEY ("BlogId") REFERENCES "Blogs" ("Id") ON DELETE SET NULL, + CONSTRAINT "FK_Posts_People_AuthorId" FOREIGN KEY ("AuthorId") REFERENCES "People" ("Id") ON DELETE SET NULL + ); + + CREATE UNIQUE INDEX "IX_Blogs_OwnerId" ON "Blogs" ("OwnerId"); + + CREATE INDEX "IX_Posts_AuthorId" ON "Posts" ("AuthorId"); + + CREATE INDEX "IX_Posts_BlogId" ON "Posts" ("BlogId"); + + */ + } +} diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/DeleteBehaviors/Relationships/Optional/Person.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/DeleteBehaviors/Relationships/Optional/Person.cs new file mode 100644 index 0000000000..c152b032d8 --- /dev/null +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/DeleteBehaviors/Relationships/Optional/Person.cs @@ -0,0 +1,19 @@ +using JetBrains.Annotations; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace JsonApiDotNetCoreTests.IntegrationTests.DeleteBehaviors.Relationships.Optional; + +[UsedImplicitly(ImplicitUseTargetFlags.Members)] +[Resource(ControllerNamespace = "JsonApiDotNetCoreTests.IntegrationTests.DeleteBehaviors.Relationships.Optional")] +public sealed class Person : Identifiable +{ + [Attr] + public string Name { get; set; } = null!; + + [HasMany] + public IList Posts { get; } = new List(); + + [HasOne] + public Blog? OwnedBlog { get; set; } +} diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/DeleteBehaviors/Relationships/Optional/Post.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/DeleteBehaviors/Relationships/Optional/Post.cs new file mode 100644 index 0000000000..e1ab61c3bf --- /dev/null +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/DeleteBehaviors/Relationships/Optional/Post.cs @@ -0,0 +1,22 @@ +using JetBrains.Annotations; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace JsonApiDotNetCoreTests.IntegrationTests.DeleteBehaviors.Relationships.Optional; + +[UsedImplicitly(ImplicitUseTargetFlags.Members)] +[Resource(ControllerNamespace = "JsonApiDotNetCoreTests.IntegrationTests.DeleteBehaviors.Relationships.Optional")] +public sealed class Post : Identifiable +{ + [Attr] + public string Title { get; set; } = null!; + + [Attr] + public string Content { get; set; } = null!; + + [HasOne] + public Blog? Blog { get; set; } + + [HasOne] + public Person? Author { get; set; } +} diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/DeleteBehaviors/Relationships/Optional/SetNullTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/DeleteBehaviors/Relationships/Optional/SetNullTests.cs new file mode 100644 index 0000000000..517379f65d --- /dev/null +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/DeleteBehaviors/Relationships/Optional/SetNullTests.cs @@ -0,0 +1,130 @@ +using FluentAssertions; +using Microsoft.EntityFrameworkCore; +using TestBuildingBlocks; +using Xunit; + +namespace JsonApiDotNetCoreTests.IntegrationTests.DeleteBehaviors.Relationships.Optional; + +public sealed class SetNullTests : IClassFixture, BloggingDbContext>> +{ + private const string OwnerName = "Jack"; + private const string AuthorName = "Jull"; + + private readonly IntegrationTestContext, BloggingDbContext> _testContext; + + public SetNullTests(IntegrationTestContext, BloggingDbContext> testContext) + { + _testContext = testContext; + } + + [Fact] + public async Task Deleting_a_blog_will_cause_the_blog_in_all_the_related_posts_to_become_null() + { + // Arrange + await StoreTestDataAsync(); + + // Act + await _testContext.RunOnDatabaseAsync(async dbContext => + { + Blog blog = await dbContext.Blogs.SingleAsync(); + dbContext.Remove(blog); + + await dbContext.SaveChangesAsync(); + }); + + // Assert + await _testContext.RunOnDatabaseAsync(async dbContext => + { + dbContext.Blogs.Should().BeEmpty(); + dbContext.Posts.Should().ContainSingle(post => post.Blog == null); + dbContext.People.Should().HaveCount(2); + await Task.Yield(); + }); + } + + [Fact] + public async Task Deleting_the_author_of_posts_will_cause_the_author_of_authored_posts_to_become_null() + { + // Arrange + await StoreTestDataAsync(); + + // Act + await _testContext.RunOnDatabaseAsync(async dbContext => + { + Person author = await dbContext.People.SingleAsync(person => person.Name == AuthorName); + dbContext.Remove(author); + + await dbContext.SaveChangesAsync(); + }); + + // Assert + await _testContext.RunOnDatabaseAsync(async dbContext => + { + dbContext.Blogs.Should().HaveCount(1); + dbContext.Posts.Should().ContainSingle(post => post.Author == null); + dbContext.People.Should().ContainSingle(person => person.Name == OwnerName); + await Task.Yield(); + }); + } + + [Fact] + public async Task Deleting_the_owner_of_a_blog_will_cause_the_owner_of_blog_to_become_null() + { + // Arrange + await StoreTestDataAsync(); + + // Act + await _testContext.RunOnDatabaseAsync(async dbContext => + { + Person owner = await dbContext.People.SingleAsync(person => person.Name == OwnerName); + dbContext.Remove(owner); + + await dbContext.SaveChangesAsync(); + }); + + // Assert + await _testContext.RunOnDatabaseAsync(async dbContext => + { + dbContext.Blogs.Should().ContainSingle(blog => blog.Owner == null); + dbContext.Posts.Should().HaveCount(1); + dbContext.People.Should().ContainSingle(person => person.Name == AuthorName); + await Task.Yield(); + }); + } + + private async Task StoreTestDataAsync() + { + Post newPost = CreateTestData(); + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + await dbContext.ClearTableAsync(); + await dbContext.ClearTableAsync(); + await dbContext.ClearTableAsync(); + + dbContext.Posts.Add(newPost); + await dbContext.SaveChangesAsync(); + }); + } + + private static Post CreateTestData() + { + return new Post + { + Title = "Cascading Deletes", + Content = "...", + Blog = new Blog + { + Name = "EF Core", + Owner = new Person + { + Name = OwnerName + } + }, + Author = new Person + { + Name = AuthorName + } + }; + } +} diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/DeleteBehaviors/Relationships/Required/Blog.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/DeleteBehaviors/Relationships/Required/Blog.cs new file mode 100644 index 0000000000..78632fa174 --- /dev/null +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/DeleteBehaviors/Relationships/Required/Blog.cs @@ -0,0 +1,19 @@ +using JetBrains.Annotations; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace JsonApiDotNetCoreTests.IntegrationTests.DeleteBehaviors.Relationships.Required; + +[UsedImplicitly(ImplicitUseTargetFlags.Members)] +[Resource(ControllerNamespace = "JsonApiDotNetCoreTests.IntegrationTests.DeleteBehaviors.Relationships.Required")] +public sealed class Blog : Identifiable +{ + [Attr] + public string Name { get; set; } = null!; + + [HasMany] + public IList Posts { get; } = new List(); + + [HasOne] + public Person Owner { get; set; } = null!; +} diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/DeleteBehaviors/Relationships/Required/BloggingDbContext.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/DeleteBehaviors/Relationships/Required/BloggingDbContext.cs new file mode 100644 index 0000000000..9b1f7a8846 --- /dev/null +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/DeleteBehaviors/Relationships/Required/BloggingDbContext.cs @@ -0,0 +1,93 @@ +//#define HANDLE_CLIENT_SIDE + +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore; + +// @formatter:wrap_chained_method_calls chop_always + +namespace JsonApiDotNetCoreTests.IntegrationTests.DeleteBehaviors.Relationships.Required; + +[UsedImplicitly(ImplicitUseTargetFlags.Members)] +public class BloggingDbContext : DbContext +{ + public DbSet Blogs => Set(); + public DbSet Posts => Set(); + public DbSet People => Set(); + + public BloggingDbContext(DbContextOptions options) + : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder builder) + { + builder.Entity() + .HasOne(post => post.Blog) + .WithMany(blog => blog.Posts) + .HasForeignKey("BlogId") +#if HANDLE_CLIENT_SIDE + .OnDelete(DeleteBehavior.ClientCascade) +#else + .OnDelete(DeleteBehavior.Cascade) +#endif + ; + + builder.Entity() + .HasOne(post => post.Author) + .WithMany(person => person.Posts) + .HasForeignKey("AuthorId") +#if HANDLE_CLIENT_SIDE + .OnDelete(DeleteBehavior.ClientCascade) +#else + .OnDelete(DeleteBehavior.Cascade) +#endif + ; + + builder.Entity() + .HasOne(blog => blog.Owner) + .WithOne(person => person.OwnedBlog) + .HasForeignKey("OwnerId") +#if HANDLE_CLIENT_SIDE + .OnDelete(DeleteBehavior.ClientCascade) +#else + .OnDelete(DeleteBehavior.Cascade) +#endif + ; + + // Generated SQL: + /* + + CREATE TABLE "People" ( + "Id" integer GENERATED BY DEFAULT AS IDENTITY, + "Name" text NOT NULL, + CONSTRAINT "PK_People" PRIMARY KEY ("Id") + ); + + CREATE TABLE "Blogs" ( + "Id" integer GENERATED BY DEFAULT AS IDENTITY, + "Name" text NOT NULL, + "OwnerId" integer NOT NULL, + CONSTRAINT "PK_Blogs" PRIMARY KEY ("Id"), + CONSTRAINT "FK_Blogs_People_OwnerId" FOREIGN KEY ("OwnerId") REFERENCES "People" ("Id") ON DELETE CASCADE + ); + + CREATE TABLE "Posts" ( + "Id" integer GENERATED BY DEFAULT AS IDENTITY, + "Title" text NOT NULL, + "Content" text NOT NULL, + "BlogId" integer NOT NULL, + "AuthorId" integer NOT NULL, + CONSTRAINT "PK_Posts" PRIMARY KEY ("Id"), + CONSTRAINT "FK_Posts_Blogs_BlogId" FOREIGN KEY ("BlogId") REFERENCES "Blogs" ("Id") ON DELETE CASCADE, + CONSTRAINT "FK_Posts_People_AuthorId" FOREIGN KEY ("AuthorId") REFERENCES "People" ("Id") ON DELETE CASCADE + ); + + CREATE UNIQUE INDEX "IX_Blogs_OwnerId" ON "Blogs" ("OwnerId"); + + CREATE INDEX "IX_Posts_AuthorId" ON "Posts" ("AuthorId"); + + CREATE INDEX "IX_Posts_BlogId" ON "Posts" ("BlogId"); + + */ + } +} diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/DeleteBehaviors/Relationships/Required/CascadeTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/DeleteBehaviors/Relationships/Required/CascadeTests.cs new file mode 100644 index 0000000000..edb5966300 --- /dev/null +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/DeleteBehaviors/Relationships/Required/CascadeTests.cs @@ -0,0 +1,130 @@ +using FluentAssertions; +using Microsoft.EntityFrameworkCore; +using TestBuildingBlocks; +using Xunit; + +namespace JsonApiDotNetCoreTests.IntegrationTests.DeleteBehaviors.Relationships.Required; + +public sealed class CascadeTests : IClassFixture, BloggingDbContext>> +{ + private const string OwnerName = "Jack"; + private const string AuthorName = "Jull"; + + private readonly IntegrationTestContext, BloggingDbContext> _testContext; + + public CascadeTests(IntegrationTestContext, BloggingDbContext> testContext) + { + _testContext = testContext; + } + + [Fact] + public async Task Deleting_a_blog_will_cascade_delete_all_the_related_posts() + { + // Arrange + await StoreTestDataAsync(); + + // Act + await _testContext.RunOnDatabaseAsync(async dbContext => + { + Blog blog = await dbContext.Blogs.SingleAsync(); + dbContext.Remove(blog); + + await dbContext.SaveChangesAsync(); + }); + + // Assert + await _testContext.RunOnDatabaseAsync(async dbContext => + { + dbContext.Blogs.Should().BeEmpty(); + dbContext.Posts.Should().BeEmpty(); + dbContext.People.Should().HaveCount(2); + await Task.Yield(); + }); + } + + [Fact] + public async Task Deleting_the_author_of_posts_will_cause_the_authored_posts_to_be_cascade_deleted() + { + // Arrange + await StoreTestDataAsync(); + + // Act + await _testContext.RunOnDatabaseAsync(async dbContext => + { + Person author = await dbContext.People.SingleAsync(person => person.Name == AuthorName); + dbContext.Remove(author); + + await dbContext.SaveChangesAsync(); + }); + + // Assert + await _testContext.RunOnDatabaseAsync(async dbContext => + { + dbContext.Blogs.Should().HaveCount(1); + dbContext.Posts.Should().BeEmpty(); + dbContext.People.Should().ContainSingle(person => person.Name == OwnerName); + await Task.Yield(); + }); + } + + [Fact] + public async Task Deleting_the_owner_of_a_blog_will_cause_the_blog_to_be_cascade_deleted() + { + // Arrange + await StoreTestDataAsync(); + + // Act + await _testContext.RunOnDatabaseAsync(async dbContext => + { + Person owner = await dbContext.People.SingleAsync(person => person.Name == OwnerName); + dbContext.Remove(owner); + + await dbContext.SaveChangesAsync(); + }); + + // Assert + await _testContext.RunOnDatabaseAsync(async dbContext => + { + dbContext.Blogs.Should().BeEmpty(); + dbContext.Posts.Should().BeEmpty(); + dbContext.People.Should().ContainSingle(person => person.Name == AuthorName); + await Task.Yield(); + }); + } + + private async Task StoreTestDataAsync() + { + Post newPost = CreateTestData(); + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + await dbContext.ClearTableAsync(); + await dbContext.ClearTableAsync(); + await dbContext.ClearTableAsync(); + + dbContext.Posts.Add(newPost); + await dbContext.SaveChangesAsync(); + }); + } + + private static Post CreateTestData() + { + return new Post + { + Title = "Cascading Deletes", + Content = "...", + Blog = new Blog + { + Name = "EF Core", + Owner = new Person + { + Name = OwnerName + } + }, + Author = new Person + { + Name = AuthorName + } + }; + } +} diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/DeleteBehaviors/Relationships/Required/Person.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/DeleteBehaviors/Relationships/Required/Person.cs new file mode 100644 index 0000000000..56f6ca66e1 --- /dev/null +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/DeleteBehaviors/Relationships/Required/Person.cs @@ -0,0 +1,19 @@ +using JetBrains.Annotations; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace JsonApiDotNetCoreTests.IntegrationTests.DeleteBehaviors.Relationships.Required; + +[UsedImplicitly(ImplicitUseTargetFlags.Members)] +[Resource(ControllerNamespace = "JsonApiDotNetCoreTests.IntegrationTests.DeleteBehaviors.Relationships.Required")] +public sealed class Person : Identifiable +{ + [Attr] + public string Name { get; set; } = null!; + + [HasMany] + public IList Posts { get; } = new List(); + + [HasOne] + public Blog OwnedBlog { get; set; } = null!; +} diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/DeleteBehaviors/Relationships/Required/Post.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/DeleteBehaviors/Relationships/Required/Post.cs new file mode 100644 index 0000000000..2af5f009d5 --- /dev/null +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/DeleteBehaviors/Relationships/Required/Post.cs @@ -0,0 +1,22 @@ +using JetBrains.Annotations; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace JsonApiDotNetCoreTests.IntegrationTests.DeleteBehaviors.Relationships.Required; + +[UsedImplicitly(ImplicitUseTargetFlags.Members)] +[Resource(ControllerNamespace = "JsonApiDotNetCoreTests.IntegrationTests.DeleteBehaviors.Relationships.Required")] +public sealed class Post : Identifiable +{ + [Attr] + public string Title { get; set; } = null!; + + [Attr] + public string Content { get; set; } = null!; + + [HasOne] + public Blog Blog { get; set; } = null!; + + [HasOne] + public Person Author { get; set; } = null!; +}