嵌套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
}
AddDevice
和AddSensor
方法打开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
我得到以下异常:
我可以将获取子对象的代码移出第一个连接范围(这可以工作),但假设我不想这样做。
这个异常是否意味着不可能在单个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;
}
标题>
问题是两个DataReader
对象不能同时对数据库打开(除非启用了MARS
)。这种限制是设计出来的。在我看来,你有几个选项:
- 在连接字符串上启用
MARS
;添加MultipleActiveResultSets=True
如果真的没有必要,不要使用 - 使用Dapper,它可以做所有这些(包括填充
Sensor
),可能更快。
DataReader
。但是你编写代码的方式,这是非常必要的。加载设备后填充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文档