Open Closed

Unable to delete one-to-one relation, SQL server unreachable due to non-stop querying #8720


User avatar
0
s.beckers created
  • ABP Framework version: v9.0.4

  • UI Type: MVC

  • Database System: EF Core (SQL Server)

  • Tiered (for MVC) or Auth Server Separated (for Angular): no

  • Steps to reproduce the issue:

Hi,

I've created a test project to simulate the issue: http://cdn.fuzed.app/share/abp/AbpHardDelete.zip

using System;
using System.Threading.Tasks;

namespace AbpHardDelete.Books;

public class BooksAppService : AbpHardDeleteAppService, IBooksAppService
{
    private readonly IBookRepository _bookRepository;

    public BooksAppService(IBookRepository bookRepository)
    {
        _bookRepository = bookRepository;
    }

    public async Task HardDeleteTest()
    {
        Book book = await _bookRepository.GetAsync(Guid.Parse("bef9de79-60d9-4837-840c-f2d04cd58514"));

        book.Author = null;

        await _bookRepository.UpdateAsync(book);
    }
}

The following request fails after timeout:
image.png

The database is spammed with queries:
image.png

Do I miss something here or is this a framework / EF core level issue?

Best regards,
Steff Beckers


24 Answer(s)
  • User Avatar
    0
    EngincanV created
    Support Team .NET Developer

    Hi, normally it should not be like that. It seems there is a mis-configuration in the database side. I will check your project and write you back asap.

  • User Avatar
    0
    EngincanV created
    Support Team .NET Developer

    In your db configuration it seems you set the one-to-one relationship as required, however, this is not what you want (because you made the Author as nullable (Author?)), so you should make the following change (set IsRequired(false)):

            builder.Entity<Book>(b =>
            {
                b.ToTable(AbpHardDeleteConsts.DbTablePrefix + "Books", AbpHardDeleteConsts.DbSchema);
                b.ConfigureByConvention(); //auto configure for the base class props
                b.HasOne(x => x.Author).WithOne(x => x.Book)
                    .HasForeignKey<Author>(x => x.BookId)
    -               .IsRequired()
    +               .IsRequired(false)
                    .OnDelete(DeleteBehavior.Cascade);
            });
    

    If you don't change the configuration like that, you can't set the Author as null, and SQL query will be generated over and over again.

  • User Avatar
    0
    s.beckers created

    Hi @EngincanV,

    I've tested IsRequired(false) and it solves the query issue, but the Author itself isn't deleted. An author can't exist on its own in this case and now we have a "ghost" Author record, since BookId is now allowed to be NULL. So I don't think we should use IsRequired(false). IsRequired configures whether this is a required relationship (i.e. whether the foreign key property(s) can be assigned null). It doesn't ensure that a Book requires an Author. It only ensures that the FK is required/can't be NULL => an Author must have a BookId.

    image.png

    Also see the EF core docs: https://learn.microsoft.com/en-us/ef/core/modeling/relationships/one-to-one#required-one-to-one

    image.png

    I've created a new and better (relation wise) example using the ASP.NET Core Web API template, where the removal works as expected:
    https://cdn.fuzed.app/share/abp/EFCorePlayground.zip

    Entities:

    public class User
    {
        public Guid Id { get; set; }
    
        public string Name { get; set; }
    
        public UserProfile? Profile { get; set; }
    }
    
    public class UserProfile
    {
        public Guid Id { get; set; }
    
        public string Bio { get; set; }
    
        public Guid UserId { get; set; }
    
        public User User { get; set; }
    }
    

    DbContext:

    public class AppDbContext : DbContext
    {
        public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
        {
        }
    
        public DbSet<User> Users { get; set; }
    
        public DbSet<UserProfile> UserProfiles { get; set; }
    
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<User>()
                .HasOne(x => x.Profile)
                .WithOne(x => x.User)
                .HasForeignKey<UserProfile>(x => x.UserId)
                .IsRequired()
                .OnDelete(DeleteBehavior.Cascade);
        }
    }
    

    Controller:

    [Route("api/[controller]")]
    [ApiController]
    public class UsersController : ControllerBase
    {
        private readonly AppDbContext _context;
    
        public UsersController(AppDbContext context)
        {
            _context = context;
        }
    
        ...
    
        // DELETE: api/users/{id}/profile
        [HttpDelete("{id}/profile")]
        public async Task<IActionResult> DeleteUserProfile(Guid id)
        {
            User? user = await _context.Users.Include(x => x.Profile).FirstOrDefaultAsync(x => x.Id == id);
            if (user == null)
            {
                return NotFound();
            }
    
            user.Profile = null;
    
            await _context.SaveChangesAsync();
    
            return NoContent();
        }
        
        ...
    }
    

    Thanks in advance!

    Best regards,
    Steff Beckers

  • User Avatar
    0
    EngincanV created
    Support Team .NET Developer

    I've tested IsRequired(false) and it solves the query issue, but the Author itself isn't deleted. An author can't exist on its own in this case and now we have a "ghost" Author record, since BookId is now allowed to be NULL. So I don't think we should use IsRequired(false). IsRequired configures whether this is a required relationship (i.e. whether the foreign key property(s) can be assigned null). It doesn't ensure that a Book requires an Author. It only ensures that the FK is required/can't be NULL => an Author must have a BookId.

    In your design, it's normal not to delete the author itself because while you are setting the author as null it just ensures it does not have any reference for the related author. So, the cascade delete option of EF Core does not work in that case.

    I've created a new and better (relation wise) example using the ASP.NET Core Web API template, where the removal works as expected:
    https://cdn.fuzed.app/share/abp/EFCorePlayground.zip

    Yes, this is what it should look like. Now you are defining the UserId as required and made it as not nullable, in the previous example, it was nullable and in that case, a SQL statement loop occurred.

  • User Avatar
    0
    s.beckers created

    Hi @EngincanV,

    I have created a 3rd example app, of the User & UserProfile example inside an ABP framework solution:
    https://cdn.fuzed.app/share/abp/AbpEFCorePlayground.zip

    This time the SQL query loop occurs again, within ABP framework, using the same configuration as previous example.
    So I think this is some framework issue that needs to be fixed.

    User entity:

    using System;
    using Volo.Abp.Domain.Entities.Auditing;
    
    namespace AbpEFCorePlayground.Users;
    
    public class User : FullAuditedAggregateRoot<Guid>
    {
        public User(
            Guid id,
            string name)
        {
            Id = id;
            Name = name;
        }
    
        private User()
        {
        }
    
        public string Name { get; set; }
    
        public UserProfile? Profile { get; set; }
    }
    

    UserProfile entity:

    using System;
    using Volo.Abp.Domain.Entities.Auditing;
    
    namespace AbpEFCorePlayground.Users;
    
    public class UserProfile : FullAuditedEntity<Guid>
    {
        public Guid Id { get; set; }
    
        public string Bio { get; set; }
    
        public Guid UserId { get; set; }
    
        public User User { get; set; }
    }
    

    UsersDataSeedContributor:

    using System;
    using System.Threading.Tasks;
    using Volo.Abp.Data;
    using Volo.Abp.DependencyInjection;
    using Volo.Abp.Uow;
    
    namespace AbpEFCorePlayground.Users;
    
    public class UsersDataSeedContributor : IDataSeedContributor, ITransientDependency
    {
        private readonly IUserRepository _userRepository;
    
        public UsersDataSeedContributor(IUserRepository userRepository)
        {
            _userRepository = userRepository;
        }
    
        [UnitOfWork]
        public virtual async Task SeedAsync(DataSeedContext context)
        {
            User user = new User(
                id: Guid.Parse("c64b873e-c067-43e0-ae00-07992a880837"),
                name: "Steff Beckers")
            {
                Profile = new UserProfile()
                {
                    Bio = "Software Developer"
                }
            };
    
            await _userRepository.DeleteAsync(x => x.Id == user.Id);
            await _userRepository.InsertAsync(user);
        }
    }
    

    EF entity configuration:

            builder.Entity<User>(b =>
            {
                b.ToTable(AbpEFCorePlaygroundConsts.DbTablePrefix + "Users", AbpEFCorePlaygroundConsts.DbSchema);
                b.ConfigureByConvention(); //auto configure for the base class props
    
                b.HasOne(x => x.Profile)
                    .WithOne(x => x.User)
                    .HasForeignKey<UserProfile>(x => x.UserId)
                    .IsRequired()
                    .OnDelete(DeleteBehavior.Cascade);
            });
    
            builder.Entity<UserProfile>(b =>
            {
                b.ToTable(AbpEFCorePlaygroundConsts.DbTablePrefix + "UserProfiles", AbpEFCorePlaygroundConsts.DbSchema);
                b.ConfigureByConvention(); //auto configure for the base class props
            });
            
            Configure<AbpEntityOptions>(options =>
            {
                options.Entity<User>(e =>
                {
                    e.DefaultWithDetailsFunc = query => query.Include(x => x.Profile);
                });
            });
    

    App service:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    
    namespace AbpEFCorePlayground.Users;
    
    public class UsersAppService : AbpEFCorePlaygroundAppService, IUsersAppService
    {
        private readonly IUserRepository _userRepository;
    
        public UsersAppService(IUserRepository userRepository)
        {
            this._userRepository = userRepository;
        }
    
        public async Task DeleteUserProfileAsync()
        {
            var user = await _userRepository.GetAsync(x => x.Id == Guid.Parse("c64b873e-c067-43e0-ae00-07992a880837"));
    
            user.Profile = null;
    
            await _userRepository.UpdateAsync(user);
        }
    }
    

    image.png

    image.png

    Best regards,
    Steff Beckers

  • User Avatar
    0
    EngincanV created
    Support Team .NET Developer

    Hi @EngincanV,

    I have created a 3rd example app, of the User & UserProfile example inside an ABP framework solution:
    https://cdn.fuzed.app/share/abp/AbpEFCorePlayground.zip

    This time the SQL query loop occurs again, within ABP framework, using the same configuration as previous example.
    So I think this is some framework issue that needs to be fixed.

    User entity:

    using System; 
    using Volo.Abp.Domain.Entities.Auditing; 
     
    namespace AbpEFCorePlayground.Users; 
     
    public class User : FullAuditedAggregateRoot<Guid> 
    { 
        public User( 
            Guid id, 
            string name) 
        { 
            Id = id; 
            Name = name; 
        } 
     
        private User() 
        { 
        } 
     
        public string Name { get; set; } 
     
        public UserProfile? Profile { get; set; } 
    } 
    

    UserProfile entity:

    using System; 
    using Volo.Abp.Domain.Entities.Auditing; 
     
    namespace AbpEFCorePlayground.Users; 
     
    public class UserProfile : FullAuditedEntity<Guid> 
    { 
        public Guid Id { get; set; } 
     
        public string Bio { get; set; } 
     
        public Guid UserId { get; set; } 
     
        public User User { get; set; } 
    } 
    

    UsersDataSeedContributor:

    using System; 
    using System.Threading.Tasks; 
    using Volo.Abp.Data; 
    using Volo.Abp.DependencyInjection; 
    using Volo.Abp.Uow; 
     
    namespace AbpEFCorePlayground.Users; 
     
    public class UsersDataSeedContributor : IDataSeedContributor, ITransientDependency 
    { 
        private readonly IUserRepository _userRepository; 
     
        public UsersDataSeedContributor(IUserRepository userRepository) 
        { 
            _userRepository = userRepository; 
        } 
     
        [UnitOfWork] 
        public virtual async Task SeedAsync(DataSeedContext context) 
        { 
            User user = new User( 
                id: Guid.Parse("c64b873e-c067-43e0-ae00-07992a880837"), 
                name: "Steff Beckers") 
            { 
                Profile = new UserProfile() 
                { 
                    Bio = "Software Developer" 
                } 
            }; 
     
            await _userRepository.DeleteAsync(x => x.Id == user.Id); 
            await _userRepository.InsertAsync(user); 
        } 
    } 
    

    EF entity configuration:

            builder.Entity<User>(b => 
            { 
                b.ToTable(AbpEFCorePlaygroundConsts.DbTablePrefix + "Users", AbpEFCorePlaygroundConsts.DbSchema); 
                b.ConfigureByConvention(); //auto configure for the base class props 
     
                b.HasOne(x => x.Profile) 
                    .WithOne(x => x.User) 
                    .HasForeignKey<UserProfile>(x => x.UserId) 
                    .IsRequired() 
                    .OnDelete(DeleteBehavior.Cascade); 
            }); 
     
            builder.Entity<UserProfile>(b => 
            { 
                b.ToTable(AbpEFCorePlaygroundConsts.DbTablePrefix + "UserProfiles", AbpEFCorePlaygroundConsts.DbSchema); 
                b.ConfigureByConvention(); //auto configure for the base class props 
            }); 
             
            Configure<AbpEntityOptions>(options => 
            { 
                options.Entity<User>(e => 
                { 
                    e.DefaultWithDetailsFunc = query => query.Include(x => x.Profile); 
                }); 
            }); 
    

    App service:

    using System; 
    using System.Collections.Generic; 
    using System.Linq; 
    using System.Text; 
    using System.Threading.Tasks; 
     
    namespace AbpEFCorePlayground.Users; 
     
    public class UsersAppService : AbpEFCorePlaygroundAppService, IUsersAppService 
    { 
        private readonly IUserRepository _userRepository; 
     
        public UsersAppService(IUserRepository userRepository) 
        { 
            this._userRepository = userRepository; 
        } 
     
        public async Task DeleteUserProfileAsync() 
        { 
            var user = await _userRepository.GetAsync(x => x.Id == Guid.Parse("c64b873e-c067-43e0-ae00-07992a880837")); 
     
            user.Profile = null; 
     
            await _userRepository.UpdateAsync(user); 
        } 
    } 
    

    image.png

    image.png

    Best regards,
    Steff Beckers

    The problem is that you are setting the UserId as not nullable in the UserProfile, and this restricts you to set the user.Profile as null. This is default behaviour of EF Core. There is no problem in the ABP Framework side.You can try the same in a plain asp.net core application.

    Also, the cascade-delete does not apply in the update scenarios. For example, if you delete the related user, the relevant profile will also be deleted, because of the cascade-delete option. However, setting a user's profile as null is not related to that. To be able to set it null, you should make it as optional. And if you want to ensure, setting it null also should delete the dependent 1-1 entity you should update your code according to that.

  • User Avatar
    0
    s.beckers created

    Hi @EngincanV,

    The problem is that you are setting the UserId as not nullable in the UserProfile, and this restricts you to set the user.Profile as null. This is default behaviour of EF Core.

    Can you provide me the docs related to this default behaviour of EF Core?

    You can try the same in a plain asp.net core application.

    I have already created an ASP.NET Core Web API example app (EFCorePlayground), where this behaviour works as expected. In this example app I can just set user.Profile to null, update the user and the dependent user profile record is deleted.

    Also, the cascade-delete does not apply in the update scenarios. For example, if you delete the related user, the relevant profile will also be deleted, because of the cascade-delete option. However, setting a user's profile as null is not related to that. To be able to set it null, you should make it as optional. And if you want to ensure, setting it null also should delete the dependent 1-1 entity you should update your code according to that.

    You are correct, this support ticket is not related to cascade delete functionality.

    Best regards,
    Steff Beckers

  • User Avatar
    0
    EngincanV created
    Support Team .NET Developer

    Can you provide me the docs related to this default behaviour of EF Core?

    Sure. Here are two documentation that you can refer:

    Regards.

  • User Avatar
    0
    s.beckers created

    Hi @EngincanV,

    Sure. Here are two documentation that you can refer:

    I think these docs are not related as the issue is not related to cascade/restrict delete. I want to delete the dependent entity, instead of the principal entity.

    Did you test my working ASP.NET Core Web API example app?
    https://cdn.fuzed.app/share/abp/EFCorePlayground.zip

    The following ABP framework example app, with the same EF Core configuration, doesn't work:
    https://cdn.fuzed.app/share/abp/AbpEFCorePlayground.zip

    Best regards,
    Steff Beckers

  • User Avatar
    0
    EngincanV created
    Support Team .NET Developer

    Hi @EngincanV,

    Sure. Here are two documentation that you can refer:

    I think these docs are not related as the issue is not related to cascade/restrict delete. I want to delete the dependent entity, instead of the principal entity.

    Did you test my working ASP.NET Core Web API example app?
    https://cdn.fuzed.app/share/abp/EFCorePlayground.zip

    The following ABP framework example app, with the same EF Core configuration, doesn't work:
    https://cdn.fuzed.app/share/abp/AbpEFCorePlayground.zip

    Best regards,
    Steff Beckers

    Hi, sorry for the misunderstanding (I thought you were expecting a cascade delete/update behavior). I will check your project and write you back asap.

  • User Avatar
    0
    s.beckers created

    Hi @EngincanV,

    Any progress on this issue?

    Best regards,
    Steff Beckers

  • User Avatar
    0
    EngincanV created
    Support Team .NET Developer

    Hi @EngincanV,

    Any progress on this issue?

    Best regards,
    Steff Beckers

    Hi sorry for my late response,

    In SQL Server, a foreign key constraint only enforces referential integrity—it does not define what should happen when the navigation property (Profile) is set to null. SQL Server only responds to actual DELETE operations.

    Example: If you set user.Profile = null, EF Core only removes the reference, but SQL Server still holds the Profile record because there’s no explicit DELETE query for it.
    SQL Server requires an explicit cascade delete at the database level to automatically remove dependent entities.

    So, this is not related to ABP or EF Core, instead, it's the design of the SQL Server.

    DeleteBehavior.Cascade Only Works When the Principal Entity is Deleted. So, in your case, the relevant profile is deleted whenever the relevant user is being deleted.

    Also, there is a good description here that you might want to check: https://stackoverflow.com/a/12886309/10477283

  • User Avatar
    0
    s.beckers created

    Hi @EngincanV,

    I have the following User aggregate root:

    public class User : FullAuditedAggregateRoot<Guid>
    {
        public User(
            Guid id,
            string name)
        {
            Id = id;
            Name = name;
        }
    
        private User()
        {
        }
    
        public string Name { get; set; }
    
        public UserProfile? Profile { get; set; }
    }
    

    I also have an User record in my database (via IDataSeedContributor):

    User user = new User(
        id: Guid.Parse("c64b873e-c067-43e0-ae00-07992a880837"),
        name: "Steff Beckers")
    {
        Profile = new UserProfile()
        {
            Bio = "Software Developer"
        }
    };
    
    await _userRepository.InsertAsync(user);
    

    Now I want to delete the user's profile. What I'm thinking is:

    var user = await _userRepository.GetAsync(userId);
    
    user.Profile = null;
    
    await _userRepository.UpdateAsync(user);
    

    But this results in the error. How else should I delete the user's profile?

    Best regards,
    Steff Beckers

  • User Avatar
    0
    EngincanV created
    Support Team .NET Developer

    Hi @EngincanV,

    I have the following User aggregate root:

    public class User : FullAuditedAggregateRoot<Guid> 
    { 
        public User( 
            Guid id, 
            string name) 
        { 
            Id = id; 
            Name = name; 
        } 
     
        private User() 
        { 
        } 
     
        public string Name { get; set; } 
     
        public UserProfile? Profile { get; set; } 
    } 
    

    I also have an User record in my database (via IDataSeedContributor):

    User user = new User( 
        id: Guid.Parse("c64b873e-c067-43e0-ae00-07992a880837"), 
        name: "Steff Beckers") 
    { 
        Profile = new UserProfile() 
        { 
            Bio = "Software Developer" 
        } 
    }; 
     
    await _userRepository.InsertAsync(user); 
    

    Now I want to delete the user's profile. What I'm thinking is:

    var user = await _userRepository.GetAsync(userId); 
     
    user.Profile = null; 
     
    await _userRepository.UpdateAsync(user); 
    

    But this results in the error. How else should I delete the user's profile?

    Best regards,
    Steff Beckers

    Hi, based on your EfCorePlayground project, you can call _context.UserProfiles.Remove(user.Profile!) method before setting user.Profile to null:

            _context.UserProfiles.Remove(user.Profile!);
            user.Profile = null;
    

    If you want to do this with repository pattern, then you should inject the profile repository and use its DeleteAsync method:

    //inject the interface
    private readonly IRepository<UserProfile, Guid> _userProfileRepository;
    
    var user = await _userRepository.GetAsync(userId); 
      
    await _userProfileRepository.DeleteAsync(user.Profile!.Id);
    user.Profile = null; 
      
    await _userRepository.UpdateAsync(user); 
    
  • User Avatar
    0
    s.beckers created

    Hi @EngincanV,

    If you want to do this with repository pattern, then you should inject the profile repository and use its DeleteAsync method:

    I tried this approach, but it still results in the API non-stop spamming the database with queries.

    image.png

    I've executed the API endpoint for 1 minute, then stopped the API.

    image.png

    SQL Server Profiler output, with 15k of the same queries as result.

    image.png

  • User Avatar
    0
    s.beckers created

    Hi @EngincanV,

    Hi, based on your EfCorePlayground project, you can call _context.UserProfiles.Remove(user.Profile!) method before setting user.Profile to null:

    The EfCorePlayground project doesn't have the issue, the AbpEfCorePlayground project has.

    Best regards,
    Steff Beckers

  • User Avatar
    0
    EngincanV created
    Support Team .NET Developer

    Hi @EngincanV,

    Hi, based on your EfCorePlayground project, you can call _context.UserProfiles.Remove(user.Profile!) method before setting user.Profile to null:

    The EfCorePlayground project doesn't have the issue, the AbpEfCorePlayground project has.

    Best regards,
    Steff Beckers

    Hi, here is what you need to do to:

    1 -) In your UserProfile entity, remove the Id property (because it's already defined by the base class FullAuditedEntity<Guid>), and make the UserId as nullable:

    public class UserProfile : FullAuditedEntity<Guid>
    {
        // public Guid Id { get; set; }
    
        public string Bio { get; set; }
    
        public Guid? UserId { get; set; }
    
        public User User { get; set; }
    }
    

    2-) Open your dbcontext class and update the configuration:

                b.HasOne(x => x.Profile)
                    .WithOne(x => x.User)
                    .HasForeignKey<UserProfile>(x => x.UserId)
                    .OnDelete(DeleteBehavior.ClientSetNull);
    

    You should use the ClientSetNull as the delete-behaviour, which is default and allows you to set the Profile as null.

    3-) Update your DeleteUserProfileAsync method:

        public async Task DeleteUserProfileAsync()
        {
            var user = await _userRepository.GetAsync(x => x.Id == Guid.Parse("c64b873e-c067-43e0-ae00-07992a880837"));
    
            await _userProfileRepository.DeleteAsync(user.Profile!.Id);
            user.Profile = null;
    
            await _userRepository.UpdateAsync(user);
        }
    

    Then, it will work as you expected.

    Regards.

  • User Avatar
    0
    s.beckers created

    Hi @EngincanV,

    and make the UserId as nullable

    A UserProfile record can't exist without a reference to a User, so the UserId on UserProfile should not be nullable.

    Best regards,
    Steff Beckers

  • User Avatar
    0
    EngincanV created
    Support Team .NET Developer

    Hi @EngincanV,

    and make the UserId as nullable

    A UserProfile record can't exist without a reference to a User, so the UserId on UserProfile should not be nullable.

    Best regards,
    Steff Beckers

    Hi, then you can directly delete the userProfile instead of setting user.Profile as null. This is the only solution. If you want to set the user's profile as null it should be nullable, or if you directly delete the profile, it will be empty further on.

    Regards.

  • User Avatar
    0
    s.beckers created

    Hi @EngincanV,

    I've updated the AbpEFCorePlayground example app: https://cdn.fuzed.app/share/abp/AbpEFCorePlayground.zip

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using Volo.Abp.Domain.Repositories;
    
    namespace AbpEFCorePlayground.Users;
    
    public class UsersAppService : AbpEFCorePlaygroundAppService, IUsersAppService
    {
        private readonly IRepository<UserProfile, Guid> _userProfileRepository;
        private readonly IUserRepository _userRepository;
    
        public UsersAppService(
            IRepository<UserProfile, Guid> userProfileRepository,
            IUserRepository userRepository)
        {
            _userProfileRepository = userProfileRepository;
            _userRepository = userRepository;
        }
    
        public async Task DeleteUserProfileAsync()
        {
            var user = await _userRepository.GetAsync(x => x.Id == Guid.Parse("c64b873e-c067-43e0-ae00-07992a880837"));
    
            if (user.Profile == null)
            {
                return;
            }
    
            await _userProfileRepository.DeleteAsync(user.Profile);
        }
    }
    

    I have added an event handler for User aggregate updates:

    using System.Threading.Tasks;
    using Volo.Abp.DependencyInjection;
    using Volo.Abp.Domain.Entities.Events.Distributed;
    using Volo.Abp.EventBus.Distributed;
    
    namespace AbpEFCorePlayground.Users;
    
    public class UserEventHandlers : IDistributedEventHandler<EntityUpdatedEto<UserEto>>, ITransientDependency
    {
        public Task HandleEventAsync(EntityUpdatedEto<UserEto> eventData)
        {
            return Task.CompletedTask;
        }
    }
    

    The user profile is deleted in the database now, but not in the events.
    image.png
    Also, in our main project we disabled adding repositories for all entities, we only have repositories for aggregate roots:

    context.Services.AddAbpDbContext<AbpEFCorePlaygroundDbContext>(options =>
    {
            /* Remove "includeAllEntities: true" to create
             * default repositories only for aggregate roots */
        options.AddDefaultRepositories(includeAllEntities: true);
    });
    

    We removed "includeAllEntities: true". In this case we can't use IRepository<UserProfile, Guid>. A workaround for this could be adding a Task DeleteProfileAsync(User user) method to the IUserRepository (accessing the DbContext), but also doesn't fix the issue related to the event, since user.Profile is still filled.

    Best regards,
    Steff Beckers

  • User Avatar
    0
    s.beckers created

    Hi @EngincanV,

    I've reverted the changes in the app service.

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    
    namespace AbpEFCorePlayground.Users;
    
    public class UsersAppService : AbpEFCorePlaygroundAppService, IUsersAppService
    {
        private readonly IUserRepository _userRepository;
    
        public UsersAppService(IUserRepository userRepository)
        {
            _userRepository = userRepository;
        }
    
        public async Task DeleteUserProfileAsync()
        {
            var user = await _userRepository.GetAsync(x => x.Id == Guid.Parse("c64b873e-c067-43e0-ae00-07992a880837"));
    
            if (user.Profile == null)
            {
                return;
            }
    
            user.Profile = null;
            
            await _userRepository.UpdateAsync(user);
        }
    }
    

    I've been debugging the AbpDbContext, after I execute the delete user profile request, I can toggle between these 2 breakpoints as an infinite loop:
    AbpDbContext ApplyAbpConceptsForModifiedEntity(EntityEntry entry, bool forceApply = false)
    image.png
    AbpDbContext ApplyAbpConceptsForDeletedEntity(EntityEntry entry)
    image.png
    The entry.Reload(); is responsible for the non-stop querying in this case.

    Best regards,
    Steff Beckers

  • User Avatar
    0
    EngincanV created
    Support Team .NET Developer

    Hi, since in your entity configuration the UserProfile has a not nullable UserId field you can't set the user.Profile as null without deleting the userProfile.

  • User Avatar
    0
    s.beckers created

    Hi @EngincanV,

    Hi, since in your entity configuration the UserProfile has a not nullable UserId field you can't set the user.Profile as null without deleting the userProfile.

    Added a new DeleteProfile method in user repository:

    public class EfCoreUserRepository : EfCoreRepository<AbpEFCorePlaygroundDbContext, User, Guid>, IUserRepository
    {
        public EfCoreUserRepository(
            IDbContextProvider<AbpEFCorePlaygroundDbContext> dbContextProvider)
            : base(dbContextProvider)
        {
        }
    
        public async Task DeleteProfileAsync(User user)
        {
            if (user.Profile == null)
            {
                return;
            }
    
            AbpEFCorePlaygroundDbContext dbContext = await GetDbContextAsync();
            dbContext.Remove(user.Profile);
    
            user.Profile = null;
        }
    }
    

    Update in app service:

    public async Task DeleteUserProfileAsync()
    {
        var user = await _userRepository.GetAsync(x => x.Id == Guid.Parse("c64b873e-c067-43e0-ae00-07992a880837"));
    
        if (user.Profile == null)
        {
            return;
        }
    
        await _userRepository.DeleteProfileAsync(user);
    
        await _userRepository.UpdateAsync(user);
    }
    

    The user updated event is correct:
    image.png
    But when I continue after the breakpoint, the request still keeps executing, because of the infinite loop in the AbpDbContext:
    image.png
    image.png
    And the user profile is not deleted in the database.

    Best regards,
    Steff Beckers

  • User Avatar
    0
    EngincanV created
    Support Team .NET Developer

    Hi @EngincanV,

    Hi, since in your entity configuration the UserProfile has a not nullable UserId field you can't set the user.Profile as null without deleting the userProfile.

    Added a new DeleteProfile method in user repository:

    public class EfCoreUserRepository : EfCoreRepository<AbpEFCorePlaygroundDbContext, User, Guid>, IUserRepository 
    { 
        public EfCoreUserRepository( 
            IDbContextProvider<AbpEFCorePlaygroundDbContext> dbContextProvider) 
            : base(dbContextProvider) 
        { 
        } 
     
        public async Task DeleteProfileAsync(User user) 
        { 
            if (user.Profile == null) 
            { 
                return; 
            } 
     
            AbpEFCorePlaygroundDbContext dbContext = await GetDbContextAsync(); 
            dbContext.Remove(user.Profile); 
     
            user.Profile = null; 
        } 
    } 
    

    Update in app service:

    public async Task DeleteUserProfileAsync() 
    { 
        var user = await _userRepository.GetAsync(x => x.Id == Guid.Parse("c64b873e-c067-43e0-ae00-07992a880837")); 
     
        if (user.Profile == null) 
        { 
            return; 
        } 
     
        await _userRepository.DeleteProfileAsync(user); 
     
        await _userRepository.UpdateAsync(user); 
    } 
    

    The user updated event is correct:
    image.png
    But when I continue after the breakpoint, the request still keeps executing, because of the infinite loop in the AbpDbContext:
    image.png
    image.png
    And the user profile is not deleted in the database.

    Best regards,
    Steff Beckers

    Hi, the problem in your code is that both the User and the UserProfile entities use the soft-delete, which means when you delete a user profile, it's not deleted in normally, only its IsDeleted field set to true and it becomes invisible in your SQL queries, if you apply dataFilters and ABP applies it by default (https://abp.io/docs/9.0/framework/infrastructure/data-filtering#isoftdelete). Therefore, you can't set the user.Profile as null, because the profile is soft-deleted, not hard-deleted. This is the difference between your EfCorePlayground and AbpEfCorePlayground applications.

    If you want the UserProfile as hard deleted, then you can use the IDataFilter service. Example:

            private readonly IDataFilter _dataFilter;
    
    
            using (_dataFilter.Disable<ISoftDelete>())
            {
                //hard deletes from your db
                await _userProfileRepository.DeleteAsync(user.Profile!.Id, autoSave: true);
            }
    
Boost Your Development
ABP Live Training
Packages
See Trainings
Mastering ABP Framework Book
Do you need assistance from an ABP expert?
Schedule a Meeting
Mastering ABP Framework Book
The Official Guide
Mastering
ABP Framework
Learn More
Mastering ABP Framework Book
Made with ❤️ on ABP v9.2.0-preview. Updated on March 13, 2025, 04:08