2

The question is about a desktop application I'm creating in C# and WPF. As very common I'm using the repository pattern in my Data Access Layer for my CRUD operations. All data comes from the serverside part of the application, using a REST API.

For each repository an interface is defined. So the usercode of the repositories can rely on some consistency. You can think of my repository as ApiUserRepository : IUserRepository. If anywhere in the future another repository implementation is necessary, like FileUserRepository : IUserRepository there is no problem about incompatibility because of the principle of interfaces at all.

The repositories actually wrap API calls, and return the result of course. The methods for data operations (like GetUserById(...)) are all async.

At this time, I just return the data from the repository methods. So the GetUserById(...) method returns a User object. If the user is not found, it returns null.

Each interation with a repository can produce four types of results, I take a reading operation as example:

  1. The requested data is returned.
  2. The requested data was not found (I return null or an empty list).
  3. The data source (in this case REST API) that's used by the repository, returns an error saying the API is called the wrong way or invalid (user) input is provided.
  4. There was a technical exceptional error, Like Bad HTTP Request, Time-out, Internal server error, etc.)

Case 1. and 2. are already handled, because the return value of a repository method just contains either the data or a null (or empty list).

My question is about how to handle case 3. and 4. in a repository design: how to communicate an error message coming from the data source (in this case REST API) to the usercode of the repository? In case 3. the API server responds with an error JSON object. In case 4 the repository can detect this via other ways. The repository can easily interpret all this and that's not my question. My question is about how to communicate this error to outside the repository, so the code that uses the repo can handle this further (and display an error message on screen for example). In case 4. we can say using exceptions is justified, but case 3...

The return value of a repository method can not be used anymore because it's already utilized for case 1. and 2.

Throwing exceptions from the repository for case 3. seems to be ugly to me.

What would be the best way to solve this design problem and keep it all clean?

Don't consider my repositories as just 'HTTP clients'. It just has to be able to inform the outside world if something went wrong, and what message was responded from the server.

2
  • 2
    Our predefined close reason "Needs more focus" has an additional text saying "This question currently includes multiple questions in one. It should focus on one problem only.". So if you want to reduce the chance for this post to get closed, I would recommend to split this into two questions, not just one. This gives people a chance to vote and comment on both questions differently.
    – Doc Brown
    Feb 14, 2021 at 16:51
  • I edited the question. I hope it's clearer now. Feb 18, 2021 at 17:28

2 Answers 2

5

Repositories are elements of the domain and for this reason, I would limit the interface (arguments and returning types) to elements of this layer.

Between null and throwing exceptions, the first seems the lesser evil to me because handling "alternative" execution paths by exceptions can be more complicated (ignore? log? re-throw? log and re-throw? catch later? early? exit?) than checking nulls.

Deciding whether null is an acceptable response or not, seems someone else's problem. The repository should not assume that nulls are not supported by the consumer.

Concerning the 3rd option, it seems too complicated. It's a sort of good for-all-purposes interface. It reminds me of an HTTP Client more than a repository. Hiding the null behind a wrapper avoids null checking and possible null pointers, but it only turns one kind of checking into another and it doesn't remove the need of transforming KOs into something else if response.ko //do something -

Edit: find() vs load()

You could, however, implement an additional method in the repository which must return something or fail. For example loadUser(). Differences lay in the semantics.

While find implies search and search imply the possibility of 0 results, load implies that whoever is calling got some proof that the element must exist and not finding the entry is problematic at this point.

Like searching and loading a non-existing file. The first might result in 0 files found, the second is likely to fail. The file could have been removed by another execution before you open it so your load process must end abruptly.

4
  • Please do not copy Wikipedia text into tag wikis and tag wiki excerpts. You did so for all your suggested tag wiki edits so far, and violate the WP license. Tag wikis instead should contain information that helps the Software Engineering community, specifically and are not replacements for Wikipedia or other external sources, and tag wikis are there to help users figure out when to use a tag; also see the help centre information on how to write useful excerpts. Feb 23, 2021 at 9:47
  • @MartijnPieters you can address the claim to the meta site instead for moderators to review / edit or reject the edit.
    – Laiv
    Feb 23, 2021 at 15:18
  • I have done both; I flagged your edits, and rejected the most recent ones that are still pending. Note that I am a moderator on Stack Overflow as well. I thought you might find it helpful to learn that such tag edits are not normally accepted here rather than just have the edits disappear from under you. Feb 23, 2021 at 15:20
  • @MartijnPieters sure! Thanks for the insights!
    – Laiv
    Feb 23, 2021 at 17:49
3

The repository pattern is defined as:

REPOSITORY: A mechanism for encapsulating storage, retrieval, and search behavior which emulates a collection of objects.
- Eric Evans in DDD

Getting objects from the repository

Typically, you'd have methods to retrieve items such as getByID() returning a single item, getAll(), or some kind of find() returning a collection of items that match some criteria.

Let's start with the collection based methods:

  • Suppose that your repository is empty. getAll() would then be expected to return an empty set. This emulates best a collection of objects.

  • Suppose now that you query using some criteria that isn't met by any record. Again, the most logic return would be an empty collection. If you would return an error insted, you'd risk to be inconsistent: what would you return for a dynamic query that asks for no specific criteria on an empty repository without contracting the equivalent behavior of getAll()?

For the item based getById(). Here the choices are not so clear cut:

  • You could argue that the single item is just a convenience, to avoid working with collections made of a single element. The best option would be to return a null when nothing is found (i.e. similar to an empty set, for no items). This is in particular useful if IDs are managed externally (e.g. provided by user, or meaningful) and if the developers are used to handle the returned result with caution.
  • You could as well argue that getById() is in general used with known IDs and it's in general assumed that they exist. In this situation a null might be unexpected and generate risks if it is forgotten only once to check the validity of the returned object. In this situation, the best option would be to throw an exception.

So the choice should be driven by the practices and expectation in your context. Personally I have a preference for the null although it requires more discipline.

Changing the content of the repository

Typically you'd have some kind of add(), remove(), and perhaps update() that would change the repository content. When you use these function, you generally expect that they work.

A frequent practice is to have methods with no return value (void). In this case, you have no choice but to raise an exception in case of an unexpected error. This is very practical in fact, if you have to manage transactions that span several repositories, since you can easily catch any error in a transaction and rollback the whole transaction.

But you'll also find some repository implementations that provide these methods but returning an error code instead. It's more flexible for error handling, no problem, but if most of the time you need to check the error code and raise an exception to rollback some transactions, you'll end-up wiht lot of boiler-plate code.

The folowing strong arguments are in favor of the first approach:

I personally don’t like these methods to answer Boolean results as do full-fledged collections. That’s because in some cases answering true to an add-type operation does not guarantee success. The true results may still be subject to a transaction commit on the data store. Thus, void may be the more accurate return type in the case of a Repository.
- Vaughn Vernon in Implementing DDD

Your additional question about validation

The scenario that you describe is not crystal clear, and does not seem to be related to the repository pattern itself. Unfortunately we know to little about your specific case and I invite you to formulate a distinct question only focused on that last aspect. Nevertheless here some first thoughts:

Performing validations in the front-end to guide user, but nevertheless doing a thorough validation in the back-end is a very common situation (You cannot rely on front-end validation for ensuring corporate consistency).

An error in the back-end validation does not fatally have to cause user entry to get lost. You may probably structure the interaction in such a way to inform the user of the back-end problem and propose to edit/resubmit the data. THere are more tricky strategies to cope with concurrency errors.

Your Answer

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

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