领域层构成
- 实体(Entity) : 实体是种领域对象,它有自己的属性(状态,数据)和执行业务逻辑的方法.实体由唯一标识符(Id)表示,不同ID的两个实体被视为不同的实体.
- 值对象(Value Object) : 值对象是另外一种类型的领域对象,使用值对象的属性来判断两个值对象是否相同,而非使用ID判断.如果两个值对象的属性值全部相同就被视为同一对象.值对象通常是不可变的,大多数情况下它比实体简单.
- 聚合(Aggregate) 和 聚合根(Aggregate Root) : 聚合是由聚合根包裹在一起的一组对象(实体和值对象).聚合根是一种具有特定职责的实体.
- 仓储(Repository) (接口): 仓储是被领域层或应用层调用的数据库持久化接口.它隐藏了DBMS的复杂性,领域层中只定义仓储接口,而非实现.
- 领域服务(Domain Service) : 领域服务是一种无状态的服务,它依赖多个聚合(实体)或外部服务来实现该领域的核心业务逻辑.
- 规约(Specification) : 规约是一种强命名 ,可重用 ,可组合 ,可测试的实体过滤器.
- 领域事件(Domain Event) : 领域事件是当领域某个事件发生时,通知其它领域服务的方式,为了解耦领域服务间的依赖.
实体类
Entity类,或者实现IEntity接口。主要提供了主键类型的Id属性
复合主键
csharp
//继承非泛型Entity类
public class UserRole : Entity
{
//复合键由UserId和RoleId组成
public Guid UserId { get; set; }
public Guid RoleId { get; set; }
public DateTime CreationTime { get; set; }
public UserRole()
{
}
//获取复合主键
public override object[] GetKeys()
{
return new object[] { UserId, RoleId };
}
}
说明
- 还需要配置复合主键(应该在配置表字段那里)
- 复合主键实体不可以使用 IRepository<TEntity, TKey> 接口,因为它需要一个唯一的Id属性. 但你可以使用 IRepository.
聚合根
说明
- AggregateRoot类继承自Entity类,所以默认有Id这个属性。
- AggregateRoot 类实现了 IHasExtraProperties 和 IHasConcurrencyStamp 接口,所以具备了两个能力 可扩展、乐观并发。如果不需要,可以继承BasicAggregateRoot(或BasicAggregateRoot)
- ABP 会默认为聚合根创建仓储。也可以通过配置使ABP也可以为所有的实体创建仓储
- 聚合根实体继承AggregateRoot类,或者直接实现IAggregateRoot接口
- 聚合中的非聚合根实体的构造函数定义成internal的(所以它只能由领域层来创建),然后在聚合根实体中定义创建此实体的方法
- 聚合内实体的所有属性都有protected的set.这是为了防止实体在实体外部任意改变.
- 聚合根也可以带有复合主键(方式参见上述复合主键),要使用非泛型的AggregateRoot基类.
以订单Order、商品OrderLine实体为例
csharp
//继承AggregateRoot<TKey>类,
public class Order : AggregateRoot<Guid>
{
public virtual string ReferenceNo { get; protected set; }
public virtual int TotalItemCount { get; protected set; }
public virtual DateTime CreationTime { get; protected set; }
//它有一个OrderLine实体集合
public virtual List<OrderLine> OrderLines { get; protected set; }
protected Order()
{
}
public Order(Guid id, string referenceNo)
{
Check.NotNull(referenceNo, nameof(referenceNo));
Id = id;
ReferenceNo = referenceNo;
OrderLines = new List<OrderLine>();
}
//Order.AddProduct实现了业务规则将商品添加到订单中
public void AddProduct(Guid productId, int count)
{
if (count <= 0)
{
throw new ArgumentException(
"You can not add zero or negative count of products!",
nameof(count)
);
}
var existingLine = OrderLines.FirstOrDefault(ol => ol.ProductId == productId);
if (existingLine == null)
{
OrderLines.Add(new OrderLine(this.Id, productId, count));
}
else
{
existingLine.ChangeCount(existingLine.Count + count);
}
TotalItemCount += count;
}
}
聚合中的非聚合根实体
csharp
//OrderLine是一个具有组合键(OrderId和 ProductId)的实体.
public class OrderLine : Entity
{
public virtual Guid OrderId { get; protected set; }
public virtual Guid ProductId { get; protected set; }
public virtual int Count { get; protected set; }
protected OrderLine()
{
}
internal OrderLine(Guid orderId, Guid productId, int count)
{
OrderId = orderId;
ProductId = productId;
Count = count;
}
internal void ChangeCount(int newCount)
{
Count = newCount;
}
public override object[] GetKeys()
{
return new Object[] {OrderId, ProductId};
}
}
审计属性
审计接口、基类
IHasCreationTime :CreationTime
IMayHaveCreator :CreatorId
ICreationAuditedObject :实现自上述两个接口,基类CreationAuditedEntity 和CreationAuditedAggregateRoot
IHasModificationTime :LastModificationTime
IModificationAuditedObject :LastModificationTime、LastModifierId
IAuditedObject :CreationTime、CreatorId、LastModificationTime、LastModifierId。基类AuditedEntity 和 AuditedAggregateRoot
ISoftDelete :IsDeleted
IHasDeletionTime :IsDeleted、DeletionTime
IDeletionAuditedObject :IsDeleted、DeletionTime、DeleterId
IFullAuditedObject :创建人、创建时间、修改人、修改时间、是否删除、删除人、删除时间。基类FullAuditedEntity and FullAuditedAggregateRoot
说明
- 这些基类都有非泛型版本,可以使用 AuditedEntity 和 FullAuditedAggregateRoot 来支持复合主键
值对象类
说明
- 值对象最好设计为不可变的,构造函数构造即可。 即不要再给类设置SetProperty方法或者将属性的Setter公开
csharp
//值对象类要继承ValueObject类
public class Address : ValueObject
{
public Guid CityId { get; private set; }
public string Street { get; private set; }
public int Number { get; private set; }
private Address()
{
}
public Address(
Guid cityId,
string street,
int number)
{
CityId = cityId;
Street = street;
Number = number;
}
//实现 GetAtomicValues()方法来返回原始值
protected override IEnumerable<object> GetAtomicValues()
{
yield return Street;
yield return CityId;
yield return Number;
}
}
值对象比较
ValueObject.ValueEquals方法用于判断两个值对象是否相等
ini
Address address1 = ...
Address address2 = ...
if (address1.ValueEquals(address2)) //Check equality
{
...
}
仓储
通用仓储IRepository
ABP为每个聚合根或实体提供了 默认的通用(泛型)仓储 . 你可以在服务中注入 IRepository<TEntity, TKey>
使用标准的CRUD操作。
IRepository<TEntity, TKey> 扩展了IQueryable,所以是依赖于IQueryable的
csharp
namespace Demo
{
public class PersonAppService : ApplicationService
{
//IRepository<TEntity, TKey> 就是通用仓储
private readonly IRepository<Person, Guid> _personRepository;
public PersonAppService(IRepository<Person, Guid> personRepository)
{
_personRepository = personRepository;
}
public async Task CreateAsync(CreatePersonDto input)
{
var person = new Person(input.Name);
await _personRepository.InsertAsync(person);
}
public async Task<int> GetCountAsync(string filter)
{
return await _personRepository.CountAsync(p => p.Name.Contains(filter));
}
}
}
仓储方法
- GetAsync:获取满足条件/指定Id的唯一实体(不存在或多条会抛异常)
- FindAsync:获取满足条件/指定Id的唯一实体(不存在返回null、多条会抛异常),参数2是includeDetails = true
- GetListAsync:获取所有/满足指定条件的实体,参数2是includeDetails = false
-
- IReadOnlyList应该不可写,不可增删项,或给某项重新赋值,但是可以更改每项的里面的内容
- 查找不到不会抛异常,也不会返回null,会返回一个空的list
- GetCountAsync:获取数据库里所有实体的数量
- GetPagedListAsync:返回一个指定长度的实体列表。 他拥有 skipCount、maxResultCount 、sorting 参数
- InsertAsync,插入实体
- UpdateAsync,更新实体。参数不能是null,需要判空
- DeleteAsync:删除实体,或删除满足条件的实体。注意,实体若含有软删除,此方法不会真正删除,而是标记为"已删除"
-
- 参数2,autoSave,是否立即保存到数据库。
- HardDeleteAsync:用于删除带软删除特征的实体
- CountAsync,满足条件的实体的数量
批量操作
- InsertManyAsync
- UpdateManyAsync:当列表容器中没有数据时,不会报错。但列表中有null时,会报错
csharp
//不会报错
await _hotLadleRecordRepository.UpdateManyAsync(new List<HotLadleRecord>());
//会报错
await _hotLadleRecordRepository.UpdateManyAsync(new List<HotLadleRecord>() { null });
- DeleteManyAsync
扩展方法
- ContainsAsync
- AnyAsync、AllAsync
- CountAsync、LongCountAsync
- FirstAsync、FirstOrDefaultAsync
- LastAsync、LastOrDefaultAsync
- SingleAsync、SingleOrDefaultAsync
- MinAsync、MaxAsync
- SumAsync
- AverageAsync
- ToListAsync、ToArrayAsync
其他方法
- GetQueryableAsync:获取一个IQueryable对象,可以对其执行LINQ
- EnsureExistsAsync:根据Id/条件,确保实体存在,不存在抛异常
csharp
namespace Demo
{
public class PersonAppService : ApplicationService
{
private readonly IRepository<Person, Guid> _personRepository;
public PersonAppService(IRepository<Person, Guid> personRepository)
{
_personRepository = personRepository;
}
public async Task<List<PersonDto>> GetListAsync(string filter)
{
// 获取 IQueryable<Person>
IQueryable<Person> queryable = await _personRepository.GetQueryableAsync();
// 创建一个查询,LINQ
var query = from person in queryable
where person.Name == filter
orderby person.Name
select person;
// 创建一个查询,LINQ扩展方法
var people = queryable
.Where(p => p.Name.Contains(filter))
.OrderBy(p => p.Name)
.ToList();
// 执行查询
var people = query.ToList();
// 转DTO并返回给客户端
return await people.Select(p => new PersonDto {Name = p.Name}).ToListAsync();
}
}
}
基础仓储:不依赖IQueryable
一般不需要管这部分,了解一下
某些ORM提供程序或数据库系统可能不支持IQueryable接口,这样就不能使用IRepository<TEntity, TKey> ,因为它是基于 IQueryable 的。
这时,可以继承或实现IBasicRepository<TEntity, TPrimaryKey> 、IBasicRepository 、BasicRepositoryBase,来自定义仓储
只读仓储
看名字,应该是只支持数据库中取数据,不能修改数据库中数据。
IReadOnlyRepository<TEntity, TKey> 与 IReadOnlyBasicRepository<Tentity, TKey>接口
无主键仓储
如果你的实体没有id主键 (例如, 它可能具有复合主键) ,则需要使用IRepository,而无法使用IRepository<TEntity, TKey>。
这个仓储里面,不支持根据Id的操作方法,因为它没有主键
自定义仓储
此处需要参考文档中的引用部分,进行扩展
1、接口
csharp
//扩展 IRepository<Person, Guid> 以使用已有的通用仓储功能
public interface IPersonRepository : IRepository<Person, Guid>
{
Task<Person> FindByNameAsync(string name);
}
2、实现
csharp
public class PersonRepository : EfCoreRepository<MyDbContext, Person, Guid>, IPersonRepository
{
//依赖于数据库访问提供程序,如dbContext
public PersonRepository(IDbContextProvider<TestAppDbContext> dbContextProvider)
: base(dbContextProvider)
{
}
public async Task<Person> FindByNameAsync(string name)
{
var dbContext = await GetDbContextAsync();
return await dbContext.Set<Person>()
.Where(p => p.Name == name)
.FirstOrDefaultAsync();
}
}
SaveChanges的替代方法
由于经常需要在插入,更新或删除实体后保存更改,相应的仓储方法有一个可选的 autoSave 参数
csharp
public async Task<int> CreateAsync(string name)
{
var category = new Category {Name = name};
await _categoryRepository.InsertAsync(category, autoSave: true);
return category.Id;
}
执行IQueryable或异步方法
1、直接引用EF Core包
Volo.Abp.EntityFrameworkCore
不推荐,会造成代码污染
2、使用IRepository异步扩展方法
如在仓储中使用 CountAsync 和 FirstOrDefaultAsync 方法
3、使用IAsyncQueryableExecuter
IAsyncQueryableExecuter 是一个用于异步执行 IQueryable 对象的服务,不依赖于实际的数据库提供程序。
可以通过构造函数注入此服务,ApplicationService 和 DomainService 基类已经预属性注入了 AsyncExecuter 属性,所以你可直接使用.
ini
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.Linq;
namespace AbpDemo
{
public class ProductAppService : ApplicationService, IProductAppService
{
private readonly IRepository<Product, Guid> _productRepository;
private readonly IAsyncQueryableExecuter _asyncExecuter;
public ProductAppService(
IRepository<Product, Guid> productRepository,
IAsyncQueryableExecuter asyncExecuter)
{
_productRepository = productRepository;
_asyncExecuter = asyncExecuter;
}
public async Task<ListResultDto<ProductDto>> GetListAsync(string name)
{
//Obtain the IQueryable<T>
var queryable = await _productRepository.GetQueryableAsync();
//Create the query
var query = queryable
.Where(p => p.Name.Contains(name))
.OrderBy(p => p.Name);
//Run the query asynchronously
List<Product> products = await _asyncExecuter.ToListAsync(query);
//...
}
}
}
4、自定义仓储方法
领域服务
领域服务的生命周期是 瞬态 的,它们会自动注册到依赖注入服务.
下述情况,需要使用领域服务
- 你实现了依赖于某些服务(如存储库或其他外部服务)的核心域逻辑.
- 你需要实现的逻辑与多个聚合/实体相关,因此它不适合任何聚合
DomainService
从 DomainService 基类派生领域服务或直接实现 IDomainService 接口
- ABP 框架自动将类注册为瞬态生命周期到依赖注入系统.
- 你可以直接使用一些常用服务作为基础属性,而无需手动注入 (例如 ILogger and IGuidGenerator).
arduino
using Volo.Abp.Domain.Services;
namespace MyProject.Issues
{
public class IssueManager : DomainService
{
}
}
在应用服务中使用时,可以直接注入manager
和应用服务的比较
- 应用程序服务实现应用程序的 用例 (典型 Web 应用程序中的用户交互), 而领域服务实现 核心的、用例独立的领域逻辑.
- 应用程序服务获取/返回 数据传输对象, 领域服务方法通常获取和返回 领域对象 (实体, 值对象).
- 领域服务通常由应用程序服务或其他领域服务使用,而应用程序服务由表示层或客户端应用程序使用.
规约
如下包,abp模板已经默认安装
Volo.Abp.Specifications
虽然可以使用lambda表达式替代规约,但是规约更适合下述情况
- 可复用:假设你在代码库的许多地方都需要用到优质顾客过滤器.如果使用表达式而不创建规约,那么如果以后更改"优质顾客"的定义会发生什么?假设你想将最低余额从100000美元更改为250000美元,并添加另一个条件,成为顾客超过3年.如果使用了规约,只需修改一个类.如果在任何其他地方重复(复制/粘贴)相同的表达式,则需要更改所有的表达式.
- 可组合:可以组合多个规约来创建新规约.这是另一种可复用性.
- 命名:PremiumCustomerSpecification 更好地解释了为什么使用规约,而不是复杂的表达式.因此,如果在你的业务中使用了一个有意义的表达式,请考虑使用规约.
- 可测试:规约是一个单独(且易于)测试的对象.
何时不要使用
- 没有业务含义的表达式:不要对与业务无关的表达式和操作使用规约
- 报表:如果只是创建报表,不要创建规约,而是直接使用 IQueryable 和LINQ表达式.你甚至可以使用普通SQL、视图或其他工具生成报表。DDD不关心报表,因此从性能角度来看,查询底层数据存储的方式可能很重要。
规约的使用
定义规约:创建一个由 Specification 派生的新规约类
IsSatisfiedBy 方法可以用于检查单个对象是否满足规约
ToExpression 将规约转换为表达式,规约可以与And、Or、Not以及AndNot扩展方法组合使用
csharp
public class Customer : AggregateRoot<Guid>
{
public string Name { get; set; }
public byte Age { get; set; }
public long Balance { get; set; }
public string Location { get; set; }
}
//可以直接实现ISpecification<T>接口,但是基类Specification<T>做了大量简化.
public class Age18PlusCustomerSpecification : Specification<Customer>
{
public override Expression<Func<Customer, bool>> ToExpression()
{
//只需通过定义一个lambda表达式来定义规约
return c => c.Age >= 18;
return (customer) => (customer.Balance >= 100000);
}
}
public class CustomerService : ITransientDependency
{
public async Task BuyAlcohol(Customer customer)
{
//IsSatisfiedBy 方法可以用于检查单个对象是否满足规约
if (!new Age18PlusCustomerSpecification().IsSatisfiedBy(customer))
{
//如果顾客不满足年龄规定,则抛出异常
throw new Exception(
"这位顾客不满足年龄规定!"
);
}
//TODO...
}
}
csharp
public class CustomerManager : DomainService, ITransientDependency
{
private readonly IRepository<Customer, Guid> _customerRepository;
public CustomerManager(IRepository<Customer, Guid> customerRepository)
{
_customerRepository = customerRepository;
}
public async Task<List<Customer>> GetCustomersCanBuyAlcohol()
{
var queryable = await _customerRepository.GetQueryableAsync();
var query = queryable.Where(
//ToExpression() 方法可用于将规约转化为表达式.
//通过这种方式,你可以使用规约在数据库查询时过滤实体.
new Age18PlusCustomerSpecification().ToExpression()
);
//实际上,没有必要使用 ToExpression() 方法,因为规约会自动转换为表达式
var query = queryable.Where(
new Age18PlusCustomerSpecification()
);
return await AsyncExecuter.ToListAsync(query);
}
}
public class CustomerManager : DomainService, ITransientDependency
{
private readonly IRepository<Customer, Guid> _customerRepository;
public CustomerManager(IRepository<Customer, Guid> customerRepository)
{
_customerRepository = customerRepository;
}
public async Task<int> GetAdultPremiumCustomerCountAsync()
{
return await _customerRepository.CountAsync(
//规约可以与And、Or、Not以及AndNot扩展方法组合使用
new Age18PlusCustomerSpecification()
.And(new PremiumCustomerSpecification()).ToExpression()
);
}
}
创建一个可以复用的规约,派生自AndSpecification
csharp
public class AdultPremiumCustomerSpecification : AndSpecification<Customer>
{
public AdultPremiumCustomerSpecification()
: base(new Age18PlusCustomerSpecification(),
new PremiumCustomerSpecification())
{
}
}
public async Task<int> GetAdultPremiumCustomerCountAsync()
{
return await _customerRepository.CountAsync(
new AdultPremiumCustomerSpecification()
);
}
项目中应用,通过规约查询冷坯、热坯
数据过滤
软删除过滤器
Abp会在查询数据库时会自动过滤软删除的实体.ISoftDelete
禁用过滤器
两种方法:方法1如下,使用时禁用。方法2通过配置过滤器选项禁用
csharp
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp;
using Volo.Abp.Data;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Repositories;
namespace Acme.BookStore
{
public class MyBookService : ITransientDependency
{
//使用 IDataFilter 服务控制数据过滤.
private readonly IDataFilter _dataFilter;
private readonly IRepository<Book, Guid> _bookRepository;
public MyBookService(
IDataFilter dataFilter,
IRepository<Book, Guid> bookRepository)
{
_dataFilter = dataFilter;
_bookRepository = bookRepository;
}
public async Task<List<Book>> GetAllBooksIncludingDeletedAsync()
{
//临时禁用 ISoftDelete 过滤器
//始终与 using 搭配使用,确保代码块执行后将过滤重置为之前的状态)
using (_dataFilter.Disable<ISoftDelete>())
{
return await _bookRepository.GetListAsync();
}
}
}
}
IDataFilter.Enable 方法可以启用过滤. 可以嵌套使用 Enable 和 Disable 方法定义内部作用域.
过滤器选项
csharp
//AbpDataFilterOptions 用于设置数据过滤系统选项.
Configure<AbpDataFilterOptions>(options =>
{
//禁用了 ISoftDelete 过滤
options.DefaultStates[typeof(ISoftDelete)] = new DataFilterState(isEnabled: false);
});
自定义数据过滤
1、首先为过滤定义一个接口 (如 ISoftDelete 和 IMultiTenant) 然后用实体实现它
csharp
//IIsActive 接口可以过滤活跃/消极数据,任何实体都可以实现它
public interface IIsActive
{
bool IsActive { get; }
}
public class Book : AggregateRoot<Guid>, IIsActive
{
public string Name { get; set; }
public bool IsActive { get; set; } //Defined by IIsActive
}
2、重写你的 DbContext 的 ShouldFilterEntity 和 CreateFilterExpression 方法
csharp
// IsActiveFilterEnabled 属性用于检查是否启用了 IIsActive
protected bool IsActiveFilterEnabled => DataFilter?.IsEnabled<IIsActive>() ?? false;
//检查给定实体是否实现 IIsActive 接口,在必要时组合表达式.
protected override bool ShouldFilterEntity<TEntity>(IMutableEntityType entityType)
{
if (typeof(IIsActive).IsAssignableFrom(typeof(TEntity)))
{
return true;
}
return base.ShouldFilterEntity<TEntity>(entityType);
}
protected override Expression<Func<TEntity, bool>> CreateFilterExpression<TEntity>()
{
var expression = base.CreateFilterExpression<TEntity>();
if (typeof(IIsActive).IsAssignableFrom(typeof(TEntity)))
{
Expression<Func<TEntity, bool>> isActiveFilter =
e => !IsActiveFilterEnabled || EF.Property<bool>(e, "IsActive");
expression = expression == null
? isActiveFilter
: CombineExpressions(expression, isActiveFilter);
}
return expression;
}
如果IsActiveFilterEnabled为false,则表达式结果直接为true(即不应用IsActive过滤,所有实体都符合条件)。
如果IsActiveFilterEnabled为true,则检查实体e的"IsActive"属性值。只有当"IsActive"为true时,表达式结果才为true。
如果expression为null,则将isActiveFilter表达式赋值给expression。
如果expression不为null,则调用CombineExpressions方法来组合expression和isActiveFilter,并将结果赋值给expression。