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