mirror of
https://github.com/JKorf/CryptoExchange.Net
synced 2025-06-07 16:06:15 +00:00
Added support for caching GET requests
This commit is contained in:
parent
287aadc720
commit
6a105c6f8f
54
CryptoExchange.Net/Caching/MemoryCache.cs
Normal file
54
CryptoExchange.Net/Caching/MemoryCache.cs
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Caching
|
||||||
|
{
|
||||||
|
internal class MemoryCache
|
||||||
|
{
|
||||||
|
private readonly ConcurrentDictionary<string, CacheItem> _cache = new ConcurrentDictionary<string, CacheItem>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add a new cache entry. Will override an existing entry if it already exists
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">The key identifier</param>
|
||||||
|
/// <param name="value">Cache value</param>
|
||||||
|
public void Add(string key, object value)
|
||||||
|
{
|
||||||
|
var cacheItem = new CacheItem(DateTime.UtcNow, value);
|
||||||
|
_cache.AddOrUpdate(key, cacheItem, (key, val1) => cacheItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get a cached value
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">The key identifier</param>
|
||||||
|
/// <param name="maxAge">The max age of the cached entry</param>
|
||||||
|
/// <returns>Cached value if it was in cache</returns>
|
||||||
|
public object? Get(string key, TimeSpan maxAge)
|
||||||
|
{
|
||||||
|
_cache.TryGetValue(key, out CacheItem value);
|
||||||
|
if (value == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (DateTime.UtcNow - value.CacheTime > maxAge)
|
||||||
|
{
|
||||||
|
_cache.TryRemove(key, out _);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class CacheItem
|
||||||
|
{
|
||||||
|
public DateTime CacheTime { get; }
|
||||||
|
public object Value { get; }
|
||||||
|
|
||||||
|
public CacheItem(DateTime cacheTime, object value)
|
||||||
|
{
|
||||||
|
CacheTime = cacheTime;
|
||||||
|
Value = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -8,6 +8,7 @@ using System.Net;
|
|||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using CryptoExchange.Net.Caching;
|
||||||
using CryptoExchange.Net.Converters.JsonNet;
|
using CryptoExchange.Net.Converters.JsonNet;
|
||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces;
|
||||||
using CryptoExchange.Net.Logging.Extensions;
|
using CryptoExchange.Net.Logging.Extensions;
|
||||||
@ -17,6 +18,7 @@ using CryptoExchange.Net.RateLimiting;
|
|||||||
using CryptoExchange.Net.RateLimiting.Interfaces;
|
using CryptoExchange.Net.RateLimiting.Interfaces;
|
||||||
using CryptoExchange.Net.Requests;
|
using CryptoExchange.Net.Requests;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Clients
|
namespace CryptoExchange.Net.Clients
|
||||||
{
|
{
|
||||||
@ -85,6 +87,10 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public new RestApiOptions ApiOptions => (RestApiOptions)base.ApiOptions;
|
public new RestApiOptions ApiOptions => (RestApiOptions)base.ApiOptions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Memory cache
|
||||||
|
/// </summary>
|
||||||
|
private static MemoryCache _cache = new MemoryCache();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
@ -190,6 +196,21 @@ namespace CryptoExchange.Net.Clients
|
|||||||
Dictionary<string, string>? additionalHeaders = null,
|
Dictionary<string, string>? additionalHeaders = null,
|
||||||
int? weight = null) where T : class
|
int? weight = null) where T : class
|
||||||
{
|
{
|
||||||
|
var key = baseAddress + definition + uriParameters?.ToFormData();
|
||||||
|
if (ShouldCache(definition))
|
||||||
|
{
|
||||||
|
_logger.CheckingCache(key);
|
||||||
|
var cachedValue = _cache.Get(key, ClientOptions.CachingMaxAge);
|
||||||
|
if (cachedValue != null)
|
||||||
|
{
|
||||||
|
_logger.CacheHit(key);
|
||||||
|
var original = (WebCallResult<T>)cachedValue;
|
||||||
|
return original.Cached();
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.CacheNotHit(key);
|
||||||
|
}
|
||||||
|
|
||||||
int currentTry = 0;
|
int currentTry = 0;
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
@ -215,6 +236,12 @@ namespace CryptoExchange.Net.Clients
|
|||||||
if (await ShouldRetryRequestAsync(definition.RateLimitGate, result, currentTry).ConfigureAwait(false))
|
if (await ShouldRetryRequestAsync(definition.RateLimitGate, result, currentTry).ConfigureAwait(false))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
if (result.Success &&
|
||||||
|
ShouldCache(definition))
|
||||||
|
{
|
||||||
|
_cache.Add(key, result);
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -445,6 +472,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <param name="requestWeight">Credits used for the request</param>
|
/// <param name="requestWeight">Credits used for the request</param>
|
||||||
/// <param name="additionalHeaders">Additional headers to send with the request</param>
|
/// <param name="additionalHeaders">Additional headers to send with the request</param>
|
||||||
/// <param name="gate">The ratelimit gate to use</param>
|
/// <param name="gate">The ratelimit gate to use</param>
|
||||||
|
/// <param name="preventCaching">Whether caching should be prevented for this request</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[return: NotNull]
|
[return: NotNull]
|
||||||
protected virtual async Task<WebCallResult<T>> SendRequestAsync<T>(
|
protected virtual async Task<WebCallResult<T>> SendRequestAsync<T>(
|
||||||
@ -458,9 +486,25 @@ namespace CryptoExchange.Net.Clients
|
|||||||
ArrayParametersSerialization? arraySerialization = null,
|
ArrayParametersSerialization? arraySerialization = null,
|
||||||
int requestWeight = 1,
|
int requestWeight = 1,
|
||||||
Dictionary<string, string>? additionalHeaders = null,
|
Dictionary<string, string>? additionalHeaders = null,
|
||||||
IRateLimitGate? gate = null
|
IRateLimitGate? gate = null,
|
||||||
|
bool preventCaching = false
|
||||||
) where T : class
|
) where T : class
|
||||||
{
|
{
|
||||||
|
var key = uri.ToString() + method + signed + parameters?.ToFormData();
|
||||||
|
if (ShouldCache(method) && !preventCaching)
|
||||||
|
{
|
||||||
|
_logger.CheckingCache(key);
|
||||||
|
var cachedValue = _cache.Get(key, ClientOptions.CachingMaxAge);
|
||||||
|
if (cachedValue != null)
|
||||||
|
{
|
||||||
|
_logger.CacheHit(key);
|
||||||
|
var original = (WebCallResult<T>)cachedValue;
|
||||||
|
return original.Cached();
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.CacheNotHit(key);
|
||||||
|
}
|
||||||
|
|
||||||
int currentTry = 0;
|
int currentTry = 0;
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
@ -478,6 +522,13 @@ namespace CryptoExchange.Net.Clients
|
|||||||
if (await ShouldRetryRequestAsync(gate, result, currentTry).ConfigureAwait(false))
|
if (await ShouldRetryRequestAsync(gate, result, currentTry).ConfigureAwait(false))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
if (result.Success &&
|
||||||
|
ShouldCache(method) &&
|
||||||
|
!preventCaching)
|
||||||
|
{
|
||||||
|
_cache.Add(key, result);
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -949,5 +1000,14 @@ namespace CryptoExchange.Net.Clients
|
|||||||
|
|
||||||
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, true, null);
|
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, true, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool ShouldCache(RequestDefinition definition)
|
||||||
|
=> ClientOptions.CachingEnabled
|
||||||
|
&& definition.Method == HttpMethod.Get
|
||||||
|
&& !definition.PreventCaching;
|
||||||
|
|
||||||
|
private bool ShouldCache(HttpMethod method)
|
||||||
|
=> ClientOptions.CachingEnabled
|
||||||
|
&& method == HttpMethod.Get;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,9 @@ namespace CryptoExchange.Net.Logging.Extensions
|
|||||||
private static readonly Action<ILogger, int, DateTime, Exception?> _restApiRateLimitRetry;
|
private static readonly Action<ILogger, int, DateTime, Exception?> _restApiRateLimitRetry;
|
||||||
private static readonly Action<ILogger, int, DateTime, Exception?> _restApiRateLimitPauseUntil;
|
private static readonly Action<ILogger, int, DateTime, Exception?> _restApiRateLimitPauseUntil;
|
||||||
private static readonly Action<ILogger, int, RequestDefinition, string?, string, string, Exception?> _restApiSendRequest;
|
private static readonly Action<ILogger, int, RequestDefinition, string?, string, string, Exception?> _restApiSendRequest;
|
||||||
|
private static readonly Action<ILogger, string, Exception?> _restApiCheckingCache;
|
||||||
|
private static readonly Action<ILogger, string, Exception?> _restApiCacheHit;
|
||||||
|
private static readonly Action<ILogger, string, Exception?> _restApiCacheNotHit;
|
||||||
|
|
||||||
|
|
||||||
static RestApiClientLoggingExtensions()
|
static RestApiClientLoggingExtensions()
|
||||||
@ -65,6 +68,21 @@ namespace CryptoExchange.Net.Logging.Extensions
|
|||||||
LogLevel.Debug,
|
LogLevel.Debug,
|
||||||
new EventId(4008, "RestApiSendRequest"),
|
new EventId(4008, "RestApiSendRequest"),
|
||||||
"[Req {RequestId}] Sending {Definition} request with body {Body}, query parameters {Query} and headers {Headers}");
|
"[Req {RequestId}] Sending {Definition} request with body {Body}, query parameters {Query} and headers {Headers}");
|
||||||
|
|
||||||
|
_restApiCheckingCache = LoggerMessage.Define<string>(
|
||||||
|
LogLevel.Trace,
|
||||||
|
new EventId(4009, "RestApiCheckingCache"),
|
||||||
|
"Checking cache for key {Key}");
|
||||||
|
|
||||||
|
_restApiCacheHit = LoggerMessage.Define<string>(
|
||||||
|
LogLevel.Trace,
|
||||||
|
new EventId(4010, "RestApiCacheHit"),
|
||||||
|
"Cache hit for key {Key}");
|
||||||
|
|
||||||
|
_restApiCacheNotHit = LoggerMessage.Define<string>(
|
||||||
|
LogLevel.Trace,
|
||||||
|
new EventId(4011, "RestApiCacheNotHit"),
|
||||||
|
"Cache not hit for key {Key}");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void RestApiErrorReceived(this ILogger logger, int? requestId, HttpStatusCode? responseStatusCode, long responseTime, string? error)
|
public static void RestApiErrorReceived(this ILogger logger, int? requestId, HttpStatusCode? responseStatusCode, long responseTime, string? error)
|
||||||
@ -111,5 +129,20 @@ namespace CryptoExchange.Net.Logging.Extensions
|
|||||||
{
|
{
|
||||||
_restApiSendRequest(logger, requestId, definition, body, query, headers, null);
|
_restApiSendRequest(logger, requestId, definition, body, query, headers, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void CheckingCache(this ILogger logger, string key)
|
||||||
|
{
|
||||||
|
_restApiCheckingCache(logger, key, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void CacheHit(this ILogger logger, string key)
|
||||||
|
{
|
||||||
|
_restApiCacheHit(logger, key, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void CacheNotHit(this ILogger logger, string key)
|
||||||
|
{
|
||||||
|
_restApiCacheNotHit(logger, key, null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -331,6 +331,11 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public TimeSpan? ResponseTime { get; set; }
|
public TimeSpan? ResponseTime { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The data source of this result
|
||||||
|
/// </summary>
|
||||||
|
public ResultDataSource DataSource { get; set; } = ResultDataSource.Server;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a new result
|
/// Create a new result
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -417,6 +422,17 @@ namespace CryptoExchange.Net.Objects
|
|||||||
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, default, error);
|
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, default, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Return a copy of this result with data source set to cache
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
internal WebCallResult<T> Cached()
|
||||||
|
{
|
||||||
|
var result = new WebCallResult<T>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, default, Error);
|
||||||
|
result.DataSource = ResultDataSource.Cache;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
|
@ -188,4 +188,19 @@
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
ExponentialBackoff
|
ExponentialBackoff
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The data source of the result
|
||||||
|
/// </summary>
|
||||||
|
public enum ResultDataSource
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// From server
|
||||||
|
/// </summary>
|
||||||
|
Server,
|
||||||
|
/// <summary>
|
||||||
|
/// From cache
|
||||||
|
/// </summary>
|
||||||
|
Cache
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,16 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public TimeSpan TimestampRecalculationInterval { get; set; } = TimeSpan.FromHours(1);
|
public TimeSpan TimestampRecalculationInterval { get; set; } = TimeSpan.FromHours(1);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether caching is enabled. Caching will only be applied to GET http requests. The lifetime of cached results can be determined by the `CachingMaxAge` option
|
||||||
|
/// </summary>
|
||||||
|
public bool CachingEnabled { get; set; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The max age of a cached entry, only used when the `CachingEnabled` options is set to true. When a cached entry is older than the max age it will be discarded and a new server request will be done
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan CachingMaxAge { get; set; } = TimeSpan.FromSeconds(5);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a copy of this options
|
/// Create a copy of this options
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -34,7 +44,9 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
Proxy = Proxy,
|
Proxy = Proxy,
|
||||||
RequestTimeout = RequestTimeout,
|
RequestTimeout = RequestTimeout,
|
||||||
RateLimiterEnabled = RateLimiterEnabled,
|
RateLimiterEnabled = RateLimiterEnabled,
|
||||||
RateLimitingBehaviour = RateLimitingBehaviour
|
RateLimitingBehaviour = RateLimitingBehaviour,
|
||||||
|
CachingEnabled = CachingEnabled,
|
||||||
|
CachingMaxAge = CachingMaxAge,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -61,6 +61,12 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public TimeSpan? EndpointLimitPeriod { get; set; }
|
public TimeSpan? EndpointLimitPeriod { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether this request should never be cached
|
||||||
|
/// </summary>
|
||||||
|
public bool PreventCaching { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -48,6 +48,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <param name="requestBodyFormat">Request body format</param>
|
/// <param name="requestBodyFormat">Request body format</param>
|
||||||
/// <param name="parameterPosition">Parameter position</param>
|
/// <param name="parameterPosition">Parameter position</param>
|
||||||
/// <param name="arraySerialization">Array serialization type</param>
|
/// <param name="arraySerialization">Array serialization type</param>
|
||||||
|
/// <param name="preventCaching">Prevent request caching</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public RequestDefinition GetOrCreate(
|
public RequestDefinition GetOrCreate(
|
||||||
HttpMethod method,
|
HttpMethod method,
|
||||||
@ -59,7 +60,8 @@ namespace CryptoExchange.Net.Objects
|
|||||||
TimeSpan? endpointLimitPeriod = null,
|
TimeSpan? endpointLimitPeriod = null,
|
||||||
RequestBodyFormat? requestBodyFormat = null,
|
RequestBodyFormat? requestBodyFormat = null,
|
||||||
HttpMethodParameterPosition? parameterPosition = null,
|
HttpMethodParameterPosition? parameterPosition = null,
|
||||||
ArrayParametersSerialization? arraySerialization = null)
|
ArrayParametersSerialization? arraySerialization = null,
|
||||||
|
bool? preventCaching = null)
|
||||||
{
|
{
|
||||||
|
|
||||||
if (!_definitions.TryGetValue(method + path, out var def))
|
if (!_definitions.TryGetValue(method + path, out var def))
|
||||||
@ -74,6 +76,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
ArraySerialization = arraySerialization,
|
ArraySerialization = arraySerialization,
|
||||||
RequestBodyFormat = requestBodyFormat,
|
RequestBodyFormat = requestBodyFormat,
|
||||||
ParameterPosition = parameterPosition,
|
ParameterPosition = parameterPosition,
|
||||||
|
PreventCaching = preventCaching ?? false
|
||||||
};
|
};
|
||||||
_definitions.TryAdd(method + path, def);
|
_definitions.TryAdd(method + path, def);
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user