The most common use of Unity Call Handlers (or Interceptors) is for cross-cutting concerns. I’ve demonstrated the use of such handlers in the past for things such as logging or caching. However, there’s another use for these handlers that allows us to build reusable blocks of business-related code that can be composed together to act in a number of ways: –
- Filtering out data that is not appropriate for the target method
- Enriching data before consumption by the target method
- Enriching the return data after consumption by the target method
Call Handlers as a pipeline
This is possible because of the way call handlers work – they form a pipeline whereby each handler has the opportunity to prematurely end the pipeline flow at any point, creating a return object, or amend the input argument before passing it on onto the next handler.
- The Blue lines indicate the initial flow from caller to target via each call handler.
- Any call handler may decide to prematurely return to the caller without passing onto the target, indicated by a red line.
- If the call makes it all the way to the target, it returns back up the stack to each handler, who cascades back up all the way to the source caller, shown in Green.
Filtering data with Call Handlers
Let’s start with a relatively simple example: a generic file parsing system which processes XML files dropped into folders. Each folder contains a different structure of XML file, and we have an appropriate parser class for each one. Perhaps we have ten different parsers. Now, let’s imagine that we wanted to filter out (i.e. not process) some files, for certain parsers – but not all of them – given some of the following conditions: –
- A file is too old – say, over a week old
- A file size is too big – say, over 10mb
- A file contains an attribute named “Ignore” on the root XML element
Now, if we were writing these parsers (with unit tests, of course), let’s pretend our IParser interface looked something like this: –
Imagine that each XDocument has a header that contains things like date published and size etc. etc. Also imagine that when each parser implemented ParseDocument it would first perform any tests required to ensure that certain filter conditions had not been failed. Remember, some parsers will not need to do any filtering. Some might need all three filters. Others might only need one or two.
Fragility of unit tests
Even if we abstracted the logic of these filters in helper methods – or even with interface on top of them so we could stub them out – it would still mean your unit tests for each parser growing with each extra filter that you add e.g. if you had a parser that had five filter conditions, you would have to mock out the first four in order to prove that the fifth was correctly checked. Even worse, if you decided to re-order your filter checks (let’s say that you realise that the first one is slow to run so push it to the back), your unit tests would break.
In the example above, to test that we are calling the second filter (IsDocumentTooLarge()) is being called, we have to mock the result of IsDocumentMarkedAsIgnore() first. If we swapped the order of the calls, our unit tests would break.
Using Call Handlers to act as filters
What I really want to see is code like this: –
Each of these attributes should map to a handler which performs a single check, and either passes on to the next item in the pipeline, or returns prematurely. Our unit tests on the parser would simply be ones that verify we have the attribute on the correct method, as well as tests for the actual parsing. That’s it.
Even better, as each call handler lives in its own class and is completely decoupled from any parser, we can easily apply them to other parsers very quickly and easily.
In my next post, I’ll demonstrate a simple call handler to perform one of these filters, and talk a bit more about the other two uses of CallHandlers that I mentioned at the start of this post.