diff --git a/src/ProjGraph.Lib/Services/EfAnalysis/EntityFileDiscovery.cs b/src/ProjGraph.Lib/Services/EfAnalysis/EntityFileDiscovery.cs
index ea3c68e..9f9a090 100644
--- a/src/ProjGraph.Lib/Services/EfAnalysis/EntityFileDiscovery.cs
+++ b/src/ProjGraph.Lib/Services/EfAnalysis/EntityFileDiscovery.cs
@@ -16,6 +16,11 @@ namespace ProjGraph.Lib.Services.EfAnalysis;
///
public static class EntityFileDiscovery
{
+ ///
+ /// Maximum recursion depth when searching for base class files to prevent infinite recursion
+ /// and limit search scope to reasonable project structures.
+ ///
+ private const int MaxSearchDepth = 10;
///
/// Discovers the file paths of entity files within the specified search directories.
///
@@ -173,22 +178,49 @@ public static HashSet ExtractEntityTypeNames(ClassDeclarationSyntax cont
/// For each file, it calls to process the file and add matching
/// entity type names and their file paths to the dictionary.
/// Any access errors encountered during directory traversal are ignored.
+ /// Directories like bin, obj, .git, and node_modules are skipped during traversal for performance.
///
private static async Task SearchDirectoryForEntitiesAsync(
string searchDir,
HashSet entityTypeNames,
string normalizedContextPath,
Dictionary entityFiles)
+ {
+ await SearchDirectoryRecursiveAsync(searchDir, entityTypeNames, normalizedContextPath, entityFiles);
+ }
+
+ ///
+ /// Recursively searches a directory for C# files, skipping common build and version control directories.
+ ///
+ /// The current directory to search.
+ /// A set of entity type names to search for in the C# files.
+ /// The normalized file path of the context file to exclude from the search.
+ ///
+ /// A dictionary where the keys are entity type names and the values are the corresponding file paths.
+ ///
+ /// A task that represents the asynchronous operation.
+ ///
+ /// This method implements manual recursion to avoid descending into directories that typically
+ /// contain build artifacts or dependencies (bin, obj, .git, node_modules), improving performance
+ /// for large projects.
+ ///
+ private static async Task SearchDirectoryRecursiveAsync(
+ string currentDir,
+ HashSet entityTypeNames,
+ string normalizedContextPath,
+ Dictionary entityFiles)
{
try
{
+ // Process files in the current directory
var options = new EnumerationOptions
{
- RecurseSubdirectories = true, IgnoreInaccessible = true, AttributesToSkip = FileAttributes.System
+ RecurseSubdirectories = false,
+ IgnoreInaccessible = true,
+ AttributesToSkip = FileAttributes.System
};
- foreach (var csFile in Directory.EnumerateFiles(searchDir, EfAnalysisConstants.FilePatterns.CSharpFiles,
- options))
+ foreach (var csFile in Directory.EnumerateFiles(currentDir, EfAnalysisConstants.FilePatterns.CSharpFiles, options))
{
var fullPath = Path.GetFullPath(csFile);
if (fullPath.Equals(normalizedContextPath, StringComparison.OrdinalIgnoreCase))
@@ -196,14 +228,19 @@ private static async Task SearchDirectoryForEntitiesAsync(
continue;
}
- // Skip common non-source directories that can be large
- var pathSegments = fullPath.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
- if (pathSegments.Any(s => s is "bin" or "obj" or ".git" or "node_modules"))
+ await ProcessSourceFileAsync(fullPath, entityTypeNames, entityFiles);
+ }
+
+ // Recursively process subdirectories, skipping excluded directories
+ foreach (var subDir in Directory.EnumerateDirectories(currentDir, "*", options))
+ {
+ var dirName = Path.GetFileName(subDir);
+ if (dirName is "bin" or "obj" or ".git" or "node_modules")
{
continue;
}
- await ProcessSourceFileAsync(fullPath, entityTypeNames, entityFiles);
+ await SearchDirectoryRecursiveAsync(subDir, entityTypeNames, normalizedContextPath, entityFiles);
}
}
catch
@@ -386,43 +423,85 @@ private static DirectoryInfo FindSolutionRoot(string startDirectory, int maxLeve
/// if the files are found; otherwise, the dictionary will be empty.
///
///
- /// This method iterates through the provided base class names and attempts to locate their corresponding
- /// file paths by calling the method. If a file is found, it is added
- /// to the resulting dictionary. If no file is found for a base class name, it is skipped.
+ /// This method recursively searches for base class files while skipping common build and version control
+ /// directories (bin, obj, .git, node_modules) to improve performance for large projects.
///
public static Dictionary SearchForBaseClassFiles(
HashSet baseClassNames,
DirectoryInfo solutionRoot)
{
var baseClassFiles = new Dictionary();
+ SearchForBaseClassFilesRecursive(solutionRoot.FullName, baseClassNames, baseClassFiles, 0, MaxSearchDepth);
+ return baseClassFiles;
+ }
+
+ ///
+ /// Recursively searches for base class files, skipping common build and version control directories.
+ ///
+ /// The current directory to search.
+ /// A set of base class names to search for.
+ ///
+ /// A dictionary to store the found base class files where the keys are base class names
+ /// and the values are the corresponding file paths.
+ ///
+ /// The current recursion depth.
+ /// The maximum recursion depth to prevent infinite recursion.
+ ///
+ /// This method implements manual recursion to avoid descending into directories that typically
+ /// contain build artifacts or dependencies (bin, obj, .git, node_modules), improving performance
+ /// for large projects.
+ ///
+ private static void SearchForBaseClassFilesRecursive(
+ string currentDir,
+ HashSet baseClassNames,
+ Dictionary baseClassFiles,
+ int currentDepth,
+ int maxDepth)
+ {
+ if (currentDepth >= maxDepth || baseClassFiles.Count == baseClassNames.Count)
+ {
+ return;
+ }
try
{
var options = new EnumerationOptions
{
- RecurseSubdirectories = true, IgnoreInaccessible = true, MaxRecursionDepth = 10
+ RecurseSubdirectories = false,
+ IgnoreInaccessible = true,
+ AttributesToSkip = FileAttributes.System
};
- foreach (var file in Directory.EnumerateFiles(solutionRoot.FullName, "*.cs", options))
+ // Process files in the current directory
+ foreach (var file in Directory.EnumerateFiles(currentDir, "*.cs", options))
{
- var fullPath = Path.GetFullPath(file);
- // Skip common non-source directories that can be large
- var pathSegments = fullPath.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
- if (pathSegments.Any(s => s is "bin" or "obj" or ".git" or "node_modules"))
+ var fileName = Path.GetFileNameWithoutExtension(file);
+ if (baseClassNames.Contains(fileName))
{
- continue;
+ var fullPath = Path.GetFullPath(file);
+ baseClassFiles.TryAdd(fileName, fullPath);
+
+ if (baseClassFiles.Count == baseClassNames.Count)
+ {
+ return;
+ }
}
+ }
- var fileName = Path.GetFileNameWithoutExtension(file);
- if (!baseClassNames.Contains(fileName))
+ // Recursively process subdirectories, skipping excluded directories
+ foreach (var subDir in Directory.EnumerateDirectories(currentDir, "*", options))
+ {
+ var dirName = Path.GetFileName(subDir);
+ if (dirName is "bin" or "obj" or ".git" or "node_modules")
{
continue;
}
- baseClassFiles.TryAdd(fileName, fullPath);
+ SearchForBaseClassFilesRecursive(subDir, baseClassNames, baseClassFiles, currentDepth + 1, maxDepth);
+
if (baseClassFiles.Count == baseClassNames.Count)
{
- break;
+ return;
}
}
}
@@ -430,7 +509,5 @@ public static Dictionary SearchForBaseClassFiles(
{
// Ignore access errors
}
-
- return baseClassFiles;
}
}
\ No newline at end of file