Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Check whether PropertyInfo.SetValue will throw an ArgumentException

I have inherited some code that tries to set a property:

object target = ...    // some instance on which we want to set a property
object value = ...     // some value - in this case, a string
var propertyInfo = ... // some property of target - in this case, not a string

try
{
    propertyInfo.SetValue(obj, value, null);
}
catch (ArgumentException)
{
    // We go off and look for our own way of converting between
    // the type of value and the type of the property.
}

In current usage the exception is caught and thrown a lot, so I would like to make a check first:

if (propertyInfo.PropertyType.IsAssignableFrom(value.GetType())
{
    // Try/catch as above
}
else
{
    // Do the manual conversion as if the exception had been thrown.
}

This runs much faster. However, my one concern is that IsAssignableFrom could return false for some pair of types where SetValue would in fact succeed. (This would cause us to look for the manual conversion when we don't need to, and possibly fail to set a value altogether.)

The spec for SetValue says

value cannot be converted to the type of PropertyType

which is not exactly the same as assignability.

(If IsAssignableFrom returns true in cases where SetValue fails, that's fine - the manual conversion will still happen.)

Can someone confirm for me whether this is possible or not?

like image 436
Rawling Avatar asked Dec 23 '15 10:12

Rawling


1 Answers

As you suspected, a value of type short is assignable to a property of type int through reflection, although typeof(int).IsAssignableFrom(typeof(short)) returns false.

Looking at the stacktrace of the ArgumentException:

  at System.RuntimeType.TryChangeType(Object value, Binder binder, CultureInfo culture, Boolean needsSpecialCast)
  at System.RuntimeType.CheckValue(Object value, Binder binder, CultureInfo culture, BindingFlags invokeAttr)
  at System.Reflection.MethodBase.CheckArguments(Object[] parameters, Binder binder, BindingFlags invokeAttr, CultureInfo culture, Signature sig)
  at System.Reflection.RuntimeMethodInfo.InvokeArgumentsCheck(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
  at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
  at System.Reflection.RuntimePropertyInfo.SetValue(Object obj, Object value, BindingFlags invokeAttr, Binder binder, Object[] index, CultureInfo culture)
  at System.Reflection.RuntimePropertyInfo.SetValue(Object obj, Object value, Object[] index)
  at System.Reflection.PropertyInfo.SetValue(Object obj, Object value)
  at ConsoleApplication3.Program.Main(String[] args) in ...

The exception is thrown in RuntimeType.TryChangeType. Looking at the source in mscorlib:

// System.RuntimeType
[SecurityCritical]
private object TryChangeType(object value, Binder binder, CultureInfo culture, bool needsSpecialCast)
{
    ...
    if (this.IsInstanceOfType(value))
    {
      return value;
    }
    ...
      if (RuntimeType.CanValueSpecialCast(valueType, this))
      {
    ...
  }
  throw new ArgumentException(string.Format(CultureInfo.CurrentUICulture, Environment.GetResourceString("Arg_ObjObjEx"), value.GetType(), this));
}

Unfortunately there are many internal functions called to determine if an ArgumentException should be thrown. I suspect that the conversion of primitives are handled somewhere around this function:

// System.RuntimeType
[SecurityCritical]
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern bool CanValueSpecialCast(RuntimeType valueType, RuntimeType targetType);

There is some interesting code in System.RuntimeType.CheckValue(object, Binder, CultureInfo, BindingFlags) as well:

bool flag = base.IsPointer || this.IsEnum || base.IsPrimitive;
if (flag)
{
  ...
  if (RuntimeType.CanValueSpecialCast(valueType, this))
  {
    ...
  }
}

So it looks like pointer, enum and primitive types are handled in a different way. I discourage you to try to copy that logic. It might be easier to just use IsAssignableFrom, and handle integral types separately. Handling pointer, enum and primitive type arguments looks very complicated, I would just fallback to try-catch there. However, you can cache if an argument error happened so subsequent calls on the same method with same parameter types might be somewhat faster.

like image 132
Tamas Hegedus Avatar answered Oct 09 '22 03:10

Tamas Hegedus