You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在领域驱动设计(DDD)与.NET环境下,如何建模远程SQL Server的直接交互?(基于ABP.io框架的最佳实践咨询)

问题:DDD+ABP.io框架下,远程SQL Server交互的层级建模困惑

我正在基于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.HttpApiREST API实现,将应用服务暴露为REST端点
Acme.Bomb.HttpApi.HostREST 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.29 14:07:32