在 DDD 中,我应该如何模拟与外部 SQL 服务器的直接交互?

In DDD, how should I model direct interaction with an external SQL Server?

在域驱动设计和 .NET 的上下文中,我应该如何模拟与远程 SQL 服务器(使用 System.Data.SqlClient.SqlConnection)的直接交互?

我需要执行诸如调用存储过程和取回 ID 之类的操作。稍后我可能需要做一些事情,比如获取实体及其 ID 的列表,这样我就可以创建具有对这些实体的软引用的本地类似物。

据我理解,DDD说这样的功能应该是域服务的一部分,如果在与外部系统交互时我们还需要检查核心业务规则,或者如果调用外部系统只是一个应用程序服务的一部分与保留核心业务不变量无关的副作用。

域服务更接近核心域,而应用程序服务比核心域更上一层。这就是我的困境:我应该把这个逻辑放在哪里?

可以在我的领域项目(核心领域模型所在的地方)中添加对 System.Data.SqlClient.SqlConnection 的引用吗?想到这里我就很难过。我觉得这不是域服务问题的一部分。

或者在我的应用程序项目(依赖于域并拥有应用程序服务)中添加对 System.Data.SqlClient.SqlConnection 的引用并创建专门的应用程序服务以与远程 SQL 服务器进行交互会更好吗?然后该应用程序服务将调用域(通过存储库,或使用域服务)以在使用 System.Data.SqlClient.SqlConnection.

在远程服务器中创建远程 ID 后在本地保存它。

你推荐什么?

编辑

我意识到我的问题可能过于宽泛或者我没有提供足够的信息。

在我使用 ABP.io 框架的解决方案中,我有这些项目:

Project Description
Acme.Bomb.Domain Core domain model, contains aggregates, entities and value objects.
Acme.Bomb.Domain.Shared Shared kernel, contains enums, constants, utils, custom attributes, and even some shared value objects that will never change depending on context, and I didn't want to duplicate their code.
Acme.Bomb.EntityFrameworkCore Contains my DbContext(s) and low lever infrastructure code for persisting data.
Acme.Bomb.EntityFrameworkCore.Migrations Contains database migration code specific to EF Core.
Acme.Bomb.HttpApi REST API implementation, exposes application services as REST endpoints.
Acme.Bomb.HttpApi.Host REST API runtime server.
Acme.Bomb.Application Application layer, contains app service implementations, including simple CRUD services or services that call to other external (REST) services, repository implementations, etc.
Acme.Bomb.Application.Contracts Application layer contracts, contains interface declarations for everything implemented in 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
    {
        // A domain service, which is not really necessary...
        // I could have done all its tasks in this app service,
        // because there is no real business logic happening in it
        // at the moment, but this is what I had refactored to
        // up to this moment of posting, so I'm including it here:
        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);

            // Use SqlConnection here to basically do a RPC:
            // 1. Call a stored procedure on the remote server,
            //    called sp_CreateCompany, which has a few parameters
            //    that I wil be filling using newCompanyRequest above.

            // var connection = new SqlConnection(...);
            // ... call the remote procedure
            // var remoteEntityId = ...GetValue<int>(...);

            // 2. If successful, I get back the ID of the new company
            //    as it was created on the remote system, and then
            // 3. I store this ID locally:

            // newCompanyRequest.RemoteReference = remoteEntityId;


            // 4. And now now I can call a domain service that takes care
            //    of the rest of the domain business regarding accepting
            //    a new company request:

            await _newCompanyRequestsManager.Accept(newCompanyRequest);
        }

        public Task Reject(RejectNewCompanyRequestDto input)
        {
            throw new NotImplementedException();
        }
    }
}

那么这会是执行 DDD 时的“最佳实践”吗?至少在你看来。我知道一个好的开发人员应该遵循模式是一种指导,而不是......一种宗教。

感谢您的帮助!

In the context of Domain Driven Design and .NET, how should I model direct interaction with a remote SQL Server (using a System.Data.SqlClient.SqlConnection)?

与您提取任何其他基础设施问题的方式完全相同。鉴于您似乎在此处连接到外部系统,我可能会认为这是一个反腐败层,但最终它只是在 domain 中定义并在 infrastructure 层中实现的接口。

所以基本上你可以期望在 domain 层中有一个类似于 ISomeService 的接口,并在 infrastructure 层中实现该接口。应用程序服务将通过该接口而不是使用 SqlConnection 与外部系统通信。尽可能确保 ISomeService 抽象是由业务概念而不是技术细节驱动的,否则它不会是一个非常有用的抽象。

查看 CollaboratorService interface and implementation 中的具体示例。