When you say:
Solution should ignore constructors that include objects farther up the "stack" (ie If Bar had a constructor which takes a Foo, we can ignore it).
I think it's the same as saying that if a constructor is cyclic it should be ignored so I'm going to ignore all constructors that lead to a reference cycle (based on the descend criteria). Besides, if you have a constructor with a reference cycle I believe you're not gonna be able to display the way it's constructed.
So, first I wrote an extension method to check whether a given constructor is cyclic or not:
public static class ConstructorExtension
{
public static bool IsCyclic(
this ConstructorInfo ctor, Func<ParameterInfo, bool> shouldDescend)
{
var refMatrix = new Dictionary<Type, HashSet<Type>>();
foreach (var param in ctor.GetParameters())
{
if (shouldDescend(param) &&
IsCyclic(param.ParameterType, shouldDescend, refMatrix))
{ return true; }
}
return false;
}
private static bool IsCyclic(
Type argType,
Func<ParameterInfo, bool> shouldDescend,
Dictionary<Type, HashSet<Type>> refMatrix)
{
foreach (var ctor in argType.GetConstructors())
{
foreach (var param in ctor.GetParameters())
{
if (!shouldDescend(param))
{ continue; }
if (refMatrix.TryGetValue(argType, out var argDeps))
{ argDeps.Add(param.ParameterType); }
else
{ refMatrix[argType] = new HashSet<Type> { param.ParameterType }; }
if ((refMatrix.TryGetValue(param.ParameterType, out var paramDeps) &&
paramDeps.Contains(argType)) ||
IsCyclic(param.ParameterType, shouldDescend, refMatrix))
{ return true; }
}
}
return false;
}
}
Then, I implemented these classes that will help to maintain the structure of the non-cyclic constructors and in printing/building as needed:
public class TypeNode
{
public Type ArgType { get; set; }
public int TotalParamerters { get; set; }
public string ParamName { get; set; }
public override string ToString()
{
return $"{ArgType} {ParamName}";
}
public virtual object Build(params object[] args)
{
if (args == null ||
args.Length == 0 ||
!ArgType.IsAssignableFrom(args[0].GetType()))
{ return default; }
return args[0];
}
public virtual TypeNode Clone()
{
return new TypeNode
{
TotalParamerters = TotalParamerters,
ArgType = ArgType,
ParamName = ParamName
};
}
}
public class ComplexTypeNode : TypeNode
{
public List<TypeNode> Children { get; set; }
public override string ToString()
{
var args = string.Join(
", ",
Children?
.Select(child => child.ToString())
.ToArray() ?? new string[0]);
var paramName = string.IsNullOrEmpty(ParamName)
? ""
: $" {ParamName}";
return $"{ArgType} ({args}){paramName}";
}
public override object Build(params object[] args)
{
if (args?.Length != TotalParamerters)
{ throw new ArgumentException(nameof(args)); }
var taken = 0;
var parameters = new List<object>();
foreach (var child in Children)
{
var objs = args
.Skip(taken)
.Take(child.TotalParamerters)
.ToArray();
taken += child.TotalParamerters;
parameters.Add(child.Build(objs));
}
return Activator.CreateInstance(ArgType, parameters.ToArray());
}
public override TypeNode Clone()
{
return new ComplexTypeNode
{
ArgType = ArgType,
TotalParamerters = TotalParamerters,
ParamName = ParamName,
Children = Children.Select(child => child.Clone()).ToList()
};
}
}
So, finally by tweaking a bit the solution of my previous answer you can get all constructs by using the above classes:
private static IEnumerable<ComplexTypeNode> GetConstructs(Type t)
{
foreach (var ctor in t.GetConstructors())
{
if (ctor.IsCyclic(ShouldDescend))
{ continue; }
var constructs = new List<ComplexTypeNode>()
{
new ComplexTypeNode
{
TotalParamerters = 0,
Children = new List<TypeNode>(),
ArgType = t
}
};
foreach (var param in ctor.GetParameters())
{
if (ShouldDescend(param))
{
constructs = GetConstructs(param.ParameterType)
.SelectMany(
childNode => constructs.Select(
node =>
{
var newNode = (ComplexTypeNode)node.Clone();
var child = (ComplexTypeNode)childNode.Clone();
child.ParamName = param.Name;
newNode.Children.Add(child);
newNode.TotalParamerters += child.TotalParamerters;
return newNode;
}))
.ToList();
}
else
{
constructs.ForEach(node =>
{
node.Children.Add(new TypeNode
{
ArgType = param.ParameterType,
TotalParamerters = 1,
ParamName = param.Name
});
node.TotalParamerters += 1;
});
}
}
foreach (var construct in constructs)
{ yield return construct; }
}
}
Note that I extracted the criteria to descend into a new method:
private static bool ShouldDescend(ParameterInfo param)
{
return param.ParameterType.IsClass && param.ParameterType != typeof(string);
}
Now, if you run the following code:
var constructs = GetConstructs(typeof(Foo)).ToArray();
foreach (var construct in constructs)
{ Console.WriteLine(construct.ToString()); }
var obj1 = (Foo)constructs[0].Build(1, "a", "b");
var obj2 = (Foo)constructs[1].Build(2, 3, 4);
var obj3 = (Foo)constructs[2].Build();
Console.WriteLine();
Console.WriteLine($"obj1.P1: {obj1.P1}, obj1.P2.P3: {obj1.P2.P3}, obj1.P2.P4: {obj1.P2.P4}");
Console.WriteLine($"obj2.P1: {obj2.P1}, obj2.P2.P5: {obj2.P2.P5}, obj2.P2.P6: {obj2.P2.P6}");
Console.WriteLine($"obj3.P1: {obj3.P1}");
You'll get:
ConsoleApp.Foo (System.Int32 p1, ConsoleApp.Bar (System.String p3, System.String p4) p2)
ConsoleApp.Foo (System.Int32 p1, ConsoleApp.Bar (System.Int32 p5, System.Int32 p6) p2)
ConsoleApp.Foo ()
obj1.P1: 1, obj1.P2.P3: a, obj1.P2.P4: b
obj2.P1: 2, obj2.P2.P5: 3, obj2.P2.P6: 4
obj3.P1: 0
I only did a few tests but I think that's something you can work on.