ABP 框架实战系列(三)-领域层深入篇

ABP 领域层-实体(Entities)

实体是DDD(领域驱动设计)的核心概念之一。Eric Evans是这样描述的“很多对象不是通过它们的属性定义的,而是通过一连串的连续性事件和标识定义的”。

在框架实战系列(二)-领域层介绍篇中有做代码简单介绍(推荐关注该公众号,干货满满)

在此我们深入理解实体概念

在ABP 中,实体类继承Entity类,如下所示,Product为一个实体

    public class Product : Entity
    {
        public virtual string Name { get; set; }
        public virtual string Code { get; set; }
        public virtual DateTime CreationTime { get; set; }

        public Product()
        {
            CreationTime=DateTime.Now;
        }

    }

在ABP 框架中Entity 实体类部分内容如下所示,它有一个主键ID,所有继承自Entity的类,都使用该主键,你可以设置主键的值为任何格式,如下代码所示,它为泛型,可以为string,Guid或者其它数据类型。同时实体类重写了 equality (==) 操作符用来判断两个实体对象是否相等(两个实体的Id是否相等)。还定义了一个IsTransient()方法来检测实体是否有Id属性。

public abstract class Entity<TPrimaryKey> : IEntity<TPrimaryKey>
  {
    /// <summary>Unique identifier for this entity.</summary>
    public virtual TPrimaryKey Id { get; set; }

    /// <summary>
    /// Checks if this entity is transient (it has not an Id).
    /// </summary>
    /// <returns>True, if this entity is transient</returns>
    public virtual bool IsTransient()
    {
      if (EqualityComparer<TPrimaryKey>.Default.Equals(this.Id, default (TPrimaryKey)))
        return true;
      if (typeof (TPrimaryKey) == typeof (int))
        return Convert.ToInt32((object) this.Id) <= 0;
      return typeof (TPrimaryKey) == typeof (long) && Convert.ToInt64((object) this.Id) <= 0L;
    }

    public virtual bool EntityEquals(object obj)
    {
      if (obj == null || !(obj is Entity<TPrimaryKey>))
        return false;
      if (this == obj)
        return true;
      Entity<TPrimaryKey> entity = (Entity<TPrimaryKey>) obj;
      if (this.IsTransient() && entity.IsTransient())
        return false;
      Type type1 = this.GetType();
      Type type2 = entity.GetType();
      if (!type1.GetTypeInfo().IsAssignableFrom(type2) && !type2.GetTypeInfo().IsAssignableFrom(type1))
        return false;
      if (this is IMayHaveTenant && entity is IMayHaveTenant)
      {
        int? tenantId1 = this.As<IMayHaveTenant>().TenantId;
        int? tenantId2 = entity.As<IMayHaveTenant>().TenantId;
        if (!(tenantId1.GetValueOrDefault() == tenantId2.GetValueOrDefault() & tenantId1.HasValue == tenantId2.HasValue))
          return false;
      }
      return (!(this is IMustHaveTenant) || !(entity is IMustHaveTenant) || this.As<IMustHaveTenant>().TenantId == entity.As<IMustHaveTenant>().TenantId) && this.Id.Equals((object) entity.Id);
    }

    public override string ToString() => string.Format("[{0} {1}]", (object) this.GetType().Name, (object) this.Id);
 }

接口约定

在很多应用程序中,很多实体具有像CreationTime的属性(数据库表也有该字段)用来指示该实体是什么时候被创建的。APB提供了一些有用的接口来实现这些类似的功能。也就是说,为这些实现了这些接口的实体,提供了一个通用的编码方式(通俗的说只要实现指定的接口就能实现指定的功能),以ABPUser 为例,如下所示AbpUser<TUser>类,继承了很多默认实现接口。

  • IAudited 审计
  • ISoftDelete 软删除
  • IPassivable 激活/闲置状态

public abstract class AbpUser<TUser> : AbpUserBase, IFullAudited<TUser>, 
      IAudited<TUser>, IAudited, ICreationAudited, IHasCreationTime, 
      IModificationAudited, IHasModificationTime, ICreationAudited<TUser>, 
      IModificationAudited<TUser>, IFullAudited, IDeletionAudited, 
      IHasDeletionTime, ISoftDelete, IDeletionAudited<TUser>
    where TUser : AbpUser<TUser>

ABP领域层——仓储(Repositories)

ABP框架实战系列(一)-持久层介绍篇 中有涉及仓储概念的讲解,(推荐关注该公众号,干货满满)

在这里,我们深入对仓储部分加以讨论,在ABP中,仓储类要实现IRepository接口.最好的方式是针对不同仓储对象定义各自不同的接口。 对于仓储类IRepository定义了许多泛型的方法。比如:Select、Insert、Update、Delete方法(CRUD操作)。在大多数的时候,,这些方法已足已应付一般实体的需要。如果这些方对于实体来说已足够,我们便不需要再去创建这个实体所需的仓储接口/类。即为自定义仓储

IRepository定义了从数据库中检索实体的常用方法。在ABP框架中AbpRepositoryBase派生了IRepository接口

  public abstract class AbpRepositoryBase<TEntity, TPrimaryKey> : IRepository<TEntity, TPrimaryKey>, IRepository, ITransientDependency, IUnitOfWorkManagerAccessor
    where TEntity : class, IEntity<TPrimaryKey>
 

接口方法主要包括:Query、GetAll、Single、Insert、Update、Delete、Count等几大方法的重载。

  GetAll():IQueryable<TEntity>
  GetAllIncluding(params Expression<Func<TEntity,object>>[] propertySelectors):IQueryable<TEntity>
  GetAllList():List<TEntity>
  GetAllListAsync():Task<List<TEntity>>
  GetAllList(Expression<Func<TEntity,bool>> predicate):List<TEntity>
  GetAllListAsync(Expression<Func<TEntity,bool>> predicate):Task<List<TEntity>>
  Query<T>(Func<IQueryable<TEntity>,T> queryMethod):T
  Get(TPrimaryKey id):TEntity
  GetAsync(TPrimaryKey id):Task<TEntity>
  Single(Expression<Func<TEntity,bool>> predicate):TEntity
  SingleAsync(Expression<Func<TEntity,bool>> predicate):Task<TEntity>
  FirstOrDefault(TPrimaryKey id):TEntity
  FirstOrDefaultAsync(TPrimaryKey id):Task<TEntity>
  FirstOrDefault(Expression<Func<TEntity,bool>> predicate):TEntity
  FirstOrDefaultAsync(Expression<Func<TEntity,bool>> predicate):Task<TEntity>
  Load(TPrimaryKey id):TEntity
  Insert(TEntity entity):TEntity
  InsertAsync(TEntity entity):Task<TEntity>
  InsertAndGetId(TEntity entity):TPrimaryKey
  InsertAndGetIdAsync(TEntity entity):Task<TPrimaryKey>
  InsertOrUpdate(TEntity entity):TEntity
  InsertOrUpdateAsync(TEntity entity):Task<TEntity>
  InsertOrUpdateAndGetId(TEntity entity):TPrimaryKey
  InsertOrUpdateAndGetIdAsync(TEntity entity):Task<TPrimaryKey>
  Update(TEntity entity):TEntity
  UpdateAsync(TEntity entity):Task<TEntity>
  Update(TPrimaryKey id, Action<TEntity> updateAction):TEntity
  UpdateAsync(TPrimaryKey id, Func<TEntity,Task> updateAction):Task<TEntity>
  Delete(TEntity entity):void
  DeleteAsync(TEntity entity):Task
  Delete(TPrimaryKey id):void
  DeleteAsync(TPrimaryKey id):Task
  Delete(Expression<Func<TEntity,bool>> predicate):void
  DeleteAsync(Expression<Func<TEntity,bool>> predicate):Task
  Count():int
  CountAsync():Task<int>
  Count(Expression<Func<TEntity,bool>> predicate):int
  CountAsync(Expression<Func<TEntity,bool>> predicate):Task<int>
  LongCount():long
  LongCountAsync():Task<long>
  LongCount(Expression<Func<TEntity,bool>> predicate):long
  LongCountAsync(Expression<Func<TEntity,bool>> predicate):Task<long>

在ABP 框架中,若直接在NuGet中安装的ABP 或者在官网下载的ABP 模板,很少会看到IRepository相关代码,看到应用的,基本都在包括在 EntityFrameworkCore类库中,系统默认给出自定义仓库的基本格式。

 public abstract class TestProjectRepositoryBase<TEntity, TPrimaryKey> : EfCoreRepositoryBase<TestProjectDbContext, TEntity, TPrimaryKey>
        where TEntity : class, IEntity<TPrimaryKey>
    {
        protected TestProjectRepositoryBase(IDbContextProvider<TestProjectDbContext> dbContextProvider)
            : base(dbContextProvider)
        {


            
        }

        // Add your common methods for all repositories
    }

    /// <summary>
    /// Base class for custom repositories of the application.
    /// This is a shortcut of <see cref="TestProjectRepositoryBase{TEntity,TPrimaryKey}"/> for <see cref="int"/> primary key.
    /// </summary>
    /// <typeparam name="TEntity">Entity type</typeparam>
    public abstract class TestProjectRepositoryBase<TEntity> : TestProjectRepositoryBase<TEntity, int>, IRepository<TEntity>
        where TEntity : class, IEntity<int>
    {
        protected TestProjectRepositoryBase(IDbContextProvider<TestProjectDbContext> dbContextProvider)
            : base(dbContextProvider)
        {

         
        }

        // Do not add any method here, add to the class above (since this inherits it)!!!
    }


仓储的实现
ABP在设计上是采取不指定特定ORM框架或其它存取数据库技术的方式。只要实现IRepository接口,任何框架都可以使用。

仓储要使用NHibernate或EF来实现都很简单,当你使用NHibernate或EntityFramework,如果提供的方法已足够使用,你就不需要为你的实体创建仓储对象了。我们可以直接注入IRepository<TEntity>(或IRepository<TEntity, TPrimaryKey>)。

管理数据库连接
数据库连接的开启和关闭,在仓储方法中,ABP会自动化的进行连接管理。

当仓储方法被调用后,数据库连接会自动开启且启动事务。当仓储方法执行结束并且返回以后,所有的实体变化都会被储存, 事务被提交并且数据库连接被关闭,一切都由ABP自动化的控制。如果仓储方法抛出任何类型的异常,事务会自动地回滚并且数据连接会被关闭。上述所有操作在实现了IRepository接口的仓储类所有公开的方法中都可以被调用。

如果仓储方法调用其它仓储方法(即便是不同仓储的方法),它们共享同一个连接和事务。连接会由仓储方法调用链最上层的那个仓储方法所管理。更多关于数据库管理,详见UnitOfWork文件。

仓储的生命周期
所有的仓储对象都是暂时性的。这就是说,它们是在有需要的时候才会被创建。ABP大量的使用依赖注入,当仓储类需要被注入的时候,新的类实体会由注入容器会自动地创建。

仓储的最佳实践
对于一个T类型的实体,是可以使用IRepository<T>。但别任何情况下都创建定制化的仓储,除非我们真的很需要。预定义仓储方法已经足够应付各种案例。
假如你正创建定制的仓储(可以实现IRepository<TEntity>)
仓储类应该是无状态的。这意味着, 你不该定义仓储等级的状态对象并且仓储方法的调用也不应该影响到其它调用。    
当仓储可以使用相根据注入,尽可较少或是不相根据于其它服务。

ABP领域层——工作单元(Unit Of work)

  • 通用连接和事务管理方法连接和事务管理是使用数据库的应用程序最重要的概念之一,应用程序中,有两个通用的方来创建/释放一个数据库连接:
    • 长连接
    • 短连接
  • ABP的连接和事务管理
    • ABP提供长连接、短连接模型,Repository是数据库操作的类,在执行Repository库中的方法的时候,ABP开启了一个数据库连接,并且在进入仓库方法的时候,会启用一个事务,因此,你可以安全的使用连接于仓储中的方法,若方法执行过程中产生异常,事务会被回滚并且释放掉连接。在这个模式中,仓储方法是单元性的(Unit of work)。 查看源码部分:
  public class EfCoreRepositoryBase<TDbContext, TEntity> :
      EfCoreRepositoryBase<TDbContext, TEntity, int>, IRepository<TEntity>, 
      IRepository<TEntity, int>, IRepository, ITransientDependency
    where TDbContext : DbContext
    where TEntity : class, IEntity<int>
  {
    public EfCoreRepositoryBase(IDbContextProvider<TDbContext> dbContextProvider)
      : base(dbContextProvider)
    {
    }

  • 工作单元

    工作单元的关键字为(Unit of work),为仓储提供事务服务,一共可以以两种方式使用

    • 在方法上添加[UnitOfWorkAttribute]确保事务一致性

    • 在方法体重使用IUnitOfWorkManager.Begin(...)、IUnitOfWorkManager.Begin(...)、xxunitOfWork.Complete()确保事务一致性。

      ABP 框架推荐使用UnitOfWorkattribute的在方法体上加属性的方式。

    • 禁用工作单元(Disabling unit of work)

      方法体上设置如为[UnitOfWork(IsDisabled = true)]

    • 无事务的工作单元(Non-transactional unit of work):[UnitOfWork(false)]

    • 工作单元调用其它工作单元(A unit of work method calls another)

      • 工作单元方法(一个贴上UnitOfWork属性标签的方法)调用另一个工作单元方法,他们共享同一个连接和事务
      • 如果创建了一个不同的线程/任务,它使用自己所属的工作单元
    • 自动化的saving changes (Automatically saving changes)

      当我们使用工作单元到方法上,ABP自动的储存所有变化于方法的末端。

  • 选项:IsolationLevel、Timeout等基本属性,可以通过配置或者初始化赋值修改

  • 方法:工作单元系统运作是无缝且不可视的。但是,在有些特例下,你需要调用它的方法。ABP储存所有的变化于工作单元的尾端,你不需要做任何事情。但是,有些时候,你或许会想要在工作单元的过程中就储存所有变化。 你可以注入IUnitOfWorkManager并且调用IUnitOfWorkManager.Current.SaveChanges()方法,即可完成保存工作。

  • 事件:工作单元具有Completed/Failed/Disposed事件

ABP领域层——数据过滤器(Data filters)

在数据库开发中,我们一般会运用软删除(soft-delete)模式,即不直接从数据库删除数据,而是标记这笔数据为已删除。因此,如果实体被软删除了,那么它就应该不会在应用程序中被检索到。要达到这种效果,我们需要在每次检索实体的查询语句上添加SQL的Where条件IsDeleted = false。这是个乏味的工作,但它是个容易被忘掉的事情。因此,我们应该要有个自动的机制来处理这些问题。

ABP提供数据过滤器(Data filters),它使用自动化的,基于规则的过滤查询。ABP已经有一些预定义的过滤器,当然也可以自行创建你专属的过滤器。

  • <font color=blue>过滤</font>
  • 预定义过滤器
    • 软删除接口(ISoftDelete)
    • 多租接口(IMustHaveTenant)
    • 多租接口(IMayHaveTenant)
  • 禁用过滤器

可以在工作单元(unit of work)中调用DisableFilter方法来禁用某个过滤器

var people1 = _personRepository.GetAllList();
using(_unitOfWorkManager.Current.DisableFilter(AbpDataFilters.SoftDelete)) {
      var people2 = _personRepository.GetAllList();
   }

var people3 = _personRepository.GetAllList();
  • 启用过滤器

    在工作单元(unit of work)中使用EnableFilter方法启用过滤器,如同DisableFilter方法一般(两者互为正反两面)

  • 设定过滤器参数

过滤器可以被参数化(parametric)。IMustHaveTenant过滤器是这类过滤器的一个范本,因为当前租户(tenant)的Id是在执行时期决定的。对于这些过滤器,如果真有需要,我们可以改变过滤器的值

CurrentUnitOfWork.SetFilterParameter("PersonFilter", "personId", 42);
  • 自定义过滤器

  • 其它对象关系映射工具

    ABP数据过滤器仅实现在Entity Framework上。对于其它ORM工具则尚不可用。

ABP领域层——领域事件(Domain events)

在C#中,一个类可以定义其专属的事件并且其它类可以注册该事件并监听,当事件被触发时可以获得事件通知。这对于对于桌面应用程序或独立的Windows Service来说非常有用。但是, 对于Web应用程序来说会有点问题,因为对象是根据请求(request)被创建并且它们的生命周期都很短暂。我们很难注册其它类别的事件。同样地,直接注册其它类别的事件也造成了类之间的耦合性。

在应用系统中,领域事件被用于解耦并且重用(re-use)商业逻辑。

事件总线

事件总线为一个单体(singleton)的对象,它由所有其它类所共享,可通过它触发和处理事件

获取默认实例

EventBus.Default.Trigger(...); //触发事件

注入IEventBus事件接口(Injecting IEventBus)

public class TaskAppService : ApplicaService {
     public IEventBus EventBus { get; set; }
     public TaskAppService() {
        EventBus = NullEventBus.Instance;
     }
  }

事件参数继承于EventData类,源码如下

    [Serializable]
    public abstract class EventData : IEventData
    {
        /// <summary>
        /// The time when the event occurred.
        /// </summary>
        public DateTime EventTime { get; set; }

        /// <summary>
        /// The object which triggers the event (optional).
        /// </summary>
        public object EventSource { get; set; }

        /// <summary>
        /// Constructor.
        /// </summary>
        protected EventData()
        {
            EventTime = Clock.Now;
        }
    }

定义事件

ABP定义AbpHandledExceptionData事件并且在异常发生的时候自动地触发这个事件。这在你想要取得更多关于异常的信息时特别有用(即便ABP已自动地纪录所有的异常)。你可以注册这个事件并且设定它的触发时机是在异常发生的时候。

ABP也提供在实体变更方面许多的通用事件数据类: EntityCreatedEventData, EntityUpdatedEventData和EntityDeletedEventData。它们被定义在Abp.Events.Bus.Entitis命名空间中。当某个实体新增/更新/删除后,这些事件会由ABP自动地触发。如果你有一个Person实体,可以注册到EntityCreatedEventData,事件会在新的Person实体创建且插入到数据库后被触发。这些事件也支持继承。如果Student类继承自Person类,并且你注册到EntityCreatedEventData中,接着你将会在Person或Student新增后收到触发。

触发事件

public class TaskAppService : ApplicationService {
     public IEventBus EventBus { get; set; }
     public TaskAppService() {
        EventBus = NullEventBus.Instance;
     }

     public void CompleteTask(CompleteTaskInput input) {
        //TODO: 已完成数据库上的任务
        EventBus.Trigger(new TaskCompletedEventData { TaskId = 42 } );
     }
  }

事件处理

要进行事件的处理,你应该要实现IEventHandler接口如下所示

public class ActivityWriter : IEventHandler<EventData>, ITransientDependency {
     public void HandleEvent(EventData eventData) {
        WriteActivity("A task is completed by id = " + eventData.TaskId);
     }
  } 

处理多个事件(Handling multiple events)

 public class ActivityWriter :
     IEventHandler<TaskCompletedEventData>,
     IEventHandler<TaskCreatedEventData>,
     ITransientDependency
  {
     public void HandleEvent(TaskCompletedEventData eventData) {
        //TODO: 处理事件
     }
     public void HandleEvent(TaskCreatedEventData eventData) {
        //TODO: 处理事件
     }
  }

注册处理器

  • 自动型Automatically
    ABP框架扫描所有继承了IEventHandler的接口实现类,并注册到事件总线中,当事件发生时,可以通过DI来实例化对象,并调用事件方法。
  • 手动型(Manually)
EventBus.Register<TaskCompletedEventData>(eventData =>
{
   WriteActivity("A task is completed by id = " + eventData.TaskId);
});

取消注册事件

//注册一个事件

EventBus.Unregister<TaskCompletedEventData>(handler);
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,922评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,591评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,546评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,467评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,553评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,580评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,588评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,334评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,780评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,092评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,270评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,925评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,573评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,194评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,437评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,154评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,127评论 2 352

推荐阅读更多精彩内容