using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Threading; using System.Threading.Tasks; namespace ServiceStack { public class CachedHttpClient : ICachedServiceClient { public TimeSpan? ClearCachesOlderThan { get; set; } public TimeSpan? ClearExpiredCachesOlderThan { get; set; } public int CleanCachesWhenCountExceeds { get; set; } public int CacheCount { get { return cache.Count; } } private long cacheHits; public long CacheHits { get { return cacheHits; } } private long notModifiedHits; public long NotModifiedHits { get { return notModifiedHits; } } private long errorFallbackHits; public long ErrorFallbackHits { get { return errorFallbackHits; } } private long cachesAdded; public long CachesAdded { get { return cachesAdded; } } private long cachesRemoved; public long CachesRemoved { get { return cachesRemoved; } } private ConcurrentDictionary cache = new ConcurrentDictionary(); private readonly Action existingRequestFilter; private readonly ResultsFilterHttpDelegate existingResultsFilter; private readonly ResultsFilterHttpResponseDelegate existingResultsFilterResponse; private ExceptionFilterHttpDelegate existingExceptionFilter; private readonly JsonHttpClient client; public CachedHttpClient(JsonHttpClient client, ConcurrentDictionary cache) : this(client) { if (cache != null) this.cache = cache; } public CachedHttpClient(JsonHttpClient client) { this.client = client; ClearExpiredCachesOlderThan = TimeSpan.FromHours(1); CleanCachesWhenCountExceeds = 1000; existingRequestFilter = client.RequestFilter; existingResultsFilter = client.ResultsFilter; existingResultsFilterResponse = client.ResultsFilterResponse; existingExceptionFilter = client.ExceptionFilter; client.RequestFilter = OnRequestFilter; client.ResultsFilter = OnResultsFilter; client.ResultsFilterResponse = OnResultsFilterResponse; client.ExceptionFilter = OnExceptionFilter; } private void OnRequestFilter(HttpRequestMessage webReq) { if (existingRequestFilter != null) existingRequestFilter(webReq); HttpCacheEntry entry; if (webReq.Method.Method == HttpMethods.Get && cache.TryGetValue(webReq.RequestUri.ToString(), out entry)) { if (entry.ETag != null) webReq.Headers.IfNoneMatch.Add(new EntityTagHeaderValue( entry.ETag.StripWeakRef(), entry.ETag.StartsWith("W/"))); if (entry.LastModified != null) webReq.Headers.IfModifiedSince = entry.LastModified.Value; } } private object OnResultsFilter(Type responseType, string httpMethod, string requestUri, object request) { var ret = existingResultsFilter != null ? existingResultsFilter(responseType, httpMethod, requestUri, request) : null; HttpCacheEntry entry; if (httpMethod == HttpMethods.Get && cache.TryGetValue(requestUri, out entry)) { if (!entry.ShouldRevalidate()) { Interlocked.Increment(ref cacheHits); return entry.Response; } } return ret; } public object OnExceptionFilter(HttpResponseMessage webRes, string requestUri, Type responseType) { if (existingExceptionFilter != null) { var response = existingExceptionFilter(webRes, requestUri, responseType); if (response != null) return response; } HttpCacheEntry entry; if (cache.TryGetValue(requestUri, out entry)) { if (webRes.StatusCode == HttpStatusCode.NotModified) { Interlocked.Increment(ref notModifiedHits); return entry.Response; } if (entry.CanUseCacheOnError()) { Interlocked.Increment(ref errorFallbackHits); return entry.Response; } } return null; } private void OnResultsFilterResponse(HttpResponseMessage webRes, object response, string httpMethod, string requestUri, object request) { if (existingResultsFilterResponse != null) existingResultsFilterResponse(webRes, response, httpMethod, requestUri, request); if (httpMethod != HttpMethods.Get || response == null || webRes == null) return; var eTag = webRes.Headers.ETag != null ? webRes.Headers.ETag.Tag : null; if (eTag == null && webRes.Content.Headers.LastModified == null) return; var entry = new HttpCacheEntry(response) { ETag = eTag, ContentLength = webRes.Content.Headers.ContentLength }; if (webRes.Content.Headers.LastModified != null) entry.LastModified = webRes.Content.Headers.LastModified.Value.UtcDateTime; entry.Age = webRes.Headers.Age; var cacheControl = webRes.Headers.CacheControl; if (cacheControl != null) { if (cacheControl.NoCache) return; if (cacheControl.MaxAge != null) entry.MaxAge = cacheControl.MaxAge.Value; entry.MustRevalidate = cacheControl.MustRevalidate; entry.NoCache = cacheControl.NoCache; entry.SetMaxAge(entry.MaxAge); cache[requestUri] = entry; Interlocked.Increment(ref cachesAdded); var runCleanupAfterEvery = CleanCachesWhenCountExceeds; if (cachesAdded % runCleanupAfterEvery == 0 && cache.Count > CleanCachesWhenCountExceeds) { if (ClearExpiredCachesOlderThan != null) RemoveExpiredCachesOlderThan(ClearExpiredCachesOlderThan.Value); if (ClearCachesOlderThan != null) RemoveCachesOlderThan(ClearCachesOlderThan.Value); } } } public void SetCache(ConcurrentDictionary cache) { if (cache == null) throw new ArgumentNullException("cache"); this.cache = cache; } public int RemoveCachesOlderThan(TimeSpan age) { var keysToRemove = new List(); var now = DateTime.UtcNow; foreach (var entry in cache) { if (now - entry.Value.Created > age) keysToRemove.Add(entry.Key); } foreach (var key in keysToRemove) { HttpCacheEntry ignore; if (cache.TryRemove(key, out ignore)) Interlocked.Increment(ref cachesRemoved); } return keysToRemove.Count; } public int RemoveExpiredCachesOlderThan(TimeSpan age) { var keysToRemove = new List(); var now = DateTime.UtcNow; foreach (var entry in cache) { if (now - entry.Value.Expires > age) keysToRemove.Add(entry.Key); } foreach (var key in keysToRemove) { HttpCacheEntry ignore; if (cache.TryRemove(key, out ignore)) Interlocked.Increment(ref cachesRemoved); } return keysToRemove.Count; } public void Dispose() { client.Dispose(); } public void SetCredentials(string userName, string password) { client.SetCredentials(userName, password); } public Task GetAsync(IReturn requestDto) { return client.GetAsync(requestDto); } public Task GetAsync(object requestDto) { return client.GetAsync(requestDto); } public Task GetAsync(string relativeOrAbsoluteUrl) { return client.GetAsync(relativeOrAbsoluteUrl); } public Task GetAsync(IReturnVoid requestDto) { return client.GetAsync(requestDto); } public Task DeleteAsync(IReturn requestDto) { return client.DeleteAsync(requestDto); } public Task DeleteAsync(object requestDto) { return client.DeleteAsync(requestDto); } public Task DeleteAsync(string relativeOrAbsoluteUrl) { return client.DeleteAsync(relativeOrAbsoluteUrl); } public Task DeleteAsync(IReturnVoid requestDto) { return client.DeleteAsync(requestDto); } public Task PostAsync(IReturn requestDto) { return client.PostAsync(requestDto); } public Task PostAsync(object requestDto) { return client.PostAsync(requestDto); } public Task PostAsync(string relativeOrAbsoluteUrl, object request) { return client.PostAsync(relativeOrAbsoluteUrl, request); } public Task PostAsync(IReturnVoid requestDto) { return client.PostAsync(requestDto); } public Task PutAsync(IReturn requestDto) { return client.PutAsync(requestDto); } public Task PutAsync(object requestDto) { return client.PutAsync(requestDto); } public Task PutAsync(string relativeOrAbsoluteUrl, object request) { return client.PutAsync(relativeOrAbsoluteUrl, request); } public Task PutAsync(IReturnVoid requestDto) { return client.PutAsync(requestDto); } public Task SendAsync(string httpMethod, string absoluteUrl, object request, CancellationToken token = default(CancellationToken)) { return client.SendAsync(httpMethod, absoluteUrl, request, token); } public Task CustomMethodAsync(string httpVerb, IReturn requestDto) { return client.CustomMethodAsync(httpVerb, requestDto); } public Task CustomMethodAsync(string httpVerb, object requestDto) { return client.CustomMethodAsync(httpVerb, requestDto); } public Task CustomMethodAsync(string httpVerb, IReturnVoid requestDto) { return client.CustomMethodAsync(httpVerb, requestDto); } public Task CustomMethodAsync(string httpVerb, string relativeOrAbsoluteUrl, object request) { return client.CustomMethodAsync(httpVerb, relativeOrAbsoluteUrl, request); } public void CancelAsync() { client.CancelAsync(); } public void SendOneWay(object requestDto) { client.SendOneWay(requestDto); } public void SendOneWay(string relativeOrAbsoluteUri, object requestDto) { client.SendOneWay(relativeOrAbsoluteUri, requestDto); } public void SendAllOneWay(IEnumerable requests) { client.SendAllOneWay(requests); } public void AddHeader(string name, string value) { client.AddHeader(name, value); } public void ClearCookies() { client.ClearCookies(); } public Dictionary GetCookieValues() { return client.GetCookieValues(); } public void SetCookie(string name, string value, TimeSpan? expiresIn = null) { client.SetCookie(name, value, expiresIn); } public void Get(IReturnVoid request) { client.Get(request); } public TResponse Get(IReturn requestDto) { return client.Get(requestDto); } public TResponse Get(object requestDto) { return client.Get(requestDto); } public TResponse Get(string relativeOrAbsoluteUrl) { return client.Get(relativeOrAbsoluteUrl); } public IEnumerable GetLazy(IReturn> queryDto) { return client.GetLazy(queryDto); } public void Delete(IReturnVoid requestDto) { client.Delete(requestDto); } public TResponse Delete(IReturn request) { return client.Delete(request); } public TResponse Delete(object request) { return client.Delete(request); } public TResponse Delete(string relativeOrAbsoluteUrl) { return client.Delete(relativeOrAbsoluteUrl); } public void Post(IReturnVoid requestDto) { client.Post(requestDto); } public TResponse Post(IReturn requestDto) { return client.Post(requestDto); } public TResponse Post(object requestDto) { return client.Post(requestDto); } public TResponse Post(string relativeOrAbsoluteUrl, object request) { return client.Post(relativeOrAbsoluteUrl, request); } public void Put(IReturnVoid requestDto) { client.Put(requestDto); } public TResponse Put(IReturn requestDto) { return client.Put(requestDto); } public TResponse Put(object requestDto) { return client.Put(requestDto); } public TResponse Put(string relativeOrAbsoluteUrl, object requestDto) { return client.Put(relativeOrAbsoluteUrl, requestDto); } public void Patch(IReturnVoid requestDto) { client.Patch(requestDto); } public TResponse Patch(IReturn requestDto) { return client.Patch(requestDto); } public TResponse Patch(object requestDto) { return client.Patch(requestDto); } public TResponse Patch(string relativeOrAbsoluteUrl, object requestDto) { return client.Patch(relativeOrAbsoluteUrl, requestDto); } public TResponse Send(string httpMethod, string relativeOrAbsoluteUrl, object request) { return client.Send(httpMethod, relativeOrAbsoluteUrl, request); } public void CustomMethod(string httpVerb, IReturnVoid requestDto) { client.CustomMethod(httpVerb, requestDto); } public TResponse CustomMethod(string httpVerb, IReturn requestDto) { return client.CustomMethod(httpVerb, requestDto); } public TResponse CustomMethod(string httpVerb, object requestDto) { return client.CustomMethod(httpVerb, requestDto); } public TResponse PostFile(string relativeOrAbsoluteUrl, Stream fileToUpload, string fileName, string mimeType) { return client.PostFile(relativeOrAbsoluteUrl, fileToUpload, fileName, mimeType); } public TResponse PostFileWithRequest(Stream fileToUpload, string fileName, object request, string fieldName = "upload") { return client.PostFileWithRequest(fileToUpload, fileName, request, fieldName); } public TResponse PostFileWithRequest(string relativeOrAbsoluteUrl, Stream fileToUpload, string fileName, object request, string fieldName = "upload") { return client.PostFileWithRequest(relativeOrAbsoluteUrl, fileToUpload, fileName, request, fieldName); } public TResponse PostFilesWithRequest(object request, IEnumerable files) { return client.PostFilesWithRequest(request, files); } public TResponse PostFilesWithRequest(string relativeOrAbsoluteUrl, object request, IEnumerable files) { return client.PostFilesWithRequest(relativeOrAbsoluteUrl, request, files); } public TResponse Send(object request) { return client.Send(request); } public List SendAll(IEnumerable requests) { return client.SendAll(requests); } public void Publish(object requestDto) { client.Publish(requestDto); } public void PublishAll(IEnumerable requestDtos) { client.PublishAll(requestDtos); } public Task SendAsync(object requestDto, CancellationToken token) { return client.SendAsync(requestDto, token); } public Task> SendAllAsync(IEnumerable requests, CancellationToken token) { return client.SendAllAsync(requests, token); } public Task PublishAsync(object requestDto, CancellationToken token) { return client.PublishAsync(requestDto, token); } public Task PublishAllAsync(IEnumerable requestDtos, CancellationToken token) { return client.PublishAllAsync(requestDtos, token); } public string SessionId { get { return client.SessionId; } set { client.SessionId = value; } } public int Version { get { return client.Version; } set { client.Version = value; } } } public static class CachedHttpClientExtensions { public static IServiceClient WithCache(this JsonHttpClient client) { return new CachedHttpClient(client); } public static IServiceClient WithCache(this JsonHttpClient client, ConcurrentDictionary cache) { return new CachedHttpClient(client, cache); } internal static string StripWeakRef(this string eTag) { return eTag != null && eTag.StartsWith("W/") ? eTag.Substring(2) : eTag; } } }