mirror of
https://github.com/JKorf/CryptoExchange.Net.git
synced 2026-08-11 16:32:57 +00:00
Websocket performance update (#261)
Performance update: Authentication Added Ed25519 signing support for NET8.0 and newer Added static methods on ApiCredentials to create credentials of a specific type Added static ApiCredentials.ReadFromFile method to read a key from file Added required abstract SupportedCredentialTypes property on AuthenticationProvider base class General Performance Added checks before logging statements to prevent overhead of building the log string if logging is not needed Added ExchangeHelpers.ProcessQueuedAsync method to process updates async Replaced locking object types from object to Lock in NET9.0 and newer Replaced some Task response types with ValueTask to prevent allocation overhead on hot paths Updated Json ArrayConverter to reduce some allocation overhead Updated Json BoolConverter to prevent boxing Updated Json DateTimeConverter to prevent boxing Updated Json EnumConverter caching to reduce lookup overhead Updated ExtensionMethods.CreateParamString to reduce allocations Updated ExtensionMethods.AppendPath to reduce overhead REST Refactored REST message processing to separate IRestMessageHandler instance Split RestApiClient.PrepareAsync into CheckTimeSync and RateLimitAsync Updated IRequest.Accept type from string to MediaTypeWithQualityHeaderValue to prevent creation on each request Updated IRequest.GetHeaders response type from KeyValuePair<string, string[]>[] to HttpRequestHeaders to prevent additional mapping Updated IResponse.ResponseHeaders type from KeyValuePair<string, string[]>[] to HttpResponseHeaders to prevent additional mapping Updated WebCallResult RequestHeaders and ResponseHeaders types to HttpRequestHeaders and HttpResponseHeaders Removed unnecessary empty dictionary initializations for each request Removed CallResult creation in internal methods to prevent having to create multiple versions for different result types Socket Added HighPerformance websocket client implementation which significantly reduces memory overhead and improves speed but with certain limitations Added MaxIndividualSubscriptionsPerConnection setting in SocketApiClient to limit the number of individual stream subscriptions on a connection Added SocketIndividualSubscriptionCombineTarget option to set the target number of individual stream subscriptions per connection Added new websocket message handling logic which is faster and reduces memory allocation Added UseUpdatedDeserialization option to toggle between updated deserialization and old deserialization Added Exchange property to DataEvent to prevent additional mapping overhead for Shared apis Refactored message callback to be sync instead of async to prevent async overhead Refactored CryptoExchangeWebSocketClient.IncomingKbps calculation to significantly reduce overhead Moved websocket client creation from SocketApiClient to SocketConnection Removed DataEvent.As and DataEvent.ToCallResult methods in favor of single ToType method Removed DataEvent creation on lower levels to prevent having to create multiple versions for different result types Removed Subscription<TSubResponse, TUnsubResponse> as its no longer used Other Added null check to ParameterCollection for required parameters Added Net10.0 target framework Updated dependency versions Updated Shared asset aliases check to be culture invariant Updated Error string representation Updated some namespaces Updated SymbolOrderBook processing of buffered updates to prevent additional allocation Removed ExchangeEvent type which is no longer needed Removed unused usings
This commit is contained in:
@@ -1,8 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.Objects
|
||||
namespace CryptoExchange.Net.Objects
|
||||
{
|
||||
/// <summary>
|
||||
/// An alias used by the exchange for an asset commonly known by another name
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.Objects
|
||||
{
|
||||
@@ -23,7 +21,8 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <summary>
|
||||
/// Map the common name to an exchange name for an asset. If there is no alias the input name is returned
|
||||
/// </summary>
|
||||
public string CommonToExchangeName(string commonName) => !AutoConvertEnabled ? commonName : Aliases.FirstOrDefault(x => x.CommonAssetName == commonName)?.ExchangeAssetName ?? commonName;
|
||||
public string CommonToExchangeName(string commonName) =>
|
||||
!AutoConvertEnabled ? commonName : Aliases.FirstOrDefault(x => x.CommonAssetName.Equals(commonName, StringComparison.InvariantCulture))?.ExchangeAssetName ?? commonName;
|
||||
|
||||
/// <summary>
|
||||
/// Map the exchange name to a common name for an asset. If there is no alias the input name is returned
|
||||
@@ -33,7 +32,7 @@ namespace CryptoExchange.Net.Objects
|
||||
if (!AutoConvertEnabled)
|
||||
return exchangeName;
|
||||
|
||||
var alias = Aliases.FirstOrDefault(x => x.ExchangeAssetName == exchangeName);
|
||||
var alias = Aliases.FirstOrDefault(x => x.ExchangeAssetName.Equals(exchangeName, StringComparison.InvariantCulture));
|
||||
if (alias == null || alias.Type == AliasType.OnlyToExchange)
|
||||
return exchangeName;
|
||||
|
||||
|
||||
@@ -14,6 +14,11 @@ namespace CryptoExchange.Net.Objects
|
||||
{
|
||||
private static readonly Task<bool> _completed = Task.FromResult(true);
|
||||
private Queue<TaskCompletionSource<bool>> _waits = new Queue<TaskCompletionSource<bool>>();
|
||||
#if NET9_0_OR_GREATER
|
||||
private readonly Lock _waitsLock = new Lock();
|
||||
#else
|
||||
private readonly object _waitsLock = new object();
|
||||
#endif
|
||||
private bool _signaled;
|
||||
private readonly bool _reset;
|
||||
|
||||
@@ -38,7 +43,7 @@ namespace CryptoExchange.Net.Objects
|
||||
try
|
||||
{
|
||||
Task<bool> waiter = _completed;
|
||||
lock (_waits)
|
||||
lock (_waitsLock)
|
||||
{
|
||||
if (_signaled)
|
||||
{
|
||||
@@ -57,7 +62,7 @@ namespace CryptoExchange.Net.Objects
|
||||
|
||||
registration = ct.Register(() =>
|
||||
{
|
||||
lock (_waits)
|
||||
lock (_waitsLock)
|
||||
{
|
||||
tcs.TrySetResult(false);
|
||||
|
||||
@@ -85,7 +90,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// </summary>
|
||||
public void Set()
|
||||
{
|
||||
lock (_waits)
|
||||
lock (_waitsLock)
|
||||
{
|
||||
if (!_reset)
|
||||
{
|
||||
@@ -106,7 +111,9 @@ namespace CryptoExchange.Net.Objects
|
||||
toRelease.TrySetResult(true);
|
||||
}
|
||||
else if (!_signaled)
|
||||
{
|
||||
_signaled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.Objects
|
||||
@@ -214,7 +214,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <summary>
|
||||
/// The headers sent with the request
|
||||
/// </summary>
|
||||
public KeyValuePair<string, string[]>[]? RequestHeaders { get; set; }
|
||||
public HttpRequestHeaders? RequestHeaders { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The request id
|
||||
@@ -244,7 +244,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <summary>
|
||||
/// The response headers
|
||||
/// </summary>
|
||||
public KeyValuePair<string, string[]>[]? ResponseHeaders { get; set; }
|
||||
public HttpResponseHeaders? ResponseHeaders { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The time between sending the request and receiving the response
|
||||
@@ -257,14 +257,14 @@ namespace CryptoExchange.Net.Objects
|
||||
public WebCallResult(
|
||||
HttpStatusCode? code,
|
||||
Version? httpVersion,
|
||||
KeyValuePair<string, string[]>[]? responseHeaders,
|
||||
HttpResponseHeaders? responseHeaders,
|
||||
TimeSpan? responseTime,
|
||||
string? originalData,
|
||||
int? requestId,
|
||||
string? requestUrl,
|
||||
string? requestBody,
|
||||
HttpMethod? requestMethod,
|
||||
KeyValuePair<string, string[]>[]? requestHeaders,
|
||||
HttpRequestHeaders? requestHeaders,
|
||||
Error? error) : base(error)
|
||||
{
|
||||
ResponseStatusCode = code;
|
||||
@@ -370,7 +370,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <summary>
|
||||
/// The headers sent with the request
|
||||
/// </summary>
|
||||
public KeyValuePair<string, string[]>[]? RequestHeaders { get; set; }
|
||||
public HttpRequestHeaders? RequestHeaders { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The request id
|
||||
@@ -400,7 +400,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <summary>
|
||||
/// The response headers
|
||||
/// </summary>
|
||||
public KeyValuePair<string, string[]>[]? ResponseHeaders { get; set; }
|
||||
public HttpResponseHeaders? ResponseHeaders { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The time between sending the request and receiving the response
|
||||
@@ -418,7 +418,7 @@ namespace CryptoExchange.Net.Objects
|
||||
public WebCallResult(
|
||||
HttpStatusCode? code,
|
||||
Version? httpVersion,
|
||||
KeyValuePair<string, string[]>[]? responseHeaders,
|
||||
HttpResponseHeaders? responseHeaders,
|
||||
TimeSpan? responseTime,
|
||||
long? responseLength,
|
||||
string? originalData,
|
||||
@@ -426,7 +426,7 @@ namespace CryptoExchange.Net.Objects
|
||||
string? requestUrl,
|
||||
string? requestBody,
|
||||
HttpMethod? requestMethod,
|
||||
KeyValuePair<string, string[]>[]? requestHeaders,
|
||||
HttpRequestHeaders? requestHeaders,
|
||||
ResultDataSource dataSource,
|
||||
[AllowNull] T data,
|
||||
Error? error) : base(data, originalData, error)
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using CryptoExchange.Net.Attributes;
|
||||
|
||||
namespace CryptoExchange.Net.Objects
|
||||
namespace CryptoExchange.Net.Objects
|
||||
{
|
||||
/// <summary>
|
||||
/// What to do when a request would exceed the rate limit
|
||||
|
||||
@@ -79,7 +79,20 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <returns></returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return ErrorCode != null ? $"[{GetType().Name}.{ErrorType}] {ErrorCode}: {Message ?? ErrorDescription}" : $"[{GetType().Name}.{ErrorType}] {Message ?? ErrorDescription}";
|
||||
return Code != null
|
||||
? $"[{GetType().Name}.{ErrorType}] {Code}: {GetErrorDescription()}"
|
||||
: $"[{GetType().Name}.{ErrorType}] {GetErrorDescription()}";
|
||||
}
|
||||
|
||||
private string GetErrorDescription()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(Message))
|
||||
return Message!;
|
||||
|
||||
if (ErrorDescription != "Unknown error" || Exception == null)
|
||||
return ErrorDescription!;
|
||||
|
||||
return Exception.Message;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.Objects.Errors
|
||||
{
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.Objects.Errors
|
||||
namespace CryptoExchange.Net.Objects.Errors
|
||||
{
|
||||
/// <summary>
|
||||
/// Error info
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.Objects.Errors
|
||||
{
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.Objects.Errors
|
||||
namespace CryptoExchange.Net.Objects.Errors
|
||||
{
|
||||
/// <summary>
|
||||
/// Type of error
|
||||
|
||||
@@ -8,7 +8,8 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
public class ApiOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// If true, the CallResult and DataEvent objects will also include the originally received json data in the OriginalData property
|
||||
/// If true, the CallResult and DataEvent objects will also include the originally received string data in the OriginalData property.
|
||||
/// Note that this comes at a performance cost
|
||||
/// </summary>
|
||||
public bool? OutputOriginalData { get; set; }
|
||||
|
||||
|
||||
@@ -14,7 +14,8 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
public ApiProxy? Proxy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If true, the CallResult and DataEvent objects will also include the originally received json data in the OriginalData property
|
||||
/// If true, the CallResult and DataEvent objects will also include the originally received string data in the OriginalData property.
|
||||
/// Note that this comes at a performance cost
|
||||
/// </summary>
|
||||
public bool OutputOriginalData { get; set; } = false;
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace CryptoExchange.Net.Objects.Options
|
||||
{
|
||||
|
||||
@@ -32,10 +32,24 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
/// <summary>
|
||||
/// The amount of subscriptions that should be made on a single socket connection. Not all API's support multiple subscriptions on a single socket.
|
||||
/// Setting this to a higher number increases subscription speed because not every subscription needs to connect to the server, but having more subscriptions on a
|
||||
/// single connection will also increase the amount of traffic on that single connection, potentially leading to issues.
|
||||
/// single connection will also increase the amount of traffic on that single connection, potentially leading to issues or delays.
|
||||
/// <para>
|
||||
/// This setting counts each Subscribe request as one instead of counting the individual subscriptions as <see cref="SocketIndividualSubscriptionCombineTarget"/> does
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public int? SocketSubscriptionsCombineTarget { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The amount of subscriptions that should be made on a single socket connection. Not all API's support multiple subscriptions on a single socket.
|
||||
/// Setting this to a higher number increases subscription speed because not every subscription needs to connect to the server, but having more subscriptions on a
|
||||
/// single connection will also increase the amount of traffic on that single connection, potentially leading to issues or delays.
|
||||
/// <para>
|
||||
/// This setting counts the individual subscriptions in a request instead of counting subscriptions in batched request as one as <see cref="SocketSubscriptionsCombineTarget"/> does.
|
||||
/// </para>
|
||||
/// <para>Defaults to 20</para>
|
||||
/// </summary>
|
||||
public int SocketIndividualSubscriptionCombineTarget { get; set; } = 20;
|
||||
|
||||
/// <summary>
|
||||
/// The max amount of connections to make to the server. Can be used for API's which only allow a certain number of connections. Changing this to a high value might cause issues.
|
||||
/// </summary>
|
||||
@@ -61,6 +75,11 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
/// </remarks>
|
||||
public int? ReceiveBufferSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not to use the updated deserialization logic, default is true
|
||||
/// </summary>
|
||||
public bool UseUpdatedDeserialization { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Create a copy of this options
|
||||
/// </summary>
|
||||
@@ -82,6 +101,7 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
item.RateLimitingBehaviour = RateLimitingBehaviour;
|
||||
item.RateLimiterEnabled = RateLimiterEnabled;
|
||||
item.ReceiveBufferSize = ReceiveBufferSize;
|
||||
item.UseUpdatedDeserialization = UseUpdatedDeserialization;
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.Objects.Options
|
||||
{
|
||||
|
||||
@@ -13,6 +13,15 @@ namespace CryptoExchange.Net.Objects
|
||||
/// </summary>
|
||||
public class ParameterCollection : Dictionary<string, object>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public new void Add(string key, object value)
|
||||
{
|
||||
if (value == null)
|
||||
throw new ArgumentNullException(key);
|
||||
|
||||
base.Add(key, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add an optional parameter. Not added if value is null
|
||||
/// </summary>
|
||||
@@ -21,7 +30,7 @@ namespace CryptoExchange.Net.Objects
|
||||
public void AddOptional(string key, object? value)
|
||||
{
|
||||
if (value != null)
|
||||
Add(key, value);
|
||||
base.Add(key, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -31,7 +40,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <param name="value"></param>
|
||||
public void AddString(string key, decimal value)
|
||||
{
|
||||
Add(key, value.ToString(CultureInfo.InvariantCulture));
|
||||
base.Add(key, value.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -42,7 +51,7 @@ namespace CryptoExchange.Net.Objects
|
||||
public void AddOptionalString(string key, decimal? value)
|
||||
{
|
||||
if (value != null)
|
||||
Add(key, value.Value.ToString(CultureInfo.InvariantCulture));
|
||||
base.Add(key, value.Value.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -52,7 +61,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <param name="value"></param>
|
||||
public void AddString(string key, int value)
|
||||
{
|
||||
Add(key, value.ToString(CultureInfo.InvariantCulture));
|
||||
base.Add(key, value.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -63,7 +72,7 @@ namespace CryptoExchange.Net.Objects
|
||||
public void AddOptionalString(string key, int? value)
|
||||
{
|
||||
if (value != null)
|
||||
Add(key, value.Value.ToString(CultureInfo.InvariantCulture));
|
||||
base.Add(key, value.Value.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -73,7 +82,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <param name="value"></param>
|
||||
public void AddString(string key, long value)
|
||||
{
|
||||
Add(key, value.ToString(CultureInfo.InvariantCulture));
|
||||
base.Add(key, value.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -84,7 +93,7 @@ namespace CryptoExchange.Net.Objects
|
||||
public void AddOptionalString(string key, long? value)
|
||||
{
|
||||
if (value != null)
|
||||
Add(key, value.Value.ToString(CultureInfo.InvariantCulture));
|
||||
base.Add(key, value.Value.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -94,7 +103,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <param name="value"></param>
|
||||
public void AddMilliseconds(string key, DateTime value)
|
||||
{
|
||||
Add(key, DateTimeConverter.ConvertToMilliseconds(value));
|
||||
base.Add(key, DateTimeConverter.ConvertToMilliseconds(value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -105,7 +114,7 @@ namespace CryptoExchange.Net.Objects
|
||||
public void AddOptionalMilliseconds(string key, DateTime? value)
|
||||
{
|
||||
if (value != null)
|
||||
Add(key, DateTimeConverter.ConvertToMilliseconds(value));
|
||||
base.Add(key, DateTimeConverter.ConvertToMilliseconds(value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -115,7 +124,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <param name="value"></param>
|
||||
public void AddMillisecondsString(string key, DateTime value)
|
||||
{
|
||||
Add(key, DateTimeConverter.ConvertToMilliseconds(value).Value.ToString(CultureInfo.InvariantCulture));
|
||||
base.Add(key, DateTimeConverter.ConvertToMilliseconds(value).Value.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -126,7 +135,7 @@ namespace CryptoExchange.Net.Objects
|
||||
public void AddOptionalMillisecondsString(string key, DateTime? value)
|
||||
{
|
||||
if (value != null)
|
||||
Add(key, DateTimeConverter.ConvertToMilliseconds(value).Value.ToString(CultureInfo.InvariantCulture));
|
||||
base.Add(key, DateTimeConverter.ConvertToMilliseconds(value).Value.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -136,7 +145,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <param name="value"></param>
|
||||
public void AddSeconds(string key, DateTime value)
|
||||
{
|
||||
Add(key, DateTimeConverter.ConvertToSeconds(value));
|
||||
base.Add(key, DateTimeConverter.ConvertToSeconds(value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -147,7 +156,7 @@ namespace CryptoExchange.Net.Objects
|
||||
public void AddOptionalSeconds(string key, DateTime? value)
|
||||
{
|
||||
if (value != null)
|
||||
Add(key, DateTimeConverter.ConvertToSeconds(value));
|
||||
base.Add(key, DateTimeConverter.ConvertToSeconds(value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -157,7 +166,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <param name="value"></param>
|
||||
public void AddSecondsString(string key, DateTime value)
|
||||
{
|
||||
Add(key, DateTimeConverter.ConvertToSeconds(value).ToString()!);
|
||||
base.Add(key, DateTimeConverter.ConvertToSeconds(value).ToString()!);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -168,7 +177,7 @@ namespace CryptoExchange.Net.Objects
|
||||
public void AddOptionalSecondsString(string key, DateTime? value)
|
||||
{
|
||||
if (value != null)
|
||||
Add(key, DateTimeConverter.ConvertToSeconds(value).ToString()!);
|
||||
base.Add(key, DateTimeConverter.ConvertToSeconds(value).ToString()!);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -181,7 +190,7 @@ namespace CryptoExchange.Net.Objects
|
||||
#endif
|
||||
where T : struct, Enum
|
||||
{
|
||||
Add(key, EnumConverter<T>.GetString(value)!);
|
||||
base.Add(key, EnumConverter<T>.GetString(value)!);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -197,7 +206,7 @@ namespace CryptoExchange.Net.Objects
|
||||
where T : struct, Enum
|
||||
{
|
||||
var stringVal = EnumConverter<T>.GetString(value)!;
|
||||
Add(key, int.Parse(stringVal)!);
|
||||
base.Add(key, int.Parse(stringVal)!);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -213,7 +222,7 @@ namespace CryptoExchange.Net.Objects
|
||||
where T : struct, Enum
|
||||
{
|
||||
if (value != null)
|
||||
Add(key, EnumConverter<T>.GetString(value));
|
||||
base.Add(key, EnumConverter<T>.GetString(value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -229,7 +238,7 @@ namespace CryptoExchange.Net.Objects
|
||||
if (value != null)
|
||||
{
|
||||
var stringVal = EnumConverter<T>.GetString(value);
|
||||
Add(key, int.Parse(stringVal));
|
||||
base.Add(key, int.Parse(stringVal));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,7 +252,7 @@ namespace CryptoExchange.Net.Objects
|
||||
if (this.Any())
|
||||
throw new InvalidOperationException("Can't set body when other parameters already specified");
|
||||
|
||||
Add(Constants.BodyPlaceHolderKey, body);
|
||||
base.Add(Constants.BodyPlaceHolderKey, body);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.Objects
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Queue for processing items
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Item type</typeparam>
|
||||
public class ProcessQueue<T>
|
||||
{
|
||||
private readonly Channel<T> _channel;
|
||||
private readonly Func<T, Task> _processor;
|
||||
private Task? _processTask;
|
||||
private CancellationTokenSource? _cts;
|
||||
private bool _processTillEmpty;
|
||||
|
||||
/// <summary>
|
||||
/// Event for when an exception is thrown in the processing handler
|
||||
/// </summary>
|
||||
public event Action<Exception>? Exception;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="processor">The function to async handle the updates</param>
|
||||
/// <param name="maxQueuedItems">The max number of items to be queued up. When happens when the queue is full and a new write is attempted can be specified with <see>fullMode</see></param>
|
||||
/// <param name="fullBehavior">What should happen if the queue contains <see>maxQueuedItems</see> pending items. If no max is set this setting is ignored</param>
|
||||
public ProcessQueue(Func<T, Task> processor, int? maxQueuedItems = null, QueueFullBehavior? fullBehavior = null)
|
||||
{
|
||||
_processor = processor;
|
||||
if (maxQueuedItems == null)
|
||||
{
|
||||
_channel = Channel.CreateUnbounded<T>(new UnboundedChannelOptions
|
||||
{
|
||||
AllowSynchronousContinuations = false,
|
||||
SingleReader = true,
|
||||
SingleWriter = true
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
_channel = Channel.CreateBounded<T>(new BoundedChannelOptions(maxQueuedItems.Value)
|
||||
{
|
||||
AllowSynchronousContinuations = false,
|
||||
SingleReader = true,
|
||||
SingleWriter = true,
|
||||
FullMode = MapMode(fullBehavior)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private BoundedChannelFullMode MapMode(QueueFullBehavior? behavior) =>
|
||||
behavior switch
|
||||
{
|
||||
QueueFullBehavior.DropOldest => BoundedChannelFullMode.DropOldest,
|
||||
QueueFullBehavior.DropNewest => BoundedChannelFullMode.DropNewest,
|
||||
QueueFullBehavior.DropWrite => BoundedChannelFullMode.DropWrite,
|
||||
_ => BoundedChannelFullMode.DropWrite
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Start the processing of the queue
|
||||
/// </summary>
|
||||
public Task StartAsync()
|
||||
{
|
||||
_cts = new CancellationTokenSource();
|
||||
_processTask = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await foreach (var item in _channel.Reader.ReadAllAsync(_cts.Token).ConfigureAwait(false))
|
||||
{
|
||||
if (_cts.IsCancellationRequested && !_processTillEmpty) // Items might still be processed even if CT is canceled
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
await _processor.Invoke(item).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Exception?.Invoke(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stop processing the queue
|
||||
/// </summary>
|
||||
/// <param name="discardPending">Whether updates still pending in the queue should be discarded</param>
|
||||
/// <returns></returns>
|
||||
public async Task StopAsync(bool discardPending = true)
|
||||
{
|
||||
if (_processTask == null)
|
||||
return;
|
||||
|
||||
_processTillEmpty = !discardPending;
|
||||
_cts!.Cancel();
|
||||
|
||||
await _processTask.ConfigureAwait(false);
|
||||
_channel.Writer.TryComplete(_processTask.Exception);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write an update to queue
|
||||
/// </summary>
|
||||
public bool Write(T item)
|
||||
{
|
||||
if (_cts?.IsCancellationRequested == true)
|
||||
return false;
|
||||
|
||||
var write = _channel.Writer.TryWrite(item);
|
||||
if (!write)
|
||||
LibraryHelpers.StaticLogger?.Log(LogLevel.Warning, "Failed to write item to process queue. Item will be discarded");
|
||||
|
||||
return write;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,15 +30,15 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <summary>
|
||||
/// Query parameters
|
||||
/// </summary>
|
||||
public IDictionary<string, object> QueryParameters { get; set; }
|
||||
public IDictionary<string, object>? QueryParameters { get; set; }
|
||||
/// <summary>
|
||||
/// Body parameters
|
||||
/// </summary>
|
||||
public IDictionary<string, object> BodyParameters { get; set; }
|
||||
public IDictionary<string, object>? BodyParameters { get; set; }
|
||||
/// <summary>
|
||||
/// Request headers
|
||||
/// </summary>
|
||||
public IDictionary<string, string> Headers { get; set; }
|
||||
public IDictionary<string, string>? Headers { get; set; }
|
||||
/// <summary>
|
||||
/// Array serialization type
|
||||
/// </summary>
|
||||
@@ -58,9 +58,9 @@ namespace CryptoExchange.Net.Objects
|
||||
public RestRequestConfiguration(
|
||||
RequestDefinition requestDefinition,
|
||||
string baseAddress,
|
||||
IDictionary<string, object> queryParams,
|
||||
IDictionary<string, object> bodyParams,
|
||||
IDictionary<string, string> headers,
|
||||
IDictionary<string, object>? queryParams,
|
||||
IDictionary<string, object>? bodyParams,
|
||||
IDictionary<string, string>? headers,
|
||||
ArrayParametersSerialization arraySerialization,
|
||||
HttpMethodParameterPosition parametersPosition,
|
||||
RequestBodyFormat bodyFormat)
|
||||
@@ -83,8 +83,12 @@ namespace CryptoExchange.Net.Objects
|
||||
public IDictionary<string, object> GetPositionParameters()
|
||||
{
|
||||
if (ParameterPosition == HttpMethodParameterPosition.InBody)
|
||||
{
|
||||
BodyParameters ??= new Dictionary<string, object>();
|
||||
return BodyParameters;
|
||||
}
|
||||
|
||||
QueryParameters ??= new Dictionary<string, object>();
|
||||
return QueryParameters;
|
||||
}
|
||||
|
||||
@@ -94,7 +98,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <param name="urlEncode">Whether to URL encode the parameter string if creating new</param>
|
||||
public string GetQueryString(bool urlEncode = true)
|
||||
{
|
||||
return _queryString ?? QueryParameters.CreateParamString(urlEncode, ArraySerialization);
|
||||
return _queryString ?? QueryParameters?.CreateParamString(urlEncode, ArraySerialization) ?? string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -6,8 +6,7 @@ namespace CryptoExchange.Net.Objects.Sockets
|
||||
/// <summary>
|
||||
/// An update received from a socket update subscription
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the data</typeparam>
|
||||
public class DataEvent<T>
|
||||
public class DataEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// The timestamp the data was received
|
||||
@@ -29,6 +28,11 @@ namespace CryptoExchange.Net.Objects.Sockets
|
||||
/// </summary>
|
||||
public string? Symbol { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The exchange name
|
||||
/// </summary>
|
||||
public string Exchange { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The original data that was received, only available when OutputOriginalData is set to true in the client options
|
||||
/// </summary>
|
||||
@@ -39,6 +43,29 @@ namespace CryptoExchange.Net.Objects.Sockets
|
||||
/// </summary>
|
||||
public SocketUpdateType? UpdateType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public DataEvent(
|
||||
string exchange,
|
||||
DateTime receiveTimestamp,
|
||||
string? originalData)
|
||||
{
|
||||
Exchange = exchange;
|
||||
OriginalData = originalData;
|
||||
ReceiveTime = receiveTimestamp;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{StreamId} - {(Symbol == null ? "" : (Symbol + " - "))}{UpdateType}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public class DataEvent<T> : DataEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// The received data deserialized into an object
|
||||
/// </summary>
|
||||
@@ -47,75 +74,13 @@ namespace CryptoExchange.Net.Objects.Sockets
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public DataEvent(T data, string? streamId, string? symbol, string? originalData, DateTime receiveTimestamp, SocketUpdateType? updateType)
|
||||
public DataEvent(
|
||||
string exchange,
|
||||
T data,
|
||||
DateTime receiveTimestamp,
|
||||
string? originalData): base(exchange, receiveTimestamp, originalData)
|
||||
{
|
||||
Data = data;
|
||||
StreamId = streamId;
|
||||
Symbol = symbol;
|
||||
OriginalData = originalData;
|
||||
ReceiveTime = receiveTimestamp;
|
||||
UpdateType = updateType;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new DataEvent with data in the from of type K based on the current DataEvent. Topic, OriginalData and ReceivedTimestamp will be copied over
|
||||
/// </summary>
|
||||
/// <typeparam name="K">The type of the new data</typeparam>
|
||||
/// <param name="data">The new data</param>
|
||||
/// <returns></returns>
|
||||
public DataEvent<K> As<K>(K data)
|
||||
{
|
||||
return new DataEvent<K>(data, StreamId, Symbol, OriginalData, ReceiveTime, UpdateType)
|
||||
{
|
||||
DataTime = DataTime
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new DataEvent with data in the from of type K based on the current DataEvent. OriginalData and ReceivedTimestamp will be copied over
|
||||
/// </summary>
|
||||
/// <typeparam name="K">The type of the new data</typeparam>
|
||||
/// <param name="data">The new data</param>
|
||||
/// <param name="symbol">The new symbol</param>
|
||||
/// <returns></returns>
|
||||
public DataEvent<K> As<K>(K data, string? symbol)
|
||||
{
|
||||
return new DataEvent<K>(data, StreamId, symbol, OriginalData, ReceiveTime, UpdateType)
|
||||
{
|
||||
DataTime = DataTime
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new DataEvent with data in the from of type K based on the current DataEvent. OriginalData and ReceivedTimestamp will be copied over
|
||||
/// </summary>
|
||||
/// <typeparam name="K">The type of the new data</typeparam>
|
||||
/// <param name="data">The new data</param>
|
||||
/// <param name="streamId">The new stream id</param>
|
||||
/// <param name="symbol">The new symbol</param>
|
||||
/// <param name="updateType">The type of update</param>
|
||||
/// <returns></returns>
|
||||
public DataEvent<K> As<K>(K data, string streamId, string? symbol, SocketUpdateType updateType)
|
||||
{
|
||||
return new DataEvent<K>(data, streamId, symbol, OriginalData, ReceiveTime, updateType)
|
||||
{
|
||||
DataTime = DataTime
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy the WebCallResult to a new data type
|
||||
/// </summary>
|
||||
/// <typeparam name="K">The new type</typeparam>
|
||||
/// <param name="exchange">The exchange the result is for</param>
|
||||
/// <param name="data">The data</param>
|
||||
/// <returns></returns>
|
||||
public ExchangeEvent<K> AsExchangeEvent<K>(string exchange, K data)
|
||||
{
|
||||
return new ExchangeEvent<K>(exchange, this.As<K>(data))
|
||||
{
|
||||
DataTime = DataTime
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -123,7 +88,7 @@ namespace CryptoExchange.Net.Objects.Sockets
|
||||
/// </summary>
|
||||
/// <param name="symbol"></param>
|
||||
/// <returns></returns>
|
||||
public DataEvent<T> WithSymbol(string symbol)
|
||||
public DataEvent<T> WithSymbol(string? symbol)
|
||||
{
|
||||
Symbol = symbol;
|
||||
return this;
|
||||
@@ -161,36 +126,19 @@ namespace CryptoExchange.Net.Objects.Sockets
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a CallResult from this DataEvent
|
||||
/// Create a new DataEvent of the new type
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public CallResult<T> ToCallResult()
|
||||
public DataEvent<TNew> ToType<TNew>(TNew data)
|
||||
{
|
||||
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);
|
||||
return new DataEvent<TNew>(Exchange, data, ReceiveTime, OriginalData)
|
||||
{
|
||||
StreamId = StreamId,
|
||||
UpdateType = UpdateType,
|
||||
Symbol = Symbol
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{StreamId} - {(Symbol == null ? "" : (Symbol + " - "))}{(UpdateType == null ? "" : (UpdateType + " - "))}{Data}";
|
||||
}
|
||||
public override string ToString() => base.ToString().TrimEnd('-') + Data?.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
using CryptoExchange.Net.Sockets.HighPerf;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.Objects.Sockets
|
||||
{
|
||||
/// <summary>
|
||||
/// Subscription to a data stream
|
||||
/// </summary>
|
||||
public class HighPerfUpdateSubscription
|
||||
{
|
||||
private readonly HighPerfSocketConnection _connection;
|
||||
internal readonly HighPerfSubscription _subscription;
|
||||
|
||||
#if NET9_0_OR_GREATER
|
||||
private readonly Lock _eventLock = new Lock();
|
||||
#else
|
||||
private readonly object _eventLock = new object();
|
||||
#endif
|
||||
|
||||
private bool _connectionEventsSubscribed = true;
|
||||
private readonly List<Action> _connectionClosedEventHandlers = new List<Action>();
|
||||
|
||||
/// <summary>
|
||||
/// Event when the connection is closed and will not be reconnected
|
||||
/// </summary>
|
||||
public event Action ConnectionClosed
|
||||
{
|
||||
add { lock (_eventLock) _connectionClosedEventHandlers.Add(value); }
|
||||
remove { lock (_eventLock) _connectionClosedEventHandlers.Remove(value); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event when an exception happens during the handling of the data
|
||||
/// </summary>
|
||||
public event Action<Exception> Exception
|
||||
{
|
||||
add => _subscription.Exception += value;
|
||||
remove => _subscription.Exception -= value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The id of the socket
|
||||
/// </summary>
|
||||
public int SocketId => _connection.SocketId;
|
||||
|
||||
/// <summary>
|
||||
/// The id of the subscription
|
||||
/// </summary>
|
||||
public int Id => _subscription.Id;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="connection">The socket connection the subscription is on</param>
|
||||
/// <param name="subscription">The subscription</param>
|
||||
public HighPerfUpdateSubscription(HighPerfSocketConnection connection, HighPerfSubscription subscription)
|
||||
{
|
||||
_connection = connection;
|
||||
_connection.ConnectionClosed += HandleConnectionClosedEvent;
|
||||
|
||||
_subscription = subscription;
|
||||
}
|
||||
|
||||
private void UnsubscribeConnectionEvents()
|
||||
{
|
||||
lock (_eventLock)
|
||||
{
|
||||
if (!_connectionEventsSubscribed)
|
||||
return;
|
||||
|
||||
_connection.ConnectionClosed -= HandleConnectionClosedEvent;
|
||||
_connectionEventsSubscribed = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleConnectionClosedEvent()
|
||||
{
|
||||
UnsubscribeConnectionEvents();
|
||||
|
||||
List<Action> handlers;
|
||||
lock (_eventLock)
|
||||
handlers = _connectionClosedEventHandlers.ToList();
|
||||
|
||||
foreach(var callback in handlers)
|
||||
callback();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Close the subscription
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task CloseAsync()
|
||||
{
|
||||
return _connection.CloseAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
using CryptoExchange.Net.Sockets;
|
||||
using CryptoExchange.Net.Sockets.Default;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.Objects.Sockets
|
||||
@@ -14,7 +15,12 @@ namespace CryptoExchange.Net.Objects.Sockets
|
||||
private readonly SocketConnection _connection;
|
||||
internal readonly Subscription _subscription;
|
||||
|
||||
private object _eventLock = new object();
|
||||
#if NET9_0_OR_GREATER
|
||||
private readonly Lock _eventLock = new Lock();
|
||||
#else
|
||||
private readonly object _eventLock = new object();
|
||||
#endif
|
||||
|
||||
private bool _connectionEventsSubscribed = true;
|
||||
private List<Action> _connectionClosedEventHandlers = new List<Action>();
|
||||
private List<Action> _connectionLostEventHandlers = new List<Action>();
|
||||
|
||||
@@ -73,6 +73,11 @@ namespace CryptoExchange.Net.Objects.Sockets
|
||||
/// The buffer size to use for receiving data
|
||||
/// </summary>
|
||||
public int? ReceiveBufferSize { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not to use the updated deserialization logic
|
||||
/// </summary>
|
||||
public bool UseUpdatedDeserialization { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
|
||||
Reference in New Issue
Block a user