项目

应用服务最佳实践与规范

本文档基于领域驱动设计原则,为在您的模块和应用程序中实现应用服务类提供最佳实践。

请务必首先阅读 应用服务 文档。

总体原则

  • 务必为每个聚合根创建一个应用服务。

应用服务接口

  • 务必应用契约包中为每个应用服务定义一个接口
  • 务必继承 IApplicationService 接口。
  • 务必在接口名称后使用 AppService 后缀(例如:IProductAppService)。
  • 务必为服务的输入和输出创建 DTO(数据传输对象)。
  • 切勿在服务方法中获取/返回实体。
  • 务必根据 DTO 最佳实践 定义 DTO。

输出

  • 避免为相同或相关实体定义过多的输出 DTO。相反,为一个实体定义基础DTO 和详细DTO。

基础 DTO

务必为聚合根定义一个基础DTO。

  • 包含聚合根上所有的原始属性
    • 例外:出于安全原因可以排除某些属性(例如 User.Password)。
  • 包含实体的所有子集合,其中集合中的每个项都是一个简单的关系 DTO
  • 继承自聚合根(以及实现 IHasExtraProperties 的实体)的可扩展实体 DTO 类之一。

示例:

[Serializable]
public class IssueDto : ExtensibleFullAuditedEntityDto<Guid>
{
    public string Title { get; set; }
    public string Text { get; set; }
    public Guid? MilestoneId { get; set; }
    public Collection<IssueLabelDto> Labels { get; set; }
}

[Serializable]
public class IssueLabelDto
{
    public Guid IssueId { get; set; }
    public Guid LabelId { get; set; }
}

详细 DTO

如果实体引用了其他聚合根,务必为该实体定义一个详细DTO。

  • 包含实体上所有的原始属性
    • 例外1:出于安全原因可以排除某些属性(例如 User.Password)。
    • 例外2:务必排除引用属性(如上面示例中的 MilestoneId)。将为引用属性添加详细信息。
  • 为每个引用属性包含一个基础 DTO 属性。
  • 包含实体的所有子集合,其中集合中的每个项都是相关实体的基础 DTO

示例:

[Serializable]
public class IssueWithDetailsDto : ExtensibleFullAuditedEntityDto<Guid>
{
    public string Title { get; set; }
    public string Text { get; set; }
    public MilestoneDto Milestone { get; set; }
    public Collection<LabelDto> Labels { get; set; }
}

[Serializable]
public class MilestoneDto : ExtensibleEntityDto<Guid>
{
    public string Name { get; set; }
    public bool IsClosed { get; set; }
}

[Serializable]
public class LabelDto : ExtensibleEntityDto<Guid>
{
    public string Name { get; set; }
    public string Color { get; set; }
}

输入

  • 切勿在输入 DTO 中定义任何未在服务类中使用的属性。
  • 切勿在应用服务方法之间共享输入 DTO。
  • 切勿让一个输入 DTO 类继承自另一个输入 DTO 类。
    • 可以从一个抽象的基础 DTO 类继承,并以这种方式在不同 DTO 之间共享一些属性。但是,在这种情况下必须非常小心,因为操作基础 DTO 会影响所有相关的 DTO 和服务方法。作为良好实践,应避免这样做。

方法

  • 务必将服务方法定义为异步方法,并使用 Async 后缀。
  • 切勿在方法名中重复实体名称。
    • 示例:在 IProductAppService 中定义 GetAsync(...),而不是 GetProductAsync(...)

获取单个实体

  • 务必使用 GetAsync 方法名
  • 务必使用原始方法参数获取 Id。
  • 返回详细 DTO。示例:
Task<QuestionWithDetailsDto> GetAsync(Guid id);

获取实体列表

  • 务必使用 GetListAsync 方法名
  • 务必获取一个单独的 DTO 参数用于过滤排序分页(如果需要)。
    • 务必尽可能实现可选过滤器。
    • 务必将排序和分页属性实现为可选,并提供默认值。
    • 务必限制最大分页大小(出于性能原因)。
  • 务必返回一个详细 DTO 列表。示例:
Task<List<QuestionWithDetailsDto>> GetListAsync(QuestionListQueryDto queryDto);

创建新实体

  • 务必使用 CreateAsync 方法名
  • 务必获取一个专门的输入 DTO 来创建实体。
  • 务必让 DTO 类继承自 ExtensibleObject(或任何其他实现 IHasExtraProperties 的类),以便在需要时允许传递额外属性。
  • 务必使用数据注解进行输入验证。
    • 尽可能在领域层之间共享常量(通过定义在领域共享包中的常量)。
  • 务必为新创建的实体返回详细 DTO。
  • 务必只要求创建实体的最少信息,但提供将其他属性设置为可选属性的可能性。

示例方法

Task<QuestionWithDetailsDto> CreateAsync(CreateQuestionDto questionDto);

相关的DTO

[Serializable]
public class CreateQuestionDto : ExtensibleObject
{
    [Required]
    [StringLength(QuestionConsts.MaxTitleLength,
                  MinimumLength = QuestionConsts.MinTitleLength)]
    public string Title { get; set; }
    
    [StringLength(QuestionConsts.MaxTextLength)]
    public string Text { get; set; } //Optional
    
    public Guid? CategoryId { get; set; } //Optional
}

更新现有实体

  • 务必使用 UpdateAsync 方法名
  • 务必获取一个专门的输入 DTO 来更新实体。
  • 务必让 DTO 类继承自 ExtensibleObject(或任何其他实现 IHasExtraProperties 的类),以便在需要时允许传递额外属性。
  • 务必将实体的 Id 作为一个独立的原始参数获取。不要包含在更新 DTO 中。
  • 务必使用数据注解进行输入验证。
    • 尽可能在领域层之间共享常量(通过定义在领域共享包中的常量)。
  • 务必为更新后的实体返回详细 DTO。

示例:

Task<QuestionWithDetailsDto> UpdateAsync(Guid id, UpdateQuestionDto updateQuestionDto);

删除现有实体

  • 务必使用 DeleteAsync 方法名
  • 务必使用原始方法参数获取 Id。示例:
Task DeleteAsync(Guid id);

其他方法

  • 可以定义其他方法来对实体执行操作。示例:
Task<int> VoteAsync(Guid id, VoteType type);

此方法对问题进行投票并返回问题的当前得分。

应用服务实现

  • 务必将应用层开发为完全独立于 Web 层
  • 务必应用层实现应用服务接口。
    • 务必使用命名约定。例如:为 IProductAppService 接口创建 ProductAppService 类。
    • 务必继承自 ApplicationService 基类。
  • 务必将所有公共方法设为 virtual,以便开发人员可以继承和重写它们。
  • 切勿创建私有方法。相反,应将它们设为 protected virtual,以便开发人员可以继承和重写它们。

使用仓储

  • 务必使用专门设计的仓储(如 IProductRepository)。
  • 切勿使用泛型仓储(如 IRepository<Product>)。

查询数据

  • 切勿在应用服务方法内部使用 LINQ/SQL 从数据库查询数据。从数据源执行 LINQ/SQL 查询是仓储的职责。

额外属性

  • 务必使用 MapExtraPropertiesTo 扩展方法(参见)或配置对象映射器(MapExtraProperties),以允许应用程序开发人员能够扩展对象和服务。

操作 / 删除实体

  • 务必始终从仓储中获取所有相关实体以对其执行操作。
  • 务必在更新实体后调用仓储的 Update/UpdateAsync 方法。因为并非所有数据库 API 都支持更改跟踪和自动更新。

处理文件

  • 切勿在应用服务中使用任何 Web 组件,如 IFormFileStream。如果您想处理文件,可以使用 byte[]
  • 务必使用 Controller 来处理文件上传,然后将文件的 byte[] 传递给应用服务方法。

使用其他应用服务

  • 切勿使用同一模块/应用程序的其他应用服务。相反:
    • 使用领域层来执行所需任务。
    • 在必要时,提取一个新类并在应用服务之间共享以实现代码复用。但要小心,不要耦合两个用例。它们起初可能看起来很相似,但随着时间的推移可能会向不同的方向发展。因此,请谨慎使用代码共享。
  • 可以使用其他模块的应用服务,仅当:
    • 它们是另一个模块/微服务的一部分。
    • 当前模块仅引用了所使用模块的应用契约包。

另请参阅

在本文档中