Skip to content

[Enhancement]: Avoid per-folder DB calls in FolderManager.SearchFiles (honour recursive flag) #6581

Open
@timi-ty

Description

@timi-ty

Is there an existing issue for this?

  • I have searched the existing issues

Description of problem

Searching Global Assets (or any folder tree) on large portals causes thousands of database round-trips and saturates CPU.

Repro (customer scale)

  1. Portal with ≈ 67 000 files in ≈ 2 700 folders.
  2. Open Global Assets → search for a single letter (e.g. “d”).
  3. Runtime ≈ 60 s, server at 100 % CPU. Logs show ~2 700 calls to dbo.GetFiles with @Recursive = 0.

Why it happens
FolderManager.SearchFiles always calls GetFiles(folderId, false, false) and then iterates every sub-folder in C#, producing one DB hit per folder. Each individual query is cheap; the volume is the bottleneck.

Description of solution

Refactor FolderManager.SearchFiles to pass the recursive flag straight through to the stored procedure, then filter the single result set in memory while respecting permissions.

private IEnumerable<IFileInfo> SearchFiles(IFolderInfo folder, Regex regex, bool recursive)
{
    var files = CBO.Instance.FillCollection<FileInfo>(
        DataProvider.Instance().GetFiles(folder.FolderID, false, recursive));

    if (!recursive)
        return files.Where(f => regex.IsMatch(f.FileName)).Cast<IFileInfo>();

    var allowed = GetFolders(folder.PortalID)
        .Where(f => f.FolderPath.StartsWith(folder.FolderPath) &&
                    FolderPermissionController.Instance.CanViewFolder(f))
        .Select(f => f.FolderPath)
        .ToHashSet();

    return files
        .Where(f => regex.IsMatch(f.FileName) && allowed.Contains(f.Folder))
        .Cast<IFileInfo>();
}

No schema or stored-procedure changes required.

Description of alternatives considered

Alternative Outcome
Additional indexes (IX_Folders_PortalPath, IX_Files_PortalID_FolderID_INC) ≈ 7 % fewer reads but still ~60 s runtime.
Modify the GetFiles SP Larger impact radius.

Anything else?

Observation

It’s likely that the SearchFiles function was written this way to enforce per-folder permission checks.
You can achieve the same result more efficiently by first retrieving all files and then filtering by permissions in C#.


Current implementation

foreach (var subFolder in this.GetFolders(folder))
{
    if (FolderPermissionController.Instance.CanViewFolder(subFolder))
    {
        files.AddRange(this.SearchFiles(subFolder, regex, true));
    }
}

Suggested implementation

return fileCollection
    .Where(f => regex.IsMatch(f.FileName) &&
                allowedFolderPaths.Contains(f.Folder))
    .Cast<IFileInfo>();

Key differences

Aspect Current approach Suggested approach
When permissions are checked Before each recursive search After collecting all files
Performance impact Multiple recursive calls; may be slower on deep trees Single pass through file list; simpler and faster
Readability Nested logic inside SearchFiles Clear LINQ pipeline with explicit filters

Both methods honor folder-level permissions, but the suggested version decouples file retrieval from permission evaluation, leading to improved performance.

Current Implementation Takes Up To 1 Minute To Search "d" on an installation with ~2k folders and ~60k files

DNN_Search_For_d_57_secs_original.mp4

Do you be plan to contribute code for this enhancement?

  • Yes

Would you be interested in sponsoring this enhancement?

  • Yes

Code of Conduct

  • I agree to follow this project's Code of Conduct

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions