Skip to content

Instantly share code, notes, and snippets.

@JKamsker
Created April 2, 2025 17:08
Show Gist options
  • Save JKamsker/28d9fd5136b6da3dcd6972e8d9d93231 to your computer and use it in GitHub Desktop.
Save JKamsker/28d9fd5136b6da3dcd6972e8d9d93231 to your computer and use it in GitHub Desktop.

Revisions

  1. JKamsker created this gist Apr 2, 2025.
    304 changes: 304 additions & 0 deletions GetDependencyTree.cs
    Original 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
    };
    }
    }