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
请注意,这对效率没有帮助,但如果这段代码不太热,这不是主要问题。
我在我们的数据访问层之上做了一些抽象,以抽象出我们使用的是 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
请注意,这对效率没有帮助,但如果这段代码不太热,这不是主要问题。