C# - TransactionScope 在 using 块之外?
C# - TransactionScope outside of a using block?
我的查询比标题显示的要复杂一些。也许这是一个愚蠢的问题,但我在网上搜索时找不到任何确定的答案。目前,我正在以我自己的方式实现 Repository/Unit-of-Work 模式,它看起来有点像这样:
// Note: methods are async for conventions, not because
// they're truly async
public interface IUnitOfWork
{
Task Begin();
// The Task<int> is the numbers of rows affected by this commit
Task<int> Commit();
Task Rollback();
}
存储库或多或少可以这样表示:
public interface IWriteableRepository<T>
where T : class
{
EntityEntry<T> Insert(T item);
// Other CRUD methods removed for brevity; they're
// of similar signatures
}
想法是 IUnitOfWork
将在内部持有一些 TransactionScope
实例并处理相应的逻辑。
然后我有两个问题。首先,如果每个 IUnitOfWork
和 IWriteableRepository<T>
实例都注入了 DbContext
的不同实例(我暂时使用 EntityFrameworkCore),调用 DbContext.BeginTransactionAsync()
会产生一个事务以下代码中两者的范围?
await this.UnitOfWork.Begin();
this.Repository.Insert(someEntity);
var rows = await this.UnitOfWork.Commit();
换句话说,存储库是仅对调用 Begin()
时创建的事务进行操作,还是完全独立操作?
我的第二个顾虑与实现 IUnitOfWork
接口有关。到目前为止,我的方法大致是
public class UnitOfWork : IUnitOfWork
{
public UnitOfWork(DbContext context)
{
this.Context = context;
}
private DbContext Context { get; set; }
private TransactionScope Transaction { get; set; }
public async Task Begin()
{
if (this.Scope == null)
{
this.Transaction = await this.Context
.Database
.BeginTransactionAsync();
}
}
public async Task<int> Commit()
{
if (this.Scope != null)
{
var rows = await this.Context.SaveChangesAsync(false);
this.Scope.Commit();
this.Context.AcceptAllChanges();
return rows;
}
}
public Task Rollback()
{
if (this.Scope != null)
{
this.Scope.Rollback();
this.Scope.Dispose();
this.Scope = null;
}
return Task.CompletedTask;
}
}
我主要不确定 Rollback()
方法是否可以改进。我觉得明确处理 object 是不正确的。我还有其他方法可以处理摆脱 TransactionScope
吗?
就我而言,这是我想出的解决方案 - 我绝对不建议遵循它,而且我确信我的团队在提出时必须解决一些问题...
因为我们需要多个数据库引擎(Mongo 和 EF/SQL),所以我们将与数据库的交互包装在存储库和工作单元模式中。我们所有的存储库都是按数据库引擎实现的,例如 IMongoRepository<T> : IWriteableRepository<T>
,IWriteableRepository<T>
无法抽象的方法由 IMongoRepository<T>
实现。这很有效,我不介意使用这种模式。
IUnitOfWork
也是每个数据库引擎实现的,因为 Mongo、SQL 等将以不同方式处理事务。在维护可注入对象的同时共享上下文的问题已经通过使用工厂解决了,例如
public class FooService
{
public FooService(
IUnitOfWorkFactory<EntityFrameworkUnitOfWork> factory,
IRepositoryContext context,
IWriteableRepository<Bar> repository)
{
this.UnitOfWorkFactory = factory;
this.Context = context;
this.Repository = repository;
}
private IUnitOfWorkFactory<EntityFrameworkUnitOfWork> UnitOfWorkFactory { get; set; }
private IRepositoryContext Context { get; set; }
private IWriteableRepository<Bar> Repository { get; set; }
public bool RemoveBar(int baz)
{
// IUnitOfWorkFactory<T>.Begin(IRepositoryContext)
// where T : IUnitOfWork, new()
//
// 1) Creates a new IUnitOfWork instance by calling parameterless constructor
// 2) Call UseContext(IRepositoryContext) on UoW, passing in the context;
// This causes the UoW to use the passed-in context
// 3) Calls .Begin() on the UoW
// 4) Returns the UoW
using (var unitOfWork = this.UnitOfWorkFactory.Begin(this.Context))
{
var bar = this.Repository
.Query()
.First(x => x.Baz == baz);
this.Repository.Remove(bar);
var (success, rows) = unitOfWork.Commit();
return success && rows > 0;
}
}
}
EntityFrameworkUnitOfWork
(或任何 IUnitOfWork
)可以根据需要实现 Begin
、Commit
和 Rollback
。 IUnitOfWork
还实现了 IDisposable
以确保底层事务对象得到清理。使用相同的上下文还可以确保事务肯定会应用到使用该上下文的存储库。
此外,如果检查以确保一次只打开一个事务,则可以传入 IUnitOfWork
而不是工厂;但是,为了消除这种与实现的耦合,我们创建了一个工厂。这不仅确保我们每个 using
块只有一个交易,而且我们能够拥有一个 using
块,而无需在我们的消费代码中触及 IUnitOfWork
的构造函数。
作为免责声明,我全心全意地同意您不应将 ORM 包装在存储库中。它会使您的数据库代码变得混乱并增加不必要的复杂性。我们正在使用这些存储库来努力使我们的数据库交互在做简单的事情时不可知。否则我们会在我们的存储库实现中变得更加具体,从而消除其他存储库模式实现所遭受的许多魔力。通常,一旦方法超越了单个记录操作,数据库引擎和驱动程序就会有不同的想法。
最后一点:很明显,如果您注入的存储库不适合您注入的上下文(例如,IMongoContext
和 IEntityFrameworkRepository<Bar>
),您的代码将不会不在与数据库的事务中 运行。这不是问题的原因是
- 在大多数情况下,使用与存储库中不同的上下文已经是荒谬的
- 您必须在使用代码中管理上下文和存储库,因此会意识到上下文冲突
我的查询比标题显示的要复杂一些。也许这是一个愚蠢的问题,但我在网上搜索时找不到任何确定的答案。目前,我正在以我自己的方式实现 Repository/Unit-of-Work 模式,它看起来有点像这样:
// Note: methods are async for conventions, not because
// they're truly async
public interface IUnitOfWork
{
Task Begin();
// The Task<int> is the numbers of rows affected by this commit
Task<int> Commit();
Task Rollback();
}
存储库或多或少可以这样表示:
public interface IWriteableRepository<T>
where T : class
{
EntityEntry<T> Insert(T item);
// Other CRUD methods removed for brevity; they're
// of similar signatures
}
想法是 IUnitOfWork
将在内部持有一些 TransactionScope
实例并处理相应的逻辑。
然后我有两个问题。首先,如果每个 IUnitOfWork
和 IWriteableRepository<T>
实例都注入了 DbContext
的不同实例(我暂时使用 EntityFrameworkCore),调用 DbContext.BeginTransactionAsync()
会产生一个事务以下代码中两者的范围?
await this.UnitOfWork.Begin();
this.Repository.Insert(someEntity);
var rows = await this.UnitOfWork.Commit();
换句话说,存储库是仅对调用 Begin()
时创建的事务进行操作,还是完全独立操作?
我的第二个顾虑与实现 IUnitOfWork
接口有关。到目前为止,我的方法大致是
public class UnitOfWork : IUnitOfWork
{
public UnitOfWork(DbContext context)
{
this.Context = context;
}
private DbContext Context { get; set; }
private TransactionScope Transaction { get; set; }
public async Task Begin()
{
if (this.Scope == null)
{
this.Transaction = await this.Context
.Database
.BeginTransactionAsync();
}
}
public async Task<int> Commit()
{
if (this.Scope != null)
{
var rows = await this.Context.SaveChangesAsync(false);
this.Scope.Commit();
this.Context.AcceptAllChanges();
return rows;
}
}
public Task Rollback()
{
if (this.Scope != null)
{
this.Scope.Rollback();
this.Scope.Dispose();
this.Scope = null;
}
return Task.CompletedTask;
}
}
我主要不确定 Rollback()
方法是否可以改进。我觉得明确处理 object 是不正确的。我还有其他方法可以处理摆脱 TransactionScope
吗?
就我而言,这是我想出的解决方案 - 我绝对不建议遵循它,而且我确信我的团队在提出时必须解决一些问题...
因为我们需要多个数据库引擎(Mongo 和 EF/SQL),所以我们将与数据库的交互包装在存储库和工作单元模式中。我们所有的存储库都是按数据库引擎实现的,例如 IMongoRepository<T> : IWriteableRepository<T>
,IWriteableRepository<T>
无法抽象的方法由 IMongoRepository<T>
实现。这很有效,我不介意使用这种模式。
IUnitOfWork
也是每个数据库引擎实现的,因为 Mongo、SQL 等将以不同方式处理事务。在维护可注入对象的同时共享上下文的问题已经通过使用工厂解决了,例如
public class FooService
{
public FooService(
IUnitOfWorkFactory<EntityFrameworkUnitOfWork> factory,
IRepositoryContext context,
IWriteableRepository<Bar> repository)
{
this.UnitOfWorkFactory = factory;
this.Context = context;
this.Repository = repository;
}
private IUnitOfWorkFactory<EntityFrameworkUnitOfWork> UnitOfWorkFactory { get; set; }
private IRepositoryContext Context { get; set; }
private IWriteableRepository<Bar> Repository { get; set; }
public bool RemoveBar(int baz)
{
// IUnitOfWorkFactory<T>.Begin(IRepositoryContext)
// where T : IUnitOfWork, new()
//
// 1) Creates a new IUnitOfWork instance by calling parameterless constructor
// 2) Call UseContext(IRepositoryContext) on UoW, passing in the context;
// This causes the UoW to use the passed-in context
// 3) Calls .Begin() on the UoW
// 4) Returns the UoW
using (var unitOfWork = this.UnitOfWorkFactory.Begin(this.Context))
{
var bar = this.Repository
.Query()
.First(x => x.Baz == baz);
this.Repository.Remove(bar);
var (success, rows) = unitOfWork.Commit();
return success && rows > 0;
}
}
}
EntityFrameworkUnitOfWork
(或任何 IUnitOfWork
)可以根据需要实现 Begin
、Commit
和 Rollback
。 IUnitOfWork
还实现了 IDisposable
以确保底层事务对象得到清理。使用相同的上下文还可以确保事务肯定会应用到使用该上下文的存储库。
此外,如果检查以确保一次只打开一个事务,则可以传入 IUnitOfWork
而不是工厂;但是,为了消除这种与实现的耦合,我们创建了一个工厂。这不仅确保我们每个 using
块只有一个交易,而且我们能够拥有一个 using
块,而无需在我们的消费代码中触及 IUnitOfWork
的构造函数。
作为免责声明,我全心全意地同意您不应将 ORM 包装在存储库中。它会使您的数据库代码变得混乱并增加不必要的复杂性。我们正在使用这些存储库来努力使我们的数据库交互在做简单的事情时不可知。否则我们会在我们的存储库实现中变得更加具体,从而消除其他存储库模式实现所遭受的许多魔力。通常,一旦方法超越了单个记录操作,数据库引擎和驱动程序就会有不同的想法。
最后一点:很明显,如果您注入的存储库不适合您注入的上下文(例如,IMongoContext
和 IEntityFrameworkRepository<Bar>
),您的代码将不会不在与数据库的事务中 运行。这不是问题的原因是
- 在大多数情况下,使用与存储库中不同的上下文已经是荒谬的
- 您必须在使用代码中管理上下文和存储库,因此会意识到上下文冲突