1
0
mirror of https://github.com/JKorf/CryptoExchange.Net.git synced 2026-08-12 08:53:01 +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,
HttpMethod.Get,
new List<KeyValuePair<string, IEnumerable<string>>>(),
ResultDataSource.Server,
new TestObjectResult(),
null);
var asResult = result.AsError<TestObject2>(new ServerError("TestError2"));
@@ -150,6 +151,7 @@ namespace CryptoExchange.Net.UnitTests
null,
HttpMethod.Get,
new List<KeyValuePair<string, IEnumerable<string>>>(),
ResultDataSource.Server,
new TestObjectResult(),
null);
var asResult = result.As<TestObject2>(result.Data.InnerData);
@@ -72,7 +72,6 @@ namespace CryptoExchange.Net.UnitTests
result = messageEvent.Data;
rstEvent.Set();
});
subObj.HandleUpdatesBeforeConfirmation = true;
sub.AddSubscription(subObj);
// act
@@ -107,7 +106,6 @@ namespace CryptoExchange.Net.UnitTests
original = messageEvent.OriginalData;
rstEvent.Set();
});
subObj.HandleUpdatesBeforeConfirmation = true;
sub.AddSubscription(subObj);
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="auth">If the requests should be authenticated</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="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="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(
RestApiClient apiClient,
Uri uri,
@@ -434,6 +434,20 @@ namespace CryptoExchange.Net.Authentication
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 />
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.Threading;
using System.Threading.Tasks;
using CryptoExchange.Net.Caching;
using CryptoExchange.Net.Converters.JsonNet;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Logging.Extensions;
@@ -17,6 +18,7 @@ using CryptoExchange.Net.RateLimiting;
using CryptoExchange.Net.RateLimiting.Interfaces;
using CryptoExchange.Net.Requests;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace CryptoExchange.Net.Clients
{
@@ -75,7 +77,8 @@ namespace CryptoExchange.Net.Clients
{ HttpMethod.Get, HttpMethodParameterPosition.InUri },
{ HttpMethod.Post, HttpMethodParameterPosition.InBody },
{ HttpMethod.Delete, HttpMethodParameterPosition.InBody },
{ HttpMethod.Put, HttpMethodParameterPosition.InBody }
{ HttpMethod.Put, HttpMethodParameterPosition.InBody },
{ new HttpMethod("Patch"), HttpMethodParameterPosition.InBody },
};
/// <inheritdoc />
@@ -84,6 +87,10 @@ namespace CryptoExchange.Net.Clients
/// <inheritdoc />
public new RestApiOptions ApiOptions => (RestApiOptions)base.ApiOptions;
/// <summary>
/// Memory cache
/// </summary>
private static MemoryCache _cache = new MemoryCache();
/// <summary>
/// ctor
@@ -149,7 +156,7 @@ namespace CryptoExchange.Net.Clients
/// <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>(
protected virtual Task<WebCallResult<T>> SendAsync<T>(
string baseAddress,
RequestDefinition definition,
ParameterCollection? parameters,
@@ -157,15 +164,67 @@ namespace CryptoExchange.Net.Clients
Dictionary<string, string>? additionalHeaders = null,
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;
while (true)
{
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)
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)}]")));
TotalRequestsMade++;
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))
continue;
if (result.Success &&
ShouldCache(definition))
{
_cache.Add(key, result);
}
return result;
}
}
@@ -186,7 +251,6 @@ namespace CryptoExchange.Net.Clients
/// </summary>
/// <param name="baseAddress">Host and schema</param>
/// <param name="definition">Request definition</param>
/// <param name="parameters">Request 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</param>
@@ -195,7 +259,6 @@ namespace CryptoExchange.Net.Clients
protected virtual async Task<CallResult> PrepareAsync(
string baseAddress,
RequestDefinition definition,
ParameterCollection? parameters,
CancellationToken cancellationToken,
Dictionary<string, string>? additionalHeaders = null,
int? weight = null)
@@ -263,25 +326,27 @@ namespace CryptoExchange.Net.Clients
/// </summary>
/// <param name="baseAddress">Host and schema</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>
/// <returns></returns>
protected virtual IRequest CreateRequest(
string baseAddress,
RequestDefinition definition,
ParameterCollection? parameters,
ParameterCollection? uriParameters,
ParameterCollection? bodyParameters,
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 parameterPosition = definition.ParameterPosition ?? ParameterPositions[definition.Method];
var arraySerialization = definition.ArraySerialization ?? ArraySerialization;
var bodyFormat = definition.RequestBodyFormat ?? RequestBodyFormat;
var requestId = ExchangeHelpers.NextId();
var parameterPosition = definition.ParameterPosition ?? ParameterPositions[definition.Method];
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)
{
try
@@ -290,13 +355,14 @@ namespace CryptoExchange.Net.Clients
this,
uri,
definition.Method,
uriParameters,
bodyParameters,
uriParams,
bodyParams,
headers,
definition.Authenticated,
arraySerialization,
parameterPosition,
bodyFormat);
bodyFormat
);
}
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
uri = uri.SetParameters(uriParameters, arraySerialization);
uri = uri.SetParameters(uriParams, arraySerialization);
var request = RequestFactory.Create(definition.Method, uri, requestId);
request.Accept = Constants.JsonContentHeader;
@@ -342,8 +398,8 @@ namespace CryptoExchange.Net.Clients
if (parameterPosition == HttpMethodParameterPosition.InBody)
{
var contentType = bodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader;
if (bodyParameters.Count != 0)
WriteParamBody(request, bodyParameters, contentType);
if (bodyParams.Count != 0)
WriteParamBody(request, bodyParams, contentType);
else
request.SetContent(RequestBodyEmptyContent, contentType);
}
@@ -416,6 +472,7 @@ namespace CryptoExchange.Net.Clients
/// <param name="requestWeight">Credits used for 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="preventCaching">Whether caching should be prevented for this request</param>
/// <returns></returns>
[return: NotNull]
protected virtual async Task<WebCallResult<T>> SendRequestAsync<T>(
@@ -429,9 +486,25 @@ namespace CryptoExchange.Net.Clients
ArrayParametersSerialization? arraySerialization = null,
int requestWeight = 1,
Dictionary<string, string>? additionalHeaders = null,
IRateLimitGate? gate = null
IRateLimitGate? gate = null,
bool preventCaching = false
) 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;
while (true)
{
@@ -449,6 +522,13 @@ namespace CryptoExchange.Net.Clients
if (await ShouldRetryRequestAsync(gate, result, currentTry).ConfigureAwait(false))
continue;
if (result.Success &&
ShouldCache(method) &&
!preventCaching)
{
_cache.Add(key, result);
}
return result;
}
}
@@ -588,47 +668,47 @@ namespace CryptoExchange.Net.Clients
if (error.Code == null || error.Code == 0)
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))
// 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);
if (!valid)
{
// 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]");
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
var parsedError = TryParseError(accessor);
if (parsedError != null)
// 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>();
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)
{
// Request exception, can't reach server for instance
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)
{
if (cancellationToken != default && canceledException.CancellationToken == cancellationToken)
{
// 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
{
// 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
@@ -738,7 +818,8 @@ namespace CryptoExchange.Net.Clients
signed,
arraySerialization,
parameterPosition,
bodyFormat);
bodyFormat
);
}
catch (Exception ex)
{
@@ -804,7 +885,11 @@ namespace CryptoExchange.Net.Clients
if (contentType == Constants.JsonContentHeader)
{
// 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);
}
else if (contentType == Constants.FormContentHeader)
@@ -877,14 +962,14 @@ namespace CryptoExchange.Net.Clients
{
var timeSyncParams = GetTimeSyncInfo();
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 (!timeSyncParams.SyncTime || DateTime.UtcNow - timeSyncParams.TimeSyncState.LastSyncTime < timeSyncParams.RecalculationInterval)
{
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;
@@ -913,7 +998,16 @@ namespace CryptoExchange.Net.Clients
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 Microsoft.Extensions.Logging;
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
@@ -52,11 +53,6 @@ namespace CryptoExchange.Net.Clients
/// </summary>
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>
/// The rate limiters
/// </summary>
@@ -72,6 +68,11 @@ namespace CryptoExchange.Net.Clients
/// </summary>
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 />
public double IncomingKbps
{
@@ -136,6 +137,16 @@ namespace CryptoExchange.Net.Clients
/// <returns></returns>
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>
/// Add a query to periodically send on each connection
/// </summary>
@@ -198,12 +209,11 @@ namespace CryptoExchange.Net.Clients
while (true)
{
// 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)
return socketResult.As<UpdateSubscription>(null);
socketConnection = socketResult.Data;
subscription.HandleUpdatesBeforeConfirmation = subscription.HandleUpdatesBeforeConfirmation || HandleMessageBeforeConfirmation;
// Add a subscription on the socket connection
var success = socketConnection.AddSubscription(subscription);
@@ -250,11 +260,18 @@ namespace CryptoExchange.Net.Clients
if (!subResult)
{
waitEvent?.Set();
_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
var unsubscribe = subResult.Error is CancellationRequestedError;
await socketConnection.CloseAsync(subscription, unsubscribe).ConfigureAwait(false);
return new CallResult<UpdateSubscription>(subResult.Error!);
var isTimeout = subResult.Error is CancellationRequestedError;
if (isTimeout && subscription.Confirmed)
{
// No response received, but the subscription did receive updates. We'll assume success
}
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!);
@@ -278,34 +295,41 @@ namespace CryptoExchange.Net.Clients
/// <summary>
/// Send a query on a socket connection to the BaseAddress and wait for the response
/// </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="ct">Cancellation token</param>
/// <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>
/// Send a query on a socket connection and wait for the response
/// </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="query">The query</param>
/// <param name="ct">Cancellation token</param>
/// <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)
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;
var released = false;
await semaphoreSlim.WaitAsync().ConfigureAwait(false);
try
{
var socketResult = await GetSocketConnection(url, query.Authenticated).ConfigureAwait(false);
var socketResult = await GetSocketConnection(url, query.Authenticated, true).ConfigureAwait(false);
if (!socketResult)
return socketResult.As<T>(default);
return socketResult.As<THandlerResponse>(default);
socketConnection = socketResult.Data;
@@ -318,7 +342,7 @@ namespace CryptoExchange.Net.Clients
var connectResult = await ConnectIfNeededAsync(socketConnection, query.Authenticated).ConfigureAwait(false);
if (!connectResult)
return new CallResult<T>(connectResult.Error!);
return new CallResult<THandlerResponse>(connectResult.Error!);
}
finally
{
@@ -329,10 +353,13 @@ namespace CryptoExchange.Net.Clients
if (socketConnection.PausedActivity)
{
_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>
@@ -356,7 +383,11 @@ namespace CryptoExchange.Net.Clients
if (!authenticated || socket.Authenticated)
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>
@@ -370,7 +401,7 @@ namespace CryptoExchange.Net.Clients
return new CallResult(new NoApiCredentialsError());
_logger.AttemptingToAuthenticate(socket.SocketId);
var authRequest = GetAuthenticationRequest();
var authRequest = GetAuthenticationRequest(socket);
if (authRequest != null)
{
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
/// </summary>
/// <returns></returns>
protected internal virtual Query? GetAuthenticationRequest() => throw new NotImplementedException();
protected internal virtual Query? GetAuthenticationRequest(SocketConnection connection) => throw new NotImplementedException();
/// <summary>
/// Adds a system subscription. Used for example to reply to ping requests
@@ -444,19 +475,31 @@ namespace CryptoExchange.Net.Clients
/// </summary>
/// <param name="address">The address the socket is for</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>
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)
&& s.Value.Tag.TrimEnd('/') == address.TrimEnd('/')
&& s.Value.ApiClient.GetType() == GetType()
&& (s.Value.Authenticated == authenticated || !authenticated) && s.Value.Connected).OrderBy(s => s.Value.UserSubscriptionCount).FirstOrDefault();
var result = socketResult.Equals(default(KeyValuePair<int, SocketConnection>)) ? null : socketResult.Value;
if (result != null)
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.ApiClient.GetType() == GetType()
&& (s.Value.Authenticated == authenticated || !authenticated)
&& s.Value.Connected);
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
return new CallResult<SocketConnection>(result);
return new CallResult<SocketConnection>(connection);
}
var connectionAddress = await GetConnectionUrlAsync(address, authenticated).ConfigureAwait(false);
@@ -473,6 +516,7 @@ namespace CryptoExchange.Net.Clients
var socket = CreateSocket(connectionAddress.Data!);
var socketConnection = new SocketConnection(_logger, this, socket, address);
socketConnection.UnhandledMessage += HandleUnhandledMessage;
socketConnection.DedicatedRequestConnection = dedicatedRequestConnection;
foreach (var ptg in PeriodicTaskRegistrations)
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>
/// <returns></returns>
protected virtual WebSocketParameters GetWebSocketParameters(string address)
=> new(new Uri(address), ClientOptions.AutoReconnect)
=> new(new Uri(address), ClientOptions.ReconnectPolicy)
{
KeepAliveInterval = KeepAliveInterval,
ReconnectInterval = ClientOptions.ReconnectInterval,
@@ -592,8 +636,8 @@ namespace CryptoExchange.Net.Clients
var tasks = new List<Task>();
{
var socketList = socketConnections.Values;
foreach (var sub in socketList)
tasks.Add(sub.CloseAsync());
foreach (var connection in socketList.Where(s => !s.DedicatedRequestConnection))
tasks.Add(connection.CloseAsync());
}
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
@@ -616,6 +660,23 @@ namespace CryptoExchange.Net.Clients
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>
/// Log the current state of connections and subscriptions
/// </summary>
@@ -699,11 +760,18 @@ namespace CryptoExchange.Net.Clients
public override void Dispose()
{
_disposing = true;
if (socketConnections.Sum(s => s.Value.UserSubscriptionCount) > 0)
var tasks = new List<Task>();
{
_logger.DisposingSocketClient();
_ = UnsubscribeAllAsync();
var socketList = socketConnections.Values.Where(x => x.UserSubscriptionCount > 0 || x.Connected);
if (socketList.Any())
_logger.DisposingSocketClient();
foreach (var connection in socketList)
{
tasks.Add(connection.CloseAsync());
}
}
semaphoreSlim?.Dispose();
base.Dispose();
}
@@ -81,7 +81,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
private static object ParseObject(ref Utf8JsonReader reader, object result, Type objectType)
{
if (reader.TokenType != JsonTokenType.StartArray)
throw new Exception("1");
throw new Exception("Not an array");
if (!_typeAttributesCache.TryGetValue(objectType, out var attributes))
attributes = CacheTypeAttributes(objectType);
@@ -92,8 +92,11 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
if (reader.TokenType == JsonTokenType.EndArray)
break;
var attribute = attributes.SingleOrDefault(a => a.ArrayProperty.Index == index);
var targetType = attribute.PropertyInfo.PropertyType;
var attribute = attributes.SingleOrDefault(a => a.ArrayProperty.Index == index);
if (attribute == null)
continue;
var targetType = attribute.PropertyInfo.PropertyType;
object? value = 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))
{
// Parse 1637745563.000 format
if (doubleValue <= 0)
return default;
if (doubleValue < 19999999999)
return ConvertFromSeconds(doubleValue);
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}";
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 />
+3 -3
View File
@@ -6,9 +6,9 @@
<PackageId>CryptoExchange.Net</PackageId>
<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>
<PackageVersion>7.5.1</PackageVersion>
<AssemblyVersion>7.5.1</AssemblyVersion>
<FileVersion>7.5.1</FileVersion>
<PackageVersion>7.7.2</PackageVersion>
<AssemblyVersion>7.7.2</AssemblyVersion>
<FileVersion>7.7.2</FileVersion>
<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>
<RepositoryType>git</RepositoryType>
+3
View File
@@ -96,6 +96,9 @@ namespace CryptoExchange.Net
var formData = HttpUtility.ParseQueryString(string.Empty);
foreach (var kvp in parameters)
{
if (kvp.Value is null)
continue;
if (kvp.Value.GetType().IsArray)
{
var array = (Array)kvp.Value;
@@ -16,10 +16,6 @@ namespace CryptoExchange.Net.Interfaces
/// </summary>
public int Id { get; }
/// <summary>
/// Whether this listener can handle data
/// </summary>
public bool CanHandleData { get; }
/// <summary>
/// The identifiers for this processor
/// </summary>
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 System.Threading.Tasks;
@@ -59,5 +60,11 @@ namespace CryptoExchange.Net.Interfaces
/// <param name="subscription">The subscription to unsubscribe</param>
/// <returns></returns>
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?> _restApiRateLimitPauseUntil;
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()
@@ -65,6 +68,21 @@ namespace CryptoExchange.Net.Logging.Extensions
LogLevel.Debug,
new EventId(4008, "RestApiSendRequest"),
"[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)
@@ -111,5 +129,20 @@ namespace CryptoExchange.Net.Logging.Extensions
{
_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
/// </summary>
/// <returns></returns>
public Task<bool> WaitAsync(TimeSpan? timeout = null)
public Task<bool> WaitAsync(TimeSpan? timeout = null, CancellationToken ct = default)
{
lock (_waits)
{
@@ -44,22 +44,29 @@ namespace CryptoExchange.Net.Objects
}
else
{
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
if(timeout != null)
{
var cancellationSource = new CancellationTokenSource(timeout.Value);
var registration = cancellationSource.Token.Register(() =>
{
lock (_waits)
{
tcs.TrySetResult(false);
if (ct.IsCancellationRequested)
return _completed;
// Not the cleanest but it works
_waits = new Queue<TaskCompletionSource<bool>>(_waits.Where(i => i != tcs));
}
}, useSynchronizationContext: false);
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
if (timeout.HasValue)
{
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);
return tcs.Task;
}
+20 -3
View File
@@ -331,6 +331,11 @@ namespace CryptoExchange.Net.Objects
/// </summary>
public TimeSpan? ResponseTime { get; set; }
/// <summary>
/// The data source of this result
/// </summary>
public ResultDataSource DataSource { get; set; } = ResultDataSource.Server;
/// <summary>
/// Create a new result
/// </summary>
@@ -344,6 +349,7 @@ namespace CryptoExchange.Net.Objects
/// <param name="requestBody"></param>
/// <param name="requestMethod"></param>
/// <param name="requestHeaders"></param>
/// <param name="dataSource"></param>
/// <param name="data"></param>
/// <param name="error"></param>
public WebCallResult(
@@ -357,6 +363,7 @@ namespace CryptoExchange.Net.Objects
string? requestBody,
HttpMethod? requestMethod,
IEnumerable<KeyValuePair<string, IEnumerable<string>>>? requestHeaders,
ResultDataSource dataSource,
[AllowNull] T data,
Error? error) : base(data, originalData, error)
{
@@ -370,6 +377,7 @@ namespace CryptoExchange.Net.Objects
RequestBody = requestBody;
RequestHeaders = requestHeaders;
RequestMethod = requestMethod;
DataSource = dataSource;
}
/// <summary>
@@ -393,7 +401,7 @@ namespace CryptoExchange.Net.Objects
/// Create a new error result
/// </summary>
/// <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>
/// Copy the WebCallResult to a new data type
@@ -403,7 +411,7 @@ namespace CryptoExchange.Net.Objects
/// <returns></returns>
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>
@@ -414,7 +422,16 @@ namespace CryptoExchange.Net.Objects
/// <returns></returns>
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 />
+4
View File
@@ -13,5 +13,9 @@
/// Form content type header
/// </summary>
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>
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>
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>
/// Create a copy of this options
/// </summary>
@@ -34,7 +44,9 @@ namespace CryptoExchange.Net.Objects.Options
Proxy = Proxy,
RequestTimeout = RequestTimeout,
RateLimiterEnabled = RateLimiterEnabled,
RateLimitingBehaviour = RateLimitingBehaviour
RateLimitingBehaviour = RateLimitingBehaviour,
CachingEnabled = CachingEnabled,
CachingMaxAge = CachingMaxAge,
};
}
}
@@ -1,4 +1,5 @@
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Objects.Sockets;
using System;
namespace CryptoExchange.Net.Objects.Options
@@ -9,15 +10,15 @@ namespace CryptoExchange.Net.Objects.Options
public class SocketExchangeOptions : ExchangeOptions
{
/// <summary>
/// Whether or not the socket should automatically reconnect when losing connection
/// </summary>
public bool AutoReconnect { get; set; } = true;
/// <summary>
/// Time to wait between reconnect attempts
/// The fixed time to wait between reconnect attempts, only used when `ReconnectPolicy` is set to `ReconnectPolicy.ExponentialBackoff`
/// </summary>
public TimeSpan ReconnectInterval { get; set; } = TimeSpan.FromSeconds(5);
/// <summary>
/// Reconnect policy
/// </summary>
public ReconnectPolicy ReconnectPolicy { get; set; } = ReconnectPolicy.FixedDelay;
/// <summary>
/// Max number of concurrent resubscription tasks per socket after reconnecting a socket
/// </summary>
@@ -57,7 +58,7 @@ namespace CryptoExchange.Net.Objects.Options
{
ApiCredentials = ApiCredentials?.Copy(),
OutputOriginalData = OutputOriginalData,
AutoReconnect = AutoReconnect,
ReconnectPolicy = ReconnectPolicy,
DelayAfterConnect = DelayAfterConnect,
MaxConcurrentResubscriptionsPerSocket = MaxConcurrentResubscriptionsPerSocket,
ReconnectInterval = ReconnectInterval,
@@ -3,6 +3,7 @@ using CryptoExchange.Net.Converters.SystemTextJson;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
namespace CryptoExchange.Net.Objects
{
@@ -193,5 +194,18 @@ namespace CryptoExchange.Net.Objects
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>
public TimeSpan? EndpointLimitPeriod { get; set; }
/// <summary>
/// Whether this request should never be cached
/// </summary>
public bool PreventCaching { get; set; }
/// <summary>
/// ctor
/// </summary>
@@ -48,6 +48,7 @@ namespace CryptoExchange.Net.Objects
/// <param name="requestBodyFormat">Request body format</param>
/// <param name="parameterPosition">Parameter position</param>
/// <param name="arraySerialization">Array serialization type</param>
/// <param name="preventCaching">Prevent request caching</param>
/// <returns></returns>
public RequestDefinition GetOrCreate(
HttpMethod method,
@@ -59,7 +60,8 @@ namespace CryptoExchange.Net.Objects
TimeSpan? endpointLimitPeriod = null,
RequestBodyFormat? requestBodyFormat = null,
HttpMethodParameterPosition? parameterPosition = null,
ArrayParametersSerialization? arraySerialization = null)
ArrayParametersSerialization? arraySerialization = null,
bool? preventCaching = null)
{
if (!_definitions.TryGetValue(method + path, out var def))
@@ -74,6 +76,7 @@ namespace CryptoExchange.Net.Objects
ArraySerialization = arraySerialization,
RequestBodyFormat = requestBodyFormat,
ParameterPosition = parameterPosition,
PreventCaching = preventCaching ?? false
};
_definitions.TryAdd(method + path, def);
}
+78 -11
View File
@@ -14,9 +14,14 @@ namespace CryptoExchange.Net.Objects.Sockets
public DateTime Timestamp { get; set; }
/// <summary>
/// The topic of the update, what symbol/asset etc..
/// The stream producing the update
/// </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>
/// 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>
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;
Topic = topic;
StreamId = streamId;
Symbol = symbol;
OriginalData = originalData;
Timestamp = timestamp;
UpdateType = updateType;
@@ -50,7 +56,7 @@ namespace CryptoExchange.Net.Objects.Sockets
/// <returns></returns>
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>
@@ -58,11 +64,11 @@ namespace CryptoExchange.Net.Objects.Sockets
/// </summary>
/// <typeparam name="K">The type of the new data</typeparam>
/// <param name="data">The new data</param>
/// <param name="topic">The new topic</param>
/// <param name="symbol">The new symbol</param>
/// <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>
@@ -70,12 +76,73 @@ namespace CryptoExchange.Net.Objects.Sockets
/// </summary>
/// <typeparam name="K">The type of the new data</typeparam>
/// <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>
/// <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>();
/// <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>
public TimeSpan ReconnectInterval { get; set; } = TimeSpan.FromSeconds(5);
/// <summary>
/// Reconnect policy
/// </summary>
public ReconnectPolicy ReconnectPolicy { get; set; } = ReconnectPolicy.FixedDelay;
/// <summary>
/// Proxy for the connection
/// </summary>
public ApiProxy? Proxy { get; set; }
/// <summary>
/// Whether the socket should automatically reconnect when connection is lost
/// </summary>
public bool AutoReconnect { get; set; }
/// <summary>
/// The maximum time of no data received before considering the connection lost and closting/reconnecting the socket
/// </summary>
@@ -68,11 +68,11 @@ namespace CryptoExchange.Net.Objects.Sockets
/// ctor
/// </summary>
/// <param name="uri">Uri</param>
/// <param name="autoReconnect">Auto reconnect</param>
public WebSocketParameters(Uri uri, bool autoReconnect)
/// <param name="policy">Reconnect policy</param>
public WebSocketParameters(Uri uri, ReconnectPolicy policy)
{
Uri = uri;
AutoReconnect = autoReconnect;
ReconnectPolicy = policy;
}
}
}
@@ -101,7 +101,7 @@ namespace CryptoExchange.Net.RateLimiting
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));
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);
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)
{
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()
{
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 startCurrentWindow = checkTime.AddTicks(-(checkTime.Ticks % TimePeriod.Ticks));
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 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)
{
Limit = limit;
@@ -89,7 +94,10 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
removedWeight += entry.Weight;
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)
{
var handler = new HttpClientHandler();
handler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
if (proxy != null)
{
handler.Proxy = new WebProxy
@@ -47,6 +47,7 @@ namespace CryptoExchange.Net.Sockets
private ProcessState _processState;
private DateTime _lastReconnectTime;
private string _baseAddress;
private int _reconnectAttempt;
private const int _receiveBufferSize = 1048576;
private const int _sendBufferSize = 4096;
@@ -246,12 +247,12 @@ namespace CryptoExchange.Net.Sockets
await _closeTask.ConfigureAwait(false);
_closeTask = null;
if (!Parameters.AutoReconnect)
if (Parameters.ReconnectPolicy == ReconnectPolicy.Disabled)
{
_processState = ProcessState.Idle;
await (OnClose?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
return;
}
}
if (!_stopRequested)
{
@@ -259,9 +260,9 @@ namespace CryptoExchange.Net.Sockets
await (OnReconnecting?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
}
var sinceLastReconnect = DateTime.UtcNow - _lastReconnectTime;
if (sinceLastReconnect < Parameters.ReconnectInterval)
await Task.Delay(Parameters.ReconnectInterval - sinceLastReconnect).ConfigureAwait(false);
// Delay here to prevent very repid looping when a connection to the server is accepted and immediately disconnected
var initialDelay = GetReconnectDelay();
await Task.Delay(initialDelay).ConfigureAwait(false);
while (!_stopRequested)
{
@@ -282,13 +283,17 @@ namespace CryptoExchange.Net.Sockets
_ctsSource = new CancellationTokenSource();
while (_sendBuffer.TryDequeue(out _)) { } // Clear send buffer
_reconnectAttempt++;
var connected = await ConnectInternalAsync().ConfigureAwait(false);
if (!connected)
{
await Task.Delay(Parameters.ReconnectInterval).ConfigureAwait(false);
// Delay between reconnect attempts
var delay = GetReconnectDelay();
await Task.Delay(delay).ConfigureAwait(false);
continue;
}
_reconnectAttempt = 0;
_lastReconnectTime = DateTime.UtcNow;
await (OnReconnected?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
break;
@@ -298,6 +303,24 @@ namespace CryptoExchange.Net.Sockets
_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 />
public virtual void Send(int id, string data, int weight)
{
@@ -439,11 +462,19 @@ namespace CryptoExchange.Net.Sockets
{
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);
if (!limitResult)
try
{
await (OnRequestRateLimited?.Invoke(data.Id) ?? Task.CompletedTask).ConfigureAwait(false);
continue;
var limitResult = await Parameters.RateLimiter.ProcessAsync(_logger, data.Id, RateLimitItemType.Request, requestDefinition, _baseAddress, null, data.Weight, Parameters.RateLimitingBehaviour, _ctsSource.Token).ConfigureAwait(false);
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.Objects;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.Requests;
using System;
using System.Collections.Generic;
using System.Threading;
@@ -18,11 +19,6 @@ namespace CryptoExchange.Net.Sockets
/// </summary>
public int Id { get; } = ExchangeHelpers.NextId();
/// <summary>
/// Can handle data
/// </summary>
public bool CanHandleData => true;
/// <summary>
/// Has this query been completed
/// </summary>
@@ -115,8 +111,9 @@ namespace CryptoExchange.Net.Sockets
/// Wait untill timeout or the request is competed
/// </summary>
/// <param name="timeout"></param>
/// <param name="ct">Cancellation token</param>
/// <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 />
public virtual CallResult<object> Deserialize(IMessageAccessor message, Type type) => message.Deserialize(type);
@@ -145,16 +142,17 @@ namespace CryptoExchange.Net.Sockets
/// <summary>
/// Query
/// </summary>
/// <typeparam name="TResponse">Response object type</typeparam>
public abstract class Query<TResponse> : Query
/// <typeparam name="TServerResponse">The type returned from the server</typeparam>
/// <typeparam name="THandlerResponse">The type to be returned to the caller</typeparam>
public abstract class Query<TServerResponse, THandlerResponse> : Query
{
/// <inheritdoc />
public override Type? GetMessageType(IMessageAccessor message) => typeof(TResponse);
public override Type? GetMessageType(IMessageAccessor message) => typeof(TServerResponse);
/// <summary>
/// The typed call result
/// </summary>
public CallResult<TResponse>? TypedResult => (CallResult<TResponse>?)Result;
public CallResult<THandlerResponse>? TypedResult => (CallResult<THandlerResponse>?)Result;
/// <summary>
/// ctor
@@ -171,7 +169,7 @@ namespace CryptoExchange.Net.Sockets
{
Completed = true;
Response = message.Data;
Result = HandleMessage(connection, message.As((TResponse)message.Data));
Result = HandleMessage(connection, message.As((TServerResponse)message.Data));
_event.Set();
ContinueAwaiter?.WaitOne();
return Result;
@@ -183,7 +181,7 @@ namespace CryptoExchange.Net.Sockets
/// <param name="connection"></param>
/// <param name="message"></param>
/// <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 />
public override void Timeout()
@@ -192,7 +190,7 @@ namespace CryptoExchange.Net.Sockets
return;
Completed = true;
Result = new CallResult<TResponse>(new CancellationRequestedError(null, "Query timeout", null));
Result = new CallResult<THandlerResponse>(new CancellationRequestedError(null, "Query timeout", null));
ContinueAwaiter?.Set();
_event.Set();
}
@@ -200,10 +198,35 @@ namespace CryptoExchange.Net.Sockets
/// <inheritdoc />
public override void Fail(Error error)
{
Result = new CallResult<TResponse>(error);
Result = new CallResult<THandlerResponse>(error);
Completed = true;
ContinueAwaiter?.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 readonly object _listenersLock;
private readonly List<IMessageProcessor> _listeners;
@@ -443,7 +448,7 @@ namespace CryptoExchange.Net.Sockets
// 4. Get the listeners interested in this message
List<IMessageProcessor> processors;
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)
{
@@ -451,7 +456,7 @@ namespace CryptoExchange.Net.Sockets
{
List<string> listenerIds;
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));
UnhandledMessage?.Invoke(_accessor);
}
@@ -478,6 +483,10 @@ namespace CryptoExchange.Net.Sockets
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
object? deserialized = null;
desCache?.TryGetValue(messageType, out deserialized);
@@ -498,7 +507,7 @@ namespace CryptoExchange.Net.Sockets
try
{
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;
}
catch (Exception ex)
@@ -604,7 +613,7 @@ namespace CryptoExchange.Net.Sockets
bool shouldCloseConnection;
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)
Status = SocketStatus.Closing;
}
@@ -686,27 +695,30 @@ namespace CryptoExchange.Net.Sockets
/// </summary>
/// <param name="query">Query to send</param>
/// <param name="continueEvent">Wait event for when the socket message handler can continue</param>
/// <param name="ct">Cancellation token</param>
/// <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"));
}
/// <summary>
/// Send a query request and wait for an answer
/// </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="continueEvent">Wait event for when the socket message handler can continue</param>
/// <param name="ct">Cancellation token</param>
/// <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);
return query.TypedResult ?? new CallResult<T>(new ServerError("Timeout"));
await SendAndWaitIntAsync(query, continueEvent, ct).ConfigureAwait(false);
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)
_listeners.Add(query);
@@ -723,7 +735,7 @@ namespace CryptoExchange.Net.Sockets
try
{
while (true)
while (!ct.IsCancellationRequested)
{
if (!_socket.IsOpen)
{
@@ -734,11 +746,17 @@ namespace CryptoExchange.Net.Sockets
if (query.Completed)
return;
await query.WaitAsync(TimeSpan.FromMilliseconds(500)).ConfigureAwait(false);
await query.WaitAsync(TimeSpan.FromMilliseconds(500), ct).ConfigureAwait(false);
if (query.Completed)
return;
}
if (ct.IsCancellationRequested)
{
query.Fail(new CancellationRequestedError());
return;
}
}
finally
{
@@ -798,20 +816,23 @@ namespace CryptoExchange.Net.Sockets
if (!_socket.IsOpen)
return new CallResult(new WebError("Socket not connected"));
bool anySubscriptions;
lock (_listenersLock)
anySubscriptions = _listeners.OfType<Subscription>().Any(s => s.UserSubscription);
if (!anySubscriptions)
if (!DedicatedRequestConnection)
{
// No need to resubscribe anything
_logger.NothingToResubscribeCloseConnection(SocketId);
_ = _socket.CloseAsync();
return new CallResult(null);
bool anySubscriptions;
lock (_listenersLock)
anySubscriptions = _listeners.OfType<Subscription>().Any(s => s.UserSubscription);
if (!anySubscriptions)
{
// No need to resubscribe anything
_logger.NothingToResubscribeCloseConnection(SocketId);
_ = _socket.CloseAsync();
return new CallResult(null);
}
}
bool anyAuthenticated;
lock (_listenersLock)
anyAuthenticated = _listeners.OfType<Subscription>().Any(s => s.Authenticated);
anyAuthenticated = _listeners.OfType<Subscription>().Any(s => s.Authenticated) || DedicatedRequestConnection;
if (anyAuthenticated)
{
// If we reconnected a authenticated connection we need to re-authenticate
@@ -860,6 +881,8 @@ namespace CryptoExchange.Net.Sockets
{
subscription.HandleSubQueryResponse(subQuery.Response!);
waitEvent.Set();
if (r.Result.Success)
subscription.Confirmed = true;
return r.Result;
}));
}
@@ -869,9 +892,6 @@ namespace CryptoExchange.Net.Sockets
return taskList.First(t => !t.Result.Success).Result;
}
foreach (var subscription in subList)
subscription.Confirmed = true;
if (!_socket.IsOpen)
return new CallResult(new WebError("Socket not connected"));
@@ -18,11 +18,6 @@ namespace CryptoExchange.Net.Sockets
/// </summary>
public int Id { get; set; }
/// <summary>
/// Can handle data
/// </summary>
public bool CanHandleData => Confirmed || HandleUpdatesBeforeConfirmation;
/// <summary>
/// Total amount of invocations
/// </summary>
@@ -42,11 +37,6 @@ namespace CryptoExchange.Net.Sockets
/// Has the subscription been confirmed
/// </summary>
public bool Confirmed { get; set; }
/// <summary>
/// Whether this subscription should handle update messages before confirmation
/// </summary>
public bool HandleUpdatesBeforeConfirmation { get; set; }
/// <summary>
/// Is the subscription closed
@@ -80,6 +80,10 @@ namespace CryptoExchange.Net.Testing.Comparers
else if (jObj.Type == JTokenType.Array)
{
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 arrayConverterProperty = resultObj.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true).FirstOrDefault();
var jsonConverter = ((JsonConverterAttribute)arrayConverterProperty!).ConverterType;
@@ -52,8 +52,13 @@ namespace CryptoExchange.Net.Testing.Comparers
else
{
if (dict[dictProp.Name] == default && dictProp.Value.Type != JTokenType.Null)
{
if (dictProp.Value.ToString() == "")
continue;
// Property value not correct
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)
{
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 arrayConverterProperty = resultObj.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true).FirstOrDefault();
var jsonConverter = ((JsonConverterAttribute)arrayConverterProperty!).ConverterType;
@@ -162,7 +171,7 @@ namespace CryptoExchange.Net.Testing.Comparers
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
{
@@ -180,7 +189,10 @@ namespace CryptoExchange.Net.Testing.Comparers
var enumerator = list.GetEnumerator();
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);
if (typeConverter.Length != 0 && ((JsonConverterAttribute)typeConverter.First()).ConverterType != typeof(ArrayConverter))
// Custom converter for the type, skip
@@ -260,9 +272,9 @@ namespace CryptoExchange.Net.Testing.Comparers
else if (objectValue is DateTime time)
{
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
}
@@ -278,6 +290,10 @@ namespace CryptoExchange.Net.Testing.Comparers
if (time != DateTimeConverter.ParseFromDouble(jsonValue.Value<long>()!))
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))
{
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 IsOpen => Connected;
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 TestSocket(string address)
{
Uri = new Uri(address);
}
public Task<CallResult> ConnectAsync()
{
Connected = CanConnect;
@@ -50,13 +50,17 @@ namespace CryptoExchange.Net.Testing
/// <param name="name">Method name for looking up json test values</param>
/// <param name="nestedJsonProperty">Use nested json property for compare</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>
/// <exception cref="Exception"></exception>
public async Task ValidateAsync<TUpdate>(
Func<TClient, Action<DataEvent<TUpdate>>, Task<CallResult<UpdateSubscription>>> methodInvoke,
string name,
string? nestedJsonProperty = null,
List<string>? ignoreProperties = null)
List<string>? ignoreProperties = null,
string? addressPath = null,
bool? useFirstUpdateItem = null)
{
var listener = new EnumValueTraceListener();
Trace.Listeners.Add(listener);
@@ -79,7 +83,7 @@ namespace CryptoExchange.Net.Testing
var data = Encoding.UTF8.GetString(buffer);
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);
string? lastMessage = null;
@@ -109,6 +113,7 @@ namespace CryptoExchange.Net.Testing
if (lastMessage == null)
throw new Exception($"{name} expected to {line} to be send to server but did not receive anything");
var lastMessageJson = JToken.Parse(lastMessage);
var expectedJson = JToken.Parse(line.Substring(2));
foreach(var item in expectedJson)
@@ -121,6 +126,12 @@ namespace CryptoExchange.Net.Testing
overrideKey = val.ToString();
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)
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");
if (_stjCompare == true)
SystemTextJsonComparer.CompareData(name, update, compareData, nestedJsonProperty ?? _nestedPropertyForCompare, ignoreProperties);
SystemTextJsonComparer.CompareData(name, update, compareData, nestedJsonProperty ?? _nestedPropertyForCompare, ignoreProperties, useFirstUpdateItem ?? false);
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;
}
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>())
{
apiClient.SocketFactory = new TestWebsocketFactory(socket);
@@ -136,8 +136,9 @@ namespace CryptoExchange.Net.Testing
headers,
true,
client.ArraySerialization,
client.ParameterPositions[method],
client.RequestBodyFormat);
client.ParameterPositions[method],
client.RequestBodyFormat
);
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)|
|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)|
|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)|
|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)|
@@ -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).
## 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
* 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">
<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_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_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>
</li>
<li class="nav-item"><a class="nav-link" href="#idocs_options">Options & Authorization</a>
<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_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_set">Setting Options</a></li>
<li class="nav-item"><a class="nav-link" href="#idocs_options_def">Option Definitions</a></li>
</ul>
</li>
<li class="nav-item"><a class="nav-link" href="#idocs_features">Additional Features</a>
@@ -122,37 +123,39 @@
<div class="idocs-content">
<div class="container">
<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>
<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>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>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><img src="assets/images/struct.png" /></p>
<p>These Exchanges/API's are directly supported:</p>
<table class="table table-bordered">
<tr>
<th>Exchange</th>
<th>API</th>
<th>Repository</th>
<th>Nuget</th>
</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>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>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>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>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>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>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>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>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>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>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>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>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" 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" 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" 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" 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" 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" target="_blank"><img src="https://img.shields.io/nuget/v/CoinGecko.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>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>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>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>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>
<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>Note that there are 3rd party implementations going around, but only the listed ones here are created and supported by me.</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>
<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>
<table class="table table-bordered">
<tr>
@@ -811,6 +814,15 @@
<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
============================ -->
<section id="idocs_rest">
@@ -1376,14 +1388,14 @@ await client.UnsubscribeAllAsync();</code></pre>
============================ -->
<section id="idocs_common">
<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 />
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>
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 bybitTicker = await exchangeRestClient.Bybit.V5Api.ExchangeData.GetTickers();
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.
</p>
<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 krakenSub = await exchangeSocketClient.Kraken.SpotApi.SubscribeToTickerUpdatesAsync("ETH/USD", data => {});
var okxSub = await exchangeSocketClient.OKX.UnifiedApi.ExchangeData.SubscribeToTickerUpdatesAsync("ETH-USDT", data => {});</code></pre>