Created
April 2, 2025 17:08
-
-
Save JKamsker/28d9fd5136b6da3dcd6972e8d9d93231 to your computer and use it in GitHub Desktop.
Revisions
-
JKamsker created this gist
Apr 2, 2025 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,304 @@ using System.Reflection; public class DependencyNode { public string ServiceType { get; set; } = string.Empty; public string? ImplementationType { get; set; } public string Lifetime { get; set; } = string.Empty; public string RegistrationMethod { get; set; } = "Unknown"; // e.g., Type, Factory, Instance, Collection public List<DependencyNode> Dependencies { get; set; } = new List<DependencyNode>(); public bool IsCycleDetected { get; set; } = false; public string? Message { get; set; } // For errors or notes like "Not Found", "Already Processed" } public static class ServiceCollectionExtensions { /// <summary> /// Generates a list of DependencyNode objects representing the dependency tree /// based on registered services and their constructor dependencies. /// Handles IEnumerable<T> dependencies correctly. /// Note: This shows registered dependencies, not the fully resolved runtime graph. /// </summary> /// <param name="services">The IServiceCollection to analyze.</param> /// <returns>A list of root DependencyNode objects.</returns> public static List<DependencyNode> GetDependencyTree(this IServiceCollection services) { var rootNodes = new List<DependencyNode>(); var serviceDescriptors = services.ToList(); // Create a list for easier lookup // Keep track of processed implementation types to avoid redundant processing // of the *dependencies* for the same implementation across different service registrations. var processedImplementationTypes = new HashSet<Type>(); // Keep track of nodes already added to the root list to avoid duplicates if a service // is registered multiple times (e.g. as self and as interface) var addedRootServiceTypes = new HashSet<Type>(); foreach (var descriptor in serviceDescriptors) { // Check if this exact service type has already been added as a root node if (addedRootServiceTypes.Contains(descriptor.ServiceType)) { continue; // Skip if this service type root is already processed } DependencyNode node; // Only build the full dependency subtree if the implementation type hasn't been fully processed yet, // or if there's no implementation type (factory/instance). if (descriptor.ImplementationType == null || !processedImplementationTypes.Contains(descriptor.ImplementationType)) { node = BuildNodeRecursive(descriptor, serviceDescriptors, processedImplementationTypes, new HashSet<Type>()); // Mark implementation type as processed *after* building its tree the first time if (descriptor.ImplementationType != null) { // Check again because recursion might have processed it processedImplementationTypes.Add(descriptor.ImplementationType); } } else { // If the implementation was already processed, create a simpler node indicating this. node = CreateBaseNode(descriptor); // Avoid overwriting existing message if CreateBaseNode added one node.Message = string.IsNullOrEmpty(node.Message) ? $"Implementation ({GetServiceTypeName(descriptor.ImplementationType)}) processed in another branch." : node.Message + $" | Implementation ({GetServiceTypeName(descriptor.ImplementationType)}) processed in another branch."; } rootNodes.Add(node); addedRootServiceTypes.Add(descriptor.ServiceType); } return rootNodes; } private static DependencyNode BuildNodeRecursive( ServiceDescriptor currentDescriptor, List<ServiceDescriptor> allDescriptors, HashSet<Type> globallyProcessedImplementations, HashSet<Type> visitedInBranch) // For cycle detection in the current path { var node = CreateBaseNode(currentDescriptor); // --- Stop conditions for recursion --- // 1. No implementation type (factory/instance) - cannot analyze constructor if (currentDescriptor.ImplementationType == null) { return node; } // 2. Cycle detected for this implementation type in the current branch if (!visitedInBranch.Add(currentDescriptor.ImplementationType)) { node.IsCycleDetected = true; node.Message = string.IsNullOrEmpty(node.Message) ? $"Cycle detected involving {GetServiceTypeName(currentDescriptor.ImplementationType)}." : node.Message + $" | Cycle detected involving {GetServiceTypeName(currentDescriptor.ImplementationType)}."; return node; // Stop recursion for this branch } // 3. Implementation's dependencies already fully processed globally if (globallyProcessedImplementations.Contains(currentDescriptor.ImplementationType)) { node.Message = string.IsNullOrEmpty(node.Message) ? $"Dependencies for {GetServiceTypeName(currentDescriptor.ImplementationType)} processed elsewhere." : node.Message + $" | Dependencies for {GetServiceTypeName(currentDescriptor.ImplementationType)} processed elsewhere."; // Return the node without processing dependencies again, but *after* cycle check return node; } // --- Find Constructor and Process Dependencies --- var implementationType = currentDescriptor.ImplementationType; var constructor = FindConstructor(implementationType); if (constructor != null) { var parameters = constructor.GetParameters(); if (parameters.Length > 0) { foreach (var param in parameters) { Type parameterType = param.ParameterType; // --- Handle IEnumerable<T> --- if (parameterType.IsGenericType && parameterType.GetGenericTypeDefinition() == typeof(IEnumerable<>)) { Type collectionItemType = parameterType.GetGenericArguments()[0]; var enumerableNode = new DependencyNode { ServiceType = GetServiceTypeName(parameterType), // e.g., "IEnumerable<IServiceX>" Lifetime = "N/A", // Lifetime applies to individual items RegistrationMethod = "Collection", Message = $"Resolved from all registered {GetServiceTypeName(collectionItemType)} services" }; // Find all registered services for the item type T var itemDescriptors = allDescriptors.Where(d => d.ServiceType == collectionItemType).ToList(); if (!itemDescriptors.Any()) { enumerableNode.Message += " (No services registered)"; } else { foreach (var itemDescriptor in itemDescriptors) { // Recursively build the node for each item in the collection. // Pass a *copy* of visitedInBranch. var itemNode = BuildNodeRecursive(itemDescriptor, allDescriptors, globallyProcessedImplementations, new HashSet<Type>(visitedInBranch)); enumerableNode.Dependencies.Add(itemNode); } } node.Dependencies.Add(enumerableNode); } // --- Handle Regular Dependencies --- else { // Find the registration for this dependency (simplistic: takes the last one) // More complex scenarios (multiple registrations, specific selection logic) are not covered here. var dependencyDescriptor = allDescriptors.LastOrDefault(d => d.ServiceType == parameterType); if (dependencyDescriptor != null) { // Recursively build the node for the dependency. // Pass a *copy* of visitedInBranch. var dependencyNode = BuildNodeRecursive(dependencyDescriptor, allDescriptors, globallyProcessedImplementations, new HashSet<Type>(visitedInBranch)); node.Dependencies.Add(dependencyNode); } else { // Dependency not found in the collection node.Dependencies.Add(new DependencyNode { ServiceType = GetServiceTypeName(parameterType), Lifetime = "N/A", RegistrationMethod = "Not Registered", Message = "Dependency not found in service collection." }); } } } } // else: Constructor exists but has no parameters - node.Dependencies remains empty. } else { // No suitable constructor found node.Message = string.IsNullOrEmpty(node.Message) ? $"No suitable public constructor found for {GetServiceTypeName(implementationType)}." : node.Message + $" | No suitable public constructor found for {GetServiceTypeName(implementationType)}."; } // Mark this implementation type as globally processed *after* its dependencies are handled for the first time. // Note: This happens *after* the recursive calls for its dependencies. globallyProcessedImplementations.Add(implementationType); // Remove from visitedInBranch as we are backtracking up the recursion stack visitedInBranch.Remove(implementationType); return node; } /// <summary> /// Creates a basic DependencyNode from a ServiceDescriptor. /// </summary> private static DependencyNode CreateBaseNode(ServiceDescriptor descriptor) { var node = new DependencyNode { ServiceType = GetServiceTypeName(descriptor.ServiceType), Lifetime = descriptor.Lifetime.ToString(), }; if (descriptor.ImplementationType != null) { node.ImplementationType = GetServiceTypeName(descriptor.ImplementationType); node.RegistrationMethod = "Type"; // Add mapping info if different, avoid overwriting potential cycle/processed messages later if(descriptor.ServiceType != descriptor.ImplementationType) { node.Message = $"Maps to implementation: {node.ImplementationType}"; } } else if (descriptor.ImplementationFactory != null) { node.RegistrationMethod = "Factory"; node.ImplementationType = "(Factory)"; // Indicate it's a factory } else if (descriptor.ImplementationInstance != null) { node.RegistrationMethod = "Instance"; node.ImplementationType = GetServiceTypeName(descriptor.ImplementationInstance.GetType()); node.Message = $"Instance of {node.ImplementationType}"; } else { node.RegistrationMethod = "Unknown"; } return node; } /// <summary> /// Finds the constructor to use for dependency analysis. /// Simplistic: Prefers public constructors, takes the one with the most parameters. /// Real DI containers have more complex logic (e.g., [ActivatorUtilitiesConstructor]). /// </summary> private static ConstructorInfo? FindConstructor(Type implementationType) { // Added check for non-public constructors as well, although DI typically prefers public var constructors = implementationType.GetConstructors(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); // Simple heuristic: prefer public, then prefer longest parameter list return constructors .OrderByDescending(c => c.IsPublic) // Public first .ThenByDescending(c => c.GetParameters().Length) // Then longest .FirstOrDefault(); } /// <summary> /// Helper to get a cleaner type name, especially for generic types. /// </summary> private static string GetServiceTypeName(Type? type) // Made nullable { if (type == null) return "null"; // Handle generic types if (type.IsGenericType) { try { var genericArgs = string.Join(", ", type.GetGenericArguments().Select(GetServiceTypeName)); var baseName = type.Name.Split('`')[0]; // Special handling for Nullable<T> if (type.GetGenericTypeDefinition() == typeof(Nullable<>)) { return $"{GetServiceTypeName(type.GetGenericArguments()[0])}?"; } // General generic case return $"{baseName}<{genericArgs}>"; } catch (Exception) // Handle potential issues with complex generic types { return type.Name; // Fallback } } // Handle array types if (type.IsArray) { return $"{GetServiceTypeName(type.GetElementType())}[]"; } // Handle simple types return type.Name switch { "String" => "string", "Int32" => "int", "Int64" => "long", "Boolean" => "bool", "Double" => "double", "Decimal" => "decimal", "Object" => "object", // Add other common type aliases if desired _ => type.Name }; } }