Pull all constructor options recursively?

Viewed 261

Let's say I have an object containing an object each with one or more constructors:

 Foo(int p1, Bar p2);
 
 Bar(String p3, String p4);

 Bar(int p5, int p6);    

And I want to display say in a UI what parameters are required to build a Foo. I think there are three options:

int p1, Bar p2

Or

int p1, String p3, String p4

Or

int p1, int p5, int p6

Obviously, it gets more complex as there are more objects nested. Is there a way to pull all of these options? How about in C#? Bonus points if you can show how to build a Foo dynamically having been given option 2 or 3 above.

4 Answers

You can (as suggested in comments) use reflection and recursion to achieve what you want. It's up to you what is the criteria to descend into one more level of recursion. I've implemented these classes to mimic yours:

public class Foo
{
    public Foo(int p1, Bar p2)
    { }
}
public class Bar
{
    public Bar(string p3, string p4)
    { }
    public Bar(int p5, int p6)
    { }
}

Then I've implemented this recursive method to print the possible constructs:

public static void PrintConstructors(Type t, string currentOutput)
{
    foreach (var ctor in t.GetConstructors())
    {
        var construct = currentOutput;
        foreach (var param in ctor.GetParameters())
        {
            if (param.ParameterType.IsClass && param.ParameterType != typeof(string))
            { PrintConstructors(param.ParameterType, construct); }
            construct += $"{param.ParameterType} {param.Name}, ";
        }
        if (!string.IsNullOrEmpty(construct))
        { Console.WriteLine(construct.TrimEnd(',', ' ')); }
    }
}

Calling PrintConstructors(typeof(Foo), ""); produces the following output:

System.Int32 p1, System.String p3, System.String p4
System.Int32 p1, System.Int32 p5, System.Int32 p6
System.Int32 p1, ConsoleApp.Bar p2

EDIT: An alternative when having more than one complex type in a constructor would be (without checking for circular dependencies as @Michał Turczyn stated):

public static IEnumerable<string> GetConstructs(Type t)
{
    foreach (var ctor in t.GetConstructors())
    {
        var constructs = new List<string>() { "" };
        foreach (var param in ctor.GetParameters())
        {
            if (param.ParameterType.IsClass && param.ParameterType != typeof(string))
            {
                var newConstructs = new List<string>();
                foreach (var _construct in GetConstructs(param.ParameterType))
                {
                    foreach (var construct in constructs)
                    { newConstructs.Add(construct + $"{param.ParameterType } ({_construct}) {param.Name}, "); }
                }
                constructs = newConstructs;
            }
            else
            {
                for (var i = 0; i < constructs.Count; i++)
                { constructs[i] += $"{param.ParameterType} {param.Name}, "; }
            }

        }
        foreach (var construct in constructs)
        { yield return construct.TrimEnd(',', ' '); }
    }
}

Assuming you update the constructor of Foo(int p1, Bar p2) to Foo(int p1, Bar p2, string s1, Bar y) the output you get from:

foreach (var construct in GetConstructs(typeof(Foo)))
{ Console.WriteLine(construct); }

is:

System.Int32 p1, ConsoleApp.Bar (System.String p3, System.String p4) p2, System.String s1, ConsoleApp.Bar (System.String p3, System.String p4) y
System.Int32 p1, ConsoleApp.Bar (System.Int32 p5, System.Int32 p6) p2, System.String s1, ConsoleApp.Bar (System.String p3, System.String p4) y
System.Int32 p1, ConsoleApp.Bar (System.String p3, System.String p4) p2, System.String s1, ConsoleApp.Bar (System.Int32 p5, System.Int32 p6) y
System.Int32 p1, ConsoleApp.Bar (System.Int32 p5, System.Int32 p6) p2, System.String s1, ConsoleApp.Bar (System.Int32 p5, System.Int32 p6) y

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.

I think it would be great simplification if you could define constructor of your "top class" in composition hierarchy (i.e. Foo). Then it woyld be very simple to do all thing you want:

  • want to get all possible ways of constructing Foo? Just loop over constructors
  • want to invoke correct constructor depending on input? Nothing more easy: write if-else or switch expression and match input, after that just invoke correct constructor, as we are sure one exists! :)

Sample set up and output of the program:

class Program
{
    static void Main(string[] args)
    {
        var ctors = typeof(Foo).GetConstructors(BindingFlags.Public | BindingFlags.Instance);
        foreach(var ctor in ctors)
        {
            Console.WriteLine();
            Console.WriteLine("Next constructor: ");
            var ctorParams = ctor.GetParameters();
            foreach (var param in ctorParams)
                Console.WriteLine($"\t{param.ParameterType.FullName} {param.Name}");
        }
    }
}

public class Foo
{
    private Bar _bar;
    private int _i;
    // Here define constructor for every possible way of constructing object.
    public Foo(int i, Bar bar) => (_i, _bar) = (i, bar);
    public Foo(int i, string s1, string s2) => (_i, _bar) = (i, new Bar(s1,s2));
}

public class Bar
{
    private string _s1;
    private string _s2;
    public Bar(string s1, string s2) => (_s1, _s2) = (s1, s2);
}

Output

Next constructor:
        System.Int32 i
        ConsoleApp.Bar bar

Next constructor:
        System.Int32 i
        System.String s1
        System.String s2

To extend dcg's answer

Here is how I parse data from a Console Input for double and enum types:

static void DoCalculationsForType(Type t)
    {
        Console.WriteLine(t.Name);
        Console.WriteLine("Select a calculation option otherwise it will cancel:");
        var constructs = ConstructorExtension.GetConstructs(t).ToArray();
        int optionNumber = 1;
        foreach (var construct in constructs)
        {
            Console.Write(optionNumber + ". ");
            Console.WriteLine(construct.ToString());
            optionNumber++;
        }
        string selected = Console.ReadLine();
        try
        {
            int selectedNum = int.Parse(selected);
            if ((selectedNum - 1) < constructs.Length && selectedNum > 0)
            {
                var construct = constructs[selectedNum - 1];
                object[] buildParams = new object[construct.TotalParamerters];
                for (int i = 0; i < buildParams.Length; i++)
                {
                    Console.WriteLine("Enter parameter " + (i + 1) + ":");
                    Console.WriteLine("It's type is " + construct.Children[i].ArgType.Name);
                    if (construct.Children[i].ArgType == typeof(double))
                    {
                        buildParams[i] = double.Parse(Console.ReadLine());
                    }
                    else if (construct.Children[i].ArgType.IsEnum)
                    {
                        buildParams[i] = Enum.ToObject(construct.Children[i].ArgType, int.Parse(Console.ReadLine()));
                    }
                }
                var obj = construct.Build(buildParams);
                Console.WriteLine("Calculated: " + obj);
            }
        }
        catch
        {

        }
    }
Related