ExecuteReaderAsync 丢失上下文的潜在原因

Potential cause for ExecuteReaderAsync to lose context

我在我们的数据访问层之上做了一些抽象,以抽象出我们使用的是 ole、mssql 还是其他。

奇怪的是,以下情况在使用完全相同的参数的单元测试中运行良好,但在从我们的 mvc 应用程序调用时失败。

有问题的代码是这样的:

    public override Task<IDataReader> ExecuteReaderAsync(CancellationToken cancellationToken)
    {
        var sqlCommand = _inner as SqlCommand;
        if (sqlCommand != null)
        {
            // always here because of implementation
            return sqlCommand.ExecuteReaderAsync(cancellationToken).ContinueWith(d => d.Result as IDataReader, cancellationToken);
        }

        return Task.Factory.StartNew(() => _inner.ExecuteReader(), cancellationToken);
    }

一旦我用这个替换了 DAL 中的调用:

    public override IDataReader ExecuteReader()
    {
        return _inner.ExecuteReader();
    }

一切正常。因此我得出结论,问题的根源应该在我的第一个代码片段的实现中。

有谁知道为什么这个实现在单元测试中工作得很好,但在不同的上下文中调用时失败(尽管参数完全相同)?

我对 IDataReader 的转换不正确吗? 一旦调用 ExecuteReaderAsync,调试器就不会 return.

然而,在 reader 之前还有其他异步调用不会使调试器删除其上下文,因此它不应该围绕调用层次结构导致问题。

更新:AbstractDbCommandMssql 的完整代码:

internal class AbstractDbCommandMssql : AbstractDbCommand 
{
    protected override void OnDispose()
    {
        _inner.Dispose();
    }

    protected override IDbCommand GetUnderlyingCommand()
    {
        return _inner;
    }

    private readonly DataAccessMode _mode;

    private readonly IDbCommand _inner;

    public override IDbCommand Native
    {
        get { return _inner; }
    }

    /// <summary>
    /// NICHT manuell aufrufen!
    /// </summary>
    public AbstractDbCommandMssql()
    {
        _inner = new SqlCommand();
    }

    public override IDbDataParameter AddParameter<T>(ColumnType dataType, string name, T value, ParameterDirection direction = ParameterDirection.Input)
    {
        var paramValue = EqualityComparer<T>.Default.Equals(value) ? (object)DBNull.Value : value;

        var parameter = CreateParameter(dataType, name, direction);
        parameter.Value = paramValue;

        if (dataType == ColumnType.IntIdCache)
        {
            var castedValues = value as IEnumerable<int>;
            if(castedValues == null)
                throw new ArgumentException(string.Format("The IntIdCache parameter requires the value to be IEnumerable<int>. Currently is: {0}", typeof(T)));

            var sqlParam = parameter as SqlParameter;
            if(sqlParam == null)
                throw new Exception(string.Format("Invalid type for parameter: \"{0}\".", parameter.GetType()));
            sqlParam.SqlDbType = SqlDbType.Structured;
            sqlParam.TypeName = "dbo.IntIdCache";

            ((SqlCommand) _inner).Parameters.AddWithValue(name, castedValues.ToIntIdCacheDataTable());
        }
        else
        {
            _inner.Parameters.Add(parameter);
        }

        return parameter;
    }

    public override void Prepare()
    {
        _inner.Prepare();
    }

    public override void Cancel()
    {
        _inner.Cancel();
    }

    public override IDbDataParameter CreateParameter()
    {
        return new SqlParameter();
    }

    public override IDbDataParameter CreateParameter(ColumnType dataType, string name, ParameterDirection direction)
    {
        SqlDbType targetType;

        switch (dataType)
        {
            case ColumnType.Int:
                targetType = SqlDbType.Int;
                break;
            case ColumnType.Double:
                targetType = SqlDbType.Float;
                break;
            case ColumnType.DateTime:
                targetType = SqlDbType.DateTime2;
                break;
            case ColumnType.DateTimeSmall:
                targetType = SqlDbType.SmallDateTime;
                break;
            case ColumnType.Bool:
                targetType = SqlDbType.Bit;
                break;
            case ColumnType.Guid:
                targetType = SqlDbType.UniqueIdentifier;
                break;
            case ColumnType.String:
                targetType = SqlDbType.NVarChar;
                break;
            case ColumnType.IntIdCache:
                targetType = SqlDbType.Structured;
                break;
            case ColumnType.Enum:
                targetType = SqlDbType.Int;
                break;
            default:
                throw new ArgumentOutOfRangeException("dataType", dataType, string.Format("{0} has been passed.", dataType.ToString()));
        }

        return new SqlParameter(name, targetType){ Direction = direction};
    }

    public override int ExecuteNonQuery()
    {
        return _inner.ExecuteNonQuery();
    }

    public override IDataReader ExecuteReader()
    {
        return _inner.ExecuteReader();
    }

    public override IDataReader ExecuteReader(CommandBehavior behavior)
    {
        return _inner.ExecuteReader(behavior);
    }

    public override Task<IDataReader> ExecuteReaderAsync(CancellationToken cancellationToken)
    {
        var sqlCommand = _inner as SqlCommand;
        if (sqlCommand != null)
        {
            return sqlCommand.ExecuteReaderAsync(cancellationToken).ContinueWith(d => d.Result as IDataReader, cancellationToken);
        }

        return Task.Factory.StartNew(() => _inner.ExecuteReader(), cancellationToken);
    }

    public override Task<IDataReader> ExecuteReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken)
    {
        var sqlCommand = _inner as SqlCommand;
        if (sqlCommand != null)
            return sqlCommand.ExecuteReaderAsync(behavior, cancellationToken).ContinueWith(d => d.Result as IDataReader, cancellationToken);

        return Task.Factory.StartNew(() => _inner.ExecuteReader(behavior), cancellationToken);
    }

    public override object ExecuteScalar()
    {
        return _inner.ExecuteScalar();
    }

    public override IDbConnection Connection
    {
        get { return _inner.Connection; }
        set { _inner.Connection = value; }
    }

    public override IDbTransaction Transaction
    {
        get { return _inner.Transaction; }
        set { _inner.Transaction = value; }
    }

    public override string CommandText
    {
        get { return _inner.CommandText; }
        set { _inner.CommandText = value; }
    }

    public override int CommandTimeout
    {
        get { return _inner.CommandTimeout; }
        set { _inner.CommandTimeout = value; }
    }

    public override CommandType CommandType
    {
        get { return _inner.CommandType; }
        set { _inner.CommandType = value; }
    }

    public override IDataParameterCollection Parameters
    {
        get { return _inner.Parameters; }
    }

    public override UpdateRowSource UpdatedRowSource
    {
        get { return _inner.UpdatedRowSource; }
        set { _inner.UpdatedRowSource = value; }
    }
}

调用相关方法的代码:

            using (connectionInstance = CreateConnection())
            {
// debugger steps past this line with no issues at all.
                await connectionInstance.OpenAsync(cancellationToken);

                using (command)
                {
                    command.CommandTimeout = commandTimeout;
                    command.Connection = connectionInstance.Native;
// this works for unit tests but loses context in mvc. any idea why?
//                      using (var reader = await command.ExecuteReaderAsync(cancellationToken))
// this works from mvc + unit tests
                    using (var reader = command.ExecuteReader()) 
... processing code

更新 2:

更新 3:

运行良好的单元测试调用:

失败的 Mvc 调用:

    private List<UpcomingAppointment> LoadUpcomingAppointments(int projectStructureId, IEnumerable<int> serviceProgramIds, DateTime startDate, TimeSpan previewTime, bool showInRange = true, bool showWithOverrung = true)
    {
        var provider = IoC.Instance.GetInstance<IUpcomingAppointmentsProvider>();
        var endDate = startDate.Add(previewTime);

        var task = provider.GetAppointmentsAsync(this.GetSessionCache().ParallelDataAccessor, projectStructureId, serviceProgramIds, showInRange, showWithOverrung, startDate, endDate);

        return task.Result;
    }

这似乎是经典案例ASP.NET deadlock. Don't do sync over async。如果您必须这样做,请使用安全的死锁解决方法,例如:

Task.Run(() => SomethingAsync()).Result

请注意,这对效率没有帮助,但如果这段代码不太热,这不是主要问题。