嵌套SqlConnection.Open在TransactionScope内抛出异常

本文关键字:抛出异常 TransactionScope SqlConnection Open 嵌套 | 更新日期: 2023-09-27 18:05:28

我在我的存储库单元测试中使用TransactionScope回滚测试所做的任何更改。

测试的设置和拆除过程如下所示:

[TestFixture]
public class DeviceRepositoryTests {
    private static readonly string ConnectionString =
        ConfigurationManager.ConnectionStrings["TestDB"].ConnectionString;
    private TransactionScope transaction;
    private DeviceRepository repository;
    [SetUp]
    public void SetUp() {
        transaction = new TransactionScope(TransactionScopeOption.Required);
        repository = new DeviceRepository(ConnectionString);
    }
    [TearDown]
    public void TearDown() {
        transaction.Dispose();
    }
}

有问题的测试由向数据库插入记录的代码和检索这些记录的CUT组成。

    [Test]
    public async void GetAll_DeviceHasSensors_ReturnsDevicesWithSensors() {
        int device1Id = AddDevice();
        AddSensor();
        var devices = await repository.GetAllAsync();
        // Asserts
    }

AddDeviceAddSensor方法打开sql连接并插入一行到数据库:

    private int AddDevice() {
        var sqlString = "<SQL>";
        using (var connection = CreateConnection()) 
        using (var command = new SqlCommand(sqlString, connection)) {
            var insertedId = command.ExecuteScalar();
            Assert.AreNotEqual(0, insertedId);
            return (int) insertedId;
        }
    }
    private void AddSensor() {
        const string sqlString = "<SQL>";
        using (var connection = CreateConnection()) 
        using (var command = new SqlCommand(sqlString, connection)) {
            var rowsAffected = command.ExecuteNonQuery();
            Assert.AreEqual(1, rowsAffected);
        }
    }
    private SqlConnection CreateConnection() {
        var result = new SqlConnection(ConnectionString);
        result.Open();
        return result;
    }

GetAllAsync方法打开连接,执行查询,每获取一行打开新的连接获取子对象。

public class DeviceRepository {
    private readonly string connectionString;
    public DeviceRepository(string connectionString) {
        this.connectionString = connectionString;
    }
    public async Task<List<Device>> GetAllAsync() {
        var result = new List<Device>();
        const string sql = "<SQL>";
        using (var connection = await CreateConnection())
        using (var command = GetCommand(sql, connection, null))
        using (var reader = await command.ExecuteReaderAsync()) {
            while (await reader.ReadAsync()) {
                var device = new Device {
                    Id = reader.GetInt32(reader.GetOrdinal("id"))
                };
                device.Sensors = await GetSensors(device.Id);
                result.Add(device);
            }
        }
        return result;
    }
    private async Task<List<Sensor>> GetSensors(int deviceId) {
        var result = new List<Sensor>();
        const string sql = "<SQL>";
        using (var connection = await CreateConnection()) 
        using (var command = GetCommand(sql, connection, null))
        using (var reader = await command.ExecuteReaderAsync()) {
            while (await reader.ReadAsync()) {
                // Fetch row and add object to result
            }
        }
        return result;
    }
    private async Task<SqlConnection> CreateConnection() {
        var connection = new SqlConnection(connectionString);
        await connection.OpenAsync();
        return connection;
    }
}

问题是,当GetSensors方法调用SqlConnection.Open我得到以下异常:

<>之前System.Transactions.TransactionAbortedException:事务已经中止。----> System.Transactions.TransactionPromotionException:尝试提升事务时失败。----> System.Data.SqlClient.SqlException:已经有一个与此命令相关的打开的数据读取器,必须先关闭它。——> System.ComponentModel。Win32Exception:等待操作超时之前

我可以将获取子对象的代码移出第一个连接范围(这可以工作),但假设我不想这样做。

这个异常是否意味着不可能在单个TransactionScope内打开到DB的同时连接?

<标题>编辑

GetCommand只是调用SqlCommand的构造函数并做一些日志记录。

private static SqlCommand GetCommand(string sql, SqlConnection connection, SqlParameter[] parameters) {
    LogSql(sql);
    var command = new SqlCommand(sql, connection);
    if (parameters != null)
        command.Parameters.AddRange(parameters);
    return command;
}

嵌套SqlConnection.Open在TransactionScope内抛出异常

问题是两个DataReader对象不能同时对数据库打开(除非启用了MARS)。这种限制是设计出来的。在我看来,你有几个选项:

  1. 在连接字符串上启用MARS;添加MultipleActiveResultSets=True
  2. 如果真的没有必要,不要使用DataReader。但是你编写代码的方式,这是非常必要的。加载设备后填充Sensor属性
  3. 使用Dapper,它可以做所有这些(包括填充Sensor),可能更快。

使用Dapper你可以做这样的事情(你不需要GetSensors):

public async Task<List<Device>> GetAllAsync() {
    var result = new List<Device>();
    const string sql = "<SQL>";
    using (var connection = await CreateConnection())
    using (var multi = connection.QueryMultiple(sql, parms)) {
        result = multi.Read<Device>().ToList();
        var sensors = multi.Read<Sensors>().ToList();
        result.ForEach(device => device.Sensors =
            sensors.Where(s => s.DeviceId = device.Id).ToList());
    }
    return result;
}

这里你的sql看起来像这样:

SELECT * FROM Devices
SELECT * FROM Sensors

查看Dapper的Multi Mapping文档