1

Say there is a class

public class MyClass { public decimal Id { get; set; } }

I need to dynamically assign decimals, ints, bytes etc to the Id using setter method for the Id property, something like this:

var setterMethod = typeof(MyClass).GetMethods(...)...First();
setterMethod.Invoke(myClassInstance, (int)1);

But this doesnt work because of types mismatch (int vs. decimal).

In the same time this works well:

decimal decZero = 0;
int intZero = 0;
byte byteZero = 0;
var sample1 = new MyClass{ Id = decZero };  
var sample2 = new MyClass{ Id = intZero };  
var sample3 = new MyClass{ Id = byteZero };  

what means the C# can implicitly cast numeric types.

How can I dynamically assign ints, decimals, bytes etc to the Id prop using type setter?

LINQ2Vodka
  • 2,996
  • 2
  • 27
  • 47
  • 4
    Why are you using fields and methods instead of properties? You have a Java background or something? – itsme86 Mar 30 '18 at 23:20
  • @itsme86 ah, sorry, of cource I was meaning properties, not fields. Fixing now. – LINQ2Vodka Mar 30 '18 at 23:21
  • The reason `decimal` can't be implicitly cast to `int` is because there's the potential for data loss. – itsme86 Mar 30 '18 at 23:21
  • @itsme86 I'm casting int to decimal – LINQ2Vodka Mar 30 '18 at 23:22
  • Check out [this question](https://stackoverflow.com/questions/3723934/using-propertyinfo-to-find-out-the-property-type) for a good solution. It's for getting values, but you can figure out how to do it for setting easily enough. Basically, you just have to check the property type before setting. – itsme86 Mar 30 '18 at 23:22
  • @itsme86 still wondering how C# does this implicit casting and if I can reproduce that – LINQ2Vodka Mar 30 '18 at 23:26
  • @itsme86 SetValue (from the referenced SO solution) doesn't work too – LINQ2Vodka Mar 30 '18 at 23:32
  • *"what means the C# can implicitly cast numeric types"* What you show is an **explicit** cast, not an implicit one. – Ron Beyer Mar 31 '18 at 00:25
  • @RonBeyer correct, this was a not good example. But if those three were variables of types int, decimal and bytes without any explicit casting it would still work – LINQ2Vodka Mar 31 '18 at 00:32

3 Answers3

4

(rewrote the answer to handle conversion among numeric types, along with their nullable and/or enumerable variations)

To handle the conversion between built-in numeric types, the Convert.ChangeType(object,Type) method will be your friend. Just make sure the value implements IConvertible interface (primitive .NET types like ints or doubles generally do).

When casting between enumerable and numeric types, Enum.ToObject(Type,object) should be used instead. The value given should match the enumerable underlying type, so to convert a decimal to integer-based int, an extra conversion will be necessary.

If string parsing is required, then Enum.Parse(Type,string) will be necessary for handling enumerable values. For plain numeric types, Convert.ChangeType should suffice, as long as you don't try to parse integer types from strings in decimal point format.

Finally, Convert.ChangeType doesn't work with nullable types, so the underlying type will need to be extracted first. Nullable.GetUnderlyingType(Type) is for just that.

Putting it all together, we can build an extended ChangeType method:

public static object ExtendedChangeType(object value, Type targetType)
{
    if (value == null)
        return null;

    targetType = Nullable.GetUnderlyingType(targetType) ?? targetType;
    if (targetType.IsEnum)
    {
        if (value is string)
        {
            return Enum.Parse(targetType, value as string);
        }
        else
        {
            value = Convert.ChangeType(value, Enum.GetUnderlyingType(targetType));
            return Enum.ToObject(targetType, value);
        }
    }
    else
    {
        return Convert.ChangeType(value, targetType);
    }
}

Then we can use it like this:

PropertyInfo property = typeof(MyClass).GetProperty(nameof(MyClass.Id));
var value = ExtendedChangeType((int)1, valueType);
property.SetValue(myClassInstance, value);
Alice
  • 585
  • 5
  • 16
  • 1
    @LINQ2Vodka Added handling of nullables. I just hope your requirements don't involve non-convertible values... ^^' – Alice Mar 30 '18 at 23:45
  • Will this work for casting enums to ints and bytes and back? I see that I can implement particular conversions but C# already has that functionality somewhere and I'd be happy to re-use it... What's hapenning internally when we write "int a = (decimal)1;" so it works wuthout errors? – LINQ2Vodka Mar 30 '18 at 23:51
  • 2
    Ah, yet another tricky requirement. It's possible to handle, but probably better to wrap in another method. I'll edit my answer shortly to include that. But if you have any more requirements in mind, please include them all in the main question. ^^' – Alice Mar 31 '18 at 00:02
  • Alice, I appreciate your help very much. Thank you for spending time on this! – LINQ2Vodka Mar 31 '18 at 00:03
  • You are welcome. ^^ Added an update with enumerable values handling. As a bonus, I added parsing from strings as well. Now the basic numeric types, as well as their string representations shouldn't be a problem (note: decimal-point representation cast to integer types will break, but hopefully this won't occur). – Alice Mar 31 '18 at 00:28
1

How can I dynamically assign ints, decimals, bytes etc to the Id prop using type setter?

You can try this:

var converted = Convert.ChangeType((int)10, property.PropertyType);
property.SetValue(sample2, converted);

Here it is as a fiddle that dynamically assigns ints, decimals, and bytes to a decimal property.

using System;

public class Program
{
    public static void Main()
    {
        var sample1 = new MyClass{Id = (decimal)0};
        var sample2 = new MyClass{Id = (int)0};
        var sample3 = new MyClass{Id = (byte)0};

        var property = typeof (MyClass).GetProperty(nameof(MyClass.Id));

        property.SetValue(sample1, Convert.ChangeType((decimal)10, property.PropertyType));
        property.SetValue(sample2, Convert.ChangeType((int)10, property.PropertyType));
        property.SetValue(sample3, Convert.ChangeType((byte)10, property.PropertyType));
    }
}

public class MyClass
{
    public decimal Id { get; set; }
}
Shaun Luttin
  • 133,272
  • 81
  • 405
  • 467
0

You simply can't because of covariance and contravariance however in your case is invariance but it is related.

While operator = makes implicit conversion to the desired type, MethodBase.Invoke hopes for different thing.

Debug your code, put a breakpoint at the invocation of the method. See that the method itself has following definition - {Void set_Id(System.Decimal)} which is obvious right? It takes one parameter of type decimal, while compiler can do magic for you allowing to call it implicitly during runtime things are different.

IL instructions showing what happens when you do it with assignment operator

    using System;
    using System.Collections.Generic;

    namespace ConsoleApp1
    {
        class Program
        {
            static void Main(string[] args)
            {
                var testIntance = new Test();
                testIntance.Id = (int)5;
            }
        }

        public class Test { public decimal Id { get; set; } }
    }
    //
    // IL of Main method
    //
    .method private hidebysig static 
        void Main (
            string[] args
        ) cil managed 
    {
        // Method begins at RVA 0x2050
        // Code size 17 (0x11)
        .maxstack 8

        IL_0000: newobj instance void ConsoleApp1.Test::.ctor()
        IL_0005: ldc.i4.5
        IL_0006: newobj instance void [mscorlib]System.Decimal::.ctor(int32) - not so implicit for the compiler 
        IL_000b: callvirt instance void ConsoleApp1.Test::set_Id(valuetype [mscorlib]System.Decimal)
        IL_0010: ret
    } // e

Lets create more interesting case

    namespace ConsoleApp1
    {
        class Program
        {
            static void Main(string[] args)
            {
                var a = 5;
                Test(a);
            }

            static void Test(decimal number) { }
        }
    }

    //
    // IL of the main method
    //
    .method private hidebysig static 
        void Main (
            string[] args
        ) cil managed 
    {
        // Method begins at RVA 0x2050
        // Code size 12 (0xc)
        .maxstack 8

        IL_0000: ldc.i4.5
        IL_0001: call valuetype [mscorlib]System.Decimal [mscorlib]System.Decimal::op_Implicit(int32)
        IL_0006: call void ConsoleApp1.Program::Test(valuetype [mscorlib]System.Decimal)
        IL_000b: ret
    } // end of method Program::Main

Invoke expects type to be compatible in a sense that one can "be in place of the another" (check the covariance and contravariance stuff).

With that being said lets see what what will happen if I put the following code.

public class Test { public IEnumerable<int> Id { get; set; } }

...

var setter = typeof(Test).GetMethods()[1];
setter.Invoke(new Test(), new object[] { new List<int> { 1 } });

Now it passes, because obviously List is IEnumerable and there are no violations during runtime.

So now that you know how it works maybe you can figure it out which case is best for you.

kuskmen
  • 3,648
  • 4
  • 27
  • 54
  • Do you mean the casting/conversion from int to decimal happens in the project building time? I.e. the enviroment does that work for me? – LINQ2Vodka Mar 30 '18 at 23:57
  • Yes, IL at least that's what IL instructions say that, let me edit my answer. – kuskmen Mar 31 '18 at 00:00
  • This could be a good answer, but covariance and contravariance isn't at all related to the OP's problem. You even say it's not related, why add that then? – Camilo Terevinto Mar 31 '18 at 01:25
  • @CamiloTerevinto Because it is interesting topic and the example of List and IRnumerable shows exactly that :) – kuskmen Mar 31 '18 at 08:02