0

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.

8
  • 1
    Your persistence layer is supposed to implement an interface from your domain layer. The returned object therefore cannot change. If your domain (emphasis on domain, this is not something that is decided by the technology you use) needs paging, then it should expose models or interfaces which allow your persistence layer to implement this paging. Sep 16, 2021 at 17:41
  • 1
    Phrased differently, you stumble upon the conclusion that the returned object changed as if it was an inevitable consequence, but this statement is unsupported. Your HTTP implementation is responsible for converting the HttpResponse into domain objects. Sep 16, 2021 at 17:44
  • @VincentSavard so basically, the function in my repository would not differ from persistence implementation to persistence implementation, but within the respective implementation, I would need to override said function to force it to be compatible with it. Am I on the right track?
    – apriestley
    Sep 16, 2021 at 17:50
  • That seems correct, yes. Sep 16, 2021 at 17:53
  • Thank you. I'll see how far I can get with it.
    – apriestley
    Sep 16, 2021 at 17:55

1 Answer 1

0

[...] 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.

12
  • "Adhering to the contract of the domain model" is a great way to look at it - I appreciate that. I don't think I'd ever provide a contract that promises to bring back ALL reports however. With a rest API data source it would take way too long to fulfill, and even with a traditional DB, I'd still want to enforce throttling. I believe it should be one page per contract in a GetList scenario. But in that case, the client still needs to know about the paging information.
    – apriestley
    Sep 19, 2021 at 13:11
  • Adding caching to your repositories might be an option. Otherwise, paging could become a domain concept. In this case, it's better if the model controls page size & page number, but perhaps your HTTP API makes this difficult. In that case, just return something that has all the data (entities, page infos). Sometimes it's better to be practical than dogmatic about these things.
    – doubleYou
    Sep 19, 2021 at 16:50
  • I actually tried a domain based approach. The problem with this is that the payload for any list response comes back strongly typed. It will have paging info, status info, and data. But the key for data always reflects the type. If you request reports, it will come back as reports: [{},{},{}]. Other APIs might use a generic key, like data: [{},{},{}], which I find way easier to work with in terms of deserialization. In the former case, the only way I know of is to have a wrapper domain model for each and every entity that can come back in a list.
    – apriestley
    Sep 19, 2021 at 18:20
  • To your point about caching, what would that look like in this situation? I can think of a few concepts.. like a database sitting in front of the external API, with a staleness timer, periodically refreshing itself. Sounds very complicated though. I wouldn't know where to begin.
    – apriestley
    Sep 19, 2021 at 18:28
  • Yes, caching always makes things complicated. It may or may not work for your case. I'm not sure I understand the 'strongly typed' problem. Basically, your repository returns a class with your reports and the page information. You can make this generic. Note that the model has nothing to do with deserialization - that's all done in the repository.
    – doubleYou
    Sep 20, 2021 at 6:25

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.