So I've been backwards and forwards trying a few different approaches for the most sensible way to opt-in to caching.
What I've arrived at is registering a DTO to be cached - and not a request or a route. I did try the latter two, but it didn't feel right because of the nature of how we respond to GET, POST, PUT, PATCH and DELETE requests for the same DTO.
To illustrate, the Debtor DTO (JiwaFinancials.Jiwa.JiwaServiceModel.Debtors.Debtor) is returned on a by the DebtorGETRequest which is mapped to a GET on /Debtors/{DebtorID}
The initial instinct might to be simply add to the cache the DTO with a key such as urn:/Debtors/{DebtorID}:000000001B0000001V - the urn structure there is urn:{Path}:{DTOId}
However, a POST to /Debtors, should add to the cache the DTO for the new debtor. Which would mean adding a lot of opinions and complexity about how operations are related to a specific DTO and that meant adding a lot of noise to service methods to work that out.
And it gets worse... a GET on /Debtors/{DebtorID}:000000001B0000001V/ContactNames returns the contact names for the given debtor - but we don't need a separate cache entry for urn:/Debtors/{DebtorID}/ContactNames:000000001B0000001V as we might already have that Debtor DTO cached - we just want to return part of the already cached DTO.
By instead registering a DTO to be cached, and not a request, response or route we minimise our number of cache entries for the same resource, or parts of the same resource.
So, to register the JiwaFinancials.Jiwa.JiwaServiceModel.Debtors.Debtor DTO, we simply call:
- Code: Select all
dtoCacheDictionary.Register<JiwaFinancials.Jiwa.JiwaServiceModel.Debtors.Debtor>();
And this will automatically cache the a debtor DTO for the ALL the routes involving a debtor DTO:
- GET /Debtors/{DebtorID}
- POST /Debtors
- PATCH /Debtors/{DebtorID}
- DELETE /Debtors/{DebtorID}
- GET /Debtors/{DebtorID}/ContactNames
- GET /Debtors/{DebtorID}/ContactNames/{ContactNameID}
- POST /Debtors/{DebtorID}/ContactNames
- PATCH /Debtors/{DebtorID}/ContactNames/{ContactNameID}
- DELETE /Debtors/{DebtorID}/ContactNames/{ContactNameID}
- GET /Debtors/{DebtorID}/Notes
- GET /Debtors/{DebtorID}/Notes/{NoteID}
- POST /Debtors/{DebtorID}/Notes
- PATCH /Debtors/{DebtorID}/Notes/{NoteID}
- DELETE /Debtors/{DebtorID}/Notes/{NoteID}
...and many more. As the service methods behind all those routes all do the same thing - read a debtor and return either part or all of the DTO - it makes sense to have them operate on the same cached DTO.
Adding an expiry time to the DTO is just an optional parameter:
- Code: Select all
dtoCacheDictionary.Register<JiwaFinancials.Jiwa.JiwaServiceModel.Debtors.Debtor>(TimeSpan.FromMintes(10));
And specifying other DTO's to invalidate (delete) if the debtor DTO is updated or deleted is an array of other DTO type names to remove when it invalidates:
- Code: Select all
dtoCacheDictionary.Register<JiwaFinancials.Jiwa.JiwaServiceModel.Debtors.Debtor>(TimeSpan.Zero,
new string[]
{
typeof(QueryResponse<Tables.v_Jiwa_Debtor_List>).ToString()
});
So when a debtor is modified or deleted, the above shows we want the AutoQuery for v_Jiwa_Debtor_List (a list of debtors) to be removed from the cache.
All AutoQuery operations are able to be opted into for caching by just registering the return DTO:
- Code: Select all
dtoCacheDictionary.Register<QueryResponse<Tables.v_Jiwa_Debtor_List>>();
It does mean it's not that intuitive to know what DTO type to specify - you need to know what DTO Type a request returns or updates, but it does mean there's a lot less registering of cache interest going on, smaller caches and less overall ceremony and noise.
One thing which isn't obvious, is that POST, PATCH, PUT and DELETE operations automatically update any cached DTO's - we don't remove the cache entry if present, but update or create it. Typically caching works by a GET always being the operation to cache a DTO, but we also keep the cached DTO's fresh with the other operations also as they have generated a DTO already anyway.
The other thing we do which is unconventional is we update any cached entries (or remove them) whenever our business logic objects save - and that covers everything even outside the API - including the Jiwa desktop application and any 3rd party applications using our .NET API. This is done by special internal API calls from the client application to the API.
Before I sweep through and apply the necessary changes to the rest of the service methods to accommodate caching, it might be worth knowing what are the top things anyone would want caching for - I can do those first and provide a test plugin for alpha testing to iron out any wrinkles.
So, what do you think I should be looking at as a priority? (I'm primarily asking Stuart, as he requested this feature - but anyone can chime in!)