SqlDataReader的Connection属性为Null

5

我遇到了一个奇怪的问题,我能够从调用存储过程中返回结果,但代码回溯失败。

public IEnumerable<T> ExecuteStoredProcedure<T>(string storedProcedureName, IDataMapper<T> mapper, IDictionary<string, object> parameters)
{
    using (var connection = new SqlConnection(connectionString))
    {
        using (var cmd = new SqlCommand(storedProcedureName, connection))
        {
            cmd.CommandType = CommandType.StoredProcedure;
            foreach (var key in parameters.Keys)
            {
                cmd.Parameters.AddWithValue(key, parameters[key]);
            }
            connection.Open();
            SqlDataReader reader = cmd.ExecuteReader();
            //return MapRecordsToDTOs(reader, mapper);

            //let's test:
            IEnumerable<T> result = MapRecordsToDTOs(reader, mapper);
            var x = (new List<T>(result)).Count;
            System.Diagnostics.Debug.WriteLine(x);
            return result;
        }
    }
}


private static IEnumerable<T> MapRecordsToDTOs<T>(SqlDataReader reader, IDataMapper<T> mapper)
{
    if (reader.HasRows)
    {
        while (reader.Read())
        {
            System.Diagnostics.Debug.WriteLine(reader["Id"]); //what's going on...
            yield return mapper.MapToDto((IDataRecord)reader);
        }
    }
}

调用此代码可显示变量 x 总是表示我期望从调用存储过程中看到的行数。
此外,我的调试输出显示了我期望看到的 ID 值。
然而,在返回这些结果之后,我收到错误消息An exception of type 'System.InvalidOperationException' occurred in System.Data.dll but was not handled in user code ,它来自于已执行的行 if (reader.HasRows)。我调用此请求的浏览器显示 HTTP Error 502.3 - Bad Gateway

Screenshot of Error

Screenshot of HasRows Behaviour

我怀疑的原因是系统分别计算调试时的IDX值与返回实际用户输出的方式不同。因此,在它必须返回这些值的时候,它执行了一个惰性操作来获取IEnumerable值;直到这个时候,using语句已经导致Dispose方法被调用,因此读取器的连接为null(在调试时检查reader变量的属性时可以看到这一点)。
有人以前见过这种行为吗/这是一个bug吗?还是我错过了什么显而易见的东西?

附加代码:

public interface IDataMapper<T>
{
    T MapToDto(IDataRecord record);
}

public class CurrencyMapper: IDataMapper<CurrencyDTO>
{
    const string FieldNameCode = "Code";
    const string FieldNameId = "Id";
    const string FieldNameName = "Name";
    const string FieldNameNum = "Num";
    const string FieldNameE = "E";
    const string FieldNameSymbol = "Symbol";

    public CurrencyMapper() { }

    public CurrencyDTO MapToDto(IDataRecord record)
    {
        var code = record[FieldNameCode] as string;
        var id = record[FieldNameId] as Guid?;
        var name = record[FieldNameName] as string;
        var num = record[FieldNameNum] as string;
        var e = record[FieldNameE] as int?;
        var symbol = record[FieldNameSymbol] as char?;
        return new CurrencyDTO(id, code, num, e, name, symbol);
    }
}

public class CurrencyRepository
{

    const string SPReadAll = "usp_CRUD_Currency_ReadAll";

    readonly SqlDatabase db;
    public CurrencyRepository()
    {
        db = new SqlDatabase(); //stick to SQL only for the moment for simplicity
    }
    public IEnumerable<CurrencyDTO> GetCurrencyCodes()
    {
        var mapper = new CurrencyMapper();
        return db.ExecuteStoredProcedure(SPReadAll, mapper);
    }
}

public class CurrencyDTO
{

    readonly Guid? id;
    readonly string code;
    readonly string num;
    readonly int? e;
    readonly string name;
    readonly char? symbol;

    public CurrencyDTO(Guid? id,string code,string num,int? e,string name, char? symbol)
    {
        this.id = id;
        this.code = code;
        this.num = num;
        this.e = e;
        this.name = name;
        this.symbol = symbol;
    }

    public Guid? Id { get { return id; } }
    public string Code { get { return code; } }
    public string Num { get { return num; } }
    public int? E { get { return e; } }
    public string Name { get { return name; } }
    public char? Symbol { get { return symbol; } }
}

1
你尝试过在调用 MapRecordsToDTOs 之前,在 SqlDataReader reader = cmd.ExecuteReader(); 后检查(或在代码中使用)reader.HasRows 吗? - McNets
1
你可以尝试使用:reader = await cmd.ExecuteReaderAsync(); - McNets
1
但你收到了关于 reader.HasRows 的错误信息,502.3 错误表示超时操作。此时不应该释放 SqlConnection - McNets
1
@mcNets:确认过了;然而,当我在调试模式下运行这些代码时,在调试输出中它们成功执行;但是当我到达将输出返回给调试器的代码行时,调试器会跳回到这一行;也就是说第一次执行是为了调试器的利益,第二次才是真正的事情——懒加载数据,现在我们需要在调试之外做一些事情...也就是说,我只调用了这个方法一次,但是在调试器中看起来好像执行了两次;一次是在调用时,另一次是在需要结果时(尽管那时没有调用)。 - JohnLBevan
你应该意识到你的“调试”确实会再次访问数据库,对吧?它会再次评估整个IEnumerable。我的意思是,这可能仍然是一个错误,但使用延迟加载并进行两次延迟加载并不是很好。如果你想要那些信息来调试,只需加载一次,而不是两次懒加载。 - nvoigt
显示剩余7条评论
1个回答

2

我已经暂时实现了一个解决此问题的方法。

这个方法有效:

private static IEnumerable<T> MapRecordsToDTOs<T>(SqlDataReader reader, IDataMapper<T> mapper)
{
    var list = new List<T>(); //use a list to force eager evaluation
    if (reader.HasRows)
    {
        while (reader.Read())
        {
            list.Add(mapper.MapToDto((IDataRecord)reader));
        }
    }
    return list.ToArray();
}

相对于原始版本:

private static IEnumerable<T> MapRecordsToDTOs<T>(SqlDataReader reader, IDataMapper<T> mapper)
{
    if (reader.HasRows)
    {
        while (reader.Read())
        {
            yield return mapper.MapToDto((IDataRecord)reader);
        }
    }
}

区别在于我移动了受迭代器影响的代码,使其仅遍历列表中的结果;而不依赖于编译器对对象相关要求的合理理解。
据我所知,编译器应该能够为我处理这个问题(在此确认:https://dev59.com/u2Yr5IYBdhLWcg3wpbyQ#13504789),因此我怀疑这是编译器的一个错误。
在此报告:https://connect.microsoft.com/VisualStudio/feedback/details/3113138 额外的演示代码在这里:https://gist.github.com/JohnLBevan/a910d886df577e442e2f5a9c2dd41293/

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接