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;
}
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.