[AbpvNext源码分析]-4.工作单元

[AbpvNext源码分析]-4.工作单元

2023年7月18日发(作者:)

[AbpvNext源码分析]-4.⼯作单元⼀、简要说明统⼀⼯作单元是⼀个⽐较重要的基础设施组件,它负责管理整个业务流程当中涉及到的数据库事务,⼀旦某个环节出现异常⾃动进⾏回滚处理。在 ABP vNext 框架当中,⼯作单元被独⽴出来作为⼀个单独的模块()。你可以根据⾃⼰的需要,来决定是否使⽤统⼀⼯作单元。⼆、源码分析整个

项⽬的结构如下,从下图还是可以看到我们的⽼朋友

IUnitOfWorkManager 和

IUnitOfWork ,不过也多了⼀些新东西。看⼀个模块的功能,⾸先从它的 Module ⼊⼿,我们先看⼀下

AbpUnitofWorkModule ⾥⾯的实现。2.1 ⼯作单元的初始模块打开

AbpUnitOfWorkModule ⾥⾯的代码,发现还是有点失望,⾥⾯就⼀个服务注册完成事件。public override void PreConfigureServices(ServiceConfigurationContext context){ stred(erIfNeeded);}这⾥的结构和之前看的 审计⽇志 模块类似,就是注册拦截器的作⽤,没有其他特别的操作。2.1.1 拦截器注册继续跟进代码,其实现是通过

UnitOfWorkHelper 来确定哪些类型应该集成

UnitOfWork 组件。public static void RegisterIfNeeded(IOnServiceRegistredContext context){ //

根据回调传⼊的 context

绑定的实现类型,判断是否应该为该类型注册 UnitOfWorkInterceptor

拦截器。 if (OfWorkType(eInfo())) { (); }}继续分析

UnitOfWorkHelper 内部的代码,第⼀种情况则是实现类型 (implementationType) 或类型的任⼀⽅法标注了

UnitOfWork 特性的话,都会为其注册⼯作单元拦截器。第⼆种情况则是 ABP vNext 为我们提供了⼀个新的

IUnitOfWorkEnabled 标识接⼝。只要继承了该接⼝的实现,都会被视为需要⼯作单元组件,会在系统启动的时候,⾃动为它绑定拦截器。public static bool IsUnitOfWorkType(TypeInfo implementationType){ //

第⼀种⽅式,即判断具体类型与其⽅法是否标注了 UnitOfWork

特性。 if (HasUnitOfWorkAttribute(implementationType) || AnyMethodHasUnitOfWorkAttribute(implementationType)) { return true; } //

第⼆种⽅式,即判断具体类型是否继承⾃ IUnitOfWorkEnabled

接⼝。 if (typeof(IUnitOfWorkEnabled).GetTypeInfo().IsAssignableFrom(implementationType)) { return true; } return false;}2.2 新的接⼝与抽象在 ABP vNext 当中,将⼀些 职责 从原有的⼯作单元进⾏了 分离。抽象出了

IDatabaseApi 、ISupportsRollback、ITransactionApi 这三个接⼝,这三个接⼝分别提供了不同的功能和职责。2.2.1 数据库统⼀访问接⼝这⾥以

IDatabaseApi 为例,它是提供了⼀个 数据库提供者(Database Provider) 的抽象概念,在 ABP vNext ⾥⾯,是将 EFCore 作为数据库概念来进⾏抽象的。(因为后续 MongoDb 与

MemoryDb 与其同级)你可以看作是 EF Core 的

Provider ,在 EF Core ⾥⾯我们可以实现不同的

Provider ,来让 EF Core ⽀持访问不同的数据库。⽽ ABP vNext 这么做的意图就是提供⼀个统⼀的数据库访问 API,如何理解呢?这⾥以

EFCoreDatabaseApi 为例,你查看它的实现会发现它继承并实现了

ISupportsSavingChanges ,也就是说

EFCoreDatabaseApi ⽀持

SaveChanges 操作来持久化数据更新与修改。public class EfCoreDatabaseApi : IDatabaseApi, ISupportsSavingChanges where TDbContext : IEfCoreDbContext{ public TDbContext DbContext { get; } public EfCoreDatabaseApi(TDbContext dbContext) { DbContext = dbContext; }

public Task SaveChangesAsync(CancellationToken cancellationToken = default) { return angesAsync(cancellationToken); } public void SaveChanges() { anges(); }}也就是说

SaveChanges 这个操作,是 EFCore 这个

DatabaseApi 提供了⼀种特殊操作,是该类型数据库的⼀种特殊接⼝。如果针对于某些特殊的数据库,例如

InfluxDb 等有⼀些特殊的 Api 操作时,就可以通过⼀个

DatabaseApi 类型进⾏处理。2.2.2 数据库事务接⼝通过最开始的项⽬结构会发现⼀个

ITransactionApi 接⼝,这个接⼝只定义了⼀个 事务提交操作(Commit),并提供了异步⽅法的定义。public interface ITransactionApi : IDisposable{ void Commit(); Task CommitAsync();}跳转到其典型实现

EfCoreTransactionApi 当中,可以看到该类型还实现了

ISupportsRollback 接⼝。通过这个接⼝的名字,我们⼤概就知道它的作⽤,就是提供了回滚⽅法的定义。如果某个数据库⽀持回滚操作,那么就可以为其实现该接⼝。其实这⾥按照语义,你也可以将它放在

EfCoreDatabaseApi 进⾏实现,因为回滚也是数据库提供的 API 之⼀,只是在 ABPvNext ⾥⾯⼜将其归为事务接⼝进⾏处理了。这⾥就不再详细赘述该类型的具体实现,后续会在单独的 EF Core 章节进⾏说明。2.3 ⼯作单元的原理与实现在 ABP vNext 框架当中的⼯作单元实现,与原来 ABP 框架有⼀些不⼀样。2.3.1 内部⼯作单元 (⼦⼯作单元)⾸先说内部⼯作单元的定义,现在是有⼀个新的

ChildUnitOfWork 类型作为 ⼦⼯作单元。⼦⼯作单元本⾝并不会产⽣实际的业务逻辑操作,基本所有逻辑都是调⽤

UnitOfWork 的⽅法。internal class ChildUnitOfWork : IUnitOfWork{ public Guid Id => _; public IUnitOfWorkOptions Options => _s; public IUnitOfWork Outer => _; public bool IsReserved => _rved; public bool IsDisposed => _osed; public bool IsCompleted => _leted; public string ReservationName => _ationName; public event EventHandler Failed; public event EventHandler Disposed; public IServiceProvider ServiceProvider => _eProvider; private readonly IUnitOfWork _parent; //

只有⼀个带参数的构造函数,传⼊的就是外部的⼯作单元(带事务)。 public ChildUnitOfWork([NotNull] IUnitOfWork parent) { l(parent, nameof(parent)); _parent = parent; _ += (sender, args) => { Safely(sender, args); }; _ed += (sender, args) => { Safely(sender, args); }; } //

下⾯所有 IUnitOfWork

的接⼝⽅法,都是调⽤传⼊的 UnitOfWork

实例。 public void SetOuter(IUnitOfWork outer) { _er(outer); } public void Initialize(UnitOfWorkOptions options) { _lize(options); } public void Reserve(string reservationName) { _e(reservationName); } public void SaveChanges() { _anges(); } public Task SaveChangesAsync(CancellationToken cancellationToken = default) { return _angesAsync(cancellationToken); } public void Complete() { } public Task CompleteAsync(CancellationToken cancellationToken = default) { return tedTask; } public void Rollback() { _ck(); } public Task RollbackAsync(CancellationToken cancellationToken = default) { return _ckAsync(cancellationToken); } public void OnCompleted(Func handler) { _leted(handler); } public IDatabaseApi FindDatabaseApi(string key) { return _tabaseApi(key); } public void AddDatabaseApi(string key, IDatabaseApi api) { _abaseApi(key, api); } public IDatabaseApi GetOrAddDatabaseApi(string key, Func factory) { return _ddDatabaseApi(key, factory); } public ITransactionApi FindTransactionApi(string key) { return _ansactionApi(key); } public void AddTransactionApi(string key, ITransactionApi api) { _nsactionApi(key, api); } public ITransactionApi GetOrAddTransactionApi(string key, Func factory) { return _ddTransactionApi(key, factory); } public void Dispose() { } public override string ToString() { return $"[UnitOfWork {Id}]"; }}虽然基本上所有⽅法的实现,都是调⽤的实际⼯作单元实例。但是有两个⽅法

ChildUnitOfWork 是空实现的,那就是

Complete() 和Dispose() ⽅法。这两个⽅法⼀旦在内部⼯作单元调⽤了,就会导致 事务被提前提交,所以这⾥是两个空实现。下⾯就是上述逻辑的伪代码:using(var transactioinUow = ()){ //

业务逻辑 1

。 using(var childUow1 = ()) { //

业务逻辑 2。 using(var childUow2 = ()) { //

业务逻辑 3。 te(); }

te(); } te();}以上结构⼀旦某个内部⼯作单元抛出了异常,到会导致最外层带事务的⼯作单元⽆法调⽤

Complete() ⽅法,也就能够保证我们的 数据⼀致性。2.3.2 外部⼯作单元⾸先我们查看

UnitOfWork 类型和

IUnitOfWork 的定义和属性,可以获得以下信息。每个⼯作单元是瞬时对象,因为它继承了

ITransientDependency 接⼝。每个⼯作单元都会有⼀个

Guid 作为其唯⼀标识信息。每个⼯作单元拥有⼀个

IUnitOfWorkOptions 来说明它的配置信息。这⾥的配置信息主要指⼀个⼯作单元在执⾏时的 超时时间,是否包含⼀个事务,以及它的 事务隔离级别(如果是事务性的⼯作单元的话)。每个⼯作单元存储了

IDatabaseApi 与

ITransactionApi 的集合,并提供了访问/存储接⼝。提供了两个操作事件

Failed 与

Disposed。这两个事件分别在⼯作单元执⾏失败以及被释放时(调⽤

Dispose() ⽅法)触发,开发⼈员可以挂载这两个事件提供⾃⼰的处理逻辑。⼯作单元还提供了⼀个⼯作单元完成事件组。⽤于开发⼈员在⼯作单元完成时(调⽤Complete() ⽅法)挂载⾃⼰的处理事件,因为是

List> 所以你可以指定多个,它们都会在调⽤

Complete() ⽅法之后执⾏,例如如下代码:using (var uow = _()){ leted(async () => completed = true); leted(async()=>ine("Hello ABP vNext")); te();}以上信息是我们查看了

UnitOfWork 的属性与接⼝能够直接得出的结论,接下来我会根据⼀个⼯作单元的⽣命周期来说明⼀遍⼯作单元的实现。⼀个⼯作单元的的构造是通过⼯作单元管理器实现的(IUnitOfWorkManager),通过它的

Begin() ⽅法我们会获得⼀个⼯作单元,⾄于这个⼯作单元是外部⼯作单元还是内部⼯作单元,取决于开发⼈员传⼊的参数。public IUnitOfWork Begin(UnitOfWorkOptions options, bool requiresNew = false){ l(options, nameof(options)); //

获得当前的⼯作单元。 var currentUow = Current; //

如果当前⼯作单元不为空,并且开发⼈员明确说明不需要构建新的⼯作单元时,创建内部⼯作单元。 if (currentUow != null && !requiresNew) { return new ChildUnitOfWork(currentUow); } //

调⽤ CreateNewUnitOfWork()

⽅法创建新的外部⼯作单元。 var unitOfWork = CreateNewUnitOfWork(); //

使⽤⼯作单元配置初始化外部⼯作单元。 lize(options); return unitOfWork;}这⾥需要注意的就是创建新的外部⼯作单元⽅法,它这⾥就使⽤了

IoC 容器提供的

Scope ⽣命周期,并且在创建之后会将最外部的⼯作单元设置为最新创建的⼯作单元实例。private IUnitOfWork CreateNewUnitOfWork(){ var scope = _Scope(); try { var outerUow = _Work; var unitOfWork = uiredService(); //

设置当前⼯作单元的外部⼯作单元。 er(outerUow); //

设置最外层的⼯作单元。 _tOfWork(unitOfWork); ed += (sender, args) => { _tOfWork(outerUow); e(); }; return unitOfWork; } catch { e(); throw; }}上述描述可能会有些抽象,结合下⾯这两幅图可能会帮助你的理解。我们可以在任何地⽅注⼊

IAmbientUnitOfWork 来获取当前活动的⼯作单元,关于

IAmbientUnitOfWork 与

IUnitOfWorkAccessor 的默认实现,都是使⽤的

AmbientUnitOfWork。在该类型的内部,通过

AsyncLocal 来确保在不同的 异步上下⽂切换 过程中,其值是正确且统⼀的。构造了⼀个外部⼯作单元之后,我们在仓储等地⽅进⾏数据库操作。操作完成之后,我们需要调⽤

Complete() ⽅法来说明我们的操作已经完成了。如果你没有调⽤

Complete() ⽅法,那么⼯作单元在被释放的时候,就会产⽣异常,并触发

Failed 事件。public virtual void Dispose(){ if (IsDisposed) { return; } IsDisposed = true; DisposeTransactions(); //

只有调⽤了 Complete()/CompleteAsync()

⽅法之后,IsCompleted

的值才为 True。 if (!IsCompleted || _exception != null) { OnFailed(); } OnDisposed();}所以,我们在⼿动使⽤⼯作单元管理器构造⼯作单元的时候,⼀定要注意调⽤

Complete() ⽅法。既然

Complete() ⽅法这么重要,它内部究竟做了什么事情呢?下⾯我们就来看⼀下。public virtual void Complete(){ //

是否已经进⾏了回滚操作,如果进⾏了回滚操作,则不提交⼯作单元。 if (_isRolledback) { return; } //

防⽌多次调⽤ Complete

⽅法,原理就是看 _isCompleting

或者 IsCompleted

是不是已经为 True

了。 PreventMultipleComplete(); try { _isCompleting = true; SaveChanges(); CommitTransactions(); IsCompleted = true; //

数据储存了,事务提交了,则说明⼯作单元已经完成了,遍历完成事件集合,依次调⽤这些⽅法。 OnCompleted(); } catch (Exception ex) { //

⼀旦在持久化或者是提交事务时出现了异常,则往上层抛出。 _exception = ex; throw; }}public virtual void SaveChanges(){ //

遍历集合,如果对象实现了 ISupportsSavingChanges

则调⽤相应的⽅法进⾏数据持久化。 foreach (var databaseApi in _) { (databaseApi as ISupportsSavingChanges)?.SaveChanges(); }}protected virtual void CommitTransactions(){ //

遍历事务 API

提供者,调⽤提交事务⽅法。 foreach (var transaction in _) { { (); }}protected virtual void RollbackAll(){ //

回滚操作,还是从集合⾥⾯判断是否实现了 ISupportsRollback

接⼝,来调⽤具体的实现进⾏回滚。 foreach (var databaseApi in _) { try { (databaseApi as ISupportsRollback)?.Rollback(); } catch { } } foreach (var transactionApi in _) { try { (transactionApi as ISupportsRollback)?.Rollback(); } catch { } }}这⾥可以看到,ABP vNext 完全剥离了具体事务或者回滚的实现⽅法,都是移动到具体的模块进⾏实现的,也就是说在调⽤了

Complete()⽅法之后,我们的事务就会被提交了。本⼩节从创建、提交、释放这三个⽣命周期讲解了⼯作单元的原理和实现,关于具体的事务和回滚实现,我会放在下⼀篇⽂章进⾏说明,这⾥就不再赘述了。为什么⼯作单元常常配合

using 语句块 使⽤,就是因为在提交⼯作单元之后,就可以⾃动调⽤

Dispose() ⽅法,对⼯作单元的状态进⾏校验,⽽不需要我们⼿动处理。using(var uowA = _()){ te();}2.3.3 保留⼯作单元在 ABP vNext ⾥⾯,⼯作单元有了⼀个新的动作/属性,叫做 是否保留(Is Reserved)。它的实现也⽐较简单,指定了⼀个ReservationName,然后设置

IsReserved 为

true 就完成了整个动作。那么它的作⽤是什么呢?这块内容我会在⼯作单元管理器⼩节进⾏解释。2.4 ⼯作单元管理器⼯作单元管理器在⼯作单元的原理/实现⾥⾯已经有过了解,⼯作单元管理器主要负责⼯作单元的创建。这⾥我再挑选⼀个⼯作单元模块的单元测试,来说明什么叫做 保留⼯作单元。[Fact]public async Task UnitOfWorkManager_Reservation_Test(){ _BeNull(); using (var uow1 = _e("Reservation1")) { _BeNull(); using (var uow2 = _()) { //

此时 Current

值是 Uow2

的值。 _NotBeNull(); _NotBe(); await teAsync(); } //

这个时候,因为 uow1

是保留⼯作单元,所以不会被获取到,应该为 null。 _BeNull(); //

调⽤了该⽅法,设置 uow1

的 IsReserved

属性为 false。 _eserved("Reservation1"); //

获得到了值,并且诶它的 Id

是 uow1

的值。 _NotBeNull(); _Be(); await teAsync(); } _BeNull();}通过对代码的注释和断点调试的结果,我们知道了通过

Reserved 创建的⼯作单元它的

IsReserved 属性是

true,所以我们调⽤t 访问的时候,会忽略掉保留⼯作单元,所以得到的值就是

null。但是通过调⽤

BeginReserved(string name) ⽅法,我们就可以将指定的⼯作单元置为 正常⼯作单元,这是因为调⽤了该⽅法之后,会重新调⽤⼯作单元的

Initialize() ⽅法,在该⽅法内部,⼜会将

IsReserved 设置为

false 。public virtual void Initialize(UnitOfWorkOptions options){ // ...

其他代码。 //

注意这⾥。 IsReserved = false;}保留⼯作单元的⽤途主要是在某些特殊场合,在某些特定条件下不想暴露给 **t ** 时使⽤。2.5 ⼯作单元拦截器如果我们每个地⽅都通过⼯作单元管理器来⼿动创建⼯作单元,那还是⽐较⿇烦的。ABP vNext 通过拦截器,来为特定的类型(符合规则)⾃动创建⼯作单元。关于拦截器的注册已经在⽂章最开始说明了,这⾥就不再赘述,我们直接来看拦截器的内部实现。其实在拦截器的内部,⼀样是使⽤⼯作单元拦截器我来为我们创建⼯作单元的。只不过通过拦截器的⽅式,就能够⽆感知/⽆侵⼊地为我们构造健壮的数据持久化机制。public override void Intercept(IAbpMethodInvocation invocation){ //

如果类型没有标注 UnitOfWork

特性,或者没有继承 IUnitOfWorkEnabled

接⼝,则不创建⼯作单元。 if (!OfWorkMethod(, out var unitOfWorkAttribute)) { d(); return; } //

通过⼯作单元管理器构造⼯作单元。 using (var uow = _(CreateOptions(invocation, unitOfWorkAttribute))) { d(); te(); }}关于在 Core MVC 的⼯作单元过滤器,在实现上与拦截器⼤同⼩异,后续讲解 Core Mvc 时再着重说明。三、总结ABP vNext 框架通过统⼀⼯作单元为我们提供了健壮的数据库访问与持久化机制,使得开发⼈员在进⾏软件开发时,只需要关注业务逻辑即可。不需要过多关注与数据库等基础设施的交互,这⼀切交由框架完成即可。这⾥多说⼀句,ABP vNext 本⾝就是⾯向 DDD 所设计的⼀套快速开发框架,包括值对象(ValueObject)这些领域驱动开发的特殊概念也被加⼊到框架实现当中。微服务作为 DDD 的⼀个典型实现,DDD 为微服务的划分提供理论⽀持。这⾥为⼤家推荐《领域驱动设计:软件核⼼复杂性应对之道》这本书,该书籍由领域驱动设计的提出者编写。看了之后发现在⼤型系统当中(博主之前做 ERP 的,吃过这个亏)很多时候都是凭感觉来写,没有⼀个具体的理论来⽀持软件开发。最近拜读了上述书籍之后,发现领域驱动设计(DDD)就是⼀套完整的⽅法论(当然 不是银弹)。⼤家在学习并理解了领域驱动设计之后,使⽤ ABP vNext 框架进⾏⼤型系统开发就会更加得⼼应⼿。四、后记关于本系列⽂章的更新,因为最近⾃⼰在做 物联⽹(Rust 语⾔学习、数字电路设计)相关的开发⼯作,所以 5 ⽉到 6 ⽉这段时间都没怎么去研究 ABP vNext。最近在学习领域驱动设计的过程中,发现 ABP vNext 就是为 DDD ⽽⽣的,所以趁热打铁想将后续的 ABP vNext ⽂章⼀并更新,预计在7 ⽉内会把剩余的⽂章补完(核⼼模块)。

发布者:admin,转转请注明出处:http://www.yc00.com/xiaochengxu/1689648425a273639.html

相关推荐

发表回复

评论列表(0条)

  • 暂无评论

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信