UnitOfWork以及其在ABP中的应用

摘要:
UnitOfWork模式在企业应用程序架构中被广泛使用。它可以收集DomainModel中对象状态的更改,并在适当的情况下在同一数据库连接和事务处理的上下文中一次将对象的更改提交给数据。从字面上讲,我们可以将UnitOfWork称为工作单位。从概念上讲,它是一个辅助代码块的事务。因此,有必要封装UnitOfWork。现在我们来看一下UnitOfWork的实现方法,即抽象类UnitOfWorkBase。为了便于阅读,我删除了一些与本文无关的代码。publicabstractclassUnitOfWorkBase:IUnitOfWork{publicUnitOfWorkOptionsOptions{get;privateset;}//////启动UnitOfWork的一些配置和事务初始化///////publicvoid Begin{if{throw newArgumentNullException;}PreventMultipleBegin();选项=选项;//TODO:不要停止这样的行为!

Unit Of Work(UoW)模式在企业应用架构中被广泛使用,它能够将Domain Model中对象状态的变化收集起来,并在适当的时候在同一数据库连接和事务处理上下文中一次性将对象的变更提交到数据中。

从字面上我们可以我们可以把UnitOfWork叫做工作单元,从概念上它是协助代码块的事务。为什么我们需要用UnitOfWork?有人说EF不是的DbContext的SaveChanges不就有提交变更数据的功能吗?为什么还要多一层封装?是的,如果我们项目只是用EF的话,项目又会经常变更,不用考虑那么多我们可以直接用EF,但是如果我们在支持EF的同时还需要支持Redis、NHibernate或MongoDB呢?我们怎么做统一的事务管理?所以封装一个UnitOfWork是有必要的。类似的Repository也是一样,仓储Repository的功能其实就是EF的DbSet<T>,但是我们的数据库访问技术可能会改变,所以我们需要提供一层封装,来隔离领域层或应用层对数据访问层的依赖。那么ABP是怎么定义UnitOfWork的呢?

    public interface IUnitOfWork : IActiveUnitOfWork, IUnitOfWorkCompleteHandle
    {
        /// <summary>
        /// Begins the unit of work with given options.
        /// 开始 unit of work的一些配置UnitOfWorkOptions,主要是事务的级别,超时时间,配置文件等
        /// </summary>
        /// <param name="options">Unit of work options</param>
        void Begin(UnitOfWorkOptions options);
    }

    public interface IUnitOfWorkCompleteHandle : IDisposable
    {
        /// <summary>
        /// Completes this unit of work.
        /// It saves all changes and commit transaction if exists.
        /// 统一事务提交
        /// </summary>
        void Complete();

        /// <summary>
        /// Completes this unit of work.
        /// It saves all changes and commit transaction if exists.
        /// 异步的Complete方法
        /// </summary>
        Task CompleteAsync();
    }

从接口的定义来看,UnitOfWork主要涉及到事务的提交,回滚操作这边没有再定义一个方法,因为作者用的是TransactionScope,失败了会自动回滚。当然有定义了Dispose方法。现在我们来看下UnitOfWork的实现方法,抽象类UnitOfWorkBase,我删除了一些跟本文无关的代码,方便阅读。

    public abstract class UnitOfWorkBase : IUnitOfWork
    {
        public UnitOfWorkOptions Options { get; private set; }

        /// <summary>
        /// 开始UnitOfWork的一些配置,和事务的初始化
        /// </summary>
        /// <param name="options"></param>
        public void Begin(UnitOfWorkOptions options)
        {
            if (options == null)
            {
                throw new ArgumentNullException("options");
            }

            PreventMultipleBegin();
            Options = options; //TODO: Do not set options like that!

            SetFilters(options.FilterOverrides);

            BeginUow();
        }

        /// <summary>
        /// 事务的提交,异常的捕获
        /// </summary>
        public void Complete()
        {
            PreventMultipleComplete();
            try
            {
                CompleteUow();
                _succeed = true;
                OnCompleted();
            }
            catch (Exception ex)
            {
                _exception = ex;
                throw;
            }
        }

        /// <summary>
        /// 结束事务,失败就回滚
        /// </summary>
        public void Dispose()
        {
            if (IsDisposed)
            {
                return;
            }

            IsDisposed = true;

            if (!_succeed)
            {
                OnFailed(_exception);
            }

            DisposeUow();
            OnDisposed();
        }
    }

我们知道UnitOfWorkBase是抽象类,对于不同的数据访问技术方案我们要定义不用的工作单元实现类,比如EF和NHibernate的事务实现机制是不一样的,这里我们看下EfUnitOfWork

    public class EfUnitOfWork : UnitOfWorkBase, ITransientDependency
    {
        private readonly IDictionary<Type, DbContext> _activeDbContexts;
        private readonly IIocResolver _iocResolver;
        private TransactionScope _transaction;

        /// <summary>
        /// Creates a new <see cref="EfUnitOfWork"/>.
        /// </summary>
        public EfUnitOfWork(IIocResolver iocResolver, IUnitOfWorkDefaultOptions defaultOptions)
            : base(defaultOptions)
        {
            _iocResolver = iocResolver;
            _activeDbContexts = new Dictionary<Type, DbContext>();
        }

        protected override void BeginUow()
        {
            if (Options.IsTransactional == true)
            {
                var transactionOptions = new TransactionOptions
                {
                    IsolationLevel = Options.IsolationLevel.GetValueOrDefault(IsolationLevel.ReadUncommitted),
                };

                if (Options.Timeout.HasValue)
                {
                    transactionOptions.Timeout = Options.Timeout.Value;
                }

                _transaction = new TransactionScope(
                    TransactionScopeOption.Required,
                    transactionOptions,
                    Options.AsyncFlowOption.GetValueOrDefault(TransactionScopeAsyncFlowOption.Enabled)
                    );
            }
        }

        public override void SaveChanges()
        {
            _activeDbContexts.Values.ForEach(SaveChangesInDbContext);
        }
...

上面已经定义了UnitOfWork接口和实现方法,那我们改怎么使用呢?一般的我们的使用方式是这样的,下面的场景是模拟银行转账功能,从一个账户扣钱和另一个账户加钱。下面是领域层定义的账户转账服务,我们在整个操作实现完后调用 _unitOfWork.Commit()进行提交,在领域服务构造函数注入UnitOfWork。

    // 账号转账领域服务类
    public class AccountService
    {
        private readonly IAccountRepository _productRepository;
        private readonly IUnitOfWork _unitOfWork;

        public AccountService(IAccountRepository productRepository, IUnitOfWork unitOfWork)
        {
            _productRepository = productRepository;
            _unitOfWork = unitOfWork;            
        }
        
        public void Transfer(Account from, Account to, decimal amount)
        {
            if (from.Balance >= amount)
            {
                from.Balance -= amount;
                to.Balance += amount;

                _productRepository.Save(from);
                _productRepository.Save(to);
                _unitOfWork.Commit();
            }
        } 
    }

这样的设计简单易懂,但是我们每个提交都要引用UnitOfWork会比较麻烦,那么有没有更好的设计思路呢?ABP的设计思想还是比较值得借鉴的。ABP的UnitOfWork的设计思路还是沿用作者最喜欢的切面编程,何为切面编程:通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。也就是AOP技术,ABP作者用的是Castle Windsor来实现的。一般的我们需要两步,1、继承IInterceptor接口重写Intercept方法,这样我们就可以实现动态拦截方法了,2、那么我们到底怎么才能动态代理要拦截的方法呢?我们可以继承Attribute,自定义UnitOfWorkAttribute。可能你现在还不明白,那么我们来看下具体代码吧。

    internal class UnitOfWorkInterceptor : IInterceptor
    {
        private readonly IUnitOfWorkManager _unitOfWorkManager;

        public UnitOfWorkInterceptor(IUnitOfWorkManager unitOfWorkManager)
        {
            _unitOfWorkManager = unitOfWorkManager;
        }

        public void Intercept(IInvocation invocation)
        {
            if (_unitOfWorkManager.Current != null)
            {
                //Continue with current uow
                invocation.Proceed();
                return;
            }

            var unitOfWorkAttr = UnitOfWorkAttribute.GetUnitOfWorkAttributeOrNull(invocation.MethodInvocationTarget);
            if (unitOfWorkAttr == null || unitOfWorkAttr.IsDisabled)
            {
                //No need to a uow
                invocation.Proceed();
                return;
            }

            //No current uow, run a new one
            PerformUow(invocation, unitOfWorkAttr.CreateOptions());
        }

对于Castle Windsor我们只需要像上面的UnitOfWorkInterceptor就是继承IInterceptor重写Intercept就可以实现动态代理啦。下面来看下自定义的UnitOfWorkAttribute。

    [AttributeUsage(AttributeTargets.Method)]
    public class UnitOfWorkAttribute : Attribute
    {
        /// <summary>
        /// Is this UOW transactional?
        /// Uses default value if not supplied.
        /// </summary>
        public bool? IsTransactional { get; private set; }

        /// <summary>
        /// Timeout of UOW As milliseconds.
        /// Uses default value if not supplied.
        /// </summary>
        public TimeSpan? Timeout { get; private set; }

好了,定义了UnitOfWorkAttribute,那么我们怎么让它和UnitOfWorkInterceptor结合起来对代码进行动态拦截呢?

        [UnitOfWork]
        public virtual async Task<AbpLoginResult> LoginAsync(string userNameOrEmailAddress, string plainPassword, string tenancyName = null)
        {
            if (userNameOrEmailAddress.IsNullOrEmpty())
            {
                throw new ArgumentNullException("userNameOrEmailAddress");
            }

            if (plainPassword.IsNullOrEmpty())
            {
                throw new ArgumentNullException("plainPassword");
            }

            using (_unitOfWorkManager.Current.DisableFilter(AbpDataFilters.MayHaveTenant))
            {
                TUser user;

                if (!_multiTenancyConfig.IsEnabled)
                {
                    using (_unitOfWorkManager.Current.EnableFilter(AbpDataFilters.MayHaveTenant))
                    {
                        //Log in with default denant
                        user = await FindByNameOrEmailAsync(userNameOrEmailAddress);
                        if (user == null)
                        {
                            return new AbpLoginResult(AbpLoginResultType.InvalidUserNameOrEmailAddress);
                        }
                    }
                }

上面代码是利用Attribute的特性对方法进行标识,这是第一步,现在我们已经对要拦截的代码标识了,那么我们是怎么知道它要被拦截的呢?

    internal static class UnitOfWorkRegistrar
    {
        /// <summary>
        /// Initializes the registerer.
        /// </summary>sssss
        /// <param name="iocManager">IOC manager</param>
        public static void Initialize(IIocManager iocManager)
        {
            iocManager.IocContainer.Kernel.ComponentRegistered += ComponentRegistered;
        }


        /// <summary>
        /// 拦截注册事件 
        /// </summary>
        /// <param name="key"></param>
        /// <param name="handler"></param>
        private static void ComponentRegistered(string key, IHandler handler)
        {
            if (UnitOfWorkHelper.IsConventionalUowClass(handler.ComponentModel.Implementation))
            {
                //判断如果是IRepository和IApplicationService,就注册动态代理 Intercept all methods of all repositories.
                handler.ComponentModel.Interceptors.Add(new InterceptorReference(typeof(UnitOfWorkInterceptor)));
            }
            else if (handler.ComponentModel.Implementation.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).Any(UnitOfWorkHelper.HasUnitOfWorkAttribute))
            {
                //判断如果是被标识了UnitOfWork attribute的就注册动态代理 Intercept all methods of classes those have at least one method that has UnitOfWork attribute.
                //TODO: Intecept only UnitOfWork methods, not other methods!
                handler.ComponentModel.Interceptors.Add(new InterceptorReference(typeof(UnitOfWorkInterceptor)));
            }
        }
    }

请认真看上面的方法ComponentRegistered的代码,UnitOfWorkHelper.HasUnitOfWorkAttribute就是判断是否是UnitOfWorkAttribute。

        public static bool HasUnitOfWorkAttribute(MemberInfo methodInfo)
        {
            return methodInfo.IsDefined(typeof(UnitOfWorkAttribute), true);
        }

你可能会问UnitOfWorkRegistrar的ComponentRegistered方法是什么时候执行的?那么你可以参考下我之前写的Castle Windsor常用介绍以及其在ABP项目的应用介绍 ,关于UnitOfWorkAttribute 是怎么执行的可以参考ABP之模块分析

那么到此我们被标识[UnitOfWork]的登录方法LoginAsync和所有的repositories仓储一旦被执行到就会被拦截,执行我们的代理类。本来这篇文章还想说说仓储的,但是因为篇幅可能会有点长,那就放在下次总结吧,至此UnitOfWork也就总结到此了。

参考文章:

http://www.cnblogs.com/daxnet/archive/2011/06/03/2071931.html

http://www.cnblogs.com/zhili/p/UnitOfWork.html

http://www.cnblogs.com/xishuai/p/3750154.html

免责声明:文章转载自《UnitOfWork以及其在ABP中的应用》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇windows server 2012 FTP连接报530 User 用户名 cannot log in home directory inaccessible的解决方法进阶:案例五: Dynamic 创建 Business Graphic下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

Abp vNext异常处理的缺陷/改造方案

吐槽Abp Vnext异常处理! 哎呀,是一个喷子 目前项目使用Abp VNext开发,免不了要全局处理异常、提示服务器异常信息。 1. Abp官方异常处理 Abp项目默认会启动内置的异常处理,默认不将异常信息发送到客户端。 在AppModule文件ConfigureServices方法中使用以下代码: Configure<AbpException...

入门系列- ABP 本地化

本地化 ABP的本地化系统与Microsoft.Extensions.Localization无缝集成,并与AspnetCore的本地化文档兼容. 它添加了一些实用功能和增强功能, 使其更易于在实际开发中应用. Volo.Abp.Localization Package 启动模板默认已经安装了此nuget包, 所以在大多数情况下, 你不需要手动安装它. V...

ABP官方文档翻译 3.1 实体

实体 实体类 聚合根类领域事件 常规接口 审计 软删除 激活/失活实体 实体改变事件 IEntity接口   实体是DDD(领域驱动设计)的核心概念之一。Eric Evans描述它为"An object that is not fundamentally defined by its attributes, but rather by a t...

深入浅出-多租户

ABP的多租户模块提供了创建多租户应用程序的基本功能。 维基百科中是这样定义多租户的: 软件多租户技术指的是一种软件架构,这种架构可以使用软件的单实例运行并为多个租户提供服务。租户是通过软件实例的特定权限共享通用访问的一组用户。使用多租户架构,软件应用为每个租户提供实例的专用共享,包括实例的数据、配置、用户管理、租户的私有功能和非功能属性。多租户与多实例架...

部署基于.netcore5.0的ABP框架后台Api服务端,以及使用Nginx部署Vue+Element前端应用

前面介绍了很多关于ABP框架的后台Web API 服务端,以及基于Vue+Element前端应用,本篇针对两者的联合部署,以及对部署中遇到的问题进行处理。ABP框架的后端是基于.net core5.0 的Asp.net core 应用,因此和常规的Asp.net core 应用部署一样;而Vue+Element前端应用则是基于nodejs的应用,部署方式又...

ABP使用及框架解析系列

前言 ABP ABP是“ASP.NET Boilerplate Project”的简称。 ABP的官方网站:http://www.aspnetboilerplate.com ABP在Github上的开源项目:https://github.com/aspnetboilerplate ABP其他学习博客推荐及介绍:http://www.cnblogs.com...