0

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
pnizzle
  • 6,243
  • 4
  • 52
  • 81
  • @Zer0 So FriendlyRequests is part of the AspIdentity? – Nikolaus Jan 26 '21 at 08:41
  • You need a virtual ICollection NavigationProperty for each Reference in ApplicationUser, then you can set it up the way of the Docs. – Nikolaus Jan 26 '21 at 08:44
  • @Zer0 how would I do that? I would like the cascade delete specified on this relationship only, not the whole database. Hopefully that is what you mean? – pnizzle Jan 26 '21 at 08:46
  • @Nikolaus is that answering my 2nd question in my post? I just added it. – pnizzle Jan 26 '21 at 08:50
  • If it's database first, you need configure the cascade delete directly in the database. – vernou Jan 26 '21 at 08:55
  • Like Vernou mentioned is it DatabaseFirst or ModelFirst? – Nikolaus Jan 26 '21 at 08:57
  • @Nikolaus Its model first. Entity Framework core is Model first only I believe – pnizzle Jan 26 '21 at 09:00
  • @Vernou Its model first – pnizzle Jan 26 '21 at 09:00
  • You have that virtual List I expect in the DeviceToken-class there is also a ApplicationUser-Property, isn’t it? – Nikolaus Jan 26 '21 at 09:02
  • Do you have try `builder.Entity().HasOne(e => e.Requester).OnDelete(DeleteBehavior.Cascade); builder.Entity().HasOne(e => e.Receiver).OnDelete(DeleteBehavior.Cascade);`? – vernou Jan 26 '21 at 09:02
  • Then I’ll give it a try. – Nikolaus Jan 26 '21 at 09:02
  • @Vernou the DeviceTokenEntity does not have a ApplicationUser property on it. It should, but thats a story for a different day. Will address that when I get to it :) Thanks – pnizzle Jan 26 '21 at 09:07
  • @Vernou with this: `builder.Entity().HasOne(e => e.Requester).OnDelete(DeleteBehavior.Cascade); builder.Entity().HasOne(e => e.Receiver).OnDelete(DeleteBehavior.Cascade);` its giving an error on OnDelete. I think OnDelete is not available on the return of HasOne() – pnizzle Jan 26 '21 at 09:10
  • Maybe `.HasOne(e => e.Requester).WithMany().OnDelete...` – vernou Jan 26 '21 at 09:18
  • @Vernou not working either. Have a look at my question, I have added an update section. Does that look right ? – pnizzle Jan 26 '21 at 09:23
  • I am yet to try out the solutions provided. Cheers everyone – pnizzle Jan 27 '21 at 22:40
  • @Vernou I believe you had a suggested answer that you have deleted it looks like. Was there something wrong with it, coz it looked promising? – pnizzle Jan 27 '21 at 22:42

2 Answers2

0

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);
}
Neil W
  • 7,670
  • 3
  • 28
  • 41
  • With the virtual collections FriendRequestsAsRequestor and FriendRequestsAsReceiver, how does EFCore know to populate FriendRequestsAsRequestor with just the requests that have the user as a requester when retrieving, and vice versa? How can it infer that? – pnizzle Jan 26 '21 at 09:31
  • Unless this was supposed to be my setup from the beginning so that FriendRequestsAsRequestor and FriendRequestsAsReceiver are set up as two different tables? – pnizzle Jan 26 '21 at 09:33
  • EFCore doesn't know. You tell it. See update to answer. – Neil W Jan 26 '21 at 09:41
  • They're not two different tables. They are just two different relationships to that single table. One for when user is requestor and one for when user is receiver. – Neil W Jan 26 '21 at 09:44
  • In your answer there is nowhere where you are using FriendRequestsAsRequestor and FriendRequestsAsReceiver. They are defined as properties and configured for cascade delete. But I dont see where you actually add or delete from them? Is that configuration part of the code in configure()? I'm virtually new to this. Cheers. – pnizzle Jan 26 '21 at 09:45
  • Okay, I will give this a go. Have to refactor lots before seeing if it works. – pnizzle Jan 26 '21 at 09:47
0

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();
            }
        }
    }
}
vernou
  • 6,818
  • 5
  • 30
  • 58