JsonServiceClient HTTP Client Cache

You should be able to access the response headers in the ResponseFilter (or static GlobalResponseFilter).

Also if you just want to access the response and headers and youā€™re ok with deserializing the response yourself you can just call the url without return type, e.g:

using (HttpWebResponse webRes = client.Get(requestUri))
{
    //webRes.Headers[]
    var json = webRes.GetResonseStream().ReadFully().FromUtf8Bytes();
    webRes.Close();
}

Is this a close enough example of how to do our own serialization? https://github.com/ServiceStack/ServiceStack/blob/e392265456e12077087c1cd014c918913f409bc9/src/ServiceStack.Client/ServiceClientBase.cs#L1639

However, it ode snot handle gzip or deflate compression responses.

Is there any code that does what we want. I dont really want to have to know all the permutations of what we could get back to deserialize.

GetResponse<T> is what the Service Client uses to deserialize the HttpWebResponse into a the target TResponse type.
It doesnā€™t need to handle gzip/deflate compression as itā€™s already handled by .NET WebRequest once AutomaticCompression is enabled (which it is by default).

But GetResponse<T> is private, are you inheriting ServiceClientBase? i.e. did you need for me to make it protected?

protected for GetResponse<TResponse>() would be good for the JSC for another reason.

But actually right now, I am having to convert the client.Get<HttpWebResponse>(requestUri) in my ClientCache class, since the ResultsFilter delegate needs it to return value that is typeof(TResponse), and I need to call client.Get<HttpWebResponse>(requestUri) to read the headers as well.

Iā€™m pushing through the change to make GetResponse<T> public can take up to 30mins before it will reach MyGet.

Weā€™re currently having to do some DNS configuration with forums now so there could be some downtime with forums.servicestack.net which could last a while depending on how quick the DNS servers take to propagate changes.

GetResponse<T> is now protected and on v4.0.55 on MyGet.

Making the DNS changes nowā€¦

Thanks,

Can you give me some guidance on what I need to do to deserialize the response from this:
(given a requestUri, and the Type for the responseType (as we have given to us in the ResultsFilterDelegate?)

HttpWebResponse webRes = client.Get<HttpWebResponse>(requestUri))

Iā€™ll would need to be able to deal with all the different types of responses that could be returned.

(BTW: Is all that knowledge encapsulated in ServiceClientBase.GetResponse?)

ServiceClientBase just uses GetResponse<T> to convert the HTTP Response into the target response type.

OK, so what I am hearing is that:
(Within my ServiceCache class)
I should do the same (cover the same cases) as ServiceClientBase.GetResponse() does.

But, of course, in a non-generic flavor of that code, becuase I only have the type, I donā€™t have the generic, to play with.

FYI Iā€™ve just added richer server support for Caching Headers and NotModified Responses which will let you return your response dto decorated in HttpResult, example usage:

return new HttpResult(response) {
    ETag = CalculateETag(response),
    MaxAge = TimeSpan.FromHours(24), //Optional, falls back to HttpCacheFeature.DefaultMaxAge (1hr)
    CacheControl = CacheControl.Public | CacheControl.NoStore | CacheControl.MustRevalidate,
};

The Typed Caching Headers now available on HttpResult include:

public string ETag { get; set; }
public TimeSpan? Age { get; set; }
public TimeSpan? MaxAge { get; set; }
public DateTime? Expires { get; set; }
public DateTime? LastModified { get; set; }
public CacheControl CacheControl { get; set; }

The server functionality is maintained in HttpCacheFeature.cs whilst the tests are available in CacheFeatureTests.cs - the tests should provide a good description of the whatā€™s currently supported, i.e. it automatically handles returning 304ā€™s with valid If-None-Match or If-Modified-Since headers.

The HttpCacheFeature plugin is registered by default, so you can customize it by retrieving and modifying the plugin in AppHost, e.g:

this.GetPlugin<HttpCacheFeature>().DefaultMaxAge = TimeSpan.FromMinutes(1);

Or the built-in caching functionality can be disabled entirely with:

this.Plugins.RemoveAll(x => x is HttpCacheFeature);

This change is available from v4.0.55 thatā€™s now available on MyGet.

Thereā€™s no caching support available in any .NET clients yet, but Iā€™ll look into creating something this week as time permits.

Thatā€™s quite nice on the server side.

We used a ResponseFilterAttribute called [HttpClientShouldCacheResponseAttribute(CacheDuration.VeryShort|Short|Medium|Long|VeryLong)] to pretty much do the same thing declaratively on each service operation, and defined the duration ranges of expiry in our deployment configuration.
This attribute also sets the ETag header by reading the calculated ETag value from the cached value in the current ICacheClient (based on the cachekey calculated for the request). The actual ETag is created by each service operation, iff it is declared to cache its response, which we do with another attribute [CacheResponse(CacheDuration, CacheRepresentation.PerUser|AllUsers)]

What we discovered is that the TTL for the cached response (on server side) is not desirable to be the same length as the TTL that clients should cache the response for, since clients are performing HTTP validation (with ETag or LastModified) and the service can reliably manage its own cache updates (via POST, PUT, DELETE etc). So we separated them out.

We also discovered that we needed some service operations to cache different representations per user, and others for every user. So the declarative response attributes work real well for us.

ok so youā€™re relying on using ICacheClient to maintain ETags for responses.

This approach doesnā€™t use the cache, but does require the Service implementation to calculate the ETag or LastModified for the requested resource (as itā€™s service specific). The Request.HasValidCache() lets you short-circuit the request saving Service Execution if youā€™re able to cheaply calculate the ETag or LastModified for the request otherwise returning the decorated HttpResult requires constructing the Response DTO so doesnā€™t save on Service Execution, but does save on serialization + bandwidth so cache-aware clients should still see an improvement.

It uses the Cache for existing ToOptimizedCache() responses where it saves the date the cache was created so it can respond to If-Modified-Since requests which should also save serialization + bandwidth for cache-aware clients.

FYI Iā€™ve added a CachedServiceClient in this commit.

It should work with any ServiceClientBase ServiceClient and implements IServiceClient so should be an easy drop-in replacement where you can register it with either:

container.Register(c => new JsonServiceClient(baseUrl).WithCache());

//equivalent to:
container.Register<IServiceClient>(c => 
    new CachedServiceClient(new JsonServiceClient(baseUrl)));

In order to make it work with the least friction Iā€™ve added a new NotModifiedFilter which lets you add a hook to catch 304 responses without needing to add try/catch around each call.

Iā€™m going to add a few more tests and support for async requests which shouldnā€™t affect its external API, but hopefully youā€™ll find some of the new additions useful. This change is available from v4.0.55 thatā€™s now available on MyGet.

Async support and more tests added in this commit should now be in a good state for adoption. MyGet also updated with latest changes.

Could you possibly enhance CachedServiceClient to allow the cache storage to be injected? Possibly using the ICacheClient interface. In a server to server scenario it would be particularly useful to cache out of process.

I can make the ConcurrentDictionary injectable but not replace the cache used as this would be primarily used on clients where it currently serves a majority of the use cases efficiently, refactoring to support ICacheClient would add serialization and I/O latency overhead for no gain and the impl would be awkwardly different. If you need it, you can copy the class and approach used to create your own high-level cache client which is likely what I need to do in order to support JsonHttpClient if thereā€™s demand for it.

Ok, thanks for the explanation and advice.

Iā€™ve extracted HttpCacheEntry and added overloads so ConcurrentDictionary<string, HttpCacheEntry> cache is now injectable in the constructor or replaceable with SetCache().

Iā€™ve also added a new CachedHttpClient implementation for providing the same functionality for JsonHttpClient which can be used with either:

container.Register(c => new JsonHttpClient(baseUrl).WithCache());

//equivalent to:
container.Register<IServiceClient>(c => 
    new CachedHttpClient(new JsonHttpClient(baseUrl)));

The cache client implementations can be easily substituted as seen in the shared CachedServiceClientTests.cs where they both implement a shared ICachedServiceClient interface which extends IServiceClient with some cache client specific stats and APIs.

These changes are available from v4.0.55 thatā€™s now available on MyGet.

Hey Mythz,

Sometime just over 24hrs ago, v55 broke our existing implementations of Http expiration, most likely on the service side.

Two problems have been identified.

  1. For some reason http responses for our services now return the ā€˜Cache-Control: max-age=3600ā€™, even though we have response headers which set it to ā€˜Cache-Control: no-cache, max-age=300ā€™
  2. We are getting (across most integration tests) an exception: ServiceStack.WebServiceException: 400 - Bad Request: Bad Request: An item with the same key has already been added. Bad Request

I havenā€™t analysed the latest changes in v55 in the last 24 hours, but given the two problems we are seeing now, I suspect (at this stage) that the exception might have something to do with us adding the 'Cache-Controlā€™or ā€˜Expiresā€™ expiration headers to the Response headers collection, and it already being there?

Would you know what might have changed and where?