Search code examples
c#genericstypestask-parallel-librarydataflow

Using string as parameter for type when calling constructor


I'm trying to create a type based on a string parameter and pass that into the type parameter of a constructor. It get's pretty nasty when just checking it with if-statements and I don't know how to do it more programmatically / generically.

I have tried with reflection but that only returns an object and passing an object to < T > is obviously not working.

Does anyone have an idea how to solve this in a more deliate way without the thousands of if statements?

Object creation looks like this:

                if (Options.Input1Type == "int" && Options.Output1Type == "int") return BlockBuilder.Build<int, int>(Kind, Options, TransformToSelf);
                if (Options.Input1Type == "bool" && Options.Output1Type == "bool") return BlockBuilder.Build<bool, bool>(Kind, Options, TransformToSelf);
                if (Options.Input1Type == "string" && Options.Output1Type == "string") return BlockBuilder.Build<string, string>(Kind, Options, TransformToSelf);

                if (Options.Input1Type == "bool" && Options.Output1Type == "int") return BlockBuilder.Build<bool, int>(Kind, Options, TransformToInt);
                if (Options.Input1Type == "bool" && Options.Output1Type == "string") return BlockBuilder.Build<bool, string>(Kind, Options, TransformToString);

                if (Options.Input1Type == "int" && Options.Output1Type == "bool") return BlockBuilder.Build<int, bool>(Kind, Options, TransformToBool);
                if (Options.Input1Type == "int" && Options.Output1Type == "string") return BlockBuilder.Build<int, string>(Kind, Options, TransformToString);

                if (Options.Input1Type == "string" && Options.Output1Type == "int") return BlockBuilder.Build<string, int>(Kind, Options, TransformToInt);
                if (Options.Input1Type == "string" && Options.Output1Type == "bool") return BlockBuilder.Build<string, bool>(Kind, Options, TransformToBool);

BlockBuilder looks like this:

public static IDataflowBlock Build<TIn, TOut>(string kind, BlockOptions blockOptions, Func<TIn, TOut> singleOutputExecutionFunction = null, Func<TIn, IEnumerable<TOut>> multipleOutputExecutionFunction = null)
    {
        if (singleOutputExecutionFunction == null && multipleOutputExecutionFunction == null)
            throw new ArgumentException("Missing function to execute");

        Enum.TryParse(kind, out TransformationBlocks Kind);

        switch (Kind)
        {
            case TransformationBlocks.Undefined:
                throw new ArgumentException("No block type was specified");
            case TransformationBlocks.TransformBlock:
                return new TransformBlock<TIn, TOut>(param => { return singleOutputExecutionFunction(param); }, new ExecutionDataflowBlockOptions()
                {
                    MaxMessagesPerTask = blockOptions.MaxMessagesPerTask,
                    BoundedCapacity = blockOptions.BoundedCapacity,
                    MaxDegreeOfParallelism = blockOptions.MaxDegreeOfParallelism,
                });
            case TransformationBlocks.TransformManyBlock:
                return new TransformManyBlock<TIn, TOut>(param => { return multipleOutputExecutionFunction(param); }, new ExecutionDataflowBlockOptions()
                {
                    MaxMessagesPerTask = blockOptions.MaxMessagesPerTask,
                    BoundedCapacity = blockOptions.BoundedCapacity,
                    MaxDegreeOfParallelism = blockOptions.MaxDegreeOfParallelism,
                });
            default:
                return default;
        }
    }

And the delegates / functions looks like this:

    private static T TransformToSelf<T>(T obj)
    {
        return obj;
    }

    private static string TransformToString<T>(T obj)
    {
        return Convert.ToString(obj);
    }

    private static int TransformToInt<T>(T obj)
    {
        return Convert.ToInt32(obj);
    }

    private static bool TransformToBool<T>(T obj)
    {
        return Convert.ToBoolean(obj);
    }

Solution

  • It's not easy but it's doable.

    If you can change the type of Input1Type and Input2Type to be System.Type rather than string, it's much easier.

    If not, then I would suggest you create a mapping function as susggested by @neil that maps strings to types, then use MethodInfo.MakeGenericType() to call your Build() function.

    See below for a simple example of MakeGenericType().

    using System;
    using System.Reflection;
    
    namespace make_generic_type
    {
        class Program
        {
            static void Main(string[] args)
            {
                // Normal C# usage
                var host = new Host();
                Console.WriteLine(host.GenericMethod<int, string>("Test"));
    
                // Use reflection to get type definition
                var unboundMethod = typeof(Host).GetMethod(nameof(Host.GenericMethod));
                // As the method is generic, you need to pass the type parameters in.
                // We do this by binding the type parameters with MethodInfo.MakeGenericMethod();
                var boundMethod = unboundMethod.MakeGenericMethod(new Type[]{ typeof(int), typeof(string) });
    
                // Now we have a method that we can invoke via reflection as normal
                Console.WriteLine(boundMethod.Invoke(new Host(), new object[]{ "Test"}));
            }
    
    
        }
    
        class Host{
            public string GenericMethod<TIn, TOut>(string kind)
            {
                return $"{typeof(TIn).Name}; {typeof(TOut).Name}; {kind};";
            }
        }
    }