Проблема с MySqlConnector 2.3.x

SQLQueryHolder<R> m_holder;
Dictionary<T, PreparedStatement> m_queries = new();
Dictionary<T, SQLResult> _results = new();

public bool Execute<T>(MySqlBase<T> mySqlBase)
{
    if (m_holder == null)
        return false;

    // execute all queries in the holder and pass the results
    foreach (var pair in m_holder.m_queries)
        m_holder.SetResult(pair.Key, mySqlBase.Query(pair.Value));

    return m_result.TrySetResult(m_holder);
}

public void SetResult(T index, SQLResult result)
{
    _results[index] = result;
}

public SQLResult Query(PreparedStatement stmt)
{
    try
    {
        MySqlConnection Connection = _connectionInfo.GetConnection();
        Connection.Open();

        MySqlCommand cmd = Connection.CreateCommand();
        cmd.CommandText = stmt.CommandText;
        foreach (var parameter in stmt.Parameters)
            cmd.Parameters.AddWithValue("@" + parameter.Key, parameter.Value);

        return new SQLResult(cmd.ExecuteReader(System.Data.CommandBehavior.CloseConnection));
    }
    catch (MySqlException ex)
    {
        HandleMySQLException(ex, stmt.CommandText, stmt.Parameters);
        return new SQLResult();
    }
}

public class SQLResult
{
    MySqlDataReader _reader;

    public SQLResult() { }

    public SQLResult(MySqlDataReader reader)
    {
        _reader = reader;
        NextRow();
    }

    ~SQLResult()
    {
        _reader = null;
    }

    public T Read<T>(int column)
    {
        if (_reader.IsDBNull(column))
            return default;

        var columnType = _reader.GetFieldType(column);
        if (columnType == typeof(T))
            return _reader.GetFieldValue<T>(column);

        switch (Type.GetTypeCode(columnType))
        {
            case TypeCode.SByte:
            {
                var value = _reader.GetSByte(column);
                return Unsafe.As<sbyte, T>(ref value);
            }
            case TypeCode.Byte:
            {
                var value = _reader.GetByte(column);
                return Unsafe.As<byte, T>(ref value);
            }
            case TypeCode.Int16:
            {
                var value = _reader.GetInt16(column);
                return Unsafe.As<short, T>(ref value);
            }
            case TypeCode.UInt16:
            {
                var value = _reader.GetUInt16(column);
                return Unsafe.As<ushort, T>(ref value);
            }
            case TypeCode.Int32:
            {
                var value = _reader.GetInt32(column);
                return Unsafe.As<int, T>(ref value);
            }
            case TypeCode.UInt32:
            {
                var value = _reader.GetUInt32(column);
                return Unsafe.As<uint, T>(ref value);
            }
            case TypeCode.Int64:
            {
                var value = _reader.GetInt64(column);
                return Unsafe.As<long, T>(ref value);
            }
            case TypeCode.UInt64:
            {
                var value = _reader.GetUInt64(column);
                return Unsafe.As<ulong, T>(ref value);
            }
            case TypeCode.Single:
            {
                var value = _reader.GetFloat(column);
                return Unsafe.As<float, T>(ref value);
            }
            case TypeCode.Double:
            {
                var value = _reader.GetDouble(column);
                return Unsafe.As<double, T>(ref value);
            }
        }

        return default;
    }

    public T[] ReadValues<T>(int startIndex, int numColumns)
    {
        T[] values = new T[numColumns];
        for (var c = 0; c < numColumns; ++c)
            values[c] = Read<T>(startIndex + c);

        return values;
    }

    public bool IsNull(int column)
    {
        return _reader.IsDBNull(column);
    }

    public int GetFieldCount() { return _reader.FieldCount; }

    public bool IsEmpty()
    {
        if (_reader == null)
            return true;
        
        return _reader.IsClosed || !_reader.HasRows;
    }

    public SQLFields GetFields()
    {
        object[] values = new object[_reader.FieldCount];
        _reader.GetValues(values);
        return new SQLFields(values);
    }

    public bool NextRow()
    {
        if (_reader == null)
            return false;

        if (_reader.Read())
            return true;

        _reader.Close();
        return false;
    }
}

Случилось это после того как вышла новая версия MySql Connector 2.3.x Один участник проекта сообщил, что там были изменения с кэшированием MySqlDataReader, но я вообще ничего не понял на что он намекал.

Я вообще не понимаю, что происходит закулисами, и что я могу сделать чтобы решить эту проблему. Хоть наведите на что-то, пожалуйста.

Дебаггер показывает, что содержимое моей коллекции ответов меняется после вызова вот этого метода (как оказалось это происходит случайным образом, просто в тот момент произошло сдесь):

/// <summary>
/// Attempts to transition the underlying <see cref="Task{TResult}"/> into the <see cref="TaskStatus.RanToCompletion"/> state.
/// </summary>
/// <param name="result">The result value to bind to this <see cref="Task{TResult}"/>.</param>
/// <returns>True if the operation was successful; otherwise, false.</returns>
/// <remarks>
/// This operation will return false if the <see cref="Task{TResult}"/> is already in one of the three final states:
/// <see cref="TaskStatus.RanToCompletion"/>,
/// <see cref="TaskStatus.Faulted"/>, or
/// <see cref="TaskStatus.Canceled"/>.
/// </remarks>
public bool TrySetResult(TResult result)
{
    bool rval = _task.TrySetResult(result);  //after this point i get wrong results
    if (!rval)
    {
        _task.SpinUntilCompleted();
    }

    return rval;
}

При этом я где-то прочитал (на хабрахабе) что это типа нормальная практика вызывать этот метод. Но нигде точно не могу найти что происходит внутри этого метода (даже на MSND)

UPD: Нашел, ничего он такого не делает, кроме того, что ждет пока асинхронная операция не подтвердит, что она уже вернула результат и им можно пользоваться. Вот скажите, если в моей программе практически не используются никакие асинхронные операции напрямую, только вот через коннектор, который делает что-то внутри для общения с сервером базы - каким образом из-за моего кода может появиться паралельный поток который вмешивается и изменяет уже полученные данные из базы?


Ответы (1 шт):

Автор решения: Artem

Забейте на код, там ничего не понятно всеравно. В общем проблема оказалась в том, что ребята сделали оптимизацию аля https://learn.microsoft.com/en-us/dotnet/framework/data/adonet/sql-server-connection-pooling.

После этого означает, что соединение и ридер не уничтожаются после закрытия, а работают дальше на благо другого запроса к той же базе.

Незнаю кто так придумал (долго копаться нужно) чтобы обвертка ридера работала именно так:

public class SQLResult
{
    MySqlDataReader _reader;

    public SQLResult() { }

    public SQLResult(MySqlDataReader reader)
    {
        _reader = reader;
        NextRow();
    }

    public bool NextRow()
    {
        if (_reader == null)
             return false;

        if (_reader.Read())
             return true;

        _reader.Close();
        return false;
     }
}

Получается что NextRow(); выполнялся при создании обвертки ридера без проверки возвращенной переменной. А когда дело доходило до считывания данных - данный ридер уже принадлежал другому запросу и в нем были чужие данные.

Сложность выявления проблемы была в том, что походу пустой ридер автоматически считается библиотекой уже никому не нужным и передается другому запросу вместе с соединением, потому как никто его явно не закрывал.(А пустой он был потому что пустая таблица, тоесть это возникало только с пустыми таблицами, у которых после первого чтения по задумке авторов никто уже ничего считывать не будет, так как стандартный API предполагает чтение данных именно с проверки на первое чтение) Кто бы до такого допер? А при проверке соединения - оно было в порядке, потому что было передано другому запросу.

В общем я добавил проверку и все заработало

public SQLResult(MySqlDataReader reader)
{
    _reader = reader;        
    if (!NextRow())
    {
        Dispose();
    }
}

public bool NextRow()
{
    return _reader.Read()
}

public void Dispose()
{
    if (_reader != null)
    {
        _reader.Close();
        _reader = null;
    }
}
→ Ссылка