关于程序员:ABP-框架实战系列三领域层深入篇

40次阅读

共计 11780 个字符,预计需要花费 30 分钟才能阅读完成。

  • ABP 畛域层 - 实体(Entities)
  • ABP 畛域层——仓储(Repositories)
  • ABP 畛域层——工作单元(Unit Of work)
  • ABP 畛域层——数据过滤器(Data filters)
  • ABP 畛域层——畛域事件(Domain events)

    • 事件总线
    • 定义事件
    • 触发事件
    • 事件处理
    • 注册处理器
    • 勾销注册事件

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);

博主 GitHub 地址
https://github.com/yuyue5945

关注公众号不迷路

正文完
 0