Created
December 12, 2016 16:53
-
-
Save paulczy/93fe947624bd3e7252d8292f71dc8130 to your computer and use it in GitHub Desktop.
Caching extension using ServiceStack IServiceClient and IDistributedCache on .NET Core
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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