How can we perform complex searches on a resource?Rob categorized three general approaches, ordered from least flexible to most flexible:
- Query string parameters
- Basic DTOs
- Complex DTOs with expression trees
While we'd all love to have the complex DTOs which can express boolean logic and complex search criteria, I think we can all agree that the vast majority of cases doesn't merit the effort. Most of the time we just use query string parameters, and that's a perfectly workable reality for a lot of cases.
But some of us (read: me) are purists. We don't want strings, we want objects. Looking at a request with lots of query string parameters is like looking at a function signature with lots of parameters. It's distasteful. For functions which accept complex parameters, we build a structure to hold those parameters. It's why we use model binding in our MVC actions, it's why we use anonymous objects to initialize jQuery plugins, heck for some of us these object oriented principles are why we get out of bed in the morning.
So we land on the basic DTOs for issuing searches to resources. Right? Sounds good to me. Except... Aren't these GET requests?
Aye, there's the rub. For in that GET of requests what DTOs may come, when the server have shuffled off the request body?
The fact that we are purists is what brought us here. And the fact that we are purists is what troubles us still. As a purist for object oriented design principles, we want to use a DTO for our search predicates. As a REST purist, we want to issue a GET request because we're only fetching data, not creating or updating it. A quandary indeed. Or is it?
Life just wouldn't be the same without semantics. We argue them all the time, and we also use them to our advantage all the time. This situation is no different. So instead of trying to muck with the technology to hopefully put together something that works, let's step back and look at this from a semantic point of view. Let's think about this for a moment not in terms of technologies and implementations, but in terms of patterns and domain languages.
What is the request that we are making? If we're simply fetching a resource, then clearly it should be a GET request. But... are we simply fetching a resource? Or are we doing something more? Let's look at our object-oriented principles again, since those are what brought us here in the first place. We're not necessarily fetching an instance of a resource, we're fetching a collection of resources. And a collection of objects is itself an object, with its own logic and rules operating at the collection level.
Let's examine this collection a little further. Does this collection already exist on the server? Its elements exist, and ultimately those are what we're looking to fetch. But does the collection exist? More to the point, does this specific instance of such a collection exist?
No, it doesn't. We're using custom search criteria for our predicate. We're... creating a new instance of a collection of these entities. And what, dear fellow REST purists, do we use when we're creating a new instance of something? :::drumroll::: POST.
Take a moment to think about it. Let the ramifications sink in and fit so elegantly into all of the dogmatic puritan notions we share about REST. See the beauty of it.
Just look at the semantics of our clean URLs. For fetching an instance of a Customer, we might use this:
GET /Customers/123And for creating an instance of a collection of customers, we might use this:
POST /CustomerSearchesClean. Beautiful. Poetic. We're creating a new collection of Customers, unique and unlike any other existing collection that may already be on the server. So we stuff a DTO into the request body, which contains all of the information needed to create this resource, and we get back the resource we just created.
But wait, there's more. You may have noticed that there's a possibility of a performance boost here. Now, the server makes no guarantee that this new unique instance of a CustomerSearch is going to stay there for long. Indeed, you may expect the server not to retain it at all. But it can. It can store the results, caching them if you will so as to not bother the backing data store if the user wants to run the same search again before some expiry time, and return an identifier along with the resource. So perhaps some time later the user can issue:
GET /CustomerSearches/456And that user will get back the same results from that previous search. Or maybe it's not even the same results, maybe what was cached were the criteria of the search, and that GET request on CustomerSearches is simply a way to re-run the same search again. (Not unlike how databases cache query execution plans.) The possibilities are all there, and they're all semantically sound with RESTfulness.
We have beautiful objects, beautiful URLs, and beautiful HTTP verbs. We even have the added benefit of caching results and/or query executions. The purist in me rejoices.
Now, Rob would be the first to point out that the spec actually allows for POST to be used to solve our original problem anyway. Technically there's nothing wrong with sending the search DTO here:
POST /CustomersGo ahead and read the spec for yourself. POST can indeed be used to handle a number of alternate functions besides simply adding a new instance of a resource. So Rob's right about that, and in most cases that's not only acceptable but prudent and pragmatic. But settling for that doesn't satisfy the purist in me, not one bit. The above scenario, on the other hand, does. It reminds me of the REST promise ring I metaphorically wear (or perhaps the Clean Code wristband).
Either way, it's OK to use POST for the scenario of a complex search. Both approaches satisfy the spec, and more importantly both work for the system and are easily understood and supportable. The above approach, however, also fills me with that emotion I'm told is called joy.