0

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!

9
  • So your approach is to take type, find its static TryParse method, and call that? Commented Feb 22, 2022 at 11:07
  • 1
    This smells like trying to reinvent the square wheel...a real X-Y problem. What problem are you trying to solve by implementing this? Commented Feb 22, 2022 at 11:09
  • yes that is correct. the main requirement however is that the return type should be Func<object?, (bool isSuccess, object value)>? because the consumers are going to cache it and use whenever needed. Commented Feb 22, 2022 at 11:09
  • @J... this has a very specific use case in a library. this is how it's being used. so at runtime, i only have access to the input's Type Commented Feb 22, 2022 at 11:20
  • 1
    In the alternative, if you change your function to accept any function with the bool (string, out T) signature, it's easy for callers to pass a TryParse method if one exists, easy to adapt other methods for other types, and is type safe at compile time... Commented Feb 22, 2022 at 11:25

1 Answer 1

3

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.

Sign up to request clarification or add additional context in comments.

4 Comments

brilliant! just absolutely brilliant! thank you so much for this. works like an absolute charm. the comments are really helpful for me to understand what's going on. and yeah no worries about TryParse accepting null values. no big deal there. again, can't thank you enough for this solution. btw, i credited the contribution to you in the code, if that's ok with you.
I'm glad it does the trick! Code contributed to SO is under CC BY-SA, so you're welcome to do what you want to it.
I feel that this is, to use Harry Potter parlance, like learning what a Horcrux is - it's fine to know, but to put it to use is evil.
@Enigmativity yeah i agree. this is for an extremely specific use-case. i wouldn't use it in any other situation. i needed this to work cause i'm offering the consumers of my library the ability to add a static TryParse() method to any of their own custom types and be able to modelbind incoming requests from route parameters etc.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.