Encapsulation of External API in Infrastructure Layer as Persistence

abstractiondomain-driven-designonion-architecturepersistencerepository-pattern

My question is about DDD, the Infrastructure layer, it's relation to the Domain, and specifically how we can take advantage of the ability to "swap out" one persistence implementation for another.

I have a .NET5 solution designed using Onion Architecture, that follows all of your basic guidelines:

  • A Domain layer with zero dependencies to define the structure of the data you are moving from point A to point B
  • An Application layer which defines the ways in which the data moves (queries and commands)
  • An Infrastructure layer which implements those definitions
  • And an API layer which exposes the application layer commands and queries

Usually, when examples of an Infrastructure layer are given, the persistence is implemented using a database. This makes a lot of sense because when you are creating a tutorial for creating microservices using onion architecture for each service, you want to have full control over your persistence layer so that years from now, it will still work.

But what about if you wanted to swap out the database for an external REST API? For example, if you have a base repository for a DB Context class with a function:

public async Task<T> AddAsync(T entity)
{
    _dbContext.Set<T>().Add(entity);
    await _dbContext.SaveChangesAsync();
    return entity;
}

It is conceivable to swap out the DB context for a REST client like so:

public async Task<HttpResponse> AddAsync(T entity)
{
    var response = await _httpClient.PostAsJsonAsync<T>(CreateFormData(entity));
    return response;
}

(Where "CreateFormData" would return a FormUrlEncodedContent object created using the entity passed in.)

But as you can see, the return object has changed. Instead of passing back the entity, we need to pass the HttpResponse object, because it might have metadata that we need to use later on.

One example of this would be returning a list of items. The json returned from the http call will more than likely have paging data (the page number we are on, how many pages are left, etc.), and also response status info (error codes, success codes). Where can we encapsulate this? My first thought was to build it into the domain. So you would have a special entity for Response Status and a special entity for Paging data, and a Root object to contain those two, as well as the entity you are working with. But I don't think this is a proper use for the domain, because things like paging and http response data are implementation details. You'd never have a database with those objects as tables, and so they would not be part of any domain.

So where can we put these response wrappers? And how can we apply them so that the persistence is truly swap-able?

Here's a screen-cap of the relevant piece of my solution in Visual Studio:

enter image description here

Just want to add something pertaining to an answer from doubleYou.
I think the implementation should adhere to the restrictions imposed by the datasource. For instance, if the max count allowed by the rest API is 100, I don't think we should be chaining requests together to increase that amount and "fake" the response as a singular list when it was really many lists.
We should also utilize the tools that the datasource provides. If the default response list size is 10, but the API allows a query string to specify that we want 100, we should use that instead of making ten requests and chaining the responses together.

I think what I want is a wrapper specific to the implementation. Here's an example of what I'm thinking:

public record ResponseWrapper<T>(
    int PageNumber,
    int TotalPages,
    int PageSize,
    T[] Data
) where T : Entity;

Similarly, we could do the same with a request wrapper to provide the application with the options the datasource provides.

Best Answer

[...] things like paging and http response data are implementation details.

That's the key insight. Paging is not needed by your domain - otherwise you'd have implemented it in the database as well.

Let's say you have a repository interface such as this:

interface IReportRepository // or just `IReports`
{
    // May throw `RepositoryException`
    IEnumerable<Report> GetAllReports();
}

From your domain model's perspective, there's no such thing as getting only some of the data. If the model requests all reports via GetAllReports(), your HttpReportRepository has to find a way to provide them - otherwise, it's violating the contract.

It can do this by making a request for each page internally and combining the results. It also won't return anything related to HTTP. Any errors, including HTTP status code like 404 and 500, cause a RepositoryException to be thrown.

Here's an example of how this could look like:

class HttpReportRepository : IReportRepository
{
    public IEnumerable<Report> GetAllReports()
    {
        var reports = new List<Report>();
        var page = 1;

        try {
            while (true) {
                // apiResult is basically your wrapper object
                var apiResult = _httpApi.GetReports(page);

                // deal with 404, 500, etc.
                if (apiResult.HttpStatus != 200) {
                    throw new RepositoryException();
                }

                // combine all reports
                reports.AddRange(apiResult.RetrievedEntities);

                // deal with API pecularities, such as paging
                if (page == apiResultData.PageCount) {
                    return reports;
                }

                page += 1;
            }
        } catch(SocketException ex) {
            throw new RepositoryException(ex);
        }
    }
}

Responding to your edit/comments:

I think the implementation should adhere to the restrictions imposed by the datasource.

No. The repository implementation must adhere to the contract defined by the domain model. The domain model should not consider the restrictions of the data source, because it does now "know" which data source(s) are used. That's the whole point.

As mentioned, you might want to deviate from this for practical considerations, but that's idea.

Your ResponseWrapper looks reasonable. In principle, the repository can be generic as well, but maybe it shouldn't be. Anyway, I think that's a separate question.