Home > OS >  Build an expression tree that can handle any type/class with a "TryParse()" method
Build an expression tree that can handle any type/class with a "TryParse()" method

Time:02-23

i have the following method that returns a "value parser" delegate according to the input type. it works fine but i'd like to get rid of the switch statements and type checks and be able to return a value parser delegate for any type that has a TryParse() method.

internal static Func<object?, (bool isSuccess, object value)>? ValueParser(this Type type)
{
    type = Nullable.GetUnderlyingType(type) ?? type;

    if (type.IsEnum)
        return input => (Enum.TryParse(type, ToString(input), out var res), res!);

    switch (Type.GetTypeCode(type))
    {
        case TypeCode.String:
            return input => (true, input!);

        case TypeCode.Boolean:
            return input => (bool.TryParse(ToString(input), out var res), res);

        case TypeCode.DateTime:
            return input => (DateTime.TryParse(ToString(input), out var res), res);

        //other supported types go here...

        case TypeCode.Object:
            if (type == Types.Guid)
            {
                return input => (Guid.TryParse(ToString(input), out var res), res);
            }            
            else if (type == Types.TimeSpan)
            {
                return input => (TimeSpan.TryParse(ToString(input), out var res), res!);
            }
            break;
    }

    return null; //unsupported types will cause a null return

    static string? ToString(object? value)
    {
        if (value is string x)
            return x;

        return value?.ToString();
    }
}

i believe the solution would be to build an expression tree something like below. but i haven't got the slightest clue how to go about building an expression tree correctly. so far all i have is the following:

internal static Func<object?, (bool isSuccess, object value)>? ValueParser(this Type type)
{
    type = Nullable.GetUnderlyingType(type) ?? type;

    var inputParam = Expression.Parameter(typeof(string), "input");

    if (type == Types.String)
    {
        //no need for conversion if input type is string. so delegate should simply return a tuple (true,"inputValue").
        var returnTarget = Expression.Label(type);
        var returnCall = Expression.Return(returnTarget, inputParam);
    }

    var parsedVar = Expression.Variable(type, "parsed");

    var tryParseCall = Expression.Call(
        type,
        "TryParse",
        null,
        inputParam,
        parsedVar);

    //need to compile the expression and return here.
    //if the input type doesn't have a TryParse() method, null should be returned.
    //also not sure if we need Expression.Convert() to handle value types.
}

i've been banging my head against the wall on this for a few days without much success. would really appreciate any help you can provide. thanks!

CodePudding user response:

This appears to do the trick:

private static readonly MethodInfo toStringMethod = typeof(object).GetMethod("ToString")!;
private static readonly ConstructorInfo valueTupleConstructor = typeof(ValueTuple<bool, object>).GetConstructor(new[] { typeof(bool), typeof(object) })!;

internal static Func<object?, (bool isSuccess, object value)>? ValueParser(Type type)
{
    type = Nullable.GetUnderlyingType(type) ?? type;

    if (type == typeof(string))
        return input => (true, input!);
    if (type.IsEnum)
        return input => (Enum.TryParse(type, input?.ToString(), out var res), res!);
    
    // Try and find a suitable TryParse method on Type
    var tryParseMethod = type.GetMethod("TryParse", BindingFlags.Static | BindingFlags.Public, new[] { typeof(string), type.MakeByRefType() });
    // None found or returns the wrong type? Return null.
    if (tryParseMethod == null || tryParseMethod.ReturnType != typeof(bool))
        return null;
    
    // The 'object' parameter passed into our delegate
    var inputParameter = Expression.Parameter(typeof(object), "input");
    // 'input == null ? (string)null : input.ToString()'
    var toStringConversion = Expression.Condition(
        Expression.ReferenceEqual(inputParameter, Expression.Constant(null, typeof(object))),
        Expression.Constant(null, typeof(string)),
        Expression.Call(inputParameter, toStringMethod));
    
    // 'res' variable used as the out parameter to the TryParse call
    var resultVar = Expression.Variable(type, "res");
    // 'isSuccess' variable to hold the result of calling TryParse
    var isSuccessVar = Expression.Variable(typeof(bool), "isSuccess");
    // To finish off, we need to following sequence of statements:
    //  - isSuccess = TryParse(input.ToString(), res)
    //  - new ValueTuple<bool, object>(isSuccess, (object)res)
    // A sequence of statements is done using a block, and the result of the final
    // statement is the result of the block
    var tryParseCall = Expression.Call(tryParseMethod, toStringConversion, resultVar);
    var block = Expression.Block(new[] { resultVar, isSuccessVar },
        Expression.Assign(isSuccessVar, tryParseCall),
        Expression.New(valueTupleConstructor, isSuccessVar, Expression.Convert(resultVar, typeof(object))));
    
    // Put it all together
    var lambda = Expression.Lambda<Func<object?, (bool, object)>>(block, inputParameter).Compile();
    return lambda;
}

See it on dotnetfiddle.net.

Hopefully the inline comments explain what's going on. If not, let me know and I'll improve them.

Note that if the input is null, this follows your code's convention of calling TryParse(null, out var res). This doesn't seem hugely sensible.

  • Related