I've been exploring a bit to see what this is going to take.
ServiceStack already gives us an in memory cache for free, but if you prefer Redis or something else then you can override that behaviour using a plugin with something like the following in the Configure method:
- Code: Select all
container.Register<IRedisClientsManager>(c =>
new RedisManagerPool("localhost:6379"));
container.Register(c => c.Resolve<IRedisClientsManager>().GetCacheClient());
Then - in order to make use of the cache, we've got 3 options for having responses cached:
- ToOptimizedResultUsingCache technique - which looks pretty neat, but I can't get it to return anything than gibberish (seems to be returning a compressed response which isn't indicated as such)
- Use the CacheResponse attribute around either services or the methods
- Manually manage the cache
ToOptimizedResultUsingCache would be my preferred method, as it promises the lowest possible response time for a cached result, and is an elegant solution. We'd simply wrap our existing code in our GETs with something like this:
- Code: Select all
[Authenticate]
public JiwaFinancials.Jiwa.JiwaServiceModel.Debtors.Debtor Get(DebtorGETRequest request)
{
string key = UrnId.Create<JiwaFinancials.Jiwa.JiwaServiceModel.Debtors.Debtor>(request.DebtorID);
var expireInTimespan = new TimeSpan(1, 0, 0);
return base.Request.ToOptimizedResultUsingCache(base.Cache, key, expireInTimespan, ()=>
{
// Existing code in the Get method for a debtor gets moved in here
});
}
An expiry timespan doesn't need to be provided - it's optional, so not providing one would make it never expire - we'd have to manually invalidate cache entries if necessary.
The
CacheResponse attribute approach is simple, but it's properties (such as expiry time) are set in-code at compile time - we'd probably want to set that from a setting or field in the database.
- Code: Select all
[CacheResponse(Duration=60, MaxAge=30)]
public JiwaFinancials.Jiwa.JiwaServiceModel.Debtors.Debtor Get(DebtorGETRequest request)
{
...
}
Manually managing the cache is the most flexible option - and it's quite simple - for example caching the GET on /Debtors/{DebtorID} would be replacing the existing service method with this:
- Code: Select all
[Authenticate]
public JiwaFinancials.Jiwa.JiwaServiceModel.Debtors.Debtor Get(DebtorGETRequest request)
{
string key = UrnId.Create<JiwaFinancials.Jiwa.JiwaServiceModel.Debtors.Debtor>(request.DebtorID);
JiwaFinancials.Jiwa.JiwaServiceModel.Debtors.Debtor debtorDTO = base.Cache.Get<JiwaFinancials.Jiwa.JiwaServiceModel.Debtors.Debtor>(key);
if (debtorDTO != null)
return debtorDTO;
JiwaApplication.Manager manager = this.SessionAs<JiwaAuthUserSession>().Manager;
JiwaFinancials.Jiwa.JiwaDebtors.Debtor Debtor = Helper.Service.GetMaintenanceBusinessLogic<JiwaFinancials.Jiwa.JiwaDebtors.Debtor>(manager, this, request.DebtorID,
(d, m, s) =>
{
d.Read(request.DebtorID);
if (Helper.Service.IsStateful(s))
{
m.ObjectDictionary.Add(d.DebtorID, d);
}
});
debtorDTO = Debtor.DTO_Serialise();
base.Cache.Set<JiwaFinancials.Jiwa.JiwaServiceModel.Debtors.Debtor>(key, debtorDTO, DateTime.Now.ToUniversalTime.AddSeconds(60));
return debtorDTO;
}
Obviously, for this to be useful we'd have to also set the cache entry on POST and remove on DELETE, and generally invalidate the cache entry whenever the debtor changes. We could leverage our existing handlers that we use for webhooks (debtor saved) and put code in there to invalidate or refresh the cache entry - but I still worry about the cache getting stale and compromising the integrity of the system - and this does not just apply to the implementation of manual caching of responses, but all strategies.
I think a good approach is to add the logic in for caching and provide a way of setting a flag per route if that response is to be cached, and how long for. So this would be opt-in and this way it can always be turned off if problems occur.
We don't have anywhere to store that information, so a plugin could perhaps turn on caching once we add some fields to the standard REST API plugin - so something like this:
- Code: Select all
public class RESTAPICachingPlugin : System.MarshalByRefObject, JiwaFinancials.Jiwa.JiwaApplication.IJiwaRESTAPIPlugin
{
public void Configure(JiwaFinancials.Jiwa.JiwaApplication.Plugin.Plugin Plugin, ServiceStack.ServiceStackHost AppHost, Funq.Container Container, JiwaApplication.Manager JiwaApplicationManager)
{
// Add using the request type by adding to the static property CachedResponses which is a List<CacheResponseInfo> - CacheResponseInfo has an overloaded constructor each taking 2 arguments
JiwaFinancials.Jiwa.JiwaServiceModel.RESTAPIPlugin.CachedResponses.Add(new CacheResponseInfo(typeof(DebtorGETRequest), new TimeSpan(1, 0, 0)));
// Add using the route
JiwaFinancials.Jiwa.JiwaServiceModel.RESTAPIPlugin.CachedResponses.Add(new CacheResponseInfo("/Debtors/{DebtorID}", new TimeSpan(1, 0, 0)));
}
}
So, this is where I'm at so far with this. We should be able to get this done such that it's just plugin changes to our standard REST API plugin - so we should be able to make it all work for 07.02.01 or even 07.02.00
EDIT: So I've worked out why the ToOptimizedResultUsingCache was returning gibberish. The response was compressed TWICE. This was due to the service class being decorated with the [CompressResponse] attribute, and changing the method to return the result of ToOptimizedResultUsingCache meant that the response got compressed twice.
From the
ServiceStack docs on compression:
Note using [CompressResponse] is unnecessary when returning cached responses as ServiceStack automatically caches and returns the most optimal Response - typically compressed bytes for clients that supports compression.
Which kinda sucks because now we can't have opt-in caching at runtime unless we remove the [CompressResponse] attribute from all our service classes, which would mean non-cached routes won't return a compressed response.