Search code examples
.netarchitecturedomain-driven-designsqlconnectionabp-framework

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


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)?

I need to do things like call a stored procedure and get back an ID. Later I might need to do things like getting a list of entities and their IDs so I can create local analogues with soft references to those.

By my understanding, DDD says such functionality should be part of domain services, if while interacting with the external system we also need to check core business rules, or be part of application services if calling the external system is just a side-effect that has no bearing on preserving the core business invariants.

Domain services are closer to the core domain, while application services are a layer up from that. Here's my dilema then: where should I put this logic?

Is it okay to add a reference to System.Data.SqlClient.SqlConnection in my Domain project (which is where the core domain model is)? I feel bad thinking about this. I feel like this is not part of the domain service concern.

Or is better to add this reference to System.Data.SqlClient.SqlConnection in my Application project (which depends on Domain and holds application services) and create a specialised application service for interacting with the remote SQL Server? And then that application service will call into the domain (via a repository, or use a domain service) to locally save the remote ID after it has been created in the remote server by using System.Data.SqlClient.SqlConnection.

What do you recommend?

EDIT

I realised that my question might be too broad or I'm not giving enough information.

In my solution, which is using the ABP.io framework, I have these projects:

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.

What I want to do is write an application service that uses SqlConnection directly, like this:

// 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();
        }
    }
}

So would this be "best practice" when doing DDD? At least in your opinion. I know a good developer should follow patterns are a guidance, and not... a religion.

Thank you for any help given!


Solution

  • 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)?

    Exactly the same way you'd abstract any other infrastructure concerns. Given you seem to be connecting to an external system here I'd probably consider this an anti-corruption layer, but in the end it's just an interface defined in the domain and implemented in the infrastructure layer.

    So basically you can expect to have an interface like ISomeService in the domain layer and an implementation of that interface in the infrastructure layer. The application service would communicate with the external system through that interface rather than using a SqlConnection. Make sure that the ISomeService abstraction is driven by business concepts rather than technical details as much as possible or else it won't be a very useful abstraction.

    Have a look at the CollaboratorService interface and implementation for a concrete example.