Summary
Passing any value to --exclude-filter — including a package name that doesn't exist in the dependency graph — causes the entire component list to be wiped out ("Found 0 packages") in two situations:
- Any solution input (
.sln, .slnx, .slnf), with or without --recursive (the flag has no effect here — see root cause).
- A single project file (
.csproj etc.) without --recursive.
It works correctly only for: a single project file with --recursive
Reproduced on v6.2.0 (latest); traced back to the commit that introduced --exclude-filter (#939, 1615122, 2025-04-27) — present in every release since.
Repro
(1) Broken: solution filter, --recursive has no effect either way
dotnet-CycloneDX MySolution.slnf --disable-package-restore --exclude-filter "PackageThatDoesNotExist" --filename bom.json
dotnet-CycloneDX MySolution.slnf --recursive --disable-package-restore --exclude-filter "PackageThatDoesNotExist" --filename bom.json
(2) Broken: single project, no --recursive
dotnet-CycloneDX MyProject.csproj --disable-package-restore --exclude-filter "PackageThatDoesNotExist" --filename bom.json
(3) Works: single project, with --recursive
dotnet-CycloneDX MyProject.csproj --recursive --disable-package-restore --exclude-filter "PackageThatDoesNotExist" --filename bom.json
Cases (1) and (2) print, for every restored package:
The following orphaned packages have been removed:
-
Found 0 packages
Case (3) correctly prints No orphaned packages were found. and produces a full component list.
Root cause
ExcludeFilterHelper.RemoveOrphanedPackages (ExcludeFilterHelper.cs) does a BFS starting only from packages with IsDirectReference == true, then intersects the package set with whatever's reachable:
var queue = new Queue(packages.Where(p => p.IsDirectReference));
...
packages.IntersectWith(reachablePackages);
This runs in Runner.cs right after packages are loaded:
if (!string.IsNullOrEmpty(options.DependencyExcludeFilter))
{
ExcludeFilterHelper.ExcludePackages(packages, options.DependencyExcludeFilter);
ExcludeFilterHelper.RemoveOrphanedPackages(packages); // ~line 251
}
But at this point in the flow, IsDirectReference has not been populated for most input types:
- ProjectFileService.GetProjectDotnetDependencysAsync (the method used for a plain single project without --recursive, and also used internally, per-project, by the solution path below) never sets IsDirectReference on anything.
- ProjectFileService.RecursivelyGetProjectDotnetDependencysAsync (used only for a single project with --recursive) does set it immediately on load — this is the one path that works.
- For .sln/.slnx/.slnf input, Runner.cs branches to SolutionFileService.GetSolutionDotnetDependencys before ever checking --recursive (Runner.cs:171-182), so the flag has no effect for solutions at all. That method does contain logic that looks like it should propagate direct-reference
status per project:
// SolutionFileService.cs:185-199
var projectPackages = await _projectFileService.GetProjectDotnetDependencysAsync(...);
directReferencePackages.UnionWith(projectPackages.Where(p => p.IsDirectReference));
...
foreach (var directPackage in directReferencePackages)
{
packages.TryGetValue(directPackage, out var package);
package.IsDirectReference = true;
}
- but since GetProjectDotnetDependencysAsync never sets IsDirectReference in the first place, directReferencePackages is always empty and this block is effectively dead code.
- The only place that actually populates IsDirectReference for these paths is Runner.cs's RemoveProjectReferencesAndMakeTheirDependenciesDirect(packages) call — but that runs after the exclude-filter block (~line 344 vs ~line 251).
So for every input type except "single project + --recursive", the BFS starts from an empty set, and the filter step deletes everything.
Suggested fix
Ensure IsDirectReference is populated before the exclude-filter block runs, for all input types — e.g. by moving/duplicating the RemoveProjectReferencesAndMakeTheirDependenciesDirect call (or equivalent) earlier in Runner.cs, and/or fixing GetProjectDotnetDependencysAsync to set the flag
on load so SolutionFileService's existing propagation logic actually does something.
Environment
- cyclonedx-dotnet (CycloneDX global tool) 6.2.0+55877e2ae058ae9686783ac084d2257d3fcedab1
- .NET SDK 10.0.301
- Reproduced against a single leaf .csproj, a single .csproj with --recursive, and a multi-project .slnf
Related
Summary
Passing any value to
--exclude-filter— including a package name that doesn't exist in the dependency graph — causes the entire component list to be wiped out ("Found 0 packages") in two situations:.sln,.slnx,.slnf), with or without--recursive(the flag has no effect here — see root cause)..csprojetc.) without--recursive.It works correctly only for: a single project file with
--recursiveReproduced on
v6.2.0(latest); traced back to the commit that introduced--exclude-filter(#939, 1615122, 2025-04-27) — present in every release since.Repro
(1) Broken: solution filter,
--recursivehas no effect either waydotnet-CycloneDX MySolution.slnf --disable-package-restore --exclude-filter "PackageThatDoesNotExist" --filename bom.jsondotnet-CycloneDX MySolution.slnf --recursive --disable-package-restore --exclude-filter "PackageThatDoesNotExist" --filename bom.json(2) Broken: single project, no
--recursivedotnet-CycloneDX MyProject.csproj --disable-package-restore --exclude-filter "PackageThatDoesNotExist" --filename bom.json(3) Works: single project, with
--recursivedotnet-CycloneDX MyProject.csproj --recursive --disable-package-restore --exclude-filter "PackageThatDoesNotExist" --filename bom.jsonCases (1) and (2) print, for every restored package:
The following orphaned packages have been removed:
-
Found 0 packages
Case (3) correctly prints
No orphaned packages were found.and produces a full component list.Root cause
ExcludeFilterHelper.RemoveOrphanedPackages (ExcludeFilterHelper.cs) does a BFS starting only from packages with IsDirectReference == true, then intersects the package set with whatever's reachable:
var queue = new Queue(packages.Where(p => p.IsDirectReference));
...
packages.IntersectWith(reachablePackages);
This runs in Runner.cs right after packages are loaded:
if (!string.IsNullOrEmpty(options.DependencyExcludeFilter))
{
ExcludeFilterHelper.ExcludePackages(packages, options.DependencyExcludeFilter);
ExcludeFilterHelper.RemoveOrphanedPackages(packages); // ~line 251
}
But at this point in the flow, IsDirectReference has not been populated for most input types:
status per project:
// SolutionFileService.cs:185-199
var projectPackages = await _projectFileService.GetProjectDotnetDependencysAsync(...);
directReferencePackages.UnionWith(projectPackages.Where(p => p.IsDirectReference));
...
foreach (var directPackage in directReferencePackages)
{
packages.TryGetValue(directPackage, out var package);
package.IsDirectReference = true;
}
So for every input type except "single project + --recursive", the BFS starts from an empty set, and the filter step deletes everything.
Suggested fix
Ensure IsDirectReference is populated before the exclude-filter block runs, for all input types — e.g. by moving/duplicating the RemoveProjectReferencesAndMakeTheirDependenciesDirect call (or equivalent) earlier in Runner.cs, and/or fixing GetProjectDotnetDependencysAsync to set the flag
on load so SolutionFileService's existing propagation logic actually does something.
Environment
Related