Created
April 2, 2025 17:08
-
-
Save JKamsker/28d9fd5136b6da3dcd6972e8d9d93231 to your computer and use it in GitHub Desktop.
IServiceProvider GetDependencyTree
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 characters
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