简体   繁体   中英

How to properly setup ApplicationUser and FriendRequest entities to allow for cascade deleting

I have the traditional ApplicationUser (IdentityUser), and that user can send a friend request to another ApplicationUser . I currently have the following general entity classes:

public class ApplicationUser : IdentityUser
{
    public virtual List<DeviceToken> DeviceTokens { get; set; } = new List<DeviceToken>();
    public string DisplayName { get; set; }
}

public class FriendRequest
{
    public int Id { get; set; }
    public DateTime DateRequested { get; set; }
    public ApplicationUser Requester { get; set; }
    public ApplicationUser Receiver { get; set; }
}

I have ran database-update etc and this is working fine. However when I go into my SQLServer to try to delete an ApplicationUser, it tells me that The DELETE statement conflicted with the REFERENCE constraint "FK_FriendRequest_AspNetUsers_RequesterId" .

So I have decided to implement a cascade delete flow from the ApplicationUser to the friend requests that they are part of.

I have tried the resource on here by Microsoft on configuring cascade delete but I cannot figure out how to apply it to my case:

builder.Entity<ApplicationUser>()
    .HasMany(e => e.FriendRequests)//No such property, no idea how to address
    .OnDelete(DeleteBehavior.ClientCascade);
  1. How do I set up this cascade delete scenario?

  2. Also how do I add a property to ApplicationUser that refers to all the FriendRequests they are part of, and make sure EFCore knows I am referring to that existing FriendRequest entity/table?


Update

Following the suggested approach of adding a virtual property to ApplicationUser, would this be way forward:

public class ApplicationUser : IdentityUser
{
    public virtual List<DeviceToken> DeviceTokens { get; set; } = new List<DeviceToken>();
    public string DisplayName { get; set; }
    public ICollection<FriendRequest> FriendRequests { get; }
}

builder.Entity<ApplicationUser>()
    .HasMany(u => u.FriendRequests)
    .WithOne(u => u.Requester)
    .OnDelete(DeleteBehavior.ClientCascade); //not sure about this

builder.Entity<ApplicationUser>()
    .HasMany(u => u.FriendRequests)
    .WithOne(u => u.Requester)
    .OnDelete(DeleteBehavior.ClientCascade); //not sure about this

Your ApplicationUser needs 2 virtual ICollections.

public class ApplicationUser 
{
    public int Id { get; set; }
    public string DisplayName { get; set; }
    public virtual ICollection<FriendRequest> FriendRequestsAsRequestor { get; set; }
    public virtual ICollection<FriendRequest> FriendRequestsAsReceiver { get; set; }
}

public class FriendRequest
{
    public int Id { get; set; }
    public DateTime DateRequested { get; set; }
    public int RequestorId { get; set; }
    public ApplicationUser Requestor { get; set; }
    public int ReceiverId { get; set; }
    public ApplicationUser Receiver { get; set; }
}

public class ApplicationUserConfig : IEntityTypeConfiguration<ApplicationUser>
{
    public void Configure(EntityTypeBuilder<ApplicationUser> builder)
    {
        builder.HasMany(au => au.FriendRequestsAsRequestor)
            .WithOne(fr => fr.Requestor)
            .HasForeignKey(fr => fr.RequestorId)
            .OnDelete(DeleteBehavior.Cascade);

        builder.HasMany(au => au.FriendRequestsAsReceiver)
            .WithOne(fr => fr.Receiver)
            .HasForeignKey(fr => fr.ReceiverId)
            .OnDelete(DeleteBehavior.Cascade);
    }
}

Use:

void AddFriendRequest(int requestorId, int receiverId)
{
    var ctxt = new DbContext();
    FriendRequest fr = new FriendRequest
    {
        RequestorId = requestorId;
        ReceiverId = receiverId;
        DateRequested = DateTime.Now;
    }

    ctxt.FriendRequests.Add(fr);
    ctxt.SaveChanges();
}

List<FriendRequest> GetFriendRequests()
{
    var ctxt = new DbContext();
    return ctxt.FriendRequests
        .Include(fr => fr.Requestor)
        .Include(fr => fr.Receiver)
        .ToList(); 
}

ApplicationUser GetUserWithFriendRequests(int id)
{
    var ctxt = new DbContext();
    return ctxt.ApplicationUser
        .Include(au => au.FriendRequestsAsRequestor)
        .Include(au => au.FriendRequestsAsReceiver)
        .SingleOrDefault(au => au.Id == id);
}

I have tried the resource on here by Microsoft on configuring cascade delete but I cannot figure out how to apply it to my case:

builder.Entity<ApplicationUser>()
    .HasMany(e => e.FriendRequests)//No such property, no idea how to address
    .OnDelete(DeleteBehavior.ClientCascade);

From the doc of DeleteBehavior :

ClientCascade: For entities being tracked by the DbContext, dependent entities will be deleted when the related principal is deleted. If the database has been created from the model using Entity Framework Migrations or the EnsureCreated() method, then the behavior in the database is to generate an error if a foreign key constraint is violated.

In this case, it's the client (the .NET app) and not the DB that ensure the cascade delete. If the client fail to do the cascade delete (related entity not tracked), the db will generate the error you see.

Maybe the DeleteBehavior.Cascade is more appropriate to your code first scenario:

Cascade: For entities being tracked by the DbContext, dependent entities will be deleted when the related principal is deleted. If the database has been created from the model using Entity Framework Migrations or the EnsureCreated() method, then the behavior in the database is the same as is described above for tracked entities. Keep in mind that some databases cannot easily support this behavior, especially if there are cycles in relationships, in which case it may be better to use ClientCascade which will allow EF to perform cascade deletes on loaded entities even if the database does not support this. This is the default for required relationships. That is, for relationships that have non-nullable foreign keys.

If you try this, you go with this SQL script migration (I assume the SGBDR is SQL Server):

CREATE TABLE [ApplicationUser] (
    [Id] int NOT NULL IDENTITY,
    [DisplayName] nvarchar(max) NULL,
    CONSTRAINT [PK_ApplicationUser] PRIMARY KEY ([Id])
);
GO

CREATE TABLE [FriendRequests] (
    [Id] int NOT NULL IDENTITY,
    [DateRequested] datetime2 NOT NULL,
    [RequesterId] int NULL,
    [ReceiverId] int NULL,
    CONSTRAINT [PK_FriendRequests] PRIMARY KEY ([Id]),
    CONSTRAINT [FK_FriendRequests_ApplicationUser_ReceiverId] FOREIGN KEY ([ReceiverId]) REFERENCES [ApplicationUser] ([Id]) ON DELETE CASCADE,
    CONSTRAINT [FK_FriendRequests_ApplicationUser_RequesterId] FOREIGN KEY ([RequesterId]) REFERENCES [ApplicationUser] ([Id]) ON DELETE CASCADE
);
GO

And when it's apply, this produce this error:

Introducing FOREIGN KEY constraint 'FK_FriendRequests_ApplicationUser_RequesterId' on table 'FriendRequests' may cause cycles or multiple cascade paths.
Specify ON DELETE NO ACTION or ON UPDATE NO ACTION, or modify other FOREIGN KEY constraints.

First time I see this error, then I will refer to this question with @onedaywhen's answer :

SQL Server does simple counting of cascade paths and, rather than trying to work out whether any cycles actually exist, it assumes the worst and refuses to create the referential actions (CASCADE)...

A no perfect solution is to use DeleteBehavior.Cascade and ensure all related entities are tracked before the delete:

public class ApplicationUser
{
    public int Id { get; set; }
    public string DisplayName { get; set; }
    public ICollection<FriendRequest> RequestedRequests { get; set; }
    public ICollection<FriendRequest> RecevedRequests { get; set; }
}

public class FriendRequest
{
    public int Id { get; set; }
    public DateTime DateRequested { get; set; }
    public ApplicationUser Requester { get; set; }
    public ApplicationUser Receiver { get; set; }
}

public class MyDbContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        base.OnConfiguring(optionsBuilder);
        optionsBuilder.UseSqlServer("***");
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
        modelBuilder.Entity<FriendRequest>()
            .HasOne(r => r.Requester)
            .WithMany(u => u.RequestedRequests)
            .OnDelete(DeleteBehavior.ClientCascade);
        modelBuilder.Entity<FriendRequest>()
            .HasOne(r => r.Receiver)
            .WithMany(u => u.RecevedRequests)
            .OnDelete(DeleteBehavior.ClientCascade);
    }

    public DbSet<ApplicationUser> Users { get; set; }
    public DbSet<FriendRequest> FriendRequests { get; set; }

    public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default)
    {
        PrepareUserToDeleting();
        return base.SaveChangesAsync();
    }

    public override int SaveChanges(bool acceptAllChangesOnSuccess)
    {
        PrepareUserToDeleting();
        return base.SaveChanges();
    }

    private void PrepareUserToDeleting()
    {
        // For each deleted user entity
        foreach(var entry in ChangeTracker.Entries<ApplicationUser>().Where(e => e.State == EntityState.Deleted))
        {
            var user = entry.Entity;
            // If RecevedRequests isn't loaded
            if (user.RecevedRequests == null)
            {
                //Then load RecevedRequests
                entry.Collection(u => u.RecevedRequests).Load();
            }
            // Idem with RequestedRequests
            if (user.RequestedRequests == null)
            {
                entry.Collection(u => u.RequestedRequests).Load();
            }
        }
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM