基于洋葱架构的.NET Core后端:邮件通知应置于基础设施层还是应用层?
关于洋葱架构中邮件通知分层的正确实践
你的当前实现不符合洋葱/整洁架构的设计原则,问题主要出在业务逻辑的拆分和依赖方向上,以下是具体分析和修正方案:
问题所在
- 业务逻辑泄露到表示层:"收到新消息时发送邮件"是明确的业务规则,属于"发送消息"用例的一部分,不应放在表示层(控制器)中。如果后续业务规则变更(比如部分用户免通知、新增短信通知),你需要修改控制器代码,违反了单一职责和开闭原则。
- 应用层用例不完整:
UserService.SendMessage只完成了消息存储的核心逻辑,但完整用例应包含"发送消息+通知接收用户"的全流程,拆分到表示层会导致用例逻辑碎片化。 - 依赖关系不符合架构原则:洋葱架构要求内层(应用/领域层)定义抽象,外层(基础设施/表示层)实现或依赖这些抽象。你直接在表示层调用基础设施层的具体
EmailService,会导致表示层与具体实现强耦合,难以扩展。
正确的分层实现方式
1. 应用层定义抽象
在应用层创建通知服务的抽象接口,这个接口代表业务用例需要的能力,而非具体实现:
// 应用层接口 public interface INotificationService { Task NotifyUserAboutNewMessage(Message message); }
2. 基础设施层实现抽象
在基础设施层编写具体的邮件发送实现,依赖应用层的抽象,负责与外部邮件系统交互:
// 基础设施层实现 public class EmailNotificationService : INotificationService { private readonly IEmailClient _emailClient; public EmailNotificationService(IEmailClient emailClient) { _emailClient = emailClient; } public async Task NotifyUserAboutNewMessage(Message message) { // 具体的邮件发送逻辑,比如构造邮件内容、调用邮件客户端发送 var email = new EmailMessage { To = message.RecipientEmail, Subject = "你收到了新消息", Content = $"来自{message.SenderName}的消息:{message.Content}" }; await _emailClient.Send(email); } }
3. 应用层封装完整用例
将"发送消息+通知用户"的完整业务逻辑封装在应用层的用例类(或服务)中,确保业务逻辑内聚:
// 应用层用例类 public class SendMessageUseCase { private readonly IUserRepository _userRepository; private readonly INotificationService _notificationService; public SendMessageUseCase(IUserRepository userRepository, INotificationService notificationService) { _userRepository = userRepository; _notificationService = notificationService; } public async Task<MessageResult> Execute(Message message) { // 应用层验证逻辑(比如检查消息合法性、用户权限) var validationErrors = ValidateMessage(message); if (validationErrors.Any()) { return MessageResult.Failure(validationErrors); } // 执行核心业务逻辑:保存消息 var result = await _userRepository.SendMessage(message); // 执行通知逻辑(业务规则的一部分) await _notificationService.NotifyUserAboutNewMessage(message); return result; } private List<string> ValidateMessage(Message message) { var errors = new List<string>(); if (string.IsNullOrWhiteSpace(message.Content)) { errors.Add("消息内容不能为空"); } if (message.RecipientId == Guid.Empty) { errors.Add("接收用户ID无效"); } return errors; } }
4. 表示层仅调用应用层用例
控制器只负责处理HTTP请求/响应、DTO转换,调用应用层的用例即可,不包含任何业务逻辑:
// 表示层控制器 [ApiController] [Route("messages")] public class MessagesController : ControllerBase { private readonly SendMessageUseCase _sendMessageUseCase; private readonly IMapper _mapper; public MessagesController(SendMessageUseCase sendMessageUseCase, IMapper mapper) { _sendMessageUseCase = sendMessageUseCase; _mapper = mapper; } [HttpPost] public async Task<IActionResult> SendMessage(CreateMessageDto dto) { var message = _mapper.Map<Message>(dto); var result = await _sendMessageUseCase.Execute(message); if (!result.IsSuccess) { return BadRequest(result.Errors); } return Ok(result.MessageId); } }
为什么这样符合洋葱架构
- 依赖方向正确:基础设施层依赖应用层的抽象,应用层不依赖任何外层实现,符合洋葱架构"内层定义规则,外层提供实现"的核心原则。
- 业务逻辑内聚:完整的用例逻辑封装在应用层,避免了逻辑碎片化,便于维护和修改。
- 扩展性强:如果后续需要新增通知方式(比如短信、APP推送),只需在基础设施层新增
INotificationService的实现,通过依赖注入替换即可,无需修改应用层和表示层代码。
内容的提问来源于stack exchange,提问作者alEN
相关产品推荐
相关产品推荐

