Конструктор десятичного массива байтов в сериализации Binaryformatter

Я столкнулся с очень неприятной проблемой, которую не могу определить.
Я запускаю очень большое бизнес-приложение ASP.Net, содержащее многие тысячи объектов; Он использует сериализацию/десериализацию в памяти с помощью MemoryStream, чтобы клонировать состояние приложения (страховые контракты) и передавать его другим модулям. Он работал нормально в течение многих лет. Теперь иногда, не систематически, при сериализации выдает исключение

Конструктор массива десятичных байтов требует массива длины четыре, содержащего действительные десятичные байты.

Запуск одного и того же приложения с теми же данными, 3 раза из 5 работает. Я включил все исключения CLR, Отладка - Исключения - Исключение CLR - Включено, поэтому я предполагаю, что если произойдет неправильная инициализация/назначение десятичного поля, программа должна остановиться. Этого не происходит.
Я пытался разделить сериализацию на более простые объекты, но это очень сложно, попытаться определить поле, вызывающее проблему. С рабочей версии в производстве и этой я перешел с .Net 3.5 на .NET 4.0, и были внесены последовательные изменения в часть пользовательского интерфейса, а не в бизнес-часть. Терпеливо пройдусь по всем изменениям.

Это похоже на старомодные проблемы C, когда char *p пишет туда, куда не должен, и только в процессе сериализации, когда он проверяет все данные, проблема появляется.

Возможно ли что-то подобное в управляемой среде .Net? Приложение огромно, но я не вижу аномального роста памяти. Что может быть способом отладки и отслеживания проблемы?

Ниже приведена часть трассировки стека

[ArgumentException: Decimal byte array constructor requires an array of length four containing valid decimal bytes.]
   System.Decimal.OnSerializing(StreamingContext ctx) +260

[SerializationException: Value was either too large or too small for a Decimal.]
   System.Decimal.OnSerializing(StreamingContext ctx) +6108865
   System.Runtime.Serialization.SerializationEvents.InvokeOnSerializing(Object obj, StreamingContext context) +341
   System.Runtime.Serialization.Formatters.Binary.WriteObjectInfo.InitSerialize(Object obj, ISurrogateSelector surrogateSelector, StreamingContext context, SerObjectInfoInit serObjectInfoInit, IFormatterConverter converter, ObjectWriter objectWriter, SerializationBinder binder) +448
   System.Runtime.Serialization.Formatters.Binary.ObjectWriter.Write(WriteObjectInfo objectInfo, NameInfo memberNameInfo, NameInfo typeNameInfo) +969
   System.Runtime.Serialization.Formatters.Binary.ObjectWriter.Serialize(Object graph, Header[] inHeaders, __BinaryWriter serWriter, Boolean fCheck) +1016
   System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.Serialize(Stream serializationStream, Object graph, Header[] headers, Boolean fCheck) +319
   System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.Serialize(Stream serializationStream, Object graph) +17
   Allianz.Framework.Helpers.BinaryUtilities.SerializeCompressObject(Object obj) in D:\SVN\SUV\branches\SUVKendo\DotNet\Framework\Allianz.Framework.Helpers\BinaryUtilities.cs:98
   Allianz.Framework.Session.State.BusinessLayer.BLState.SaveNewState(State state) in 

Извините за длинный рассказ и неопределенный вопрос, я буду очень признателен за любую помощь.


person Marco Furlan    schedule 09.08.2013    source источник
comment
Я столкнулся с той же ошибкой, она оказалась вызвана объединением (используя StructLayout(LayoutKind.Explicit), где я случайно интерпретировал логическое значение как десятичное. Остальная часть приложения использовала десятичное число как обычный 0, но только во время десериализация в отдельном приложении вызовет эту ошибку, я понял проблему, выполнив Decimal.GetBytes перед сериализацией и заметив, что от 3-го до последнего байта было 1 вместо 0, как и ожидалось.   -  person BrandonAGr    schedule 23.03.2014


Ответы (2)


Это очень интересно; это фактически не чтение или запись данных в это время - это вызов обратного вызова перед сериализацией, также известного как [OnSerializing], который здесь отображается на decimal.OnSerializing. Что это делает, так это пытается проверить работоспособность битов, но похоже, что это просто ошибка в BCL. Вот реализация в 4.5 (кашель "рефлекторный" кашель):

[OnSerializing]
private void OnSerializing(StreamingContext ctx)
{
    try
    {
        this.SetBits(GetBits(this));
    }
    catch (ArgumentException exception)
    {
        throw new SerializationException(Environment.GetResourceString("Overflow_Decimal"), exception);
    }
}

GetBits получает массив lo/mid/hi/flags, поэтому мы можем быть уверены, что массив, переданный SetBits, не равен нулю и имеет правильную длину. Итак, чтобы это не получилось, часть, которая должна дать сбой, находится в SetBits, здесь:

private void SetBits(int[] bits)
{
    ....

    int num = bits[3];
    if (((num & 0x7f00ffff) == 0) && ((num & 0xff0000) <= 0x1c0000))
    {
        this.lo = bits[0];
        this.mid = bits[1];
        this.hi = bits[2];
        this.flags = num;
        return;
    }
    throw new ArgumentException(Environment.GetResourceString("Arg_DecBitCtor"));
}

По сути, если тест if проходит, мы входим, присваиваем значения и успешно выходим; если тест if не пройден, он выдает исключение. bits[3] - это фрагмент flags, который содержит знак и масштаб, IIRC. Итак, вопрос в следующем: как вы получили недействительный decimal с неработающим фрагментом flags?

цитата из MSDN:

Четвертый элемент возвращаемого массива содержит масштабный коэффициент и знак. Он состоит из следующих частей: Биты с 0 по 15, младшее слово, не используются и должны быть равны нулю. Биты с 16 по 23 должны содержать показатель степени от 0 до 28, который указывает степень 10 для деления целого числа. Биты с 24 по 30 не используются и должны быть равны нулю. Бит 31 содержит знак: 0 означает положительный, а 1 означает отрицательный.

Итак, чтобы провалить этот тест:

  • показатель степени недействителен (вне 0-28)
  • нижнее слово не равно нулю
  • старший байт (исключая MSB) не равен нулю

К сожалению, у меня нет волшебного способа узнать, какой decimal недействителен...

Единственные способы, которыми я могу думать о поиске здесь, это:

  • разбросайте GetBits / new decimal(bits) по всему коду - возможно, как метод void SanityCheck(this decimal) (возможно, с [Conditional("DEBUG")] или чем-то еще)
  • добавьте методы [OnSerializing] в свою основную модель предметной области, где-нибудь (может быть, в консоли), чтобы вы могли видеть, над каким объектом он работал, когда он взорвался
person Marc Gravell    schedule 09.08.2013
comment
Сообщение об исключении тоже кажется вводящим в заблуждение - оно говорит о массиве из 4 десятичных байтов, а для decimal такого конструктора нет... Да и что такое десятичный байт? - person Matthew Watson; 09.08.2013
comment
@MatthewWatson действительно - здесь мы даже не используем конструктор; обычно SetBits вызывается public Decimal(int[] bits), что отчасти объясняет это сообщение, но да: сбивает с толку. - person Marc Gravell; 09.08.2013

@ Марк, ты почти получил правильный ответ. Чего не хватает, так это причины, по которой это происходит.

Я получаю ту же ошибку и могу заверить, что это определенно ошибка в .NET framework.

Как вы получаете исключение «Конструктор массива десятичных байтов требует массива длины четыре, содержащего допустимые десятичные байты.»?

Что ж, если вы используете чистый код C#, вы никогда не увидите это исключение. Но если вы получите десятичную переменную из кода C++, вы увидите ее, если маршалинг выполняется от VARIANT в C до System.Decimal в C#. Причина — ошибка именно в функции Decimal.SetBits(), как вы уже поняли.

Я перевел структуру DECIMAL (wtypes.h) с C на C#, которая выглядит так:

[StructLayout(LayoutKind.Sequential, Pack=1)]
public struct DECIMAL
{
    public UInt16 wReserved;
    public Byte   scale;
    public Byte   sign;
    public UInt32 Hi32;
    public UInt32 Lo32;
    public UInt32 Mid32;
}

Но то, что Microsoft определяет в .NET framework для System.Decimal, отличается:

[Serializable, StructLayout(LayoutKind.Sequential), ComVisible(true)]
public struct Decimal : IFormattable, ....
{
    private int flags;
    private int hi;
    private int lo;
    private int mid;
}

Когда эта структура передается из C в .NET, она упаковывается в структуру VARIANT и транслируется с помощью маршалинга .NET в управляемый код.

А теперь самое интересное: VARIANT имеет следующее определение (oaidl.h, упрощенное):

struct tagVARIANT        // 16 byte
{
    union                // 16 byte
    {
        VARTYPE vt;      // 2 byte
        WORD wReserved1; // 2 byte
        WORD wReserved2; // 2 byte
        WORD wReserved3; // 2 byte
        union            // 8 byte
        {
            LONGLONG llVal;
            LONG lVal;
            BYTE bVal;
            SHORT iVal;
            FLOAT fltVal;
            ....
            etc..
        }
        DECIMAL decVal;  // 16 byte      
    }
};

Это очень опасное определение, потому что DECIMAL находится вне объединения, где хранятся все остальные значения. DECIMAL и VARIANT имеют одинаковый размер! Это означает, что важный элемент VARIANT.vt, определяющий тип VARIANT, совпадает с DECIMAL.wReserved. Это может привести к серьезным ошибкам:

 void XYZ(DECIMAL& k_Dec)
 {
    VARIANT k_Var;
    k_Var.vt     = VT_DECIMAL; // WRONG ORDER !
    k_Var.decVal = k_Dec;
    .....
 }

Этот код НЕ будет работать, потому что значение vt переопределяется при назначении decVal.

Правильно:

 void XYZ(DECIMAL& k_Dec)
 {
    VARIANT k_Var;
    k_Var.decVal = k_Dec;        
    k_Var.vt     = VT_DECIMAL;
    .....
 }

И что теперь происходит: значение VT_DECIMAL (14) записывается в DECIMAL.wReserved Итак, после маршалинга в .NET у вас будет System.Decimal.flags = 14 (предполагаемый масштаб и знак равны 0) И теперь возникает ошибка в классе System.Decimal:

private void SetBits(int[] bits)
{
    ....

    int num = bits[3];
    if (((num & 0x7F00FFFF) == 0) && ((num & 0xFF0000) <= 0x1C0000))
    {
        this.lo = bits[0];
        this.mid = bits[1];
        this.hi = bits[2];
        this.flags = num;
        return;
    }
    throw new ArgumentException(Environment.GetResourceString("Arg_DecBitCtor"));
}

Правильным было бы заменить 0x7F00FFFF на 0x7F000000, потому что DECIMAL.wReserved совершенно не имеет значения. Это поле никогда не используется. Просто заполните VARIANT.vt, иначе он должен быть равен нулю, чтобы избежать этой ошибки.

К счастью, я нашел простое ВРЕМЕННОЕ РЕШЕНИЕ. Если у вас есть десятичная переменная d_Param, полученная в результате маршалинга из VARIANT, используйте следующий код для ее исправления:

int[] s32_Bits = Decimal.GetBits(d_Param);
s32_Bits[3] = (int)((uint)s32_Bits[3] & 0xFFFF0000); // set DECIMAL.wReserved = 0
d_Param1 = new Decimal(s32_Bits);

Это работает отлично.

person Elmue    schedule 01.02.2020