Skip to content

CountAsync includes soft-deleted related entities while ToListAsync filters them #7142

@rgiosa

Description

@rgiosa

Hi team, I'm not sure if this is a bug or the expected behavior, but I'm seeing an inconsistency between CountAsync() and ToListAsync() when working with entities that use ISoftDelete, especially when they are related via .Include(...).

public async Task<GetListGuestTasksOutputDto> GetListAsync(GetListGuestTasksInputDto input)
{
    var query = _workflowCardTaskRepository.GetAll()
        .Include(x => x.WorkflowCard)
        .Include(x => x.CurrentWorkflowPhase.Workflow)
        .Include(x => x.CreatorUser)
        .Where(x => 
            x.AssigneeUserId == AbpSession.UserId && 
            x.Status == input.Status
        );

    var totalCount = await query.CountAsync(); // result: 2

    // result: 1 record
    var entities = await query
        .OrderBy(x => x.CreationTime)
        .Skip(input.SkipCount)
        .Take(input.MaxResultCount)
        .ToListAsync();

    var result = new GetListGuestTasksOutputDto { Total = totalCount };

    foreach (var item in entities)
    {
        var dto = ObjectMapper.Map<WorkflowCardTaskDto>(item);
        result.Items.Add(dto);
    }

    return result;
}

Expected Behavior
When querying through IRepository<T>, the global filter for ISoftDelete should be consistent across .CountAsync() and .ToListAsync(), including when the ISoftDelete entity is nested through .Include() or navigation properties.

Actual Behavior
CountAsync() does not respect ISoftDelete filters of related entities included through navigation, while ToListAsync() does — this leads to divergent results and forces the developer to manually add extra filters for all possible nested deletions.

Questions

  1. Is there a supported pattern to automatically apply ISoftDelete filters on related entities in EF Core when using Include()?
  2. Is this something ABP can extend or wrap at the repository level (e.g., through IRepository<T> or IQueryable<T> extensions)?
  3. If not, is the recommendation to always filter nested IsDeleted properties manually?

Workaround Used
We added manual filters for all involved navigations:

.Where(x =>
    x.AssigneeUserId == AbpSession.UserId &&
    x.Status == input.Status &&
    !x.CurrentWorkflowPhase.IsDeleted &&
    !x.WorkflowPhaseTask.IsDeleted &&
    !x.WorkflowCard.IsDeleted &&
    !x.WorkflowCard.Workflow.IsDeleted
);

Environment
ABP Version: 9.0
EF Core Version: 8.0
Database Provider: SQL Server

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions