Layering Patterns on Repositories
This week we're sticking to the patterns and repositories theme. I started down the design patterns path with Episode 17 so start at least from there if you want to listen to the sequence more-or-less in order. In this episode, we'll look at some combinations with other patterns that make using the Repository pattern even more attractive.
Sponsor - DevIQ
Thanks to DevIQ for sponsoring this episode! Check out their list of available courses and how-to videos.
Show Notes / Transcript
Last week I mentioned how the Repository pattern works well with the Strategy pattern, so that you can use dependency injection with it. This is the first of many ways in which you can combine Repository with other patterns, and is probably the most powerful of them all (though probably taken for granted by many). I described the strategy pattern in episode 19. Let's look at two other patterns now that we can combine with Repository.
First, let's talk about a common pain point with repositories: custom queries. I talked about the need to encapsulate query logic so it doesn't leak out of your repository in episode 18. However, I saved a powerful technique for doing so until this tip. (Sorry, there's only so much I can put into each of these and keep them short.) If you follow my earlier advice and you don't leak query logic outside of your repositories, it's likely your repository implementations have a bunch of custom query methods. Maybe you have standard GetById and List methods, but then you also have ListByState, ListByModel, ListByOwner, etc. Maybe you have methods that correlate directly to business use cases or even UI concerns, like ListPremiumCustomers or ListForSearchScreen. The point is, you may find yourself with the code smell of too many one-off custom query methods on your repositories. This is pretty common, and the worse it gets the more cumbsersome it becomes to work with the repositories.
The solution to this problem is to introduce another pattern. The Specification pattern is designed to encapsulate a query within an object. I mentioned it briefly in episode 24. It's especially useful when you're using an ORM tool like EF or EF Core because not only can you encapsulate filter expressions, but you can also specify which properties to eager load. Thus, you can create a specification for a shopping basket type that might be called
BasketWithItemsByCustomerId or something similar. A typical specification I use will include the filter expression (to be used with a Where LINQ expression) and will let me specify which properties and subproperties I want the query to return with it.
What are the benefits of using this pattern? First, you eliminate duplication of query logic if you were previously letting client code create queries on the fly. Second, you establish a library of known queries that your development team can review, reuse, and discuss. These should be organized so they're extremely discoverable so there's minimal need to try and reinvent the wheel when someone needs a particular query that already exists. They also help clean up your repositories, eliminating most of the scenarios where you would need non-generic repository methods, and thus dramatically reducing how many repository implementations you need to write and maintain. Your repository code will better follow the Single Responsibility Principle and the Open/Closed Principle, and you won't need a bunch of custom IWhateverRepository interfaces. Finally, you can easily unit test your specifications' filter logic to ensure it's correct and provide examples for the team.
Another useful pattern you can use with repository is the proxy or decorator pattern to add caching. I call this the CachedRepository pattern and I've written a number of articles about it. I mention both patterns because they're functionally the same, but differ based on intent. A proxy controls access to something. A decorator adds behavior to something. A CachedRepository controls access to the real, underlying repository, exposing it only when the result isn't in the cache. In this way, it's a proxy. But it also is responsible for adding caching behavior to any repository. In this way, it's a decorator. Either way, it's an extremely useful pattern.
Most applications make a lot of queries to their database for results that don't change frequently. A lot of applications use a database to define some or all of their navigation, or the contents of common dropdownlists on forms. These and other common results are great candidates for caching, but often this behavior isn't added because of the work and complexity involved. Adding caching to a method in a data access repository isn't ideal, since it couples two unrelated concerns and breaks the single responsibility principle. It's also not very reusable. A better approach is to create a generic
CachedRepository<T> that can be used for any type that would benefit from caching. Determining whether or not to use this caching functionality can be controlled centrally for the application wherever its services are configured.
Circling back around to the specification pattern, you can combine it with the CachedRepository to help with key generation. Every cache entry needs to have a unique key, and you need to take care when constructing keys that you take into account any variables or parameters that were used for a particular query. Your specification objects know exactly which parameters they require, and can easily expose a cache key property that can be used by your CachedRepository. You can also add a property to toggle whether certain specifications should be cached at all, if that's helpful.
If you'd like to see what this looks like in a simple sample application, check out the eShopOnWeb sample on GitHub. I have a link in the show notes. There's also a free 110-page eBook that goes along with the sample that I encourage you to check out. I developed both the book and the sample for Microsoft as a free resource and they're both up-to-date with .NET Core 2.1 as of July 2018.
Do you think your team or application could be improved by better use of design patterns? I offer remote and onsite workshops guaranteed to improve your coding skills and application code quality. Contact me at ardalis.com and let's see how I can help.