1
0
mirror of https://github.com/JKorf/CryptoExchange.Net.git synced 2026-08-12 17:03:10 +00:00

Compare commits

..

28 Commits

Author SHA1 Message Date
JKorf 1ab85d4c26 Updated to version 7.7.2 2024-06-25 16:42:45 +02:00
JKorf be68115099 Fix for ratelimiting possibly creating negative waits 2024-06-25 16:14:09 +02:00
JKorf ff0550b0fb Updated to version 7.7.1 2024-06-23 22:40:13 +02:00
JKorf 1ab1e008fc Some fixes for caching 2024-06-23 22:39:54 +02:00
JKorf 6f30c72608 Updated to version 7.7.0 2024-06-23 15:38:40 +02:00
JKorf e927bc3d20 Close socket when connecting and authentication fails 2024-06-23 14:55:20 +02:00
JKorf 09ed7d1436 Small improvements 2024-06-23 14:41:59 +02:00
JKorf 6fed657ea6 Added ObjectStringConverter for double serialized objects 2024-06-21 16:49:02 +02:00
Jonnern 1555f8da0c Fix issue with rate limit guard waiting sub ms (#204) 2024-06-21 16:47:14 +02:00
JKorf 68b28fc875 Small improvements 2024-06-19 17:01:26 +02:00
JKorf 5d50d8cde8 Merge branch 'master' of https://github.com/JKorf/CryptoExchange.Net 2024-06-17 17:29:57 +02:00
JKorf 9ff673d8be Fixed unhandled exception when websocket connection is interupted while waiting for ratelimit 2024-06-17 10:53:22 +02:00
JKorf 3e5a34fb56 Added dedicated request websocket connection support 2024-06-16 16:55:44 +02:00
JKorf 64ee50d98c Added SocketConnection as parameter to GetAuthenticationRequest socket api client 2024-06-14 14:06:57 +02:00
JKorf 6a105c6f8f Added support for caching GET requests 2024-06-13 16:29:02 +02:00
JKorf 287aadc720 Added CancellationToken support for websocket queries 2024-06-13 11:58:52 +02:00
JKorf 7229438a0b Docs 2024-06-12 21:42:20 +02:00
JKorf 444af98a15 Added GateIo reference 2024-06-12 19:23:34 +02:00
JKorf 70c6fa1bbb Fixed tests 2024-06-12 18:05:26 +02:00
JKorf d27f394b46 Removed HandleUpdatesBeforeConfirmation flag, allow messages to trigger listeners even if not confirmed and mark as confirmed then. Updated websocket reconnection delay handling 2024-06-12 16:56:06 +02:00
JKorf c8c98e13d0 Updated to version 7.6.0 2024-06-11 16:32:28 +02:00
Jan Korf 9fcd722991 Feature/body uri param split (#203)
* Added support for specifying seperate uri and body parameters
* Added support for different message and handling generic types on socket queries
* Split DataEvent.Topic into StreamId and Symbol properties
* Added support for negative time values parsing
* Added some helper methods for converting DataEvent to CallResult
* Added support for GZip/Deflate automatic decompressing in the default HttpClient
* Updated some testing methods
2024-06-11 16:23:48 +02:00
JKorf 8080ecccc0 Added support for Patch requests, added SetBody to ParameterCollection for directly setting the request body 2024-06-04 09:54:24 +02:00
JKorf 4b6fa9a1b1 Delete release.yml 2024-06-02 09:38:23 +02:00
JKorf 0b6dbde7d4 docs 2024-06-01 22:56:43 +02:00
JKorf fe4d63ba75 Update index.html 2024-06-01 22:17:26 +02:00
JKorf 04bd3727ca Updated version 2024-05-07 09:56:59 +02:00
JKorf 7e6fcd03c2 Update release.yml 2024-05-07 09:50:22 +02:00
46 changed files with 921 additions and 269 deletions
-23
View File
@@ -1,23 +0,0 @@
name: Publish Nuget
on:
release:
types: [published]
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Setup .NET SDK
uses: actions/setup-dotnet@v1
- name: Build
run: dotnet build -c Release
- name: Test
run: dotnet test -c Release --no-build
- name: Pack nugets
run: dotnet pack -c Release --no-build --include-symbols -p:PackageVersion=${{github.event.release.name}} --output .
- name: Push Package to NuGet
run: dotnet nuget push "*.nupkg" --api-key ${{secrets.nuget_api_key}} --source https://api.nuget.org/v3/index.json
@@ -121,6 +121,7 @@ namespace CryptoExchange.Net.UnitTests
null, null,
HttpMethod.Get, HttpMethod.Get,
new List<KeyValuePair<string, IEnumerable<string>>>(), new List<KeyValuePair<string, IEnumerable<string>>>(),
ResultDataSource.Server,
new TestObjectResult(), new TestObjectResult(),
null); null);
var asResult = result.AsError<TestObject2>(new ServerError("TestError2")); var asResult = result.AsError<TestObject2>(new ServerError("TestError2"));
@@ -150,6 +151,7 @@ namespace CryptoExchange.Net.UnitTests
null, null,
HttpMethod.Get, HttpMethod.Get,
new List<KeyValuePair<string, IEnumerable<string>>>(), new List<KeyValuePair<string, IEnumerable<string>>>(),
ResultDataSource.Server,
new TestObjectResult(), new TestObjectResult(),
null); null);
var asResult = result.As<TestObject2>(result.Data.InnerData); var asResult = result.As<TestObject2>(result.Data.InnerData);
@@ -72,7 +72,6 @@ namespace CryptoExchange.Net.UnitTests
result = messageEvent.Data; result = messageEvent.Data;
rstEvent.Set(); rstEvent.Set();
}); });
subObj.HandleUpdatesBeforeConfirmation = true;
sub.AddSubscription(subObj); sub.AddSubscription(subObj);
// act // act
@@ -107,7 +106,6 @@ namespace CryptoExchange.Net.UnitTests
original = messageEvent.OriginalData; original = messageEvent.OriginalData;
rstEvent.Set(); rstEvent.Set();
}); });
subObj.HandleUpdatesBeforeConfirmation = true;
sub.AddSubscription(subObj); sub.AddSubscription(subObj);
var msgToSend = JsonConvert.SerializeObject(new { topic = "topic", property = 123 }); var msgToSend = JsonConvert.SerializeObject(new { topic = "topic", property = 123 });
@@ -49,11 +49,11 @@ namespace CryptoExchange.Net.Authentication
/// <param name="method">The method of the request</param> /// <param name="method">The method of the request</param>
/// <param name="auth">If the requests should be authenticated</param> /// <param name="auth">If the requests should be authenticated</param>
/// <param name="arraySerialization">Array serialization type</param> /// <param name="arraySerialization">Array serialization type</param>
/// <param name="parameterPosition">The position where the providedParameters should go</param>
/// <param name="requestBodyFormat">The formatting of the request body</param> /// <param name="requestBodyFormat">The formatting of the request body</param>
/// <param name="uriParameters">Parameters that need to be in the Uri of the request. Should include the provided parameters if they should go in the uri</param> /// <param name="uriParameters">Parameters that need to be in the Uri of the request. Should include the provided parameters if they should go in the uri</param>
/// <param name="bodyParameters">Parameters that need to be in the body of the request. Should include the provided parameters if they should go in the body</param> /// <param name="bodyParameters">Parameters that need to be in the body of the request. Should include the provided parameters if they should go in the body</param>
/// <param name="headers">The headers that should be send with the request</param> /// <param name="headers">The headers that should be send with the request</param>
/// <param name="parameterPosition">The position where the providedParameters should go</param>
public abstract void AuthenticateRequest( public abstract void AuthenticateRequest(
RestApiClient apiClient, RestApiClient apiClient,
Uri uri, Uri uri,
@@ -434,6 +434,20 @@ namespace CryptoExchange.Net.Authentication
return DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient)).Value.ToString(CultureInfo.InvariantCulture); return DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient)).Value.ToString(CultureInfo.InvariantCulture);
} }
/// <summary>
/// Return the serialized request body
/// </summary>
/// <param name="serializer"></param>
/// <param name="parameters"></param>
/// <returns></returns>
protected string GetSerializedBody(IMessageSerializer serializer, IDictionary<string, object> parameters)
{
if (parameters.Count == 1 && parameters.ContainsKey(Constants.BodyPlaceHolderKey))
return serializer.Serialize(parameters[Constants.BodyPlaceHolderKey]);
else
return serializer.Serialize(parameters);
}
/// <inheritdoc /> /// <inheritdoc />
public void Dispose() public void Dispose()
{ {
+54
View 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;
}
}
}
}
+136 -42
View File
@@ -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
{ {
@@ -75,7 +77,8 @@ namespace CryptoExchange.Net.Clients
{ HttpMethod.Get, HttpMethodParameterPosition.InUri }, { HttpMethod.Get, HttpMethodParameterPosition.InUri },
{ HttpMethod.Post, HttpMethodParameterPosition.InBody }, { HttpMethod.Post, HttpMethodParameterPosition.InBody },
{ HttpMethod.Delete, HttpMethodParameterPosition.InBody }, { HttpMethod.Delete, HttpMethodParameterPosition.InBody },
{ HttpMethod.Put, HttpMethodParameterPosition.InBody } { HttpMethod.Put, HttpMethodParameterPosition.InBody },
{ new HttpMethod("Patch"), HttpMethodParameterPosition.InBody },
}; };
/// <inheritdoc /> /// <inheritdoc />
@@ -84,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
@@ -149,7 +156,7 @@ namespace CryptoExchange.Net.Clients
/// <param name="additionalHeaders">Additional headers for this request</param> /// <param name="additionalHeaders">Additional headers for this request</param>
/// <param name="weight">Override the request weight for this request definition, for example when the weight depends on the parameters</param> /// <param name="weight">Override the request weight for this request definition, for example when the weight depends on the parameters</param>
/// <returns></returns> /// <returns></returns>
protected virtual async Task<WebCallResult<T>> SendAsync<T>( protected virtual Task<WebCallResult<T>> SendAsync<T>(
string baseAddress, string baseAddress,
RequestDefinition definition, RequestDefinition definition,
ParameterCollection? parameters, ParameterCollection? parameters,
@@ -157,15 +164,67 @@ 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 parameterPosition = definition.ParameterPosition ?? ParameterPositions[definition.Method];
return SendAsync<T>(
baseAddress,
definition,
parameterPosition == HttpMethodParameterPosition.InUri ? parameters : null,
parameterPosition == HttpMethodParameterPosition.InBody ? parameters : null,
cancellationToken,
additionalHeaders,
weight);
}
/// <summary>
/// Send a request to the base address based on the request definition
/// </summary>
/// <typeparam name="T">Response type</typeparam>
/// <param name="baseAddress">Host and schema</param>
/// <param name="definition">Request definition</param>
/// <param name="uriParameters">Request query parameters</param>
/// <param name="bodyParameters">Request body parameters</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <param name="additionalHeaders">Additional headers for this request</param>
/// <param name="weight">Override the request weight for this request definition, for example when the weight depends on the parameters</param>
/// <returns></returns>
protected virtual async Task<WebCallResult<T>> SendAsync<T>(
string baseAddress,
RequestDefinition definition,
ParameterCollection? uriParameters,
ParameterCollection? bodyParameters,
CancellationToken cancellationToken,
Dictionary<string, string>? additionalHeaders = null,
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)
{ {
currentTry++; currentTry++;
var prepareResult = await PrepareAsync(baseAddress, definition, parameters, cancellationToken, additionalHeaders, weight).ConfigureAwait(false); var prepareResult = await PrepareAsync(baseAddress, definition, cancellationToken, additionalHeaders, weight).ConfigureAwait(false);
if (!prepareResult) if (!prepareResult)
return new WebCallResult<T>(prepareResult.Error!); return new WebCallResult<T>(prepareResult.Error!);
var request = CreateRequest(baseAddress, definition, parameters, additionalHeaders); var request = CreateRequest(
baseAddress,
definition,
uriParameters,
bodyParameters,
additionalHeaders);
_logger.RestApiSendRequest(request.RequestId, definition, request.Content, request.Uri.Query, string.Join(", ", request.GetHeaders().Select(h => h.Key + $"=[{string.Join(",", h.Value)}]"))); _logger.RestApiSendRequest(request.RequestId, definition, request.Content, request.Uri.Query, string.Join(", ", request.GetHeaders().Select(h => h.Key + $"=[{string.Join(",", h.Value)}]")));
TotalRequestsMade++; TotalRequestsMade++;
var result = await GetResponseAsync<T>(request, definition.RateLimitGate, cancellationToken).ConfigureAwait(false); var result = await GetResponseAsync<T>(request, definition.RateLimitGate, cancellationToken).ConfigureAwait(false);
@@ -177,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;
} }
} }
@@ -186,7 +251,6 @@ namespace CryptoExchange.Net.Clients
/// </summary> /// </summary>
/// <param name="baseAddress">Host and schema</param> /// <param name="baseAddress">Host and schema</param>
/// <param name="definition">Request definition</param> /// <param name="definition">Request definition</param>
/// <param name="parameters">Request parameters</param>
/// <param name="cancellationToken">Cancellation token</param> /// <param name="cancellationToken">Cancellation token</param>
/// <param name="additionalHeaders">Additional headers for this request</param> /// <param name="additionalHeaders">Additional headers for this request</param>
/// <param name="weight">Override the request weight for this request</param> /// <param name="weight">Override the request weight for this request</param>
@@ -195,7 +259,6 @@ namespace CryptoExchange.Net.Clients
protected virtual async Task<CallResult> PrepareAsync( protected virtual async Task<CallResult> PrepareAsync(
string baseAddress, string baseAddress,
RequestDefinition definition, RequestDefinition definition,
ParameterCollection? parameters,
CancellationToken cancellationToken, CancellationToken cancellationToken,
Dictionary<string, string>? additionalHeaders = null, Dictionary<string, string>? additionalHeaders = null,
int? weight = null) int? weight = null)
@@ -263,25 +326,27 @@ namespace CryptoExchange.Net.Clients
/// </summary> /// </summary>
/// <param name="baseAddress">Host and schema</param> /// <param name="baseAddress">Host and schema</param>
/// <param name="definition">Request definition</param> /// <param name="definition">Request definition</param>
/// <param name="parameters">The parameters of the request</param> /// <param name="uriParameters">The query parameters of the request</param>
/// <param name="bodyParameters">The body parameters of 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>
/// <returns></returns> /// <returns></returns>
protected virtual IRequest CreateRequest( protected virtual IRequest CreateRequest(
string baseAddress, string baseAddress,
RequestDefinition definition, RequestDefinition definition,
ParameterCollection? parameters, ParameterCollection? uriParameters,
ParameterCollection? bodyParameters,
Dictionary<string, string>? additionalHeaders) Dictionary<string, string>? additionalHeaders)
{ {
parameters ??= new ParameterCollection(); var uriParams = uriParameters == null ? new ParameterCollection() : CreateParameterDictionary(uriParameters);
var bodyParams = bodyParameters == null ? new ParameterCollection() : CreateParameterDictionary(bodyParameters);
var uri = new Uri(baseAddress.AppendPath(definition.Path)); var uri = new Uri(baseAddress.AppendPath(definition.Path));
var parameterPosition = definition.ParameterPosition ?? ParameterPositions[definition.Method];
var arraySerialization = definition.ArraySerialization ?? ArraySerialization; var arraySerialization = definition.ArraySerialization ?? ArraySerialization;
var bodyFormat = definition.RequestBodyFormat ?? RequestBodyFormat; var bodyFormat = definition.RequestBodyFormat ?? RequestBodyFormat;
var requestId = ExchangeHelpers.NextId(); var requestId = ExchangeHelpers.NextId();
var parameterPosition = definition.ParameterPosition ?? ParameterPositions[definition.Method];
var headers = new Dictionary<string, string>(); var headers = new Dictionary<string, string>();
var uriParameters = parameterPosition == HttpMethodParameterPosition.InUri ? CreateParameterDictionary(parameters) : new Dictionary<string, object>();
var bodyParameters = parameterPosition == HttpMethodParameterPosition.InBody ? CreateParameterDictionary(parameters) : new Dictionary<string, object>();
if (AuthenticationProvider != null) if (AuthenticationProvider != null)
{ {
try try
@@ -290,13 +355,14 @@ namespace CryptoExchange.Net.Clients
this, this,
uri, uri,
definition.Method, definition.Method,
uriParameters, uriParams,
bodyParameters, bodyParams,
headers, headers,
definition.Authenticated, definition.Authenticated,
arraySerialization, arraySerialization,
parameterPosition, parameterPosition,
bodyFormat); bodyFormat
);
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -304,18 +370,8 @@ namespace CryptoExchange.Net.Clients
} }
} }
// Sanity check
foreach (var param in parameters)
{
if (!uriParameters.ContainsKey(param.Key) && !bodyParameters.ContainsKey(param.Key))
{
throw new Exception($"Missing parameter {param.Key} after authentication processing. AuthenticationProvider implementation " +
$"should return provided parameters in either the uri or body parameters output");
}
}
// Add the auth parameters to the uri, start with a new URI to be able to sort the parameters including the auth parameters // Add the auth parameters to the uri, start with a new URI to be able to sort the parameters including the auth parameters
uri = uri.SetParameters(uriParameters, arraySerialization); uri = uri.SetParameters(uriParams, arraySerialization);
var request = RequestFactory.Create(definition.Method, uri, requestId); var request = RequestFactory.Create(definition.Method, uri, requestId);
request.Accept = Constants.JsonContentHeader; request.Accept = Constants.JsonContentHeader;
@@ -342,8 +398,8 @@ namespace CryptoExchange.Net.Clients
if (parameterPosition == HttpMethodParameterPosition.InBody) if (parameterPosition == HttpMethodParameterPosition.InBody)
{ {
var contentType = bodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader; var contentType = bodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader;
if (bodyParameters.Count != 0) if (bodyParams.Count != 0)
WriteParamBody(request, bodyParameters, contentType); WriteParamBody(request, bodyParams, contentType);
else else
request.SetContent(RequestBodyEmptyContent, contentType); request.SetContent(RequestBodyEmptyContent, contentType);
} }
@@ -416,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>(
@@ -429,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)
{ {
@@ -449,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;
} }
} }
@@ -588,47 +668,47 @@ namespace CryptoExchange.Net.Clients
if (error.Code == null || error.Code == 0) if (error.Code == null || error.Code == 0)
error.Code = (int)response.StatusCode; error.Code = (int)response.StatusCode;
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error!); return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error!);
} }
if (typeof(T) == typeof(object)) if (typeof(T) == typeof(object))
// Success status code and expected empty response, assume it's correct // Success status code and expected empty response, assume it's correct
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, 0, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, null); return new WebCallResult<T>(statusCode, headers, sw.Elapsed, 0, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, null);
var valid = await accessor.Read(responseStream, outputOriginalData).ConfigureAwait(false); var valid = await accessor.Read(responseStream, outputOriginalData).ConfigureAwait(false);
if (!valid) if (!valid)
{ {
// Invalid json // Invalid json
var error = new ServerError("Failed to parse response: " + valid.Error!.Message, accessor.OriginalDataAvailable ? accessor.GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"); var error = new ServerError("Failed to parse response: " + valid.Error!.Message, accessor.OriginalDataAvailable ? accessor.GetOriginalString() : "[Data only available when OutputOriginal = true in client options]");
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error); return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
} }
// Json response received // Json response received
var parsedError = TryParseError(accessor); var parsedError = TryParseError(accessor);
if (parsedError != null) if (parsedError != null)
// Success status code, but TryParseError determined it was an error response // Success status code, but TryParseError determined it was an error response
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, parsedError); return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, parsedError);
var deserializeResult = accessor.Deserialize<T>(); var deserializeResult = accessor.Deserialize<T>();
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), deserializeResult.Data, deserializeResult.Error); return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult.Data, deserializeResult.Error);
} }
catch (HttpRequestException requestException) catch (HttpRequestException requestException)
{ {
// Request exception, can't reach server for instance // Request exception, can't reach server for instance
var exceptionInfo = requestException.ToLogString(); var exceptionInfo = requestException.ToLogString();
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new WebError(exceptionInfo)); return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new WebError(exceptionInfo));
} }
catch (OperationCanceledException canceledException) catch (OperationCanceledException canceledException)
{ {
if (cancellationToken != default && canceledException.CancellationToken == cancellationToken) if (cancellationToken != default && canceledException.CancellationToken == cancellationToken)
{ {
// Cancellation token canceled by caller // Cancellation token canceled by caller
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new CancellationRequestedError()); return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new CancellationRequestedError());
} }
else else
{ {
// Request timed out // Request timed out
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new WebError($"Request timed out")); return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new WebError($"Request timed out"));
} }
} }
finally finally
@@ -738,7 +818,8 @@ namespace CryptoExchange.Net.Clients
signed, signed,
arraySerialization, arraySerialization,
parameterPosition, parameterPosition,
bodyFormat); bodyFormat
);
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -804,7 +885,11 @@ namespace CryptoExchange.Net.Clients
if (contentType == Constants.JsonContentHeader) if (contentType == Constants.JsonContentHeader)
{ {
// Write the parameters as json in the body // Write the parameters as json in the body
var stringData = CreateSerializer().Serialize(parameters); string stringData;
if (parameters.Count == 1 && parameters.ContainsKey(Constants.BodyPlaceHolderKey))
stringData = CreateSerializer().Serialize(parameters[Constants.BodyPlaceHolderKey]);
else
stringData = CreateSerializer().Serialize(parameters);
request.SetContent(stringData, contentType); request.SetContent(stringData, contentType);
} }
else if (contentType == Constants.FormContentHeader) else if (contentType == Constants.FormContentHeader)
@@ -877,14 +962,14 @@ namespace CryptoExchange.Net.Clients
{ {
var timeSyncParams = GetTimeSyncInfo(); var timeSyncParams = GetTimeSyncInfo();
if (timeSyncParams == null) if (timeSyncParams == null)
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, ResultDataSource.Server, true, null);
if (await timeSyncParams.TimeSyncState.Semaphore.WaitAsync(0).ConfigureAwait(false)) if (await timeSyncParams.TimeSyncState.Semaphore.WaitAsync(0).ConfigureAwait(false))
{ {
if (!timeSyncParams.SyncTime || DateTime.UtcNow - timeSyncParams.TimeSyncState.LastSyncTime < timeSyncParams.RecalculationInterval) if (!timeSyncParams.SyncTime || DateTime.UtcNow - timeSyncParams.TimeSyncState.LastSyncTime < timeSyncParams.RecalculationInterval)
{ {
timeSyncParams.TimeSyncState.Semaphore.Release(); timeSyncParams.TimeSyncState.Semaphore.Release();
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, ResultDataSource.Server, true, null);
} }
var localTime = DateTime.UtcNow; var localTime = DateTime.UtcNow;
@@ -913,7 +998,16 @@ namespace CryptoExchange.Net.Clients
timeSyncParams.TimeSyncState.Semaphore.Release(); timeSyncParams.TimeSyncState.Semaphore.Release();
} }
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, ResultDataSource.Server, 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;
} }
} }
+109 -41
View File
@@ -8,6 +8,7 @@ using CryptoExchange.Net.RateLimiting.Interfaces;
using CryptoExchange.Net.Sockets; using CryptoExchange.Net.Sockets;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System; using System;
using System.Collections;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@@ -52,11 +53,6 @@ namespace CryptoExchange.Net.Clients
/// </summary> /// </summary>
protected internal bool UnhandledMessageExpected { get; set; } protected internal bool UnhandledMessageExpected { get; set; }
/// <summary>
/// If true a subscription will accept message before the confirmation of a subscription has been received
/// </summary>
protected bool HandleMessageBeforeConfirmation { get; set; }
/// <summary> /// <summary>
/// The rate limiters /// The rate limiters
/// </summary> /// </summary>
@@ -72,6 +68,11 @@ namespace CryptoExchange.Net.Clients
/// </summary> /// </summary>
protected List<PeriodicTaskRegistration> PeriodicTaskRegistrations { get; set; } = new List<PeriodicTaskRegistration>(); protected List<PeriodicTaskRegistration> PeriodicTaskRegistrations { get; set; } = new List<PeriodicTaskRegistration>();
/// <summary>
/// List of address to keep an alive connection to
/// </summary>
protected List<DedicatedConnectionConfig> DedicatedConnectionConfigs { get; set; } = new List<DedicatedConnectionConfig>();
/// <inheritdoc /> /// <inheritdoc />
public double IncomingKbps public double IncomingKbps
{ {
@@ -136,6 +137,16 @@ namespace CryptoExchange.Net.Clients
/// <returns></returns> /// <returns></returns>
protected internal virtual IMessageSerializer CreateSerializer() => new JsonNetMessageSerializer(); protected internal virtual IMessageSerializer CreateSerializer() => new JsonNetMessageSerializer();
/// <summary>
/// Keep an open connection to this url
/// </summary>
/// <param name="url"></param>
/// <param name="auth"></param>
protected virtual void SetDedicatedConnection(string url, bool auth)
{
DedicatedConnectionConfigs.Add(new DedicatedConnectionConfig() { SocketAddress = url, Authenticated = auth });
}
/// <summary> /// <summary>
/// Add a query to periodically send on each connection /// Add a query to periodically send on each connection
/// </summary> /// </summary>
@@ -198,12 +209,11 @@ namespace CryptoExchange.Net.Clients
while (true) while (true)
{ {
// Get a new or existing socket connection // Get a new or existing socket connection
var socketResult = await GetSocketConnection(url, subscription.Authenticated).ConfigureAwait(false); var socketResult = await GetSocketConnection(url, subscription.Authenticated, false).ConfigureAwait(false);
if (!socketResult) if (!socketResult)
return socketResult.As<UpdateSubscription>(null); return socketResult.As<UpdateSubscription>(null);
socketConnection = socketResult.Data; socketConnection = socketResult.Data;
subscription.HandleUpdatesBeforeConfirmation = subscription.HandleUpdatesBeforeConfirmation || HandleMessageBeforeConfirmation;
// Add a subscription on the socket connection // Add a subscription on the socket connection
var success = socketConnection.AddSubscription(subscription); var success = socketConnection.AddSubscription(subscription);
@@ -250,11 +260,18 @@ namespace CryptoExchange.Net.Clients
if (!subResult) if (!subResult)
{ {
waitEvent?.Set(); waitEvent?.Set();
_logger.FailedToSubscribe(socketConnection.SocketId, subResult.Error?.ToString()); var isTimeout = subResult.Error is CancellationRequestedError;
// If this was a timeout we still need to send an unsubscribe to prevent messages coming in later if (isTimeout && subscription.Confirmed)
var unsubscribe = subResult.Error is CancellationRequestedError; {
await socketConnection.CloseAsync(subscription, unsubscribe).ConfigureAwait(false); // No response received, but the subscription did receive updates. We'll assume success
return new CallResult<UpdateSubscription>(subResult.Error!); }
else
{
_logger.FailedToSubscribe(socketConnection.SocketId, subResult.Error?.ToString());
// If this was a timeout we still need to send an unsubscribe to prevent messages coming in later
await socketConnection.CloseAsync(subscription, isTimeout).ConfigureAwait(false);
return new CallResult<UpdateSubscription>(subResult.Error!);
}
} }
subscription.HandleSubQueryResponse(subQuery.Response!); subscription.HandleSubQueryResponse(subQuery.Response!);
@@ -278,34 +295,41 @@ namespace CryptoExchange.Net.Clients
/// <summary> /// <summary>
/// Send a query on a socket connection to the BaseAddress and wait for the response /// Send a query on a socket connection to the BaseAddress and wait for the response
/// </summary> /// </summary>
/// <typeparam name="T">Expected result type</typeparam> /// <typeparam name="THandlerResponse">Expected result type</typeparam>
/// <typeparam name="TServerResponse">The type returned to the caller</typeparam>
/// <param name="query">The query</param> /// <param name="query">The query</param>
/// <param name="ct">Cancellation token</param>
/// <returns></returns> /// <returns></returns>
protected virtual Task<CallResult<T>> QueryAsync<T>(Query<T> query) protected virtual Task<CallResult<THandlerResponse>> QueryAsync<TServerResponse, THandlerResponse>(Query<TServerResponse, THandlerResponse> query, CancellationToken ct = default)
{ {
return QueryAsync(BaseAddress, query); return QueryAsync(BaseAddress, query, ct);
} }
/// <summary> /// <summary>
/// Send a query on a socket connection and wait for the response /// Send a query on a socket connection and wait for the response
/// </summary> /// </summary>
/// <typeparam name="T">The expected result type</typeparam> /// <typeparam name="THandlerResponse">Expected result type</typeparam>
/// <typeparam name="TServerResponse">The type returned to the caller</typeparam>
/// <param name="url">The url for the request</param> /// <param name="url">The url for the request</param>
/// <param name="query">The query</param> /// <param name="query">The query</param>
/// <param name="ct">Cancellation token</param>
/// <returns></returns> /// <returns></returns>
protected virtual async Task<CallResult<T>> QueryAsync<T>(string url, Query<T> query) protected virtual async Task<CallResult<THandlerResponse>> QueryAsync<TServerResponse, THandlerResponse>(string url, Query<TServerResponse, THandlerResponse> query, CancellationToken ct = default)
{ {
if (_disposing) if (_disposing)
return new CallResult<T>(new InvalidOperationError("Client disposed, can't query")); return new CallResult<THandlerResponse>(new InvalidOperationError("Client disposed, can't query"));
if (ct.IsCancellationRequested)
return new CallResult<THandlerResponse>(new CancellationRequestedError());
SocketConnection socketConnection; SocketConnection socketConnection;
var released = false; var released = false;
await semaphoreSlim.WaitAsync().ConfigureAwait(false); await semaphoreSlim.WaitAsync().ConfigureAwait(false);
try try
{ {
var socketResult = await GetSocketConnection(url, query.Authenticated).ConfigureAwait(false); var socketResult = await GetSocketConnection(url, query.Authenticated, true).ConfigureAwait(false);
if (!socketResult) if (!socketResult)
return socketResult.As<T>(default); return socketResult.As<THandlerResponse>(default);
socketConnection = socketResult.Data; socketConnection = socketResult.Data;
@@ -318,7 +342,7 @@ namespace CryptoExchange.Net.Clients
var connectResult = await ConnectIfNeededAsync(socketConnection, query.Authenticated).ConfigureAwait(false); var connectResult = await ConnectIfNeededAsync(socketConnection, query.Authenticated).ConfigureAwait(false);
if (!connectResult) if (!connectResult)
return new CallResult<T>(connectResult.Error!); return new CallResult<THandlerResponse>(connectResult.Error!);
} }
finally finally
{ {
@@ -329,10 +353,13 @@ namespace CryptoExchange.Net.Clients
if (socketConnection.PausedActivity) if (socketConnection.PausedActivity)
{ {
_logger.HasBeenPausedCantSendQueryAtThisMoment(socketConnection.SocketId); _logger.HasBeenPausedCantSendQueryAtThisMoment(socketConnection.SocketId);
return new CallResult<T>(new ServerError("Socket is paused")); return new CallResult<THandlerResponse>(new ServerError("Socket is paused"));
} }
return await socketConnection.SendAndWaitQueryAsync(query).ConfigureAwait(false); if (ct.IsCancellationRequested)
return new CallResult<THandlerResponse>(new CancellationRequestedError());
return await socketConnection.SendAndWaitQueryAsync(query, null, ct).ConfigureAwait(false);
} }
/// <summary> /// <summary>
@@ -356,7 +383,11 @@ namespace CryptoExchange.Net.Clients
if (!authenticated || socket.Authenticated) if (!authenticated || socket.Authenticated)
return new CallResult(null); return new CallResult(null);
return await AuthenticateSocketAsync(socket).ConfigureAwait(false); var result = await AuthenticateSocketAsync(socket).ConfigureAwait(false);
if (!result)
await socket.CloseAsync().ConfigureAwait(false);
return result;
} }
/// <summary> /// <summary>
@@ -370,7 +401,7 @@ namespace CryptoExchange.Net.Clients
return new CallResult(new NoApiCredentialsError()); return new CallResult(new NoApiCredentialsError());
_logger.AttemptingToAuthenticate(socket.SocketId); _logger.AttemptingToAuthenticate(socket.SocketId);
var authRequest = GetAuthenticationRequest(); var authRequest = GetAuthenticationRequest(socket);
if (authRequest != null) if (authRequest != null)
{ {
var result = await socket.SendAndWaitQueryAsync(authRequest).ConfigureAwait(false); var result = await socket.SendAndWaitQueryAsync(authRequest).ConfigureAwait(false);
@@ -395,7 +426,7 @@ namespace CryptoExchange.Net.Clients
/// Should return the request which can be used to authenticate a socket connection /// Should return the request which can be used to authenticate a socket connection
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
protected internal virtual Query? GetAuthenticationRequest() => throw new NotImplementedException(); protected internal virtual Query? GetAuthenticationRequest(SocketConnection connection) => throw new NotImplementedException();
/// <summary> /// <summary>
/// Adds a system subscription. Used for example to reply to ping requests /// Adds a system subscription. Used for example to reply to ping requests
@@ -444,19 +475,31 @@ namespace CryptoExchange.Net.Clients
/// </summary> /// </summary>
/// <param name="address">The address the socket is for</param> /// <param name="address">The address the socket is for</param>
/// <param name="authenticated">Whether the socket should be authenticated</param> /// <param name="authenticated">Whether the socket should be authenticated</param>
/// <param name="dedicatedRequestConnection">Whether a dedicated request connection should be returned</param>
/// <returns></returns> /// <returns></returns>
protected virtual async Task<CallResult<SocketConnection>> GetSocketConnection(string address, bool authenticated) protected virtual async Task<CallResult<SocketConnection>> GetSocketConnection(string address, bool authenticated, bool dedicatedRequestConnection)
{ {
var socketResult = socketConnections.Where(s => (s.Value.Status == SocketConnection.SocketStatus.None || s.Value.Status == SocketConnection.SocketStatus.Connected) var socketQuery = socketConnections.Where(s => (s.Value.Status == SocketConnection.SocketStatus.None || s.Value.Status == SocketConnection.SocketStatus.Connected)
&& s.Value.Tag.TrimEnd('/') == address.TrimEnd('/') && s.Value.Tag.TrimEnd('/') == address.TrimEnd('/')
&& s.Value.ApiClient.GetType() == GetType() && s.Value.ApiClient.GetType() == GetType()
&& (s.Value.Authenticated == authenticated || !authenticated) && s.Value.Connected).OrderBy(s => s.Value.UserSubscriptionCount).FirstOrDefault(); && (s.Value.Authenticated == authenticated || !authenticated)
var result = socketResult.Equals(default(KeyValuePair<int, SocketConnection>)) ? null : socketResult.Value; && s.Value.Connected);
if (result != null)
SocketConnection connection;
if (!dedicatedRequestConnection)
{ {
if (result.UserSubscriptionCount < ClientOptions.SocketSubscriptionsCombineTarget || socketConnections.Count >= (ApiOptions.MaxSocketConnections ?? ClientOptions.MaxSocketConnections) && socketConnections.All(s => s.Value.UserSubscriptionCount >= ClientOptions.SocketSubscriptionsCombineTarget)) connection = socketQuery.Where(s => !s.Value.DedicatedRequestConnection).OrderBy(s => s.Value.UserSubscriptionCount).FirstOrDefault().Value;
}
else
{
connection = socketQuery.Where(s => s.Value.DedicatedRequestConnection).FirstOrDefault().Value;
}
if (connection != null)
{
if (connection.UserSubscriptionCount < ClientOptions.SocketSubscriptionsCombineTarget || socketConnections.Count >= (ApiOptions.MaxSocketConnections ?? ClientOptions.MaxSocketConnections) && socketConnections.All(s => s.Value.UserSubscriptionCount >= ClientOptions.SocketSubscriptionsCombineTarget))
// Use existing socket if it has less than target connections OR it has the least connections and we can't make new // Use existing socket if it has less than target connections OR it has the least connections and we can't make new
return new CallResult<SocketConnection>(result); return new CallResult<SocketConnection>(connection);
} }
var connectionAddress = await GetConnectionUrlAsync(address, authenticated).ConfigureAwait(false); var connectionAddress = await GetConnectionUrlAsync(address, authenticated).ConfigureAwait(false);
@@ -473,6 +516,7 @@ namespace CryptoExchange.Net.Clients
var socket = CreateSocket(connectionAddress.Data!); var socket = CreateSocket(connectionAddress.Data!);
var socketConnection = new SocketConnection(_logger, this, socket, address); var socketConnection = new SocketConnection(_logger, this, socket, address);
socketConnection.UnhandledMessage += HandleUnhandledMessage; socketConnection.UnhandledMessage += HandleUnhandledMessage;
socketConnection.DedicatedRequestConnection = dedicatedRequestConnection;
foreach (var ptg in PeriodicTaskRegistrations) foreach (var ptg in PeriodicTaskRegistrations)
socketConnection.QueryPeriodic(ptg.Identifier, ptg.Interval, ptg.QueryDelegate, ptg.Callback); socketConnection.QueryPeriodic(ptg.Identifier, ptg.Interval, ptg.QueryDelegate, ptg.Callback);
@@ -515,7 +559,7 @@ namespace CryptoExchange.Net.Clients
/// <param name="address">The address to connect to</param> /// <param name="address">The address to connect to</param>
/// <returns></returns> /// <returns></returns>
protected virtual WebSocketParameters GetWebSocketParameters(string address) protected virtual WebSocketParameters GetWebSocketParameters(string address)
=> new(new Uri(address), ClientOptions.AutoReconnect) => new(new Uri(address), ClientOptions.ReconnectPolicy)
{ {
KeepAliveInterval = KeepAliveInterval, KeepAliveInterval = KeepAliveInterval,
ReconnectInterval = ClientOptions.ReconnectInterval, ReconnectInterval = ClientOptions.ReconnectInterval,
@@ -592,8 +636,8 @@ namespace CryptoExchange.Net.Clients
var tasks = new List<Task>(); var tasks = new List<Task>();
{ {
var socketList = socketConnections.Values; var socketList = socketConnections.Values;
foreach (var sub in socketList) foreach (var connection in socketList.Where(s => !s.DedicatedRequestConnection))
tasks.Add(sub.CloseAsync()); tasks.Add(connection.CloseAsync());
} }
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false); await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
@@ -616,6 +660,23 @@ namespace CryptoExchange.Net.Clients
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false); await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
} }
/// <inheritdoc />
public virtual async Task<CallResult> PrepareConnectionsAsync()
{
foreach (var item in DedicatedConnectionConfigs)
{
var socketResult = await GetSocketConnection(item.SocketAddress, item.Authenticated, true).ConfigureAwait(false);
if (!socketResult)
return socketResult.AsDataless();
var connectResult = await ConnectIfNeededAsync(socketResult.Data, item.Authenticated).ConfigureAwait(false);
if (!connectResult)
return new CallResult(connectResult.Error!);
}
return new CallResult(null);
}
/// <summary> /// <summary>
/// Log the current state of connections and subscriptions /// Log the current state of connections and subscriptions
/// </summary> /// </summary>
@@ -699,11 +760,18 @@ namespace CryptoExchange.Net.Clients
public override void Dispose() public override void Dispose()
{ {
_disposing = true; _disposing = true;
if (socketConnections.Sum(s => s.Value.UserSubscriptionCount) > 0) var tasks = new List<Task>();
{ {
_logger.DisposingSocketClient(); var socketList = socketConnections.Values.Where(x => x.UserSubscriptionCount > 0 || x.Connected);
_ = UnsubscribeAllAsync(); if (socketList.Any())
_logger.DisposingSocketClient();
foreach (var connection in socketList)
{
tasks.Add(connection.CloseAsync());
}
} }
semaphoreSlim?.Dispose(); semaphoreSlim?.Dispose();
base.Dispose(); base.Dispose();
} }
@@ -81,7 +81,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
private static object ParseObject(ref Utf8JsonReader reader, object result, Type objectType) private static object ParseObject(ref Utf8JsonReader reader, object result, Type objectType)
{ {
if (reader.TokenType != JsonTokenType.StartArray) if (reader.TokenType != JsonTokenType.StartArray)
throw new Exception("1"); throw new Exception("Not an array");
if (!_typeAttributesCache.TryGetValue(objectType, out var attributes)) if (!_typeAttributesCache.TryGetValue(objectType, out var attributes))
attributes = CacheTypeAttributes(objectType); attributes = CacheTypeAttributes(objectType);
@@ -92,8 +92,11 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
if (reader.TokenType == JsonTokenType.EndArray) if (reader.TokenType == JsonTokenType.EndArray)
break; break;
var attribute = attributes.SingleOrDefault(a => a.ArrayProperty.Index == index); var attribute = attributes.SingleOrDefault(a => a.ArrayProperty.Index == index);
var targetType = attribute.PropertyInfo.PropertyType; if (attribute == null)
continue;
var targetType = attribute.PropertyInfo.PropertyType;
object? value = null; object? value = null;
if (attribute.JsonConverterType != null) if (attribute.JsonConverterType != null)
@@ -154,6 +154,8 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
if (double.TryParse(stringValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var doubleValue)) if (double.TryParse(stringValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var doubleValue))
{ {
// Parse 1637745563.000 format // Parse 1637745563.000 format
if (doubleValue <= 0)
return default;
if (doubleValue < 19999999999) if (doubleValue < 19999999999)
return ConvertFromSeconds(doubleValue); return ConvertFromSeconds(doubleValue);
if (doubleValue < 19999999999999) if (doubleValue < 19999999999999)
@@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.Serialization;
using System.Text;
using System.Text.Json.Serialization;
using System.Text.Json;
using System.Globalization;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
///
/// </summary>
/// <typeparam name="T"></typeparam>
public class ObjectStringConverter<T> : JsonConverter<T>
{
/// <inheritdoc />
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null)
return default;
var value = reader.GetString();
if (string.IsNullOrEmpty(value))
return default;
return (T?)JsonDocument.Parse(value!).Deserialize(typeof(T));
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options)
{
if (value is null)
writer.WriteStringValue("");
writer.WriteStringValue(JsonSerializer.Serialize(value, options));
}
}
}
@@ -68,6 +68,11 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
var info = $"Deserialize JsonException: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}"; var info = $"Deserialize JsonException: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}";
return new CallResult<T>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]")); return new CallResult<T>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
} }
catch (Exception ex)
{
var info = $"Unknown exception: {ex.Message}";
return new CallResult<T>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
}
} }
/// <inheritdoc /> /// <inheritdoc />
+3 -3
View File
@@ -6,9 +6,9 @@
<PackageId>CryptoExchange.Net</PackageId> <PackageId>CryptoExchange.Net</PackageId>
<Authors>JKorf</Authors> <Authors>JKorf</Authors>
<Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description> <Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description>
<PackageVersion>7.5.1</PackageVersion> <PackageVersion>7.7.2</PackageVersion>
<AssemblyVersion>7.5.1</AssemblyVersion> <AssemblyVersion>7.7.2</AssemblyVersion>
<FileVersion>7.5.1</FileVersion> <FileVersion>7.7.2</FileVersion>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance> <PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange</PackageTags> <PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange</PackageTags>
<RepositoryType>git</RepositoryType> <RepositoryType>git</RepositoryType>
+3
View File
@@ -96,6 +96,9 @@ namespace CryptoExchange.Net
var formData = HttpUtility.ParseQueryString(string.Empty); var formData = HttpUtility.ParseQueryString(string.Empty);
foreach (var kvp in parameters) foreach (var kvp in parameters)
{ {
if (kvp.Value is null)
continue;
if (kvp.Value.GetType().IsArray) if (kvp.Value.GetType().IsArray)
{ {
var array = (Array)kvp.Value; var array = (Array)kvp.Value;
@@ -16,10 +16,6 @@ namespace CryptoExchange.Net.Interfaces
/// </summary> /// </summary>
public int Id { get; } public int Id { get; }
/// <summary> /// <summary>
/// Whether this listener can handle data
/// </summary>
public bool CanHandleData { get; }
/// <summary>
/// The identifiers for this processor /// The identifiers for this processor
/// </summary> /// </summary>
public HashSet<string> ListenerIdentifiers { get; } public HashSet<string> ListenerIdentifiers { get; }
@@ -1,4 +1,5 @@
using CryptoExchange.Net.Objects.Options; using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.Objects.Sockets; using CryptoExchange.Net.Objects.Sockets;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -59,5 +60,11 @@ namespace CryptoExchange.Net.Interfaces
/// <param name="subscription">The subscription to unsubscribe</param> /// <param name="subscription">The subscription to unsubscribe</param>
/// <returns></returns> /// <returns></returns>
Task UnsubscribeAsync(UpdateSubscription subscription); Task UnsubscribeAsync(UpdateSubscription subscription);
/// <summary>
/// Prepare connections which can subsequently be used for sending websocket requests.
/// </summary>
/// <returns></returns>
Task<CallResult> PrepareConnectionsAsync();
} }
} }
@@ -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);
}
} }
} }
@@ -32,7 +32,7 @@ namespace CryptoExchange.Net.Objects
/// Wait for the AutoResetEvent to be set /// Wait for the AutoResetEvent to be set
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
public Task<bool> WaitAsync(TimeSpan? timeout = null) public Task<bool> WaitAsync(TimeSpan? timeout = null, CancellationToken ct = default)
{ {
lock (_waits) lock (_waits)
{ {
@@ -44,22 +44,29 @@ namespace CryptoExchange.Net.Objects
} }
else else
{ {
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously); if (ct.IsCancellationRequested)
if(timeout != null) return _completed;
{
var cancellationSource = new CancellationTokenSource(timeout.Value);
var registration = cancellationSource.Token.Register(() =>
{
lock (_waits)
{
tcs.TrySetResult(false);
// Not the cleanest but it works var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
_waits = new Queue<TaskCompletionSource<bool>>(_waits.Where(i => i != tcs)); if (timeout.HasValue)
} {
}, useSynchronizationContext: false); var timeoutSource = new CancellationTokenSource(timeout.Value);
var cancellationSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutSource.Token, ct);
ct = cancellationSource.Token;
} }
var registration = ct.Register(() =>
{
lock (_waits)
{
tcs.TrySetResult(false);
// Not the cleanest but it works
_waits = new Queue<TaskCompletionSource<bool>>(_waits.Where(i => i != tcs));
}
}, useSynchronizationContext: false);
_waits.Enqueue(tcs); _waits.Enqueue(tcs);
return tcs.Task; return tcs.Task;
} }
+20 -3
View File
@@ -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>
@@ -344,6 +349,7 @@ namespace CryptoExchange.Net.Objects
/// <param name="requestBody"></param> /// <param name="requestBody"></param>
/// <param name="requestMethod"></param> /// <param name="requestMethod"></param>
/// <param name="requestHeaders"></param> /// <param name="requestHeaders"></param>
/// <param name="dataSource"></param>
/// <param name="data"></param> /// <param name="data"></param>
/// <param name="error"></param> /// <param name="error"></param>
public WebCallResult( public WebCallResult(
@@ -357,6 +363,7 @@ namespace CryptoExchange.Net.Objects
string? requestBody, string? requestBody,
HttpMethod? requestMethod, HttpMethod? requestMethod,
IEnumerable<KeyValuePair<string, IEnumerable<string>>>? requestHeaders, IEnumerable<KeyValuePair<string, IEnumerable<string>>>? requestHeaders,
ResultDataSource dataSource,
[AllowNull] T data, [AllowNull] T data,
Error? error) : base(data, originalData, error) Error? error) : base(data, originalData, error)
{ {
@@ -370,6 +377,7 @@ namespace CryptoExchange.Net.Objects
RequestBody = requestBody; RequestBody = requestBody;
RequestHeaders = requestHeaders; RequestHeaders = requestHeaders;
RequestMethod = requestMethod; RequestMethod = requestMethod;
DataSource = dataSource;
} }
/// <summary> /// <summary>
@@ -393,7 +401,7 @@ namespace CryptoExchange.Net.Objects
/// Create a new error result /// Create a new error result
/// </summary> /// </summary>
/// <param name="error">The error</param> /// <param name="error">The error</param>
public WebCallResult(Error? error) : this(null, null, null, null, null, null, null, null, null, null, default, error) { } public WebCallResult(Error? error) : this(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, default, error) { }
/// <summary> /// <summary>
/// Copy the WebCallResult to a new data type /// Copy the WebCallResult to a new data type
@@ -403,7 +411,7 @@ namespace CryptoExchange.Net.Objects
/// <returns></returns> /// <returns></returns>
public new WebCallResult<K> As<K>([AllowNull] K data) public new WebCallResult<K> As<K>([AllowNull] K data)
{ {
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, data, Error); return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, data, Error);
} }
/// <summary> /// <summary>
@@ -414,7 +422,16 @@ namespace CryptoExchange.Net.Objects
/// <returns></returns> /// <returns></returns>
public new WebCallResult<K> AsError<K>(Error error) public new WebCallResult<K> AsError<K>(Error error)
{ {
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, DataSource, default, error);
}
/// <summary>
/// Return a copy of this result with data source set to cache
/// </summary>
/// <returns></returns>
internal WebCallResult<T> Cached()
{
return new WebCallResult<T>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, ResultDataSource.Cache, Data, Error);
} }
/// <inheritdoc /> /// <inheritdoc />
+4
View File
@@ -13,5 +13,9 @@
/// Form content type header /// Form content type header
/// </summary> /// </summary>
public const string FormContentHeader = "application/x-www-form-urlencoded"; public const string FormContentHeader = "application/x-www-form-urlencoded";
/// <summary>
/// Placeholder key for when request body should be set to the value of this KVP
/// </summary>
public const string BodyPlaceHolderKey = "_BODY_";
} }
} }
+34
View File
@@ -169,4 +169,38 @@
/// </summary> /// </summary>
Snapshot Snapshot
} }
/// <summary>
/// Reconnect policy
/// </summary>
public enum ReconnectPolicy
{
/// <summary>
/// Reconnect is disabled
/// </summary>
Disabled,
/// <summary>
/// Fixed delay of `ReconnectInterval` between retries
/// </summary>
FixedDelay,
/// <summary>
/// Backof policy of 2^`reconnectAttempt`, where `reconnectAttempt` has a max value of 5
/// </summary>
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,
}; };
} }
} }
@@ -1,4 +1,5 @@
using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Objects.Sockets;
using System; using System;
namespace CryptoExchange.Net.Objects.Options namespace CryptoExchange.Net.Objects.Options
@@ -9,15 +10,15 @@ namespace CryptoExchange.Net.Objects.Options
public class SocketExchangeOptions : ExchangeOptions public class SocketExchangeOptions : ExchangeOptions
{ {
/// <summary> /// <summary>
/// Whether or not the socket should automatically reconnect when losing connection /// The fixed time to wait between reconnect attempts, only used when `ReconnectPolicy` is set to `ReconnectPolicy.ExponentialBackoff`
/// </summary>
public bool AutoReconnect { get; set; } = true;
/// <summary>
/// Time to wait between reconnect attempts
/// </summary> /// </summary>
public TimeSpan ReconnectInterval { get; set; } = TimeSpan.FromSeconds(5); public TimeSpan ReconnectInterval { get; set; } = TimeSpan.FromSeconds(5);
/// <summary>
/// Reconnect policy
/// </summary>
public ReconnectPolicy ReconnectPolicy { get; set; } = ReconnectPolicy.FixedDelay;
/// <summary> /// <summary>
/// Max number of concurrent resubscription tasks per socket after reconnecting a socket /// Max number of concurrent resubscription tasks per socket after reconnecting a socket
/// </summary> /// </summary>
@@ -57,7 +58,7 @@ namespace CryptoExchange.Net.Objects.Options
{ {
ApiCredentials = ApiCredentials?.Copy(), ApiCredentials = ApiCredentials?.Copy(),
OutputOriginalData = OutputOriginalData, OutputOriginalData = OutputOriginalData,
AutoReconnect = AutoReconnect, ReconnectPolicy = ReconnectPolicy,
DelayAfterConnect = DelayAfterConnect, DelayAfterConnect = DelayAfterConnect,
MaxConcurrentResubscriptionsPerSocket = MaxConcurrentResubscriptionsPerSocket, MaxConcurrentResubscriptionsPerSocket = MaxConcurrentResubscriptionsPerSocket,
ReconnectInterval = ReconnectInterval, ReconnectInterval = ReconnectInterval,
@@ -3,6 +3,7 @@ using CryptoExchange.Net.Converters.SystemTextJson;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq;
namespace CryptoExchange.Net.Objects namespace CryptoExchange.Net.Objects
{ {
@@ -193,5 +194,18 @@ namespace CryptoExchange.Net.Objects
Add(key, int.Parse(stringVal)); Add(key, int.Parse(stringVal));
} }
} }
/// <summary>
/// Set the request body. Can be used to specify a simple value or array as the body instead of an object
/// </summary>
/// <param name="body">Body to set</param>
/// <exception cref="InvalidOperationException"></exception>
public void SetBody(object body)
{
if (this.Any())
throw new InvalidOperationException("Can't set body when other parameters already specified");
Add(Constants.BodyPlaceHolderKey, body);
}
} }
} }
@@ -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);
} }
+78 -11
View File
@@ -14,9 +14,14 @@ namespace CryptoExchange.Net.Objects.Sockets
public DateTime Timestamp { get; set; } public DateTime Timestamp { get; set; }
/// <summary> /// <summary>
/// The topic of the update, what symbol/asset etc.. /// The stream producing the update
/// </summary> /// </summary>
public string? Topic { get; set; } public string? StreamId { get; set; }
/// <summary>
/// The symbol the update is for
/// </summary>
public string? Symbol { get; set; }
/// <summary> /// <summary>
/// The original data that was received, only available when OutputOriginalData is set to true in the client options /// The original data that was received, only available when OutputOriginalData is set to true in the client options
@@ -33,10 +38,11 @@ namespace CryptoExchange.Net.Objects.Sockets
/// </summary> /// </summary>
public T Data { get; set; } public T Data { get; set; }
internal DataEvent(T data, string? topic, string? originalData, DateTime timestamp, SocketUpdateType? updateType) internal DataEvent(T data, string? streamId, string? symbol, string? originalData, DateTime timestamp, SocketUpdateType? updateType)
{ {
Data = data; Data = data;
Topic = topic; StreamId = streamId;
Symbol = symbol;
OriginalData = originalData; OriginalData = originalData;
Timestamp = timestamp; Timestamp = timestamp;
UpdateType = updateType; UpdateType = updateType;
@@ -50,7 +56,7 @@ namespace CryptoExchange.Net.Objects.Sockets
/// <returns></returns> /// <returns></returns>
public DataEvent<K> As<K>(K data) public DataEvent<K> As<K>(K data)
{ {
return new DataEvent<K>(data, Topic, OriginalData, Timestamp, UpdateType); return new DataEvent<K>(data, StreamId, Symbol, OriginalData, Timestamp, UpdateType);
} }
/// <summary> /// <summary>
@@ -58,11 +64,11 @@ namespace CryptoExchange.Net.Objects.Sockets
/// </summary> /// </summary>
/// <typeparam name="K">The type of the new data</typeparam> /// <typeparam name="K">The type of the new data</typeparam>
/// <param name="data">The new data</param> /// <param name="data">The new data</param>
/// <param name="topic">The new topic</param> /// <param name="symbol">The new symbol</param>
/// <returns></returns> /// <returns></returns>
public DataEvent<K> As<K>(K data, string? topic) public DataEvent<K> As<K>(K data, string? symbol)
{ {
return new DataEvent<K>(data, topic, OriginalData, Timestamp, UpdateType); return new DataEvent<K>(data, StreamId, symbol, OriginalData, Timestamp, UpdateType);
} }
/// <summary> /// <summary>
@@ -70,12 +76,73 @@ namespace CryptoExchange.Net.Objects.Sockets
/// </summary> /// </summary>
/// <typeparam name="K">The type of the new data</typeparam> /// <typeparam name="K">The type of the new data</typeparam>
/// <param name="data">The new data</param> /// <param name="data">The new data</param>
/// <param name="topic">The new topic</param> /// <param name="streamId">The new stream id</param>
/// <param name="symbol">The new symbol</param>
/// <param name="updateType">The type of update</param> /// <param name="updateType">The type of update</param>
/// <returns></returns> /// <returns></returns>
public DataEvent<K> As<K>(K data, string? topic, SocketUpdateType updateType) public DataEvent<K> As<K>(K data, string streamId, string? symbol, SocketUpdateType updateType)
{ {
return new DataEvent<K>(data, topic, OriginalData, Timestamp, updateType); return new DataEvent<K>(data, streamId, symbol, OriginalData, Timestamp, updateType);
}
/// <summary>
/// Specify the symbol
/// </summary>
/// <param name="symbol"></param>
/// <returns></returns>
public DataEvent<T> WithSymbol(string symbol)
{
Symbol = symbol;
return this;
}
/// <summary>
/// Specify the update type
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
public DataEvent<T> WithUpdateType(SocketUpdateType type)
{
UpdateType = type;
return this;
}
/// <summary>
/// Specify the stream id
/// </summary>
/// <param name="streamId"></param>
/// <returns></returns>
public DataEvent<T> WithStreamId(string streamId)
{
StreamId = streamId;
return this;
}
/// <summary>
/// Create a CallResult from this DataEvent
/// </summary>
/// <returns></returns>
public CallResult<T> ToCallResult()
{
return new CallResult<T>(Data, OriginalData, null);
}
/// <summary>
/// Create a CallResult from this DataEvent
/// </summary>
/// <returns></returns>
public CallResult<K> ToCallResult<K>(K data)
{
return new CallResult<K>(data, OriginalData, null);
}
/// <summary>
/// Create a CallResult from this DataEvent
/// </summary>
/// <returns></returns>
public CallResult<K> ToCallResult<K>(Error error)
{
return new CallResult<K>(default, OriginalData, error);
} }
} }
} }
@@ -26,20 +26,20 @@ namespace CryptoExchange.Net.Objects.Sockets
public IDictionary<string, string> Cookies { get; set; } = new Dictionary<string, string>(); public IDictionary<string, string> Cookies { get; set; } = new Dictionary<string, string>();
/// <summary> /// <summary>
/// The time to wait between reconnect attempts /// The fixed time to wait between reconnect attempts, only used when `ReconnectPolicy` is set to `ReconnectPolicy.ExponentialBackoff`
/// </summary> /// </summary>
public TimeSpan ReconnectInterval { get; set; } = TimeSpan.FromSeconds(5); public TimeSpan ReconnectInterval { get; set; } = TimeSpan.FromSeconds(5);
/// <summary>
/// Reconnect policy
/// </summary>
public ReconnectPolicy ReconnectPolicy { get; set; } = ReconnectPolicy.FixedDelay;
/// <summary> /// <summary>
/// Proxy for the connection /// Proxy for the connection
/// </summary> /// </summary>
public ApiProxy? Proxy { get; set; } public ApiProxy? Proxy { get; set; }
/// <summary>
/// Whether the socket should automatically reconnect when connection is lost
/// </summary>
public bool AutoReconnect { get; set; }
/// <summary> /// <summary>
/// The maximum time of no data received before considering the connection lost and closting/reconnecting the socket /// The maximum time of no data received before considering the connection lost and closting/reconnecting the socket
/// </summary> /// </summary>
@@ -68,11 +68,11 @@ namespace CryptoExchange.Net.Objects.Sockets
/// ctor /// ctor
/// </summary> /// </summary>
/// <param name="uri">Uri</param> /// <param name="uri">Uri</param>
/// <param name="autoReconnect">Auto reconnect</param> /// <param name="policy">Reconnect policy</param>
public WebSocketParameters(Uri uri, bool autoReconnect) public WebSocketParameters(Uri uri, ReconnectPolicy policy)
{ {
Uri = uri; Uri = uri;
AutoReconnect = autoReconnect; ReconnectPolicy = policy;
} }
} }
} }
@@ -101,7 +101,7 @@ namespace CryptoExchange.Net.RateLimiting
logger.RateLimitDelayingRequest(itemId, definition.Path, result.Delay, guard.Name, description); logger.RateLimitDelayingRequest(itemId, definition.Path, result.Delay, guard.Name, description);
RateLimitTriggered?.Invoke(new RateLimitEvent(_name, guard.Description, definition, host, result.Current, requestWeight, result.Limit, result.Period, result.Delay, rateLimitingBehaviour)); RateLimitTriggered?.Invoke(new RateLimitEvent(_name, guard.Description, definition, host, result.Current, requestWeight, result.Limit, result.Period, result.Delay, rateLimitingBehaviour));
await Task.Delay(result.Delay, ct).ConfigureAwait(false); await Task.Delay((int)result.Delay.TotalMilliseconds + 1, ct).ConfigureAwait(false);
await _semaphore.WaitAsync(ct).ConfigureAwait(false); await _semaphore.WaitAsync(ct).ConfigureAwait(false);
return await CheckGuardsAsync(guards, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, ct).ConfigureAwait(false); return await CheckGuardsAsync(guards, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, ct).ConfigureAwait(false);
} }
@@ -80,7 +80,10 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
private TimeSpan DetermineWaitTime(int requestWeight) private TimeSpan DetermineWaitTime(int requestWeight)
{ {
var weightToRemove = Math.Max(Current - (Limit - requestWeight), 0); var weightToRemove = Math.Max(Current - (Limit - requestWeight), 0);
return TimeSpan.FromMilliseconds(Math.Ceiling(weightToRemove / DecreaseRate) * TimePeriod.TotalMilliseconds); var result = TimeSpan.FromMilliseconds(Math.Ceiling(weightToRemove / DecreaseRate) * TimePeriod.TotalMilliseconds);
if (result < TimeSpan.Zero)
return TimeSpan.Zero;
return result;
} }
} }
} }
@@ -97,7 +97,10 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
private TimeSpan DetermineWaitTime() private TimeSpan DetermineWaitTime()
{ {
var checkTime = DateTime.UtcNow; var checkTime = DateTime.UtcNow;
return (_nextReset!.Value - checkTime) + _fixedWindowBuffer; var result = (_nextReset!.Value - checkTime) + _fixedWindowBuffer;
if (result < TimeSpan.Zero)
return TimeSpan.Zero;
return result;
} }
} }
} }
@@ -93,7 +93,10 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
var checkTime = DateTime.UtcNow; var checkTime = DateTime.UtcNow;
var startCurrentWindow = checkTime.AddTicks(-(checkTime.Ticks % TimePeriod.Ticks)); var startCurrentWindow = checkTime.AddTicks(-(checkTime.Ticks % TimePeriod.Ticks));
var wait = startCurrentWindow.Add(TimePeriod) - checkTime; var wait = startCurrentWindow.Add(TimePeriod) - checkTime;
return wait.Add(_fixedWindowBuffer); var result = wait.Add(_fixedWindowBuffer);
if (result < TimeSpan.Zero)
return TimeSpan.Zero;
return result;
} }
} }
} }
@@ -16,6 +16,11 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
private readonly List<LimitEntry> _entries; private readonly List<LimitEntry> _entries;
private int _currentWeight = 0; private int _currentWeight = 0;
/// <summary>
/// Additional wait time to apply to account for fluctuating request times
/// </summary>
private static readonly TimeSpan _slidingWindowBuffer = TimeSpan.FromMilliseconds(1000);
public SlidingWindowTracker(int limit, TimeSpan period) public SlidingWindowTracker(int limit, TimeSpan period)
{ {
Limit = limit; Limit = limit;
@@ -89,7 +94,10 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
removedWeight += entry.Weight; removedWeight += entry.Weight;
if (removedWeight >= weightToRemove) if (removedWeight >= weightToRemove)
{ {
return entry.Timestamp + TimePeriod - DateTime.UtcNow; var result = entry.Timestamp + TimePeriod + _slidingWindowBuffer - DateTime.UtcNow;
if (result < TimeSpan.Zero)
return TimeSpan.Zero;
return result;
} }
} }
@@ -19,6 +19,7 @@ namespace CryptoExchange.Net.Requests
if (client == null) if (client == null)
{ {
var handler = new HttpClientHandler(); var handler = new HttpClientHandler();
handler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
if (proxy != null) if (proxy != null)
{ {
handler.Proxy = new WebProxy handler.Proxy = new WebProxy
@@ -47,6 +47,7 @@ namespace CryptoExchange.Net.Sockets
private ProcessState _processState; private ProcessState _processState;
private DateTime _lastReconnectTime; private DateTime _lastReconnectTime;
private string _baseAddress; private string _baseAddress;
private int _reconnectAttempt;
private const int _receiveBufferSize = 1048576; private const int _receiveBufferSize = 1048576;
private const int _sendBufferSize = 4096; private const int _sendBufferSize = 4096;
@@ -246,12 +247,12 @@ namespace CryptoExchange.Net.Sockets
await _closeTask.ConfigureAwait(false); await _closeTask.ConfigureAwait(false);
_closeTask = null; _closeTask = null;
if (!Parameters.AutoReconnect) if (Parameters.ReconnectPolicy == ReconnectPolicy.Disabled)
{ {
_processState = ProcessState.Idle; _processState = ProcessState.Idle;
await (OnClose?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false); await (OnClose?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
return; return;
} }
if (!_stopRequested) if (!_stopRequested)
{ {
@@ -259,9 +260,9 @@ namespace CryptoExchange.Net.Sockets
await (OnReconnecting?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false); await (OnReconnecting?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
} }
var sinceLastReconnect = DateTime.UtcNow - _lastReconnectTime; // Delay here to prevent very repid looping when a connection to the server is accepted and immediately disconnected
if (sinceLastReconnect < Parameters.ReconnectInterval) var initialDelay = GetReconnectDelay();
await Task.Delay(Parameters.ReconnectInterval - sinceLastReconnect).ConfigureAwait(false); await Task.Delay(initialDelay).ConfigureAwait(false);
while (!_stopRequested) while (!_stopRequested)
{ {
@@ -282,13 +283,17 @@ namespace CryptoExchange.Net.Sockets
_ctsSource = new CancellationTokenSource(); _ctsSource = new CancellationTokenSource();
while (_sendBuffer.TryDequeue(out _)) { } // Clear send buffer while (_sendBuffer.TryDequeue(out _)) { } // Clear send buffer
_reconnectAttempt++;
var connected = await ConnectInternalAsync().ConfigureAwait(false); var connected = await ConnectInternalAsync().ConfigureAwait(false);
if (!connected) if (!connected)
{ {
await Task.Delay(Parameters.ReconnectInterval).ConfigureAwait(false); // Delay between reconnect attempts
var delay = GetReconnectDelay();
await Task.Delay(delay).ConfigureAwait(false);
continue; continue;
} }
_reconnectAttempt = 0;
_lastReconnectTime = DateTime.UtcNow; _lastReconnectTime = DateTime.UtcNow;
await (OnReconnected?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false); await (OnReconnected?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
break; break;
@@ -298,6 +303,24 @@ namespace CryptoExchange.Net.Sockets
_processState = ProcessState.Idle; _processState = ProcessState.Idle;
} }
private TimeSpan GetReconnectDelay()
{
if (_reconnectAttempt == 0)
{
// Means this is directly after disconnecting. Only delay if the last reconnect time is very recent
var sinceLastReconnect = DateTime.UtcNow - _lastReconnectTime;
if (sinceLastReconnect < TimeSpan.FromSeconds(5))
return TimeSpan.FromSeconds(5) - sinceLastReconnect;
return TimeSpan.FromMilliseconds(1);
}
var delay = Parameters.ReconnectPolicy == ReconnectPolicy.FixedDelay ? Parameters.ReconnectInterval : TimeSpan.FromSeconds(Math.Pow(2, Math.Min(5, _reconnectAttempt)));
if (delay > TimeSpan.Zero)
return delay;
return TimeSpan.FromMilliseconds(1);
}
/// <inheritdoc /> /// <inheritdoc />
public virtual void Send(int id, string data, int weight) public virtual void Send(int id, string data, int weight)
{ {
@@ -439,11 +462,19 @@ namespace CryptoExchange.Net.Sockets
{ {
if (Parameters.RateLimiter != null) if (Parameters.RateLimiter != null)
{ {
var limitResult = await Parameters.RateLimiter.ProcessAsync(_logger, data.Id, RateLimitItemType.Request, requestDefinition, _baseAddress, null, data.Weight, Parameters.RateLimitingBehaviour, _ctsSource.Token).ConfigureAwait(false); try
if (!limitResult)
{ {
await (OnRequestRateLimited?.Invoke(data.Id) ?? Task.CompletedTask).ConfigureAwait(false); var limitResult = await Parameters.RateLimiter.ProcessAsync(_logger, data.Id, RateLimitItemType.Request, requestDefinition, _baseAddress, null, data.Weight, Parameters.RateLimitingBehaviour, _ctsSource.Token).ConfigureAwait(false);
continue; if (!limitResult)
{
await (OnRequestRateLimited?.Invoke(data.Id) ?? Task.CompletedTask).ConfigureAwait(false);
continue;
}
}
catch (OperationCanceledException)
{
// canceled
break;
} }
} }
@@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.Sockets
{
/// <summary>
/// Dedicated connection configuration
/// </summary>
public class DedicatedConnectionConfig
{
/// <summary>
/// Socket address
/// </summary>
public string SocketAddress { get; set; } = string.Empty;
/// <summary>
/// authenticated
/// </summary>
public bool Authenticated { get; set; }
}
}
+37 -14
View File
@@ -1,6 +1,7 @@
using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Sockets; using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.Requests;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading; using System.Threading;
@@ -18,11 +19,6 @@ namespace CryptoExchange.Net.Sockets
/// </summary> /// </summary>
public int Id { get; } = ExchangeHelpers.NextId(); public int Id { get; } = ExchangeHelpers.NextId();
/// <summary>
/// Can handle data
/// </summary>
public bool CanHandleData => true;
/// <summary> /// <summary>
/// Has this query been completed /// Has this query been completed
/// </summary> /// </summary>
@@ -115,8 +111,9 @@ namespace CryptoExchange.Net.Sockets
/// Wait untill timeout or the request is competed /// Wait untill timeout or the request is competed
/// </summary> /// </summary>
/// <param name="timeout"></param> /// <param name="timeout"></param>
/// <param name="ct">Cancellation token</param>
/// <returns></returns> /// <returns></returns>
public async Task WaitAsync(TimeSpan timeout) => await _event.WaitAsync(timeout).ConfigureAwait(false); public async Task WaitAsync(TimeSpan timeout, CancellationToken ct) => await _event.WaitAsync(timeout, ct).ConfigureAwait(false);
/// <inheritdoc /> /// <inheritdoc />
public virtual CallResult<object> Deserialize(IMessageAccessor message, Type type) => message.Deserialize(type); public virtual CallResult<object> Deserialize(IMessageAccessor message, Type type) => message.Deserialize(type);
@@ -145,16 +142,17 @@ namespace CryptoExchange.Net.Sockets
/// <summary> /// <summary>
/// Query /// Query
/// </summary> /// </summary>
/// <typeparam name="TResponse">Response object type</typeparam> /// <typeparam name="TServerResponse">The type returned from the server</typeparam>
public abstract class Query<TResponse> : Query /// <typeparam name="THandlerResponse">The type to be returned to the caller</typeparam>
public abstract class Query<TServerResponse, THandlerResponse> : Query
{ {
/// <inheritdoc /> /// <inheritdoc />
public override Type? GetMessageType(IMessageAccessor message) => typeof(TResponse); public override Type? GetMessageType(IMessageAccessor message) => typeof(TServerResponse);
/// <summary> /// <summary>
/// The typed call result /// The typed call result
/// </summary> /// </summary>
public CallResult<TResponse>? TypedResult => (CallResult<TResponse>?)Result; public CallResult<THandlerResponse>? TypedResult => (CallResult<THandlerResponse>?)Result;
/// <summary> /// <summary>
/// ctor /// ctor
@@ -171,7 +169,7 @@ namespace CryptoExchange.Net.Sockets
{ {
Completed = true; Completed = true;
Response = message.Data; Response = message.Data;
Result = HandleMessage(connection, message.As((TResponse)message.Data)); Result = HandleMessage(connection, message.As((TServerResponse)message.Data));
_event.Set(); _event.Set();
ContinueAwaiter?.WaitOne(); ContinueAwaiter?.WaitOne();
return Result; return Result;
@@ -183,7 +181,7 @@ namespace CryptoExchange.Net.Sockets
/// <param name="connection"></param> /// <param name="connection"></param>
/// <param name="message"></param> /// <param name="message"></param>
/// <returns></returns> /// <returns></returns>
public virtual CallResult<TResponse> HandleMessage(SocketConnection connection, DataEvent<TResponse> message) => new CallResult<TResponse>(message.Data, message.OriginalData, null); public abstract CallResult<THandlerResponse> HandleMessage(SocketConnection connection, DataEvent<TServerResponse> message);
/// <inheritdoc /> /// <inheritdoc />
public override void Timeout() public override void Timeout()
@@ -192,7 +190,7 @@ namespace CryptoExchange.Net.Sockets
return; return;
Completed = true; Completed = true;
Result = new CallResult<TResponse>(new CancellationRequestedError(null, "Query timeout", null)); Result = new CallResult<THandlerResponse>(new CancellationRequestedError(null, "Query timeout", null));
ContinueAwaiter?.Set(); ContinueAwaiter?.Set();
_event.Set(); _event.Set();
} }
@@ -200,10 +198,35 @@ namespace CryptoExchange.Net.Sockets
/// <inheritdoc /> /// <inheritdoc />
public override void Fail(Error error) public override void Fail(Error error)
{ {
Result = new CallResult<TResponse>(error); Result = new CallResult<THandlerResponse>(error);
Completed = true; Completed = true;
ContinueAwaiter?.Set(); ContinueAwaiter?.Set();
_event.Set(); _event.Set();
} }
} }
/// <summary>
/// Query
/// </summary>
/// <typeparam name="TResponse">Response object type</typeparam>
public abstract class Query<TResponse> : Query<TResponse, TResponse>
{
/// <summary>
/// ctor
/// </summary>
/// <param name="request"></param>
/// <param name="authenticated"></param>
/// <param name="weight"></param>
protected Query(object request, bool authenticated, int weight = 1) : base(request, authenticated, weight)
{
}
/// <summary>
/// Handle the query response
/// </summary>
/// <param name="connection"></param>
/// <param name="message"></param>
/// <returns></returns>
public override CallResult<TResponse> HandleMessage(SocketConnection connection, DataEvent<TResponse> message) => message.ToCallResult();
}
} }
+45 -25
View File
@@ -175,6 +175,11 @@ namespace CryptoExchange.Net.Sockets
} }
} }
/// <summary>
/// Whether this connection should be kept alive even when there is no subscription
/// </summary>
public bool DedicatedRequestConnection { get; internal set; }
private bool _pausedActivity; private bool _pausedActivity;
private readonly object _listenersLock; private readonly object _listenersLock;
private readonly List<IMessageProcessor> _listeners; private readonly List<IMessageProcessor> _listeners;
@@ -443,7 +448,7 @@ namespace CryptoExchange.Net.Sockets
// 4. Get the listeners interested in this message // 4. Get the listeners interested in this message
List<IMessageProcessor> processors; List<IMessageProcessor> processors;
lock (_listenersLock) lock (_listenersLock)
processors = _listeners.Where(s => s.ListenerIdentifiers.Contains(listenId) && s.CanHandleData).ToList(); processors = _listeners.Where(s => s.ListenerIdentifiers.Contains(listenId)).ToList();
if (processors.Count == 0) if (processors.Count == 0)
{ {
@@ -451,7 +456,7 @@ namespace CryptoExchange.Net.Sockets
{ {
List<string> listenerIds; List<string> listenerIds;
lock (_listenersLock) lock (_listenersLock)
listenerIds = _listeners.Where(l => l.CanHandleData).SelectMany(l => l.ListenerIdentifiers).ToList(); listenerIds = _listeners.SelectMany(l => l.ListenerIdentifiers).ToList();
_logger.ReceivedMessageNotMatchedToAnyListener(SocketId, listenId, string.Join(",", listenerIds)); _logger.ReceivedMessageNotMatchedToAnyListener(SocketId, listenId, string.Join(",", listenerIds));
UnhandledMessage?.Invoke(_accessor); UnhandledMessage?.Invoke(_accessor);
} }
@@ -478,6 +483,10 @@ namespace CryptoExchange.Net.Sockets
continue; continue;
} }
if (processor is Subscription subscriptionProcessor && !subscriptionProcessor.Confirmed)
// If this message is for this listener then it is automatically confirmed, even if the subscription is not (yet) confirmed
subscriptionProcessor.Confirmed = true;
// 6. Deserialize the message // 6. Deserialize the message
object? deserialized = null; object? deserialized = null;
desCache?.TryGetValue(messageType, out deserialized); desCache?.TryGetValue(messageType, out deserialized);
@@ -498,7 +507,7 @@ namespace CryptoExchange.Net.Sockets
try try
{ {
var innerSw = Stopwatch.StartNew(); var innerSw = Stopwatch.StartNew();
processor.Handle(this, new DataEvent<object>(deserialized, null, originalData, receiveTime, null)); processor.Handle(this, new DataEvent<object>(deserialized, null, null, originalData, receiveTime, null));
totalUserTime += (int)innerSw.ElapsedMilliseconds; totalUserTime += (int)innerSw.ElapsedMilliseconds;
} }
catch (Exception ex) catch (Exception ex)
@@ -604,7 +613,7 @@ namespace CryptoExchange.Net.Sockets
bool shouldCloseConnection; bool shouldCloseConnection;
lock (_listenersLock) lock (_listenersLock)
{ {
shouldCloseConnection = _listeners.OfType<Subscription>().All(r => !r.UserSubscription || r.Closed); shouldCloseConnection = _listeners.OfType<Subscription>().All(r => !r.UserSubscription || r.Closed) && !DedicatedRequestConnection;
if (shouldCloseConnection) if (shouldCloseConnection)
Status = SocketStatus.Closing; Status = SocketStatus.Closing;
} }
@@ -686,27 +695,30 @@ namespace CryptoExchange.Net.Sockets
/// </summary> /// </summary>
/// <param name="query">Query to send</param> /// <param name="query">Query to send</param>
/// <param name="continueEvent">Wait event for when the socket message handler can continue</param> /// <param name="continueEvent">Wait event for when the socket message handler can continue</param>
/// <param name="ct">Cancellation token</param>
/// <returns></returns> /// <returns></returns>
public virtual async Task<CallResult> SendAndWaitQueryAsync(Query query, ManualResetEvent? continueEvent = null) public virtual async Task<CallResult> SendAndWaitQueryAsync(Query query, ManualResetEvent? continueEvent = null, CancellationToken ct = default)
{ {
await SendAndWaitIntAsync(query, continueEvent).ConfigureAwait(false); await SendAndWaitIntAsync(query, continueEvent, ct).ConfigureAwait(false);
return query.Result ?? new CallResult(new ServerError("Timeout")); return query.Result ?? new CallResult(new ServerError("Timeout"));
} }
/// <summary> /// <summary>
/// Send a query request and wait for an answer /// Send a query request and wait for an answer
/// </summary> /// </summary>
/// <typeparam name="T">Query response type</typeparam> /// <typeparam name="THandlerResponse">Expected result type</typeparam>
/// <typeparam name="TServerResponse">The type returned to the caller</typeparam>
/// <param name="query">Query to send</param> /// <param name="query">Query to send</param>
/// <param name="continueEvent">Wait event for when the socket message handler can continue</param> /// <param name="continueEvent">Wait event for when the socket message handler can continue</param>
/// <param name="ct">Cancellation token</param>
/// <returns></returns> /// <returns></returns>
public virtual async Task<CallResult<T>> SendAndWaitQueryAsync<T>(Query<T> query, ManualResetEvent? continueEvent = null) public virtual async Task<CallResult<THandlerResponse>> SendAndWaitQueryAsync<TServerResponse, THandlerResponse>(Query<TServerResponse, THandlerResponse> query, ManualResetEvent? continueEvent = null, CancellationToken ct = default)
{ {
await SendAndWaitIntAsync(query, continueEvent).ConfigureAwait(false); await SendAndWaitIntAsync(query, continueEvent, ct).ConfigureAwait(false);
return query.TypedResult ?? new CallResult<T>(new ServerError("Timeout")); return query.TypedResult ?? new CallResult<THandlerResponse>(new ServerError("Timeout"));
} }
private async Task SendAndWaitIntAsync(Query query, ManualResetEvent? continueEvent) private async Task SendAndWaitIntAsync(Query query, ManualResetEvent? continueEvent, CancellationToken ct = default)
{ {
lock(_listenersLock) lock(_listenersLock)
_listeners.Add(query); _listeners.Add(query);
@@ -723,7 +735,7 @@ namespace CryptoExchange.Net.Sockets
try try
{ {
while (true) while (!ct.IsCancellationRequested)
{ {
if (!_socket.IsOpen) if (!_socket.IsOpen)
{ {
@@ -734,11 +746,17 @@ namespace CryptoExchange.Net.Sockets
if (query.Completed) if (query.Completed)
return; return;
await query.WaitAsync(TimeSpan.FromMilliseconds(500)).ConfigureAwait(false); await query.WaitAsync(TimeSpan.FromMilliseconds(500), ct).ConfigureAwait(false);
if (query.Completed) if (query.Completed)
return; return;
} }
if (ct.IsCancellationRequested)
{
query.Fail(new CancellationRequestedError());
return;
}
} }
finally finally
{ {
@@ -798,20 +816,23 @@ namespace CryptoExchange.Net.Sockets
if (!_socket.IsOpen) if (!_socket.IsOpen)
return new CallResult(new WebError("Socket not connected")); return new CallResult(new WebError("Socket not connected"));
bool anySubscriptions; if (!DedicatedRequestConnection)
lock (_listenersLock)
anySubscriptions = _listeners.OfType<Subscription>().Any(s => s.UserSubscription);
if (!anySubscriptions)
{ {
// No need to resubscribe anything bool anySubscriptions;
_logger.NothingToResubscribeCloseConnection(SocketId); lock (_listenersLock)
_ = _socket.CloseAsync(); anySubscriptions = _listeners.OfType<Subscription>().Any(s => s.UserSubscription);
return new CallResult(null); if (!anySubscriptions)
{
// No need to resubscribe anything
_logger.NothingToResubscribeCloseConnection(SocketId);
_ = _socket.CloseAsync();
return new CallResult(null);
}
} }
bool anyAuthenticated; bool anyAuthenticated;
lock (_listenersLock) lock (_listenersLock)
anyAuthenticated = _listeners.OfType<Subscription>().Any(s => s.Authenticated); anyAuthenticated = _listeners.OfType<Subscription>().Any(s => s.Authenticated) || DedicatedRequestConnection;
if (anyAuthenticated) if (anyAuthenticated)
{ {
// If we reconnected a authenticated connection we need to re-authenticate // If we reconnected a authenticated connection we need to re-authenticate
@@ -860,6 +881,8 @@ namespace CryptoExchange.Net.Sockets
{ {
subscription.HandleSubQueryResponse(subQuery.Response!); subscription.HandleSubQueryResponse(subQuery.Response!);
waitEvent.Set(); waitEvent.Set();
if (r.Result.Success)
subscription.Confirmed = true;
return r.Result; return r.Result;
})); }));
} }
@@ -869,9 +892,6 @@ namespace CryptoExchange.Net.Sockets
return taskList.First(t => !t.Result.Success).Result; return taskList.First(t => !t.Result.Success).Result;
} }
foreach (var subscription in subList)
subscription.Confirmed = true;
if (!_socket.IsOpen) if (!_socket.IsOpen)
return new CallResult(new WebError("Socket not connected")); return new CallResult(new WebError("Socket not connected"));
@@ -18,11 +18,6 @@ namespace CryptoExchange.Net.Sockets
/// </summary> /// </summary>
public int Id { get; set; } public int Id { get; set; }
/// <summary>
/// Can handle data
/// </summary>
public bool CanHandleData => Confirmed || HandleUpdatesBeforeConfirmation;
/// <summary> /// <summary>
/// Total amount of invocations /// Total amount of invocations
/// </summary> /// </summary>
@@ -42,11 +37,6 @@ namespace CryptoExchange.Net.Sockets
/// Has the subscription been confirmed /// Has the subscription been confirmed
/// </summary> /// </summary>
public bool Confirmed { get; set; } public bool Confirmed { get; set; }
/// <summary>
/// Whether this subscription should handle update messages before confirmation
/// </summary>
public bool HandleUpdatesBeforeConfirmation { get; set; }
/// <summary> /// <summary>
/// Is the subscription closed /// Is the subscription closed
@@ -80,6 +80,10 @@ namespace CryptoExchange.Net.Testing.Comparers
else if (jObj.Type == JTokenType.Array) else if (jObj.Type == JTokenType.Array)
{ {
var resultObj = enumerator.Current; var resultObj = enumerator.Current;
if (resultObj is string)
// string list
continue;
var resultProps = resultObj.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast<ArrayPropertyAttribute>().SingleOrDefault())); var resultProps = resultObj.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast<ArrayPropertyAttribute>().SingleOrDefault()));
var arrayConverterProperty = resultObj.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true).FirstOrDefault(); var arrayConverterProperty = resultObj.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true).FirstOrDefault();
var jsonConverter = ((JsonConverterAttribute)arrayConverterProperty!).ConverterType; var jsonConverter = ((JsonConverterAttribute)arrayConverterProperty!).ConverterType;
@@ -52,8 +52,13 @@ namespace CryptoExchange.Net.Testing.Comparers
else else
{ {
if (dict[dictProp.Name] == default && dictProp.Value.Type != JTokenType.Null) if (dict[dictProp.Name] == default && dictProp.Value.Type != JTokenType.Null)
{
if (dictProp.Value.ToString() == "")
continue;
// Property value not correct // Property value not correct
throw new Exception($"{method}: Dictionary entry `{dictProp.Name}` has no value while input json has value {dictProp.Value}"); throw new Exception($"{method}: Dictionary entry `{dictProp.Name}` has no value while input json has value {dictProp.Value}");
}
} }
} }
} }
@@ -77,6 +82,10 @@ namespace CryptoExchange.Net.Testing.Comparers
else if (jObj.Type == JTokenType.Array) else if (jObj.Type == JTokenType.Array)
{ {
var resultObj = enumerator.Current; var resultObj = enumerator.Current;
if (resultObj is string)
// string list
continue;
var resultProps = resultObj.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast<ArrayPropertyAttribute>().SingleOrDefault())); var resultProps = resultObj.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast<ArrayPropertyAttribute>().SingleOrDefault()));
var arrayConverterProperty = resultObj.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true).FirstOrDefault(); var arrayConverterProperty = resultObj.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true).FirstOrDefault();
var jsonConverter = ((JsonConverterAttribute)arrayConverterProperty!).ConverterType; var jsonConverter = ((JsonConverterAttribute)arrayConverterProperty!).ConverterType;
@@ -162,7 +171,7 @@ namespace CryptoExchange.Net.Testing.Comparers
if (dictProp.Value.Type == JTokenType.Object) if (dictProp.Value.Type == JTokenType.Object)
{ {
CheckObject(method, dictProp, dict[dictProp.Name]!, ignoreProperties); CheckPropertyValue(method, dictProp.Value, dict[dictProp.Name]!, dict[dictProp.Name].GetType(), null, null, ignoreProperties);
} }
else else
{ {
@@ -180,7 +189,10 @@ namespace CryptoExchange.Net.Testing.Comparers
var enumerator = list.GetEnumerator(); var enumerator = list.GetEnumerator();
foreach (JToken jtoken in jObjs) foreach (JToken jtoken in jObjs)
{ {
enumerator.MoveNext(); var moved = enumerator.MoveNext();
if (!moved)
throw new Exception("Enumeration not moved; incorrect amount of results?");
var typeConverter = enumerator.Current.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true); var typeConverter = enumerator.Current.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true);
if (typeConverter.Length != 0 && ((JsonConverterAttribute)typeConverter.First()).ConverterType != typeof(ArrayConverter)) if (typeConverter.Length != 0 && ((JsonConverterAttribute)typeConverter.First()).ConverterType != typeof(ArrayConverter))
// Custom converter for the type, skip // Custom converter for the type, skip
@@ -260,9 +272,9 @@ namespace CryptoExchange.Net.Testing.Comparers
else if (objectValue is DateTime time) else if (objectValue is DateTime time)
{ {
if (time != DateTimeConverter.ParseFromString(jsonValue.Value<string>()!)) if (time != DateTimeConverter.ParseFromString(jsonValue.Value<string>()!))
throw new Exception($"{method}: {property} not equal: {jsonValue.Value<decimal>()} vs {time}"); throw new Exception($"{method}: {property} not equal: {jsonValue.Value<string>()} vs {time}");
} }
else if (propertyType.IsEnum) else if (propertyType.IsEnum || Nullable.GetUnderlyingType(propertyType)?.IsEnum == true)
{ {
// TODO enum comparing // TODO enum comparing
} }
@@ -278,6 +290,10 @@ namespace CryptoExchange.Net.Testing.Comparers
if (time != DateTimeConverter.ParseFromDouble(jsonValue.Value<long>()!)) if (time != DateTimeConverter.ParseFromDouble(jsonValue.Value<long>()!))
throw new Exception($"{method}: {property} not equal: {jsonValue.Value<decimal>()} vs {time}"); throw new Exception($"{method}: {property} not equal: {jsonValue.Value<decimal>()} vs {time}");
} }
else if (propertyType.IsEnum || Nullable.GetUnderlyingType(propertyType)?.IsEnum == true)
{
// TODO enum comparing
}
else if (jsonValue.Value<long>() != Convert.ToInt64(objectValue)) else if (jsonValue.Value<long>() != Convert.ToInt64(objectValue))
{ {
throw new Exception($"{method}: {property} not equal: {jsonValue.Value<long>()} vs {Convert.ToInt64(objectValue)}"); throw new Exception($"{method}: {property} not equal: {jsonValue.Value<long>()} vs {Convert.ToInt64(objectValue)}");
@@ -30,9 +30,14 @@ namespace CryptoExchange.Net.Testing.Implementations
public bool IsClosed => !Connected; public bool IsClosed => !Connected;
public bool IsOpen => Connected; public bool IsOpen => Connected;
public double IncomingKbps => 0; public double IncomingKbps => 0;
public Uri Uri => new("wss://test.com/ws"); public Uri Uri { get; set; }
public Func<Task<Uri?>>? GetReconnectionUrl { get; set; } public Func<Task<Uri?>>? GetReconnectionUrl { get; set; }
public TestSocket(string address)
{
Uri = new Uri(address);
}
public Task<CallResult> ConnectAsync() public Task<CallResult> ConnectAsync()
{ {
Connected = CanConnect; Connected = CanConnect;
@@ -50,13 +50,17 @@ namespace CryptoExchange.Net.Testing
/// <param name="name">Method name for looking up json test values</param> /// <param name="name">Method name for looking up json test values</param>
/// <param name="nestedJsonProperty">Use nested json property for compare</param> /// <param name="nestedJsonProperty">Use nested json property for compare</param>
/// <param name="ignoreProperties">Ignore certain properties</param> /// <param name="ignoreProperties">Ignore certain properties</param>
/// <param name="useFirstUpdateItem">Use the first item of an array update</param>
/// <param name="addressPath">Path</param>
/// <returns></returns> /// <returns></returns>
/// <exception cref="Exception"></exception> /// <exception cref="Exception"></exception>
public async Task ValidateAsync<TUpdate>( public async Task ValidateAsync<TUpdate>(
Func<TClient, Action<DataEvent<TUpdate>>, Task<CallResult<UpdateSubscription>>> methodInvoke, Func<TClient, Action<DataEvent<TUpdate>>, Task<CallResult<UpdateSubscription>>> methodInvoke,
string name, string name,
string? nestedJsonProperty = null, string? nestedJsonProperty = null,
List<string>? ignoreProperties = null) List<string>? ignoreProperties = null,
string? addressPath = null,
bool? useFirstUpdateItem = null)
{ {
var listener = new EnumValueTraceListener(); var listener = new EnumValueTraceListener();
Trace.Listeners.Add(listener); Trace.Listeners.Add(listener);
@@ -79,7 +83,7 @@ namespace CryptoExchange.Net.Testing
var data = Encoding.UTF8.GetString(buffer); var data = Encoding.UTF8.GetString(buffer);
using var reader = new StringReader(data); using var reader = new StringReader(data);
var socket = TestHelpers.ConfigureSocketClient(_client); var socket = TestHelpers.ConfigureSocketClient(_client, addressPath == null ? _baseAddress : _baseAddress.AppendPath(addressPath));
var waiter = new AutoResetEvent(false); var waiter = new AutoResetEvent(false);
string? lastMessage = null; string? lastMessage = null;
@@ -109,6 +113,7 @@ namespace CryptoExchange.Net.Testing
if (lastMessage == null) if (lastMessage == null)
throw new Exception($"{name} expected to {line} to be send to server but did not receive anything"); throw new Exception($"{name} expected to {line} to be send to server but did not receive anything");
var lastMessageJson = JToken.Parse(lastMessage); var lastMessageJson = JToken.Parse(lastMessage);
var expectedJson = JToken.Parse(line.Substring(2)); var expectedJson = JToken.Parse(line.Substring(2));
foreach(var item in expectedJson) foreach(var item in expectedJson)
@@ -121,6 +126,12 @@ namespace CryptoExchange.Net.Testing
overrideKey = val.ToString(); overrideKey = val.ToString();
overrideValue = lastMessageJson[prop.Name]?.Value<string>(); overrideValue = lastMessageJson[prop.Name]?.Value<string>();
} }
else if (val.ToString() == "-999")
{
// -999 value is used to replace parts or response messages
overrideKey = val.ToString();
overrideValue = lastMessageJson[prop.Name]?.Value<decimal>().ToString();
}
else if (lastMessageJson[prop.Name]?.Value<string>() != val.ToString() && ignoreProperties?.Contains(prop.Name) != true) else if (lastMessageJson[prop.Name]?.Value<string>() != val.ToString() && ignoreProperties?.Contains(prop.Name) != true)
throw new Exception($"{name} Expected {prop.Name} to be {val}, but was {lastMessageJson[prop.Name]?.Value<string>()}"); throw new Exception($"{name} Expected {prop.Name} to be {val}, but was {lastMessageJson[prop.Name]?.Value<string>()}");
} }
@@ -150,9 +161,9 @@ namespace CryptoExchange.Net.Testing
throw new Exception($"{name} Update send to client did not trigger in update handler"); throw new Exception($"{name} Update send to client did not trigger in update handler");
if (_stjCompare == true) if (_stjCompare == true)
SystemTextJsonComparer.CompareData(name, update, compareData, nestedJsonProperty ?? _nestedPropertyForCompare, ignoreProperties); SystemTextJsonComparer.CompareData(name, update, compareData, nestedJsonProperty ?? _nestedPropertyForCompare, ignoreProperties, useFirstUpdateItem ?? false);
else else
JsonNetComparer.CompareData(name, update, compareData, nestedJsonProperty ?? _nestedPropertyForCompare, ignoreProperties); JsonNetComparer.CompareData(name, update, compareData, nestedJsonProperty ?? _nestedPropertyForCompare, ignoreProperties, useFirstUpdateItem ?? false);
} }
} }
+5 -4
View File
@@ -57,9 +57,9 @@ namespace CryptoExchange.Net.Testing
return self == to; return self == to;
} }
internal static TestSocket ConfigureSocketClient<T>(T client) where T : BaseSocketClient internal static TestSocket ConfigureSocketClient<T>(T client, string address) where T : BaseSocketClient
{ {
var socket = new TestSocket(); var socket = new TestSocket(address);
foreach (var apiClient in client.ApiClients.OfType<SocketApiClient>()) foreach (var apiClient in client.ApiClients.OfType<SocketApiClient>())
{ {
apiClient.SocketFactory = new TestWebsocketFactory(socket); apiClient.SocketFactory = new TestWebsocketFactory(socket);
@@ -136,8 +136,9 @@ namespace CryptoExchange.Net.Testing
headers, headers,
true, true,
client.ArraySerialization, client.ArraySerialization,
client.ParameterPositions[method], client.ParameterPositions[method],
client.RequestBodyFormat); client.RequestBodyFormat
);
var signature = getSignature(uriParams, bodyParams, headers); var signature = getSignature(uriParams, bodyParams, headers);
+39
View File
@@ -19,6 +19,7 @@ The following API's are directly supported. Note that there are 3rd party implem
|Bybit|[JKorf/Bybit.Net](https://github.com/JKorf/Bybit.Net)|[![Nuget version](https://img.shields.io/nuget/v/Bybit.net.svg?style=flat-square)](https://www.nuget.org/packages/Bybit.Net)| |Bybit|[JKorf/Bybit.Net](https://github.com/JKorf/Bybit.Net)|[![Nuget version](https://img.shields.io/nuget/v/Bybit.net.svg?style=flat-square)](https://www.nuget.org/packages/Bybit.Net)|
|CoinEx|[JKorf/CoinEx.Net](https://github.com/JKorf/CoinEx.Net)|[![Nuget version](https://img.shields.io/nuget/v/CoinEx.net.svg?style=flat-square)](https://www.nuget.org/packages/CoinEx.Net)| |CoinEx|[JKorf/CoinEx.Net](https://github.com/JKorf/CoinEx.Net)|[![Nuget version](https://img.shields.io/nuget/v/CoinEx.net.svg?style=flat-square)](https://www.nuget.org/packages/CoinEx.Net)|
|CoinGecko|[JKorf/CoinGecko.Net](https://github.com/JKorf/CoinGecko.Net)|[![Nuget version](https://img.shields.io/nuget/v/CoinGecko.net.svg?style=flat-square)](https://www.nuget.org/packages/CoinGecko.Net)| |CoinGecko|[JKorf/CoinGecko.Net](https://github.com/JKorf/CoinGecko.Net)|[![Nuget version](https://img.shields.io/nuget/v/CoinGecko.net.svg?style=flat-square)](https://www.nuget.org/packages/CoinGecko.Net)|
|Gate.io|[JKorf/GateIo.Net](https://github.com/JKorf/GateIo.Net)|[![Nuget version](https://img.shields.io/nuget/v/GateIo.net.svg?style=flat-square)](https://www.nuget.org/packages/GateIo.Net)|
|Huobi/HTX|[JKorf/Huobi.Net](https://github.com/JKorf/Huobi.Net)|[![Nuget version](https://img.shields.io/nuget/v/Huobi.net.svg?style=flat-square)](https://www.nuget.org/packages/Huobi.Net)| |Huobi/HTX|[JKorf/Huobi.Net](https://github.com/JKorf/Huobi.Net)|[![Nuget version](https://img.shields.io/nuget/v/Huobi.net.svg?style=flat-square)](https://www.nuget.org/packages/Huobi.Net)|
|Kraken|[JKorf/Kraken.Net](https://github.com/JKorf/Kraken.Net)|[![Nuget version](https://img.shields.io/nuget/v/KrakenExchange.net.svg?style=flat-square)](https://www.nuget.org/packages/KrakenExchange.Net)| |Kraken|[JKorf/Kraken.Net](https://github.com/JKorf/Kraken.Net)|[![Nuget version](https://img.shields.io/nuget/v/KrakenExchange.net.svg?style=flat-square)](https://www.nuget.org/packages/KrakenExchange.Net)|
|Kucoin|[JKorf/Kucoin.Net](https://github.com/JKorf/Kucoin.Net)|[![Nuget version](https://img.shields.io/nuget/v/Kucoin.net.svg?style=flat-square)](https://www.nuget.org/packages/Kucoin.Net)| |Kucoin|[JKorf/Kucoin.Net](https://github.com/JKorf/Kucoin.Net)|[![Nuget version](https://img.shields.io/nuget/v/Kucoin.net.svg?style=flat-square)](https://www.nuget.org/packages/Kucoin.Net)|
@@ -45,6 +46,44 @@ Make a one time donation in a crypto currency of your choice. If you prefer to d
Alternatively, sponsor me on Github using [Github Sponsors](https://github.com/sponsors/JKorf). Alternatively, sponsor me on Github using [Github Sponsors](https://github.com/sponsors/JKorf).
## Release notes ## Release notes
* Version 7.7.2 - 25 Jun 2024
* Fixed ratelimiting issue possibly creating negative delays
* Version 7.7.1 - 23 Jun 2024
* Fixes for caching implementation
* Version 7.7.0 - 23 Jun 2024
* Caching support
* Caching is supported for GET requests within a certain time frame
* Enable caching by setting CachingEnabled to true in the client options
* Added DataSource to CallResult object
* Dedicated websocket connection
* Added functionality for always having a connection open which can then be used for order operations
* This eliminates the initial connection time for the first request
* WebSocket connection can be prepared by calling PrepareConnectionsAsync on the Api client, for example `await binanceSocketClient.SpotApi.PrepareConnectionsAsync()`. This is only needed initially; it will be reconnected when connection is lost.
* Added CancellationToken support for websocket queries
* Added SocketConnection parameter to SocketApiClient.GetAuthenticationRequest method
* Added ObjectStringConverter base converter for deserializing nested json strings
* Fixed websocket issue with ratelimiting and reconnecting interaction
* Fixed rate limiting issue with sub-millisecond delays
* Fixed websocket connection will now close if authentication fails because of not set credentials
* Updated websocket reconnection handling and options, added backoff policy
* Removed check for confirmed subscription as data often is pushed before the subscription is confirmed
* Version 7.6.0 - 11 Jun 2024
* Added support for specifying seperate uri and body parameters
* Added support for different message and handling generic types on socket queries
* Added support for PATCH http method requests
* Added support for setting http request body to a specific type directly
* Split DataEvent.Topic into StreamId and Symbol properties
* Added support for negative time values parsing
* Added some helper methods for converting DataEvent to CallResult
* Added support for GZip/Deflate automatic decompressing in the default HttpClient
* Updated some testing methods
* Version 7.5.2 - 07 May 2024
* Fixed SetApiCredentials not correctly being used by rate limiter causing exception
* Version 7.5.1 - 03 May 2024 * Version 7.5.1 - 03 May 2024
* Some small improvements in unit testing components * Some small improvements in unit testing components
Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

+38 -26
View File
@@ -82,16 +82,17 @@
<ul class="nav flex-column"> <ul class="nav flex-column">
<li class="nav-item"><a class="nav-link" href="#idocs_installation">Installation</a></li> <li class="nav-item"><a class="nav-link" href="#idocs_installation">Installation</a></li>
<li class="nav-item"><a class="nav-link" href="#idocs_di">Dependency Injection</a></li> <li class="nav-item"><a class="nav-link" href="#idocs_di">Dependency Injection</a></li>
<li class="nav-item"><a class="nav-link" href="#idocs_general">General Client Usage</a></li>
<li class="nav-item"><a class="nav-link" href="#idocs_rest">REST API Client</a></li> <li class="nav-item"><a class="nav-link" href="#idocs_rest">REST API Client</a></li>
<li class="nav-item"><a class="nav-link" href="#idocs_socket">Websocket API Client</a></li> <li class="nav-item"><a class="nav-link" href="#idocs_socket">Websocket API Client</a></li>
<li class="nav-item"><a class="nav-link" href="#idocs_common">Common clients</a></li> <li class="nav-item"><a class="nav-link" href="#idocs_common">Common Clients</a></li>
</ul> </ul>
</li> </li>
<li class="nav-item"><a class="nav-link" href="#idocs_options">Options & Authorization</a> <li class="nav-item"><a class="nav-link" href="#idocs_options">Options & Authorization</a>
<ul class="nav flex-column"> <ul class="nav flex-column">
<li class="nav-item"><a class="nav-link" href="#idocs_auth">Authorization</a></li> <li class="nav-item"><a class="nav-link" href="#idocs_auth">Authorization</a></li>
<li class="nav-item"><a class="nav-link" href="#idocs_options_set">Setting options</a></li> <li class="nav-item"><a class="nav-link" href="#idocs_options_set">Setting Options</a></li>
<li class="nav-item"><a class="nav-link" href="#idocs_options_def">Option definitions</a></li> <li class="nav-item"><a class="nav-link" href="#idocs_options_def">Option Definitions</a></li>
</ul> </ul>
</li> </li>
<li class="nav-item"><a class="nav-link" href="#idocs_features">Additional Features</a> <li class="nav-item"><a class="nav-link" href="#idocs_features">Additional Features</a>
@@ -122,37 +123,39 @@
<div class="idocs-content"> <div class="idocs-content">
<div class="container"> <div class="container">
<section id="idocs_intro"> <section id="idocs_intro">
<h1>CryptoExchange.Net</h1> <h1>CryptoExchange.Net & Implementations</h1>
<p>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</p> <p>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</p>
<div class="alert alert-info">All libraries can be used in the same project as well as individually, just install the exchange libraries you need!</div> <p>When access to multiple or all exchange API's is needed, the CryptoClients.Net library combines all different client libraries in a single Nuget package. The following image illustrates the structure:</p>
<p>The following API's are directly supported. Note that there are 3rd party implementations going around, but only these are created and supported by me</p> <p><img src="assets/images/struct.png" /></p>
<p>These Exchanges/API's are directly supported:</p>
<table class="table table-bordered"> <table class="table table-bordered">
<tr> <tr>
<th>Exchange</th> <th>API</th>
<th>Repository</th> <th>Repository</th>
<th>Nuget</th> <th>Nuget</th>
</tr> </tr>
<tr><td>Binance</td><td><a href="https://github.com/JKorf/Binance.Net">JKorf/Binance.Net</a></td><td><a href="https://www.nuget.org/packages/Binance.Net"><img src="https://img.shields.io/nuget/v/Binance.net.svg?style=flat-square" /></a></td></tr> <tr><td>Binance</td><td><a href="https://github.com/JKorf/Binance.Net">JKorf/Binance.Net</a></td><td><a href="https://www.nuget.org/packages/Binance.Net" target="_blank"><img src="https://img.shields.io/nuget/v/Binance.net.svg?style=flat-square" /></a></td></tr>
<tr><td>BingX</td><td><a href="https://github.com/JKorf/BingX.Net">JKorf/BingX.Net</a></td><td><a href="https://www.nuget.org/packages/JK.BingX.Net"><img src="https://img.shields.io/nuget/v/JK.BingX.net.svg?style=flat-square" /></a></td></tr> <tr><td>BingX</td><td><a href="https://github.com/JKorf/BingX.Net">JKorf/BingX.Net</a></td><td><a href="https://www.nuget.org/packages/JK.BingX.Net" target="_blank"><img src="https://img.shields.io/nuget/v/JK.BingX.net.svg?style=flat-square" /></a></td></tr>
<tr><td>Bitfinex</td><td><a href="https://github.com/JKorf/Bitfinex.Net">JKorf/Bitfinex.Net</a></td><td><a href="https://www.nuget.org/packages/Bitfinex.Net"><img src="https://img.shields.io/nuget/v/Bitfinex.net.svg?style=flat-square" /></a></td></tr> <tr><td>Bitfinex</td><td><a href="https://github.com/JKorf/Bitfinex.Net">JKorf/Bitfinex.Net</a></td><td><a href="https://www.nuget.org/packages/Bitfinex.Net" target="_blank"><img src="https://img.shields.io/nuget/v/Bitfinex.net.svg?style=flat-square" /></a></td></tr>
<tr><td>Bitget</td><td><a href="https://github.com/JKorf/Bitget.Net">JKorf/Bitget.Net</a></td><td><a href="https://www.nuget.org/packages/JK.Bitget.Net"><img src="https://img.shields.io/nuget/v/JK.Bitget.net.svg?style=flat-square" /></a></td></tr> <tr><td>Bitget</td><td><a href="https://github.com/JKorf/Bitget.Net">JKorf/Bitget.Net</a></td><td><a href="https://www.nuget.org/packages/JK.Bitget.Net" target="_blank"><img src="https://img.shields.io/nuget/v/JK.Bitget.net.svg?style=flat-square" /></a></td></tr>
<tr><td>Bybit</td><td><a href="https://github.com/JKorf/Bybit.Net">JKorf/Bybit.Net</a></td><td><a href="https://www.nuget.org/packages/Bybit.Net"><img src="https://img.shields.io/nuget/v/Bybit.net.svg?style=flat-square" /></a></td></tr> <tr><td>Bybit</td><td><a href="https://github.com/JKorf/Bybit.Net">JKorf/Bybit.Net</a></td><td><a href="https://www.nuget.org/packages/Bybit.Net" target="_blank"><img src="https://img.shields.io/nuget/v/Bybit.net.svg?style=flat-square" /></a></td></tr>
<tr><td>CoinEx</td><td><a href="https://github.com/JKorf/CoinEx.Net">JKorf/CoinEx.Net</a></td><td><a href="https://www.nuget.org/packages/CoinEx.Net"><img src="https://img.shields.io/nuget/v/CoinEx.net.svg?style=flat-square" /></a></td></tr> <tr><td>CoinEx</td><td><a href="https://github.com/JKorf/CoinEx.Net">JKorf/CoinEx.Net</a></td><td><a href="https://www.nuget.org/packages/CoinEx.Net" target="_blank"><img src="https://img.shields.io/nuget/v/CoinEx.net.svg?style=flat-square" /></a></td></tr>
<tr><td>CoinGecko</td><td><a href="https://github.com/JKorf/CoinGecko.Net">JKorf/CoinGecko.Net</a></td><td><a href="https://www.nuget.org/packages/CoinGecko.Net"><img src="https://img.shields.io/nuget/v/CoinGecko.net.svg?style=flat-square" /></a></td></tr> <tr><td>CoinGecko</td><td><a href="https://github.com/JKorf/CoinGecko.Net">JKorf/CoinGecko.Net</a></td><td><a href="https://www.nuget.org/packages/CoinGecko.Net" target="_blank"><img src="https://img.shields.io/nuget/v/CoinGecko.net.svg?style=flat-square" /></a></td></tr>
<tr><td>Huobi</td><td><a href="https://github.com/JKorf/Huobi.Net">JKorf/Huobi.Net</a></td><td><a href="https://www.nuget.org/packages/Huobi.Net"><img src="https://img.shields.io/nuget/v/Huobi.net.svg?style=flat-square" /></a></td></tr> <tr><td>Gate.io</td><td><a href="https://github.com/JKorf/GateIo.Net">JKorf/GateIo.Net</a></td><td><a href="https://www.nuget.org/packages/GateIo.Net" target="_blank"><img src="https://img.shields.io/nuget/v/GateIo.net.svg?style=flat-square" /></a></td></tr>
<tr><td>Kraken</td><td><a href="https://github.com/JKorf/Kraken.Net">JKorf/Kraken.Net</a></td><td><a href="https://www.nuget.org/packages/KrakenExchange.Net"><img src="https://img.shields.io/nuget/v/KrakenExchange.net.svg?style=flat-square" /></a></td></tr> <tr><td>Huobi</td><td><a href="https://github.com/JKorf/Huobi.Net">JKorf/Huobi.Net</a></td><td><a href="https://www.nuget.org/packages/Huobi.Net" target="_blank"><img src="https://img.shields.io/nuget/v/Huobi.net.svg?style=flat-square" /></a></td></tr>
<tr><td>Kucoin</td><td><a href="https://github.com/JKorf/Kucoin.Net">JKorf/Kucoin.Net</a></td><td><a href="https://www.nuget.org/packages/Kucoin.Net"><img src="https://img.shields.io/nuget/v/Kucoin.net.svg?style=flat-square" /></a></td></tr> <tr><td>Kraken</td><td><a href="https://github.com/JKorf/Kraken.Net">JKorf/Kraken.Net</a></td><td><a href="https://www.nuget.org/packages/KrakenExchange.Net" target="_blank"><img src="https://img.shields.io/nuget/v/KrakenExchange.net.svg?style=flat-square" /></a></td></tr>
<tr><td>Mexc</td><td><a href="https://github.com/JKorf/Mexc.Net">JKorf/Mexc.Net</a></td><td><a href="https://www.nuget.org/packages/JK.Mexc.Net"><img src="https://img.shields.io/nuget/v/JK.Mexc.net.svg?style=flat-square" /></a></td></tr> <tr><td>Kucoin</td><td><a href="https://github.com/JKorf/Kucoin.Net">JKorf/Kucoin.Net</a></td><td><a href="https://www.nuget.org/packages/Kucoin.Net" target="_blank"><img src="https://img.shields.io/nuget/v/Kucoin.net.svg?style=flat-square" /></a></td></tr>
<tr><td>OKX</td><td><a href="https://github.com/JKorf/OKX.Net">JKorf/OKX.Net</a></td><td><a href="https://www.nuget.org/packages/JK.OKX.Net"><img src="https://img.shields.io/nuget/v/JK.OKX.net.svg?style=flat-square" /></a></td></tr> <tr><td>Mexc</td><td><a href="https://github.com/JKorf/Mexc.Net">JKorf/Mexc.Net</a></td><td><a href="https://www.nuget.org/packages/JK.Mexc.Net" target="_blank"><img src="https://img.shields.io/nuget/v/JK.Mexc.net.svg?style=flat-square" /></a></td></tr>
<tr><td>OKX</td><td><a href="https://github.com/JKorf/OKX.Net">JKorf/OKX.Net</a></td><td><a href="https://www.nuget.org/packages/JK.OKX.Net" target="_blank"><img src="https://img.shields.io/nuget/v/JK.OKX.net.svg?style=flat-square" /></a></td></tr>
</table> </table>
<p>Note that there are 3rd party implementations going around, but only the listed ones here are created and supported by me.</p>
<p>Alternatively, use <a href="https://github.com/jkorf/CryptoClients.Net">CryptoClients.Net</a> which combines these packages and allows easy access to all exchange API's.</p> <p>When using multiple of these API's the <a href="https://github.com/jkorf/CryptoClients.Net">CryptoClients.Net</a> package can be used which combines these packages and allows easy access to all exchange API's.</p>
<h4>Supported Frameworks</h4> <h4>Supported Frameworks</h4>
<p> <p>
The library is targeting both <code>.NET Standard 2.0</code> and <code>.NET Standard 2.1</code> for optimal compatibility The libraries are targeting both <code>.NET Standard 2.0</code> and <code>.NET Standard 2.1</code> for optimal compatibility
</p> </p>
<table class="table table-bordered"> <table class="table table-bordered">
<tr> <tr>
@@ -811,6 +814,15 @@
<hr class="divider"> <hr class="divider">
<section id="idocs_general">
<h2>General client usage</h2>
<p>All clients work with the same principles:</p>
<ul>
<li>Mandatory parameters are non-nullable while optional parameters are nullable and will have a default value of null.</li>
<li>Any operation will return a form of <code>CallResult</code>. This result can and should be check for success. The clients will not throw exceptions.</li>
</ul>
</section>
<!-- HTML Structure <!-- HTML Structure
============================ --> ============================ -->
<section id="idocs_rest"> <section id="idocs_rest">
@@ -1376,14 +1388,14 @@ await client.UnsubscribeAllAsync();</code></pre>
============================ --> ============================ -->
<section id="idocs_common"> <section id="idocs_common">
<h2>Common Clients</h2> <h2>Common Clients</h2>
<p>CryptoClients.Net exposes some common clients. These clients aim to make using the different API's easier.</p> <p>The CryptoClients.Net client exposes some common client classes. These clients aim to make using the different API's easier.</p>
<p><b>(I)ExchangeRestClient</b><br /> <p><b>(I)ExchangeRestClient</b><br />
The <code>ExchangeRestClient</code> (or <code>ExchangeRestClient</code> when used directly) can be used to easily access REST clients for different API's. The <code>IExchangeRestClient</code> (or <code>ExchangeRestClient</code> when used directly) can be used to easily access REST clients for different API's.
</p> </p>
<p> <p>
For example, using the Binance, Bybit and Kucoin API's can be done like this: For example, using the Binance, Bybit and Kucoin API's can be done like this:
<pre><code>var exchangeRestClient = new ExchangeRestClient(); // Either construct it or inject the IExchangeRestClient into your service <pre><code>var exchangeRestClient = new ExchangeRestClient(); // Either construct it or inject the IExchangeRestClient into your service after having called 'AddCryptoClients()' during service regirations
var binanceTicker = await exchangeRestClient.Binance.SpotApi.ExchangeData.GetTickersAsync(); var binanceTicker = await exchangeRestClient.Binance.SpotApi.ExchangeData.GetTickersAsync();
var bybitTicker = await exchangeRestClient.Bybit.V5Api.ExchangeData.GetTickers(); var bybitTicker = await exchangeRestClient.Bybit.V5Api.ExchangeData.GetTickers();
var kucoinTicker = await exchangeRestClient.Kucoin.SpotApi.ExchangeData.GetTickers();</code></pre> var kucoinTicker = await exchangeRestClient.Kucoin.SpotApi.ExchangeData.GetTickers();</code></pre>
@@ -1393,7 +1405,7 @@ var kucoinTicker = await exchangeRestClient.Kucoin.SpotApi.ExchangeData.GetTicke
Similarly as the <code>(I)ExchangeRestClient</code> this client allows you to access the different Websocket clients through a single access point. Similarly as the <code>(I)ExchangeRestClient</code> this client allows you to access the different Websocket clients through a single access point.
</p> </p>
<p>For example accessing the Bitget, Kraken and OKX API's could be done like this: <p>For example accessing the Bitget, Kraken and OKX API's could be done like this:
<pre><code>var exchangeSocketClient = new ExchangeSocketClient(); // Either construct it or inject the ExchangeSocketClient into your service <pre><code>var exchangeSocketClient = new ExchangeSocketClient(); // Either construct it or inject the IExchangeSocketClient into your service after having called 'AddCryptoClients()' during service regirations
var bitgetSub = await exchangeSocketClient.Bitget.SpotApi.SubscribeToTickerUpdatesAsync("ETHUSDT", data => {}); var bitgetSub = await exchangeSocketClient.Bitget.SpotApi.SubscribeToTickerUpdatesAsync("ETHUSDT", data => {});
var krakenSub = await exchangeSocketClient.Kraken.SpotApi.SubscribeToTickerUpdatesAsync("ETH/USD", data => {}); var krakenSub = await exchangeSocketClient.Kraken.SpotApi.SubscribeToTickerUpdatesAsync("ETH/USD", data => {});
var okxSub = await exchangeSocketClient.OKX.UnifiedApi.ExchangeData.SubscribeToTickerUpdatesAsync("ETH-USDT", data => {});</code></pre> var okxSub = await exchangeSocketClient.OKX.UnifiedApi.ExchangeData.SubscribeToTickerUpdatesAsync("ETH-USDT", data => {});</code></pre>