dotnet / roslyn

The Roslyn .NET compiler provides C# and Visual Basic languages with rich code analysis APIs.
https://docs.microsoft.com/dotnet/csharp/roslyn-sdk/
MIT License
19.04k stars 4.03k forks source link

Cdbl() throws InvalidCastException on convertible object #47374

Open denis-troller opened 4 years ago

denis-troller commented 4 years ago

Version Used: Visual Studio 2019 v16.7.2 (Net Framework 7.4.2 / NetStandard 2.0 / NetCore 3.1)

Steps to Reproduce:

  1. Create a NetStandard 2.0 class library project
  2. Create a class A that implements IConvertible and returns double as its typecode
  3. Create a method that returns Object and returns an instance of class A
  4. Call Cdbl() on the object returned by previous method

Expected Behavior:

The value should be converted acording to our implementation of IConvertible.ToDouble()

Actual Behavior:

An InvalidCastException is raised.

Looking further using DnSpy, it appears the code for Microsoft.VisualBasic.CompilerServices has found itself embedded in our dll. However, it is NOT the code that is normally in the corresponding class in the separate library. It makes no provisions for checking IConvertible and fails becuase not correct conversion is found.

Here is the code that has been embedded:

       public static double ToDouble(object Value)
        {
            double value;
            if ((object)Value != (object)null)
            {
                if (Value is Enum)
                {
                    Value = RuntimeHelpers.GetObjectValue(Conversions.GetEnumValue(RuntimeHelpers.GetObjectValue(Value)));
                }
                if (Value is Boolean)
                {
                    value = (double)(-(Boolean)Value);
                }
                else if (Value is SByte)
                {
                    value = (double)((SByte)Value);
                }
                else if (Value is Byte)
                {
                    value = (double)((Byte)Value);
                }
                else if (Value is Int16)
                {
                    value = (double)((Int16)Value);
                }
                else if (Value is UInt16)
                {
                    value = (double)((UInt16)Value);
                }
                else if (Value is Int32)
                {
                    value = (double)((Int32)Value);
                }
                else if (Value is UInt32)
                {
                    value = (double)((float)((UInt32)Value));
                }
                else if (Value is Int64)
                {
                    value = (double)((Int64)Value);
                }
                else if (Value is UInt64)
                {
                    value = (double)((float)((UInt64)Value));
                }
                else if (Value is Decimal)
                {
                    value = Convert.ToDouble((Decimal)Value);
                }
                else if (Value is Single)
                {
                    value = (double)((Single)Value);
                }
                else if (!(Value is Double))
                {
                    if (!(Value is String))
                    {
                        throw new InvalidCastException();
                    }
                    value = Conversions.ToDouble((String)Value);
                }
                else
                {
                    value = (double)((Double)Value);
                }
            }
            else
            {
                value = 0;
            }
            return value;
        }

We have no idea what triggered the embedding of this in our project. Creating a new minimal project from scratch with VS2019 works and the problem does not appear. I tracked this exact same code to the Microsoft.VisaulBasic nuget package. I also found it in the repository here under src/Compilers/VisualBasic/Portable/Symbols/EmbeddedSymbols/VbCoreSourceText.vb, but I do not understand what triggers its inclusion in our dll when a brand new (simple) project does not have it.

I am logging this here because I imagine this is a compiler-related feature that embedded this strange code. Would really appreciate some help...

denis-troller commented 3 years ago

Any news on this ? I still have the same behavior on VIsual Studio 16.8 and it is a BIG problem. Code that used to work in .net framework does not work when compiled against netstandard...

I managed to reproduce it with a simpler method: 1 - Create a new solution 2 - Create a new Visual Basic Library project (netstandard 2.0) named Lib 3 - Create two classes:

Public Class TestValue
 Implements IConvertible

        Public Function GetTypeCode() As TypeCode Implements IConvertible.GetTypeCode
            Return TypeCode.Double
        End Function

        Public Function ToBoolean(provider As IFormatProvider) As Boolean Implements IConvertible.ToBoolean
            Throw New NotImplementedException()
        End Function

        Public Function ToChar(provider As IFormatProvider) As Char Implements IConvertible.ToChar
            Throw New NotImplementedException()
        End Function

        Public Function ToSByte(provider As IFormatProvider) As SByte Implements IConvertible.ToSByte
            Throw New NotImplementedException()
        End Function

        Public Function ToByte(provider As IFormatProvider) As Byte Implements IConvertible.ToByte
            Throw New NotImplementedException()
        End Function

        Public Function ToInt16(provider As IFormatProvider) As Short Implements IConvertible.ToInt16
            Throw New NotImplementedException()
        End Function

        Public Function ToUInt16(provider As IFormatProvider) As UShort Implements IConvertible.ToUInt16
            Throw New NotImplementedException()
        End Function

        Public Function ToInt32(provider As IFormatProvider) As Integer Implements IConvertible.ToInt32
            Throw New NotImplementedException()
        End Function

        Public Function ToUInt32(provider As IFormatProvider) As UInteger Implements IConvertible.ToUInt32
            Throw New NotImplementedException()
        End Function

        Public Function ToInt64(provider As IFormatProvider) As Long Implements IConvertible.ToInt64
            Throw New NotImplementedException()
        End Function

        Public Function ToUInt64(provider As IFormatProvider) As ULong Implements IConvertible.ToUInt64
            Throw New NotImplementedException()
        End Function

        Public Function ToSingle(provider As IFormatProvider) As Single Implements IConvertible.ToSingle
            Throw New NotImplementedException()
        End Function

        Public Function ToDouble(provider As IFormatProvider) As Double Implements IConvertible.ToDouble
            Return 42.0
        End Function

        Public Function ToDecimal(provider As IFormatProvider) As Decimal Implements IConvertible.ToDecimal
            Throw New NotImplementedException()
        End Function

        Public Function ToDateTime(provider As IFormatProvider) As Date Implements IConvertible.ToDateTime
            Throw New NotImplementedException()
        End Function

        Public Function IConvertible_ToString(provider As IFormatProvider) As String Implements IConvertible.ToString
            Throw New NotImplementedException()
        End Function

        Public Function ToType(conversionType As Type, provider As IFormatProvider) As Object Implements IConvertible.ToType
            Throw New NotImplementedException()
        End Function
End Class

Public Class Tester
  Public Function GetValue() as Object
    return 42.0
  End Function
  Public Function GetValue2() as Object
    return New TestValue()
  End Function

  Public Function Test1() as Double
    return Cdbl(GetValue())
  End Function
 Public Function Test2() as Double
    return Cdbl(GetValue2())
  End Function
End Class

4 - Create a .net Framework 4.8 console project and add a reference to the library In Main:

    Sub Main(args As String())
        Dim t As New Lib.Test()
        Dim v = t.Test1()
        Console.Out.WriteLine(v)
        Dim v2 = t.Test2()
        Console.Out.WriteLine(v2)
    End Sub

5 - Create a .net 5.0 console project and add a reference to the library In Main (same as above):

    Sub Main(args As String())
        Dim t As New Lib.Test()
        Dim v = t.Test1()
        Console.Out.WriteLine(v)
        Dim v2 = t.Test2()
        Console.Out.WriteLine(v2)
    End Sub

If you execute the net50 project, it throws an InvalidCastException on the call to t.Test2() Decompiling Lib.dll shows that the version generated for the .net50 exe contains the suspect code in my original submission(without support for IConvertible). If you execute the 4.8 project, it generates the same error.

If you double-target LIB to net4.8 and netstandard2.0, then the problem stays for the net5.0 project, but disappears for net4.8 The version generated for 4.8 does not have the suspect code and links to the normal Visual Basic implementation. Finally if you double-target LIB to net4.8 and net50, then everything works fine. However, this is not desirable in our setup because the build process would have to be reworked severly if EVERY library in our system where to be double-targeted instead of using netstandard (which works fine apart from that).

Any idea for a resolution or a workaround ?

Rufus31415 commented 3 years ago

I have exactly the same issue. Do you guys have a fix or a workaround?