Open Closed

Ef Core Abp Extension can not delete children entities without deleting parent entity ? #2576


User avatar
0
selinkoykiran created

Hello all, We came up to an issue with abpContext and ef core. We have an entity which is using ISoftDelete interface like below (and default behaviour is cascade delete with parent ).

And this entity is the child of parent Model aggregate root. We have a case like => we want to mark as deleted this entity and after we want to hard delete this entity from database without deleting parent Model entity. We are getting these children from db like below :

after that point we are operating deletion inside rich domain model like below (with removeall or clear it doesn't metter, it's not working):

So this operation should mark exports as deleted(in ef core state) and after saveChanges operation it should be deleting all exports from db. But nothing happened. (we also check with sql profiler , there wasn't any deletion operation which should be seen ). And as ef core state we also can't see the state change , children entity stayed as unchanged .

We've also checked this issue without abp library with just using ef core library. And with default ef core lib , we can get included children with parent, after that we marked these children with removeAll or clear methods and it worked as expected and this operation deleted all the children of the parent object without deleting parent. So we're thinking that this issue is related with abp framework library . Could you help with that issue , please ? It's really important for us.

Thank you.

  • ABP Framework version: v5.1.1
  • UI type: MVC
  • DB provider: EF Core
  • **Tiered (MVC) : yes
  • Exception message and stack trace:
  • Steps to reproduce the issue:"

11 Answer(s)
  • User Avatar
    0
    enisn created
    Support Team .NET Developer

    Have you tried to call HardDeleteAsync method to delete permanently ISoftDelete objects?

    _repository.HardDeleteAsync(exports);
    
  • User Avatar
    0
    selinkoykiran created

    Hello , Normally I shouldn't reach the entity repository like that from DDD manner , It's not a best practice. Because I should delete them from aggregate repository, so normally we don't have any repositories for entity itself. Anyway I've tried this approach , too. But it didn't work.

  • User Avatar
    0
    gterdem created
    Senior .NET Developer

    So this operation should mark exports as deleted(in ef core state) and after saveChanges operation it should be deleting all exports from db. But nothing happened. (we also check with sql profiler , there wasn't any deletion operation which should be seen ). And as ef core state we also can't see the state change , children entity stayed as unchanged .

    Maybe you have some mistakes on fluent api configurations since you are using private field for your collection and it won't be tracked as default.

    Apart from tracking, can you try updating your aggregate root after myAggregateRootObj.RemoveExports(); method like _repository.UpdateAsync(myAggregateRootObj)?

  • User Avatar
    0
    selinkoykiran created

    Yes, you should already update the aggregate root normally after children entities changes. So I've tried and that's the problem which is not working even if updating aggregate root. I don't think it's because of private field because we are using backing fields actually , and in a normal ef core project it's working as expected for example :

    Aggregate

    Entity:

    And the operation below is working :

    Because of this simple ef core project is working without problem , we think that if this issue about abp efcore implementation? I can show our configuration anytime , adding and saving changes working perfectly but in deletion , removing step with that backing fields , could it be an issue ?

  • User Avatar
    0
    gterdem created
    Senior .NET Developer

    There may be differences since we use unit of work and intercepters. I need to check.

    Can you share the entity configurations to reproduce?

  • User Avatar
    0
    selinkoykiran created

    Of course , could be , Here are our configurations :

    **OdmsDbContextModelCreatingExtensions inside: **

                /* Configure all entities here. */
                builder.Entity<Model>(b =>
                {
                    b.ToTable(OdmsDbProperties.DbTablePrefix + "Models", OdmsDbProperties.DbSchema);
                    b.ConfigureByConvention();
                    b.Property(x => x.SchemaName).HasMaxLength(ModelConsts.MaxSchemaNameLength).HasColumnName(nameof(Model.SchemaName)).IsRequired();
                    b.Property(x => x.ServerName).HasMaxLength(ModelConsts.MaxServerNameLength).HasColumnName(nameof(Model.ServerName)).IsRequired();
                    b.Property(x => x.DatabaseType).HasMaxLength(ModelConsts.MaxDatabaseTypeLength).HasColumnName(nameof(Model.DatabaseType));
                    b.Property(x => x.Password).HasMaxLength(ModelConsts.MaxEncryptedPasswordLength).HasColumnName(nameof(Model.Password));
                    b.Property(x => x.Version).HasMaxLength(ModelConsts.MaxVersionLength).HasColumnName(nameof(Model.Version));
                    // Relations
                    b.HasMany<Export>(m => m.Exports).WithOne(e => e.Model).HasForeignKey(e => e.ModelId).IsRequired();
                    b.HasMany<Import>(m => m.Imports).WithOne(i => i.Model).HasForeignKey(i => i.ModelId).IsRequired();
                    b.HasMany<Source>(m => m.Sources).WithOne(s => s.Model).HasForeignKey(s => s.ModelId).IsRequired();
                    // Index
                    b.HasIndex(x => new { x.SchemaName });
                    b.Navigation(x => x.Exports).HasField("_exports");
                    b.Metadata.FindNavigation("Exports").SetPropertyAccessMode(PropertyAccessMode.Field);
                    
                });
    
                builder.Entity<Export>(b =>
                {
                    b.ToTable(OdmsDbProperties.DbTablePrefix + "Exports", OdmsDbProperties.DbSchema);
                    b.ConfigureByConvention();
                    b.Property(x => x.ModelId).HasColumnName(nameof(Export.ModelId)).IsRequired();
                    b.Property(x => x.OperationId).HasColumnName(nameof(Export.OperationId)).IsRequired();
                    b.Property(x => x.ExportType).HasMaxLength(ExportConsts.MaxExportTypeLength).HasColumnName(nameof(Export.ExportType)).IsRequired();
                    b.Property(x => x.Result).HasMaxLength(ExportConsts.MaxResultLength).HasColumnName(nameof(Export.Result)).IsRequired();
                    // Value object
                    b.OwnsOne(x => x.ExportFile, p =>
                    {
                        p.Property(x => x.StorageId).HasColumnName(ExportConsts.ExportFileIdColumnName);
                        p.Property(x => x.Name).HasColumnName(ExportConsts.ExportFileNameColumnName);
                        p.Ignore(x => x.NameOnly);
                        p.Ignore(x => x.FullName);
                        p.Ignore(x => x.ModelType);
                        // Index
                        p.HasIndex(x => x.Name);
                    }).Navigation(x => x.ExportFile).IsRequired();
    
                      
                });
    

    **Domain Manager layer inside : **

            public virtual async Task HardDeleteExportAsync(string schemaName, string serverName, Guid fileId)
            {
                Check.NotNullOrWhiteSpace(schemaName, nameof(schemaName), ModelConsts.MaxSchemaNameLength);
                Check.NotNullOrWhiteSpace(serverName, nameof(serverName), ModelConsts.MaxServerNameLength);
    
                // Get model from database with conditional exports 
                var model = await ModelRepository.FindWithExportDetailAsync(
                    schemaName,
                    serverName,
                    x => x.IsDeleted == true && x.ExportFile.StorageId == fileId,
                    includeDetails: true // includeDetails: Set true to include all children of this aggregate
                );
    
                if (model == null)
                {
                    throw new ModelDoesNotExistException(
                        schemaName: schemaName,
                        serverName: serverName
                    );
                }
    
                //NOTE => below code not working if we have a cascade delete relation but we want to delete only children , not with parent. we need to do it from repository layer
                model.RemoveAllExports(); //model.HardDeleteExport(fileId);
    
                await ModelRepository.UpdateAsync(model,true);
            }
    

    FindWithExportDetailAsync inside which is inside repository layer :

            public virtual async Task<Model> FindWithExportDetailAsync(string schemaName, string serverName, Expression<Func<Export, bool>> expression, bool includeDetails = true, CancellationToken cancellationToken = default)
            {
                return await (await GetDbSetAsync())
                    .IncludeExportDetail(expression, includeDetails)  // Include only exports
                    .Where(x => x.SchemaName == schemaName && x.ServerName == serverName)
                    .FirstOrDefaultAsync(GetCancellationToken(cancellationToken)); // Returns null if not found
            }
    

    IncludeExportDetail method inside :

    
            public static IQueryable<Model> IncludeExportDetail(this IQueryable<Model> queryable, Expression<Func<Export, bool>> predicate, bool include = true)
            {
                if (!include)
                {
                    return queryable;
                }
    
                return queryable
                    .Include(
                    x => x.Exports.AsQueryable()
                    .Where(predicate));
            }```
    
    

    Model aggregate root and removeExport method

    public class Model : AuditedAggregateRoot<Guid>, IMultiTenant // Using Guid type as the Id key
    {
        public Guid? TenantId { get; protected set; }
    
        [NotNull]
        public virtual string SchemaName { get; protected set; } // Value object can be created for primitive types. There is no such requirement in the web API. Inputs are validated in the HTTP layer.
    
        [NotNull]
        public virtual string ServerName { get; protected set; } // Value object can be created for primitive types. There is no such requirement in the web API. Inputs are validated in the HTTP layer.
    
        public virtual DatabaseType DatabaseType { get; protected set; }
    
        public virtual string Password { get; protected set; }
    
        [NotNull]
        public virtual string Version { get; protected set; }
    
        // Don't expose mutable collections in an aggregate
        public virtual IReadOnlyCollection<Export> Exports
        {
            get
            {
                return _exports?.ToList(); // Paged operation may return without sub collection. If null then do not turn into list 
            }
        }
    
        private readonly ICollection<Export> _exports;
    
        public virtual void RemoveAllExports()
        {
            // NOTE => Clear ,or new, or removeall not working when dealing with ef core because of it only clear the list , and parent doesn't know about the relational children deletion.
            _exports.Clear();
        }
        }
    
  • User Avatar
    0
    selinkoykiran created

    Hello , Is there any progress about this issue ? It is an important problem for us. Thank you.

  • User Avatar
    0
    gterdem created
    Senior .NET Developer

    Sorry, I didn't see any problem code-wise.

    I will create a public repository with tests today. I'll share the link and results.

  • User Avatar
    0
    gterdem created
    Senior .NET Developer

    Can you try adding [UnitOfWork] attribute over your HardDeleteExportAsync method and make it virtual?

    If it doesn't work, you can isolate it in a unit of work scope. Check this repository for sample and check the unit test about it.

  • User Avatar
    0
    selinkoykiran created

    Hello , (sorry for my late response, I was dealing with another issue. ) Thanks for your suggestions and I've checked your solution and I applied all unit of work depended options. But it just didn't work. After that I saw your ISoftDelete implementation in the framework code in AbpContext :

    and I suspect of using of ISoftDelete interface (because like I mentioned before, I couldn't see the change of the entity state as deleted in our case) , so I just removed ISoftDelete interface from our child entities and all the above code that I've mentioned, worked successfully, All child entities removed without removing parent like we expect.

    I don't know the main issue, but I think maybe there could be an entity state changing problem about ISoftDelete implementation in such specific cases like ours.

    Thank you.

  • User Avatar
    0
    gterdem created
    Senior .NET Developer

    Thank you for pointing that out. We will discuss about data filters like ISoftDelete is not being tracked from the parent entity.

Made with ❤️ on ABP v9.1.0-preview. Updated on November 11, 2024, 11:11