Skip to content

Research delete behaviors with PostgreSQL and EF Core #1205

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<int>
{
[Attr]
public string Name { get; set; } = null!;

[HasMany]
public IList<Post> Posts { get; } = new List<Post>();

[HasOne]
public Person? Owner { get; set; }
}
Original file line number Diff line number Diff line change
@@ -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<Blog> Blogs => Set<Blog>();
public DbSet<Post> Posts => Set<Post>();
public DbSet<Person> People => Set<Person>();

public BloggingDbContext(DbContextOptions<BloggingDbContext> options)
: base(options)
{
}

protected override void OnModelCreating(ModelBuilder builder)
{
builder.Entity<Post>()
.HasOne(post => post.Blog)
.WithMany(blog => blog.Posts)
.HasForeignKey("BlogId")
#if HANDLE_CLIENT_SIDE
.OnDelete(DeleteBehavior.ClientSetNull)
#else
.OnDelete(DeleteBehavior.SetNull)
#endif
;

builder.Entity<Post>()
.HasOne(post => post.Author)
.WithMany(person => person.Posts)
.HasForeignKey("AuthorId")
#if HANDLE_CLIENT_SIDE
.OnDelete(DeleteBehavior.ClientSetNull)
#else
.OnDelete(DeleteBehavior.SetNull)
#endif
;

builder.Entity<Blog>()
.HasOne(blog => blog.Owner)
.WithOne(person => person.OwnedBlog)
.HasForeignKey<Blog>("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");

*/
}
}
Original file line number Diff line number Diff line change
@@ -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<int>
{
[Attr]
public string Name { get; set; } = null!;

[HasMany]
public IList<Post> Posts { get; } = new List<Post>();

[HasOne]
public Blog? OwnedBlog { get; set; }
}
Original file line number Diff line number Diff line change
@@ -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<int>
{
[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; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
using FluentAssertions;
using Microsoft.EntityFrameworkCore;
using TestBuildingBlocks;
using Xunit;

namespace JsonApiDotNetCoreTests.IntegrationTests.DeleteBehaviors.Relationships.Optional;

public sealed class SetNullTests : IClassFixture<IntegrationTestContext<TestableStartup<BloggingDbContext>, BloggingDbContext>>
{
private const string OwnerName = "Jack";
private const string AuthorName = "Jull";

private readonly IntegrationTestContext<TestableStartup<BloggingDbContext>, BloggingDbContext> _testContext;

public SetNullTests(IntegrationTestContext<TestableStartup<BloggingDbContext>, 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<Blog>();
await dbContext.ClearTableAsync<Post>();
await dbContext.ClearTableAsync<Person>();

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
}
};
}
}
Original file line number Diff line number Diff line change
@@ -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<int>
{
[Attr]
public string Name { get; set; } = null!;

[HasMany]
public IList<Post> Posts { get; } = new List<Post>();

[HasOne]
public Person Owner { get; set; } = null!;
}
Loading