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.
IServiceProvider GetDependencyTree
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
};
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment