Skip to content

Instantly share code, notes, and snippets.

@paulczy
Created December 12, 2016 16:53
Show Gist options
  • Save paulczy/93fe947624bd3e7252d8292f71dc8130 to your computer and use it in GitHub Desktop.
Save paulczy/93fe947624bd3e7252d8292f71dc8130 to your computer and use it in GitHub Desktop.
Caching extension using ServiceStack IServiceClient and IDistributedCache on .NET Core
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Distributed;
using Serilog;
using ServiceStack;
using Wire; //Akka.Serialization.Wire
namespace ServiceStackEx
{
public interface IDistributedCacheServiceClient : IServiceClient, IServiceClientAsync, IServiceGatewayAsync, IRestClientAsync, IDisposable, IReplyClient, IServiceGateway, IOneWayClient, IRestClient, IHasSessionId, IHasVersion
{
long CacheHits { get; }
long CachesAdded { get; }
long ErrorFallbackHits { get; }
long NotModifiedHits { get; }
}
public class DistributedCacheServiceClient : IDistributedCacheServiceClient
{
public ServiceClientBase Client { get; }
public TimeSpan? ClearExpiredCachesOlderThan { get; set; }
private long _cacheHits;
public long CacheHits => _cacheHits;
private long _notModifiedHits;
public long NotModifiedHits => _notModifiedHits;
private long _errorFallbackHits;
public long ErrorFallbackHits => _errorFallbackHits;
private long _cachesAdded;
public long CachesAdded => _cachesAdded;
private readonly Serilog.ILogger _log = Log.ForContext<DistributedCacheServiceClient>();
private readonly Action<HttpWebRequest> _existingRequestFilter;
private readonly ResultsFilterDelegate _existingResultsFilter;
private readonly ResultsFilterResponseDelegate _existingResultsFilterResponse;
private readonly ExceptionFilterDelegate _existingExceptionFilter;
private readonly string _cacheKeyPrefix;
private readonly Serializer _serializer;
private readonly IDistributedCache _cache;
public DistributedCacheServiceClient(ServiceClientBase client, string cacheKeyPrefix, IDistributedCache cache)
: this(client, cacheKeyPrefix)
{
_cache = cache;
if (cache != null)
_cache = cache;
}
public DistributedCacheServiceClient(ServiceClientBase client, string cacheKeyPrefix)
{
_serializer = new Serializer();
_cacheKeyPrefix = cacheKeyPrefix;
Client = client;
ClearExpiredCachesOlderThan = TimeSpan.FromMinutes(5);
_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(HttpWebRequest webReq)
{
_existingRequestFilter?.Invoke(webReq);
HttpCacheEntry entry;
if (webReq.Method == HttpMethods.Get && TryGetValue($"{_cacheKeyPrefix}::{webReq.RequestUri.ToString()}", out entry))
{
if (entry.ETag != null)
webReq.Headers[HttpRequestHeader.IfNoneMatch] = entry.ETag;
if (entry.LastModified != null)
PclExportClient.Instance.SetIfModifiedSince(webReq, entry.LastModified.Value);
}
}
private object OnResultsFilter(Type responseType, string httpMethod, string requestUri, object request)
{
var ret = _existingResultsFilter?.Invoke(responseType, httpMethod, requestUri, request);
HttpCacheEntry entry;
if (httpMethod == HttpMethods.Get && TryGetValue($"{_cacheKeyPrefix}::{requestUri}", out entry))
{
if (!entry.ShouldRevalidate())
{
Interlocked.Increment(ref _cacheHits);
return entry.Response;
}
}
return ret;
}
private object OnExceptionFilter(WebException webEx, WebResponse webRes, string requestUri, Type responseType)
{
var response = _existingExceptionFilter?.Invoke(webEx, webRes, requestUri, responseType);
if (response != null)
return response;
HttpCacheEntry entry;
if (TryGetValue($"{_cacheKeyPrefix}::{requestUri}", out entry))
{
if (webEx.IsNotModified())
{
_log.Verbose("Response is NotModified {@requestUri}", requestUri);
Interlocked.Increment(ref _notModifiedHits);
return entry.Response;
}
if (entry.CanUseCacheOnError())
{
_log.Warning("Response Error {@requestUri}", requestUri);
Interlocked.Increment(ref _errorFallbackHits);
return entry.Response;
}
}
return null;
}
private void OnResultsFilterResponse(WebResponse webRes, object response, string httpMethod, string requestUri, object request)
{
_existingResultsFilterResponse?.Invoke(webRes, response, httpMethod, requestUri, request);
if (httpMethod != HttpMethods.Get || response == null || webRes == null)
return;
var eTag = webRes.Headers[HttpHeaders.ETag];
var lastModifiedStr = webRes.Headers[HttpHeaders.LastModified];
if (eTag == null && lastModifiedStr == null)
return;
var entry = new HttpCacheEntry(response)
{
ETag = eTag,
ContentLength = webRes.ContentLength >= 0 ? webRes.ContentLength : (long?)null,
};
if (lastModifiedStr != null)
{
DateTime lastModified;
if (DateTime.TryParse(lastModifiedStr, new DateTimeFormatInfo(), DateTimeStyles.RoundtripKind, out lastModified))
entry.LastModified = lastModified.ToUniversalTime();
}
long secs;
var ageStr = webRes.Headers[HttpHeaders.Age];
if (ageStr != null && long.TryParse(ageStr, out secs))
entry.Age = TimeSpan.FromSeconds(secs);
var cacheControl = webRes.Headers[HttpHeaders.CacheControl];
if (cacheControl != null)
{
var parts = cacheControl.Split(',');
foreach (var part in parts)
{
var kvp = part.Split('=');
var key = kvp[0].Trim().ToLower();
switch (key)
{
case "max-age":
if (kvp.Length == 2 && long.TryParse(kvp[1], out secs))
entry.MaxAge = TimeSpan.FromSeconds(secs);
break;
case "must-revalidate":
entry.MustRevalidate = true;
break;
case "no-cache":
entry.NoCache = true;
break;
}
}
entry.SetMaxAge(entry.MaxAge);
Task.Run(() => SetCachedValueAsync($"{_cacheKeyPrefix}::{requestUri}", entry,
new DistributedCacheEntryOptions() { AbsoluteExpirationRelativeToNow = ClearExpiredCachesOlderThan })).Wait();
Interlocked.Increment(ref _cachesAdded);
}
}
private async Task SetCachedValueAsync<T>(string key, T value, DistributedCacheEntryOptions policy)
{
using (var stream = new MemoryStream())
{
_serializer.Serialize(value, stream);
await _cache.SetAsync(key, stream.ToArray(), policy);
_log.Verbose("Cache added {@key}", key);
}
}
private async Task<T> GetCachedValueAsync<T>(string key) where T : class
{
var data = await _cache.GetAsync(key);
if (data == null)
return null;
using (var stream = new MemoryStream(data))
return _serializer.Deserialize<T>(stream);
}
private bool TryGetValue(string key, out HttpCacheEntry entry)
{
var value = GetCachedValueAsync<HttpCacheEntry>(key).Result;
if (value != null)
{
entry = value;
_log.Verbose("Cache hit {@key}", key);
return true;
}
entry = null;
_log.Verbose("Cache miss {@key}", key);
return false;
}
#region Public Members
public void Dispose()
{
Client.Dispose();
}
public void SetCredentials(string userName, string password)
{
Client.SetCredentials(userName, password);
}
public Task<TResponse> GetAsync<TResponse>(IReturn<TResponse> requestDto)
{
return Client.GetAsync(requestDto);
}
public Task<TResponse> GetAsync<TResponse>(object requestDto)
{
return Client.GetAsync<TResponse>(requestDto);
}
public Task<TResponse> GetAsync<TResponse>(string relativeOrAbsoluteUrl)
{
return Client.GetAsync<TResponse>(relativeOrAbsoluteUrl);
}
public Task GetAsync(IReturnVoid requestDto)
{
return Client.GetAsync(requestDto);
}
public Task<TResponse> DeleteAsync<TResponse>(IReturn<TResponse> requestDto)
{
return Client.DeleteAsync(requestDto);
}
public Task<TResponse> DeleteAsync<TResponse>(object requestDto)
{
return Client.DeleteAsync<TResponse>(requestDto);
}
public Task<TResponse> DeleteAsync<TResponse>(string relativeOrAbsoluteUrl)
{
return Client.DeleteAsync<TResponse>(relativeOrAbsoluteUrl);
}
public Task DeleteAsync(IReturnVoid requestDto)
{
return Client.DeleteAsync(requestDto);
}
public Task<TResponse> PostAsync<TResponse>(IReturn<TResponse> requestDto)
{
return Client.PostAsync(requestDto);
}
public Task<TResponse> PostAsync<TResponse>(object requestDto)
{
return Client.PostAsync<TResponse>(requestDto);
}
public Task<TResponse> PostAsync<TResponse>(string relativeOrAbsoluteUrl, object request)
{
return Client.PostAsync<TResponse>(relativeOrAbsoluteUrl, request);
}
public Task PostAsync(IReturnVoid requestDto)
{
return Client.PostAsync(requestDto);
}
public Task<TResponse> PutAsync<TResponse>(IReturn<TResponse> requestDto)
{
return Client.PutAsync(requestDto);
}
public Task<TResponse> PutAsync<TResponse>(object requestDto)
{
return Client.PutAsync<TResponse>(requestDto);
}
public Task<TResponse> PutAsync<TResponse>(string relativeOrAbsoluteUrl, object request)
{
return Client.PutAsync<TResponse>(relativeOrAbsoluteUrl, request);
}
public Task PutAsync(IReturnVoid requestDto)
{
return Client.PutAsync(requestDto);
}
public Task<TResponse> CustomMethodAsync<TResponse>(string httpVerb, IReturn<TResponse> requestDto)
{
return Client.CustomMethodAsync(httpVerb, requestDto);
}
public Task<TResponse> CustomMethodAsync<TResponse>(string httpVerb, object requestDto)
{
return Client.CustomMethodAsync<TResponse>(httpVerb, requestDto);
}
public Task CustomMethodAsync(string httpVerb, IReturnVoid requestDto)
{
return Client.CustomMethodAsync(httpVerb, requestDto);
}
public Task<TResponse> CustomMethodAsync<TResponse>(string httpVerb, string relativeOrAbsoluteUrl, object request)
{
return Client.CustomMethodAsync<TResponse>(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<object> requests)
{
Client.SendAllOneWay(requests);
}
public void AddHeader(string name, string value)
{
Client.AddHeader(name, value);
}
public void ClearCookies()
{
Client.ClearCookies();
}
public Dictionary<string, string> 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<TResponse>(IReturn<TResponse> requestDto)
{
return Client.Get(requestDto);
}
public TResponse Get<TResponse>(object requestDto)
{
return Client.Get<TResponse>(requestDto);
}
public TResponse Get<TResponse>(string relativeOrAbsoluteUrl)
{
return Client.Get<TResponse>(relativeOrAbsoluteUrl);
}
public IEnumerable<TResponse> GetLazy<TResponse>(IReturn<QueryResponse<TResponse>> queryDto)
{
return Client.GetLazy(queryDto);
}
public void Delete(IReturnVoid requestDto)
{
Client.Delete(requestDto);
}
public TResponse Delete<TResponse>(IReturn<TResponse> request)
{
return Client.Delete(request);
}
public TResponse Delete<TResponse>(object request)
{
return Client.Delete<TResponse>(request);
}
public TResponse Delete<TResponse>(string relativeOrAbsoluteUrl)
{
return Client.Delete<TResponse>(relativeOrAbsoluteUrl);
}
public void Post(IReturnVoid requestDto)
{
Client.Post(requestDto);
}
public TResponse Post<TResponse>(IReturn<TResponse> requestDto)
{
return Client.Post(requestDto);
}
public TResponse Post<TResponse>(object requestDto)
{
return Client.Post<TResponse>(requestDto);
}
public TResponse Post<TResponse>(string relativeOrAbsoluteUrl, object request)
{
return Client.Post<TResponse>(relativeOrAbsoluteUrl, request);
}
public void Put(IReturnVoid requestDto)
{
Client.Put(requestDto);
}
public TResponse Put<TResponse>(IReturn<TResponse> requestDto)
{
return Client.Put(requestDto);
}
public TResponse Put<TResponse>(object requestDto)
{
return Client.Put<TResponse>(requestDto);
}
public TResponse Put<TResponse>(string relativeOrAbsoluteUrl, object requestDto)
{
return Client.Put<TResponse>(relativeOrAbsoluteUrl, requestDto);
}
public void Patch(IReturnVoid requestDto)
{
Client.Patch(requestDto);
}
public TResponse Patch<TResponse>(IReturn<TResponse> requestDto)
{
return Client.Patch(requestDto);
}
public TResponse Patch<TResponse>(object requestDto)
{
return Client.Patch<TResponse>(requestDto);
}
public TResponse Patch<TResponse>(string relativeOrAbsoluteUrl, object requestDto)
{
return Client.Patch<TResponse>(relativeOrAbsoluteUrl, requestDto);
}
public void CustomMethod(string httpVerb, IReturnVoid requestDto)
{
Client.CustomMethod(httpVerb, requestDto);
}
public TResponse CustomMethod<TResponse>(string httpVerb, IReturn<TResponse> requestDto)
{
return Client.CustomMethod(httpVerb, requestDto);
}
public TResponse CustomMethod<TResponse>(string httpVerb, object requestDto)
{
return Client.CustomMethod<TResponse>(httpVerb, requestDto);
}
public TResponse PostFile<TResponse>(string relativeOrAbsoluteUrl, Stream fileToUpload, string fileName, string mimeType)
{
return Client.PostFile<TResponse>(relativeOrAbsoluteUrl, fileToUpload, fileName, mimeType);
}
public TResponse PostFileWithRequest<TResponse>(Stream fileToUpload, string fileName, object request, string fieldName = "upload")
{
return Client.PostFileWithRequest<TResponse>(fileToUpload, fileName, request, fieldName);
}
public TResponse PostFileWithRequest<TResponse>(string relativeOrAbsoluteUrl, Stream fileToUpload, string fileName,
object request, string fieldName = "upload")
{
return Client.PostFileWithRequest<TResponse>(relativeOrAbsoluteUrl, fileToUpload, fileName, request, fieldName);
}
public TResponse PostFilesWithRequest<TResponse>(object request, IEnumerable<UploadFile> files)
{
return Client.PostFilesWithRequest<TResponse>(request, files);
}
public TResponse PostFilesWithRequest<TResponse>(string relativeOrAbsoluteUrl, object request, IEnumerable<UploadFile> files)
{
return Client.PostFilesWithRequest<TResponse>(relativeOrAbsoluteUrl, request, files);
}
public TResponse Send<TResponse>(object request)
{
return Client.Send<TResponse>(request);
}
public List<TResponse> SendAll<TResponse>(IEnumerable<object> requests)
{
return Client.SendAll<TResponse>(requests);
}
public void Publish(object requestDto)
{
Client.Publish(requestDto);
}
public void PublishAll(IEnumerable<object> requestDtos)
{
Client.PublishAll(requestDtos);
}
public Task<TResponse> SendAsync<TResponse>(object requestDto, CancellationToken token)
{
return Client.SendAsync<TResponse>(requestDto, token);
}
public Task<List<TResponse>> SendAllAsync<TResponse>(IEnumerable<object> requests, CancellationToken token)
{
return Client.SendAllAsync<TResponse>(requests, token);
}
public Task PublishAsync(object requestDto, CancellationToken token)
{
return Client.PublishAsync(requestDto, token);
}
public Task PublishAllAsync(IEnumerable<object> 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; }
}
#endregion
}
public static class DistributedCachedServiceClientExtensions
{
public static IServiceClient WithDistributedCache(this ServiceClientBase client, string cacheKeyPrefix, IDistributedCache cache)
{
return new DistributedCacheServiceClient(client, cacheKeyPrefix, cache);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment