在领域驱动设计(DDD)与.NET环境下,如何建模远程SQL Server的直接交互?(基于ABP.io框架的最佳实践咨询)
我正在基于ABP.io框架做项目,需要在DDD与.NET技术栈背景下,建模使用System.Data.SqlClient.SqlConnection与远程SQL Server的直接交互。需求是调用存储过程获取返回的ID,之后获取实体列表及其ID来创建带软引用的本地副本。
根据我的理解,DDD里这类功能的归属规则是:如果和外部系统交互时需要检查核心业务规则,功能属于领域服务;如果只是不影响核心业务不变量的副作用操作,就归为应用服务。领域服务贴近核心领域,应用服务在领域层之上。
我的困惑点:
- 这种远程SQL交互逻辑应该放在哪个层级?
- 能不能在核心领域模型项目
Acme.Bomb.Domain里添加System.Data.SqlClient.SqlConnection的引用?我感觉这不属于领域服务的职责。 - 是不是更好的做法是在依赖Domain层的
Acme.Bomb.Application项目里加引用,创建专门的应用服务处理远程交互,再调用领域层(比如仓储或领域服务)保存远程生成的ID?
补充项目架构(基于ABP.io):
| 项目名称 | 描述 |
|---|---|
| Acme.Bomb.Domain | 核心领域模型,包含聚合根、实体和值对象 |
| Acme.Bomb.Domain.Shared | 共享内核,包含枚举、常量、工具类、自定义属性,以及不随上下文变化的共享值对象(避免代码重复) |
| Acme.Bomb.EntityFrameworkCore | 包含DbContext及用于数据持久化的底层基础设施代码 |
| Acme.Bomb.EntityFrameworkCore.Migrations | 针对EF Core的数据库迁移代码 |
| Acme.Bomb.HttpApi | REST API实现,将应用服务暴露为REST端点 |
| Acme.Bomb.HttpApi.Host | REST API运行时服务器 |
| Acme.Bomb.Application | 应用层,包含应用服务实现(如简单CRUD服务、调用外部REST服务的服务)、仓储实现等 |
| Acme.Bomb.Application.Contracts | 应用层契约,包含Acme.Bomb.Application中所有实现的接口声明 |
我写了一个直接用SqlConnection的应用服务示例代码:
// Project: Acme.Bomb.Application.Contracts // File: INewCompanyRequestAppService.cs namespace Acme.Bomb.CompanyCatalog.Aggregates { public interface INewCompanyRequestAppService : ICrudAppService< NewCompanyRequestDto, Guid, PagedAndSortedResultRequestDto, CreateNewCompanyRequestDto, UpdateNewCompanyRequestDto> { Task Accept(AcceptNewCompanyRequestDto input); Task Reject(RejectNewCompanyRequestDto input); } }
// Project: Acme.Bomb.Application // File: NewCompanyRequestAppService.cs namespace Brokenthorn.BphNomenclatureManager.CompanyCatalog.Aggregates { public class NewCompanyRequestAppService : CrudAppService< NewCompanyRequest, NewCompanyRequestDto, Guid, PagedAndSortedResultRequestDto, CreateNewCompanyRequestDto, UpdateNewCompanyRequestDto>, INewCompanyRequestAppService { private readonly NewCompanyRequestsManager _newCompanyRequestsManager; public NewCompanyRequestAppService( IRepository<NewCompanyRequest, Guid> newCompanyRequestsRepository, NewCompanyRequestsManager newCompanyRequestsManager) : base(newCompanyRequestsRepository) { Check.NotNull(newCompanyRequestsRepository, nameof(newCompanyRequestsRepository)); Check.NotNull(newCompanyRequestsManager, nameof(newCompanyRequestsManager)); _newCompanyRequestsManager = newCompanyRequestsManager; } public async Task Accept(AcceptNewCompanyRequestDto input) { var newCompanyRequest = await GetEntityByIdAsync(input.Id); // 在此使用SqlConnection执行RPC操作: // 1. 调用远程服务器上名为sp_CreateCompany的存储过程, // 使用上述newCompanyRequest填充若干参数。 // var connection = new SqlConnection(...); // ... 调用远程存储过程 // var remoteEntityId = ...GetValue<int>(...); // 2. 若调用成功,获取远程系统中创建的新公司ID, // 3. 然后在本地存储该ID: // newCompanyRequest.RemoteReference = remoteEntityId; // 4. 现在调用领域服务处理接受新公司请求的其余领域业务: await _newCompanyRequestsManager.Accept(newCompanyRequest); } public Task Reject(RejectNewCompanyRequestDto input) { throw new NotImplementedException(); } } }
想请教这种实现方式符合DDD最佳实践吗?我知道模式是指导而非教条,感谢任何帮助!
先给你个明确的结论:你的实现方向是符合DDD和ABP.io架构规范的,但还有几个细节可以优化得更贴合最佳实践。
1. 交互逻辑的层级归属
你对领域服务/应用服务的划分理解是完全正确的:远程SQL调用属于外部系统交互的副作用操作,不涉及核心业务规则的校验(核心规则已经在NewCompanyRequestsManager里处理),所以放在应用层是完全合理的。
领域层只应该关注核心业务逻辑——比如“接受新公司请求时必须满足哪些业务条件”,而不关心“这个请求要同步到哪个外部系统、用什么技术同步”。把远程调用放在应用层,能保持领域层的纯净性,避免它依赖具体的外部技术实现。
2. 绝对不要在Domain层引用SqlConnection
你的直觉是对的!System.Data.SqlClient.SqlConnection属于基础设施层的具体技术实现,核心领域层(Acme.Bomb.Domain)绝对不能依赖它。领域层应该是整个系统最稳定、最不依赖外部技术的部分,只依赖领域自身的概念和规则。如果把SqlConnection塞进Domain层,会导致领域模型和具体的数据库技术绑定,完全违背了DDD“隔离核心领域”的初衷。
3. 你的实现可以优化的几个点
(1)把远程SQL交互封装成独立的基础设施服务
虽然现在直接写在应用服务里能跑,但更好的做法是把远程SQL的操作封装成一个专门的服务(比如IRemoteCompanyRepository或ICompanyRemoteService),放在基础设施层或者应用层的附属基础设施文件夹里。这样做的好处:
- 解耦:应用服务只依赖抽象接口,不依赖具体的SqlConnection实现,后续如果要把远程SQL换成REST API或者其他协议,只需要换实现类就行,不用改应用服务代码。
- 可测试:单元测试时可以轻松mock这个远程服务,不用真的连远程数据库。
示例思路:
// 先在Application.Contracts里定义抽象接口 public interface IRemoteCompanyService { Task<int> CreateCompanyAsync(CompanyCreationParams parameters); Task<List<RemoteCompanyDto>> GetCompanyListAsync(); } // 在基础设施层(比如新建Acme.Bomb.Infrastructure.RemoteSql项目,或者放在Acme.Bomb.EntityFrameworkCore里)实现这个接口 public class RemoteCompanySqlService : IRemoteCompanyService { private readonly string _remoteConnectionString; public RemoteCompanySqlService(IConfiguration configuration) { _remoteConnectionString = configuration.GetConnectionString("RemoteSqlServer"); } public async Task<int> CreateCompanyAsync(CompanyCreationParams parameters) { using var connection = new SqlConnection(_remoteConnectionString); await connection.OpenAsync(); // 调用sp_CreateCompany存储过程,返回ID // ...具体实现 } // GetCompanyListAsync实现... } // 然后在应用服务里注入这个接口 public class NewCompanyRequestAppService : CrudAppService<...>, INewCompanyRequestAppService { private readonly NewCompanyRequestsManager _newCompanyRequestsManager; private readonly IRemoteCompanyService _remoteCompanyService; public NewCompanyRequestAppService( IRepository<NewCompanyRequest, Guid> repo, NewCompanyRequestsManager manager, IRemoteCompanyService remoteCompanyService) : base(repo) { // ...赋值 } public async Task Accept(AcceptNewCompanyRequestDto input) { var newCompanyRequest = await GetEntityByIdAsync(input.Id); // 调用封装好的远程服务 var remoteEntityId = await _remoteCompanyService.CreateCompanyAsync(MapToParams(newCompanyRequest)); newCompanyRequest.RemoteReference = remoteEntityId; await _newCompanyRequestsManager.Accept(newCompanyRequest); // 别忘了保存到本地仓储 await Repository.UpdateAsync(newCompanyRequest); } }
(2)确保领域层只处理业务规则
你的NewCompanyRequestsManager设计得很好,把核心业务逻辑(比如接受请求时的状态校验、事件发布等)放在领域服务里,应用服务只负责协调外部交互和领域逻辑的调用。继续保持这个分离,不要把任何外部交互的代码塞进领域服务。
(3)处理远程调用的异常和事务
远程调用可能会失败,你需要考虑异常处理:比如远程存储过程调用失败时,要不要回滚本地的状态变更?ABP.io提供了事务机制,你可以用[UnitOfWork]特性或者手动管理事务,确保本地操作和远程操作的一致性(如果需要的话)。
最后总结
你的当前实现已经符合DDD的核心原则,只要把远程SQL交互封装成独立的基础设施服务,就能让代码更整洁、更易于维护和扩展。记住DDD的核心是隔离核心领域,让领域模型不被外部技术干扰,你的方向完全正确,细节优化后就非常标准了。
内容的提问来源于stack exchange,提问作者Paul-Sebastian Manole

