项目

工作单元(Unit of Work)

ABP的工作单元(UOW)实现为应用程序中的数据库连接与事务范围提供了抽象层和控制机制。

当启动新的工作单元时,它会创建一个环境作用域当前作用域内所有数据库操作都会参与其中,并被视为单一事务边界。这些操作会作为一个整体提交(成功时)或回滚(发生异常时)。

ABP的工作单元系统具有以下特性:

  • 符合约定俗成的设计,大多数情况下您无需手动处理工作单元
  • 与数据库提供商无关
  • 与Web框架解耦,意味着您可以在Web应用/服务之外任何类型的应用中创建工作单元作用域

约定规则

以下方法类型会自动被视为工作单元:

  • ASP.NET Core MVC 控制器操作方法
  • ASP.NET Core Razor 页面处理器方法
  • 应用服务方法
  • 仓储方法

对于这些方法会自动启动工作单元,除非已存在外围(环境) 工作单元。例如:

  • 当调用 仓储 方法时,若尚未启动任何工作单元,系统会自动开启新的Transactional工作单元,包含仓储方法中的所有操作,并在方法未抛出异常时提交事务。仓储方法本身完全无需感知工作单元或事务,它仅操作常规数据库对象(例如 EF Core 中的 DbContext ),工作单元由 ABP 框架自动处理
  • 调用 应用服务 方法时,工作单元系统以相同方式运作。若应用服务方法调用多个仓储,这些仓储不会创建新工作单元,而是加入由ABP为应用服务方法启动的当前工作单元
  • ASP.NET Core控制器操作同理。若操作通过控制器Action启动,则工作单元范围即为控制器Action的方法体

所有这些流程都由ABP框架自动处理。

数据库事务行为

虽然前文将工作单元描述为数据库事务,但实际上工作单元不一定需要具备事务性。默认情况下:

  • HTTP GET请求不会启动Transactional工作单元。它们仍会创建工作单元,但不会创建数据库事务
  • 其他所有HTTP请求类型都会启动带数据库事务的工作单元(前提是底层数据库提供商支持数据库级别事务)

这是因为HTTP GET请求不应(也不会)对数据库进行任何更改。您可以通过下文介绍的配置选项修改此行为。

默认配置选项

AbpUnitOfWorkDefaultOptions 用于配置工作单元系统的默认选项。在 模块ConfigureServices 方法中进行配置:

示例:完全禁用数据库事务

Configure<AbpUnitOfWorkDefaultOptions>(options =>
{
    options.TransactionBehavior = UnitOfWorkTransactionBehavior.Disabled;
});

选项属性

  • TransactionBehavior(枚举类型UnitOfWorkTransactionBehavior):全局配置事务行为的入口点。默认值为Auto,行为如前文"数据库事务行为"章节所述。可通过此选项启用(包括对HTTP GET请求)或禁用事务
  • TimeOutint?):设置工作单元超时时间。默认值为null,将采用底层数据库提供商的默认设置
  • IsolationLevelIsolationLevel?):当工作单元具备事务性时,用于设置数据库事务的 隔离级别

工作单元控制

某些情况下,您可能需要修改约定的事务范围、创建内部作用域或精细控制事务行为。以下章节将介绍这些可能性。

IUnitOfWorkEnabled接口

对于不符合前述约定的类(或类层次结构),这是启用工作单元的简便方式。

示例:为任意服务实现IUnitOfWorkEnabled

using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Uow;

namespace AbpDemo
{
    public class MyService : ITransientDependency, IUnitOfWorkEnabled
    {
        public virtual async Task FooAsync()
        {
            //此方法将具有工作单元作用域
        }
    }
}

此后MyService(及其所有派生类)的方法都将成为工作单元。

但需遵循以下规则才能生效:

  • 若未通过接口(如IMyService)注入服务,则服务方法必须声明为virtual(否则 动态代理/拦截 系统无法工作)
  • async方法(返回TaskTask<T>的方法)会被拦截,同步方法无法启动工作单元

注意:若FooAsync在已有工作单元作用域内被调用,则它会自动参与现有工作单元,无需IUnitOfWorkEnabled或任何其他配置

UnitOfWork特性

UnitOfWork 特性提供更丰富的控制能力,包括启用/禁用工作单元及控制事务行为。

该特性可用于类级别方法级别

示例:为类的特定方法启用工作单元

using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Uow;

namespace AbpDemo
{
    public class MyService : ITransientDependency
    {
        [UnitOfWork]
        public virtual async Task FooAsync()
        {
            //此方法具有工作单元作用域
        }
        
        public virtual async Task BarAsync()
        {
            //此方法无工作单元
        }
    }
}

示例:为类的所有方法启用工作单元

using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Uow;

namespace AbpDemo
{
    [UnitOfWork]
    public class MyService : ITransientDependency
    {
        public virtual async Task FooAsync()
        {
            //此方法具有工作单元作用域
        }
        
        public virtual async Task BarAsync()
        {
            //此方法具有工作单元作用域
        }
    }
}

同样需要遵循相同规则

  • 若未通过接口注入服务,方法必须为virtual
  • 仅异步方法会被拦截

UnitOfWork特性属性

  • IsTransactionalbool?):设置工作单元是否应具备事务性。默认值为null,表示根据约定和配置自动确定
  • TimeOutint?):设置此工作单元的超时时间。默认值为null,将回退到默认配置值
  • IsolationLevelIsolationLevel?):当工作单元具备事务性时,设置数据库事务的 隔离级别 。未设置时使用默认配置值
  • IsDisabledbool):禁用当前方法/类的工作单元

若方法在环境工作单元作用域内被调用,则UnitOfWork特性会被忽略,方法会以任何方式参与外围事务

示例:禁用控制器Action的工作单元

using System.Threading.Tasks;
using Volo.Abp.AspNetCore.Mvc;
using Volo.Abp.Uow;

namespace AbpDemo.Web
{
    public class MyController : AbpController
    {
        [UnitOfWork(IsDisabled = true)]
        public virtual async Task FooAsync()
        {
            //...
        }
    }
}

IUnitOfWorkManager服务

IUnitOfWorkManager是控制工作单元系统的核心服务。以下章节说明如何直接使用此服务(尽管大多数情况下您无需直接操作)。

开启新工作单元

使用IUnitOfWorkManager.Begin方法创建新工作单元作用域。

示例:创建新的非事务性工作单元作用域

using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Uow;

namespace AbpDemo
{
    public class MyService : ITransientDependency
    {
        private readonly IUnitOfWorkManager _unitOfWorkManager;

        public MyService(IUnitOfWorkManager unitOfWorkManager)
        {
            _unitOfWorkManager = unitOfWorkManager;
        }
        
        public virtual async Task FooAsync()
        {
            using (var uow = _unitOfWorkManager.Begin(
                requiresNew: true, isTransactional: false
            ))
            {
                //...
                
                await uow.CompleteAsync();
            }
        }
    }
}

Begin方法接收以下可选参数:

  • requiresNewbool):设为true可忽略外围工作单元,以提供选项启动新工作单元。默认值为false。若为false且存在外围工作单元,Begin方法不会真正创建新工作单元,而是静默参与现有工作单元
  • isTransactionalbool):默认值为false
  • isolationLevelIsolationLevel?):当工作单元具备事务性时,设置数据库事务的 隔离级别 。未设置时使用默认配置值
  • TimeOutint?):设置此工作单元的超时时间。默认值为null,将回退到默认配置值

当前工作单元

如前所述,工作单元具有环境性。如需访问当前工作单元,可使用IUnitOfWorkManager.Current属性。

示例:获取当前工作单元

using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Uow;

namespace AbpDemo
{
    public class MyProductService : ITransientDependency
    {
        private readonly IUnitOfWorkManager _unitOfWorkManager;

        public MyProductService(IUnitOfWorkManager unitOfWorkManager)
        {
            _unitOfWorkManager = unitOfWorkManager;
        }
        
        public async Task FooAsync()
        {
            var uow = _unitOfWorkManager.Current;
            //...
        }
    }
}

Current属性返回IUnitOfWork对象。

当前工作单元可能为null(当不存在外围工作单元时)。若您的类符合工作单元约定、已手动启用工作单元或在工作单元作用域内被调用,则该属性不会为null

SaveChangesAsync方法

有时需要调用IUnitOfWork.SaveChangesAsync()方法将当前所有更改保存到数据库。若使用EF Core,其行为完全相同。若当前工作单元具备事务性,即使已保存的更改也可以在出错时回滚(取决于数据库提供商支持)。

示例:插入实体后保存更改以获取自增ID

using System.Threading.Tasks;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories;

namespace AbpDemo
{
    public class CategoryAppService : ApplicationService, ICategoryAppService
    {
        private readonly IRepository<Category, int> _categoryRepository;

        public CategoryAppService(IRepository<Category, int> categoryRepository)
        {
            _categoryRepository = categoryRepository;
        }

        public async Task<int> CreateAsync(string name)
        {
            var category = new Category {Name = name};
            await _categoryRepository.InsertAsync(category);
            
            //保存更改以获取自增ID
            await UnitOfWorkManager.Current.SaveChangesAsync();
            
            return category.Id;
        }
    }
}

此示例使用自增 int 类型作为 Category 实体 的主键。自增主键需要将实体保存到数据库后才能获取新实体的ID。

此示例中的 应用服务 继承自基类ApplicationService,该类已注入IUnitOfWorkManager服务(通过UnitOfWorkManager属性)。故无需手动注入。

由于获取当前工作单元十分常见,框架还提供了CurrentUnitOfWork属性作为UnitOfWorkManager.Current的快捷方式。因此上例可改写为:

await CurrentUnitOfWork.SaveChangesAsync();
SaveChanges()的替代方案

由于在插入、更新或删除实体后保存更改是常见需求,相应的 仓储 方法提供了可选参数autoSave。因此上述CreateAsync方法可改写为:

public async Task<int> CreateAsync(string name)
{
    var category = new Category {Name = name};
    await _categoryRepository.InsertAsync(category, autoSave: true);
    return category.Id;
}

若您的目的仅是在创建/更新/删除实体后保存更改,建议使用autoSave选项而非手动调用CurrentUnitOfWork.SaveChangesAsync()

注意1:当工作单元无错误结束时,所有更改会自动保存。因此除非确有必要,不要调用SaveChangesAsync()或设置autoSavetrue

注意2:若使用Guid作为主键,则永远不需要为获取生成ID而保存更改,因为Guid键在应用程序中设置,创建新实体后立即可用

其他IUnitOfWork属性/方法

  • OnCompleted方法接收回调Action,在工作单元成功完成时触发(此时可确保所有更改已保存)
  • FailedDisposed事件可用于在工作单元失败或释放时接收通知
  • CompleteRollback方法用于完成(提交)或回滚当前工作单元,通常由ABP内部使用,但也可在使用IUnitOfWorkManager.Begin手动启动事务时使用
  • Options属性可用于获取启动工作单元时使用的选项
  • Items字典可用于在同一工作单元内存储和获取任意对象,可作为实现自定义逻辑的切入点

ASP.NET Core集成

工作单元系统与ASP.NET Core完全集成。当使用ASP.NET Core MVC控制器或Razor Pages时能正常工作。框架为工作单元系统定义了Action过滤器和页面过滤器。

使用ASP.NET Core时通常无需额外配置工作单元

工作单元中间件

AbpUnitOfWorkMiddleware是可在ASP.NET Core请求管道中启用工作单元的中间件。当需要扩大工作单元范围以覆盖其他中间件时可能需要使用。

示例:

app.UseUnitOfWork();
app.UseConfiguredEndpoints();

扩展阅读

在本文档中