项目

规范模式

规范模式用于为实体和其他业务对象定义命名的、可重用的、可组合且可测试的过滤器

规范是领域层的一部分。

安装

当你使用启动模板时,这个包已经安装好了。所以大多数情况下,你不需要手动安装它。

Volo.Abp.Specifications 包安装到你的项目中。当当前文件夹是你的项目根目录(.csproj)时,你可以在命令行终端中使用 ABP CLIadd-package命令:

abp add-package Volo.Abp.Specifications

定义规范

假设你有一个如下定义的客户实体:

using System;
using Volo.Abp.Domain.Entities;

namespace MyProject
{
    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; }
    }
}

你可以创建一个继承自Specification<Customer>的新规范类。

示例:选择18岁以上客户的规范:

using System;
using System.Linq.Expressions;
using Volo.Abp.Specifications;

namespace MyProject
{
    public class Age18PlusCustomerSpecification : Specification<Customer>
    {
        public override Expression<Func<Customer, bool>> ToExpression()
        {
            return c => c.Age >= 18;
        }
    }
}

你只需定义一个 lambda 表达式 来定义规范。

或者,你可以直接实现ISpecification<T>接口,但Specification<T>基类使其更加简化。

使用规范

规范有两种常见用途。

IsSatisfiedBy

IsSatisfiedBy方法可用于检查单个对象是否满足规范。

示例:如果客户不满足年龄规范则抛出异常

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

namespace MyProject
{
    public class CustomerService : ITransientDependency
    {
        public async Task BookRoom(Customer customer)
        {
            if (!new Age18PlusCustomerSpecification().IsSatisfiedBy(customer))
            {
                throw new Exception(
                    "该客户不满足年龄规范!"
                );
            }
            
            //TODO...
        }
    }
}

ToExpression 和 仓储

ToExpression()方法可用于将规范用作表达式。这样,你可以在从数据库查询时使用规范来过滤实体

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.Domain.Services;

namespace MyProject
{
    public class CustomerManager : DomainService, ITransientDependency
    {
        private readonly IRepository<Customer, Guid> _customerRepository;

        public CustomerManager(IRepository<Customer, Guid> customerRepository)
        {
            _customerRepository = customerRepository;
        }

        public async Task<List<Customer>> GetCustomersCanBookRoom()
        {
            var queryable = await _customerRepository.GetQueryableAsync();
            var query = queryable.Where(
                new Age18PlusCustomerSpecification().ToExpression()
            );
            
            return await AsyncExecuter.ToListAsync(query);
        }
    }
}

规范会被正确转换为SQL/数据库查询,并在DBMS端高效执行。虽然这与规范无关,但如果你想了解更多关于AsyncExecuter的信息,请参阅 仓储 文档。

实际上,使用ToExpression()方法并不是必须的,因为规范会自动转换为表达式。这样也可以工作:

var queryable = await _customerRepository.GetQueryableAsync();
var query = queryable.Where(
    new Age18PlusCustomerSpecification()
);

组合规范

规范的一个强大特性是它们可以使用AndOrNotAndNot扩展方法进行组合。

假设你有另一个如下定义的规范:

using System;
using System.Linq.Expressions;
using Volo.Abp.Specifications;

namespace MyProject
{
    public class PremiumCustomerSpecification : Specification<Customer>
    {
        public override Expression<Func<Customer, bool>> ToExpression()
        {
            return (customer) => (customer.Balance >= 100000);
        }
    }
}

你可以将PremiumCustomerSpecificationAge18PlusCustomerSpecification组合起来,查询成年高级客户的数量,如下所示:

using System;
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.Domain.Services;
using Volo.Abp.Specifications;

namespace MyProject
{
    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(
                new Age18PlusCustomerSpecification()
                .And(new PremiumCustomerSpecification()).ToExpression()
            );
        }
    }
}

如果你想将这个组合作为另一个可重用的规范,你可以创建一个继承自AndSpecification的组合规范类:

using Volo.Abp.Specifications;

namespace MyProject
{
    public class AdultPremiumCustomerSpecification : AndSpecification<Customer>
    {
        public AdultPremiumCustomerSpecification() 
            : base(new Age18PlusCustomerSpecification(),
                   new PremiumCustomerSpecification())
        {
        }
    }
}

现在,你可以重写GetAdultPremiumCustomerCountAsync方法,如下所示:

public async Task<int> GetAdultPremiumCustomerCountAsync()
{
    return await _customerRepository.CountAsync(
        new AdultPremiumCustomerSpecification()
    );
}

通过这些示例,你看到了规范的力量。如果你稍后更改PremiumCustomerSpecification,比如将余额从100,000更改为200,000,所有查询和组合规范都将受到影响。这是减少代码重复的好方法!

讨论

虽然规范模式比C# lambda表达式更早出现,但它通常与表达式进行比较。一些开发人员可能认为它不再需要,我们可以直接将表达式传递给仓储或领域服务,如下所示:

var count = await _customerRepository.CountAsync(c => c.Balance > 100000 && c.Age => 18);

由于 ABP 的 仓储 支持表达式,这是一个完全有效的用法。你不必在你的应用程序中定义或使用任何规范,你可以直接使用表达式。

那么,规范的意义是什么?我们为什么以及何时应该考虑使用它们?

何时使用?

使用规范的一些好处:

  • 可重用性:想象一下,你需要在代码库的许多地方使用高级客户过滤器。如果你使用表达式而不创建规范,如果你稍后更改“高级客户”的定义会发生什么?假设你想将最低余额从$100,000更改为$250,000,并添加另一个条件,即客户必须超过3年。如果你使用了规范,你只需更改一个类。如果你到处重复(复制/粘贴)相同的表达式,你需要更改所有地方。
  • 可组合性:你可以组合多个规范来创建新的规范。这是另一种类型的可重用性。
  • 命名PremiumCustomerSpecification比复杂的表达式更好地解释了意图。所以,如果你有一个在你的业务中有意义的表达式,考虑使用规范。
  • 可测试性:规范是一个单独(且容易)测试的对象。

何时不使用?

  • 非业务表达式:不要将规范用于非业务相关的表达式和操作。
  • 报表:如果你只是创建报表,不要创建规范,而是直接使用IQueryable和LINQ表达式。你甚至可以使用纯SQL、视图或其他工具进行报表。DDD不一定关心报表,所以从性能的角度来看,查询底层数据存储的方式可能很重要。
在本文档中