应用服务最佳实践与规范
本文档基于领域驱动设计原则,为在您的模块和应用程序中实现应用服务类提供最佳实践。
请务必首先阅读 应用服务 文档。
总体原则
- 务必为每个聚合根创建一个应用服务。
应用服务接口
- 务必在应用契约包中为每个应用服务定义一个
接口。 - 务必继承
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)。将为引用属性添加详细信息。
- 例外1:出于安全原因可以排除某些属性(例如
- 为每个引用属性包含一个基础 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 组件,如
IFormFile或Stream。如果您想处理文件,可以使用byte[]。 - 务必使用
Controller来处理文件上传,然后将文件的byte[]传递给应用服务方法。
使用其他应用服务
- 切勿使用同一模块/应用程序的其他应用服务。相反:
- 使用领域层来执行所需任务。
- 在必要时,提取一个新类并在应用服务之间共享以实现代码复用。但要小心,不要耦合两个用例。它们起初可能看起来很相似,但随着时间的推移可能会向不同的方向发展。因此,请谨慎使用代码共享。
- 可以使用其他模块的应用服务,仅当:
- 它们是另一个模块/微服务的一部分。
- 当前模块仅引用了所使用模块的应用契约包。
抠丁客


