1
0
mirror of https://github.com/JKorf/CryptoExchange.Net synced 2025-09-03 06:01:40 +00:00

Compare commits

..

7 Commits

Author SHA1 Message Date
JKorf
73764970b0 . 2025-08-25 19:11:50 +02:00
JKorf
9ba29035b2 Merge branch 'master' into feature/code-analyzis 2025-08-25 17:37:46 +02:00
Jkorf
b215cccda4 Updated to version 9.6.0 2025-08-25 10:17:35 +02:00
Jkorf
3eda488361 Updated CryptoExchange.Net.Protobuf to CryptoExchange.Net version 9.5.0 2025-08-25 10:15:14 +02:00
Jkorf
993a44de35 Updated to version 9.6.0 2025-08-25 10:03:17 +02:00
Jkorf
99465f99a1 Fixed test 2025-08-25 10:00:42 +02:00
Jkorf
d42de1fe90 Added support for parsing REST response even though status indicates error 2025-08-25 09:58:03 +02:00
23 changed files with 172 additions and 185 deletions

View File

@ -6,9 +6,9 @@
<PackageId>CryptoExchange.Net.Protobuf</PackageId> <PackageId>CryptoExchange.Net.Protobuf</PackageId>
<Authors>JKorf</Authors> <Authors>JKorf</Authors>
<Description>Protobuf support for CryptoExchange.Net</Description> <Description>Protobuf support for CryptoExchange.Net</Description>
<PackageVersion>9.5.0</PackageVersion> <PackageVersion>9.6.0</PackageVersion>
<AssemblyVersion>9.5.0</AssemblyVersion> <AssemblyVersion>9.6.0</AssemblyVersion>
<FileVersion>9.5.0</FileVersion> <FileVersion>9.6.0</FileVersion>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance> <PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<PackageTags>CryptoExchange;CryptoExchange.Net</PackageTags> <PackageTags>CryptoExchange;CryptoExchange.Net</PackageTags>
<RepositoryType>git</RepositoryType> <RepositoryType>git</RepositoryType>
@ -41,7 +41,7 @@
<DocumentationFile>CryptoExchange.Net.Protobuf.xml</DocumentationFile> <DocumentationFile>CryptoExchange.Net.Protobuf.xml</DocumentationFile>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="CryptoExchange.Net" Version="9.5.0" /> <PackageReference Include="CryptoExchange.Net" Version="9.6.0" />
<PackageReference Include="protobuf-net" Version="3.2.56" /> <PackageReference Include="protobuf-net" Version="3.2.56" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -5,6 +5,9 @@
Protobuf support for CryptoExchange.Net. Protobuf support for CryptoExchange.Net.
## Release notes ## Release notes
* Version 9.6.0 - 25 Aug 2025
* Updated CryptoExchange.Net version to 9.6.0
* Version 9.5.0 - 19 Aug 2025 * Version 9.5.0 - 19 Aug 2025
* Updated CryptoExchange.Net version to 9.5.0 * Updated CryptoExchange.Net version to 9.5.0

View File

@ -28,7 +28,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
return new CallResult(null); return new CallResult(null);
} }
public override Query GetSubQuery(SocketConnection connection) => new TestQuery("sub", new object(), false, 1); protected override Query GetSubQuery(SocketConnection connection) => new TestQuery("sub", new object(), false, 1);
public override Query GetUnsubQuery() => new TestQuery("unsub", new object(), false, 1); protected override Query GetUnsubQuery(SocketConnection connection) => new TestQuery("unsub", new object(), false, 1);
} }
} }

View File

@ -28,7 +28,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
return new CallResult(null); return new CallResult(null);
} }
public override Query GetSubQuery(SocketConnection connection) => new TestChannelQuery(_channel, "subscribe", false, 1); protected override Query GetSubQuery(SocketConnection connection) => new TestChannelQuery(_channel, "subscribe", false, 1);
public override Query GetUnsubQuery() => new TestChannelQuery(_channel, "unsubscribe", false, 1); protected override Query GetUnsubQuery(SocketConnection connection) => new TestChannelQuery(_channel, "unsubscribe", false, 1);
} }
} }

View File

@ -1,132 +0,0 @@
//using System;
//using System.IO;
//using System.Net.WebSockets;
//using System.Security.Authentication;
//using System.Text;
//using System.Threading.Tasks;
//using CryptoExchange.Net.Interfaces;
//using CryptoExchange.Net.Objects;
//namespace CryptoExchange.Net.UnitTests.TestImplementations
//{
// public class TestSocket: IWebsocket
// {
// public bool CanConnect { get; set; }
// public bool Connected { get; set; }
// public event Func<Task> OnClose;
//#pragma warning disable 0067
// public event Func<Task> OnReconnected;
// public event Func<Task> OnReconnecting;
// public event Func<int, Task> OnRequestRateLimited;
//#pragma warning restore 0067
// public event Func<int, Task> OnRequestSent;
// public event Func<WebSocketMessageType, ReadOnlyMemory<byte>, Task> OnStreamMessage;
// public event Func<Exception, Task> OnError;
// public event Func<Task> OnOpen;
// public Func<Task<Uri>> GetReconnectionUrl { get; set; }
// public int Id { get; }
// public bool ShouldReconnect { get; set; }
// public TimeSpan Timeout { get; set; }
// public Func<string, string> DataInterpreterString { get; set; }
// public Func<byte[], string> DataInterpreterBytes { get; set; }
// public DateTime? DisconnectTime { get; set; }
// public string Url { get; }
// public bool IsClosed => !Connected;
// public bool IsOpen => Connected;
// public bool PingConnection { get; set; }
// public TimeSpan PingInterval { get; set; }
// public SslProtocols SSLProtocols { get; set; }
// public Encoding Encoding { get; set; }
// public int ConnectCalls { get; private set; }
// public bool Reconnecting { get; set; }
// public string Origin { get; set; }
// public int? RatelimitPerSecond { get; set; }
// public double IncomingKbps => throw new NotImplementedException();
// public Uri Uri => new Uri("");
// public TimeSpan KeepAliveInterval { get; set; }
// public static int lastId = 0;
// public static object lastIdLock = new object();
// public TestSocket()
// {
// lock (lastIdLock)
// {
// Id = lastId + 1;
// lastId++;
// }
// }
// public Task<CallResult> ConnectAsync()
// {
// Connected = CanConnect;
// ConnectCalls++;
// if (CanConnect)
// InvokeOpen();
// return Task.FromResult(CanConnect ? new CallResult(null) : new CallResult(new CantConnectError()));
// }
// public bool Send(int requestId, string data, int weight)
// {
// if(!Connected)
// throw new Exception("Socket not connected");
// OnRequestSent?.Invoke(requestId);
// return true;
// }
// public void Reset()
// {
// }
// public Task CloseAsync()
// {
// Connected = false;
// DisconnectTime = DateTime.UtcNow;
// OnClose?.Invoke();
// return Task.FromResult(0);
// }
// public void SetProxy(string host, int port)
// {
// throw new NotImplementedException();
// }
// public void Dispose()
// {
// }
// public void InvokeClose()
// {
// Connected = false;
// DisconnectTime = DateTime.UtcNow;
// Reconnecting = true;
// OnClose?.Invoke();
// }
// public void InvokeOpen()
// {
// OnOpen?.Invoke();
// }
// public void InvokeMessage(string data)
// {
// OnStreamMessage?.Invoke(WebSocketMessageType.Text, new ReadOnlyMemory<byte>(Encoding.UTF8.GetBytes(data))).Wait();
// }
// public void SetProxy(ApiProxy proxy)
// {
// throw new NotImplementedException();
// }
// public void InvokeError(Exception error)
// {
// OnError?.Invoke(error);
// }
// public Task ReconnectAsync() => Task.CompletedTask;
// }
//}

View File

@ -7,6 +7,7 @@ indent_style = space
indent_size = 4 indent_size = 4
trim_trailing_whitespace = true trim_trailing_whitespace = true
charset = utf-8 charset = utf-8
max_line_length = 140
insert_final_newline = true insert_final_newline = true
# ReSharper code style properties # ReSharper code style properties

View File

@ -465,7 +465,11 @@ public abstract class AuthenticationProvider
public abstract class AuthenticationProvider<TApiCredentials> : AuthenticationProvider where TApiCredentials : ApiCredentials public abstract class AuthenticationProvider<TApiCredentials> : AuthenticationProvider where TApiCredentials : ApiCredentials
{ {
/// <inheritdoc /> /// <inheritdoc />
#pragma warning disable IDE1006 // Naming Styles
#pragma warning disable CA1707 // Naming Styles
protected new TApiCredentials _credentials => (TApiCredentials)base._credentials; protected new TApiCredentials _credentials => (TApiCredentials)base._credentials;
#pragma warning restore IDE1006 // Naming Styles
#pragma warning restore CA1707 // Naming Styles
/// <summary> /// <summary>
/// ctor /// ctor

View File

@ -124,7 +124,16 @@ public abstract class BaseApiClient : IDisposable, IBaseApiClient
/// <summary> /// <summary>
/// Dispose /// Dispose
/// </summary> /// </summary>
public virtual void Dispose() public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Dispose
/// </summary>
public virtual void Dispose(bool disposing)
{ {
_disposing = true; _disposing = true;
} }

View File

@ -119,10 +119,24 @@ public abstract class BaseClient : IDisposable
/// <summary> /// <summary>
/// Dispose /// Dispose
/// </summary> /// </summary>
public virtual void Dispose() public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Dispose
/// </summary>
public virtual void Dispose(bool disposing)
{
if (disposing)
{ {
_logger.Log(LogLevel.Debug, "Disposing client"); _logger.Log(LogLevel.Debug, "Disposing client");
foreach (var client in ApiClients) foreach (var client in ApiClients)
client.Dispose(); client.Dispose();
} }
} }
}

View File

@ -59,8 +59,20 @@ public class CryptoBaseClient : IDisposable
/// <summary> /// <summary>
/// Dispose /// Dispose
/// </summary> /// </summary>
public void Dispose() public void Dispose(bool disposing)
{
if (disposing)
{ {
_serviceCache.Clear(); _serviceCache.Clear();
} }
} }
/// <summary>
/// Dispose
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}

View File

@ -239,7 +239,7 @@ public abstract class RestApiClient : BaseApiClient, IRestApiClient
additionalHeaders); additionalHeaders);
_logger.RestApiSendRequest(request.RequestId, definition, request.Content, string.IsNullOrEmpty(request.Uri.Query) ? "-" : request.Uri.Query, string.Join(", ", request.GetHeaders().Select(h => h.Key + $"=[{string.Join(",", h.Value)}]"))); _logger.RestApiSendRequest(request.RequestId, definition, request.Content, string.IsNullOrEmpty(request.Uri.Query) ? "-" : 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>(definition, request, definition.RateLimitGate, cancellationToken).ConfigureAwait(false);
if (result.Error is not CancellationRequestedError) if (result.Error is not CancellationRequestedError)
{ {
var originalData = OutputOriginalData ? result.OriginalData : "[Data only available when OutputOriginal = true]"; var originalData = OutputOriginalData ? result.OriginalData : "[Data only available when OutputOriginal = true]";
@ -424,11 +424,13 @@ public abstract class RestApiClient : BaseApiClient, IRestApiClient
/// <summary> /// <summary>
/// Executes the request and returns the result deserialized into the type parameter class /// Executes the request and returns the result deserialized into the type parameter class
/// </summary> /// </summary>
/// <param name="requestDefinition">The request definition</param>
/// <param name="request">The request object to execute</param> /// <param name="request">The request object to execute</param>
/// <param name="gate">The ratelimit gate used</param> /// <param name="gate">The ratelimit gate used</param>
/// <param name="cancellationToken">Cancellation token</param> /// <param name="cancellationToken">Cancellation token</param>
/// <returns></returns> /// <returns></returns>
protected virtual async Task<WebCallResult<T>> GetResponseAsync<T>( protected virtual async Task<WebCallResult<T>> GetResponseAsync<T>(
RequestDefinition requestDefinition,
IRequest request, IRequest request,
IRateLimitGate? gate, IRateLimitGate? gate,
CancellationToken cancellationToken) CancellationToken cancellationToken)
@ -448,7 +450,7 @@ public abstract class RestApiClient : BaseApiClient, IRestApiClient
var outputOriginalData = ApiOptions.OutputOriginalData ?? ClientOptions.OutputOriginalData; var outputOriginalData = ApiOptions.OutputOriginalData ?? ClientOptions.OutputOriginalData;
accessor = CreateAccessor(); accessor = CreateAccessor();
if (!response.IsSuccessStatusCode) if (!response.IsSuccessStatusCode && !requestDefinition.TryParseOnNonSuccess)
{ {
// Error response // Error response
var readResult = await accessor.Read(responseStream, true).ConfigureAwait(false); var readResult = await accessor.Read(responseStream, true).ConfigureAwait(false);
@ -488,7 +490,7 @@ public abstract class RestApiClient : BaseApiClient, IRestApiClient
} }
// Json response received // Json response received
var parsedError = TryParseError(response.ResponseHeaders, accessor); var parsedError = TryParseError(requestDefinition, response.ResponseHeaders, accessor);
if (parsedError != null) if (parsedError != null)
{ {
if (parsedError is ServerRateLimitError rateError) if (parsedError is ServerRateLimitError rateError)
@ -541,10 +543,11 @@ public abstract class RestApiClient : BaseApiClient, IRestApiClient
/// This method will be called for each response to be able to check if the response is an error or not. /// This method will be called for each response to be able to check if the response is an error or not.
/// If the response is an error this method should return the parsed error, else it should return null /// If the response is an error this method should return the parsed error, else it should return null
/// </summary> /// </summary>
/// <param name="requestDefinition">Request definition</param>
/// <param name="accessor">Data accessor</param> /// <param name="accessor">Data accessor</param>
/// <param name="responseHeaders">The response headers</param> /// <param name="responseHeaders">The response headers</param>
/// <returns>Null if not an error, Error otherwise</returns> /// <returns>Null if not an error, Error otherwise</returns>
protected virtual Error? TryParseError(KeyValuePair<string, string[]>[] responseHeaders, IMessageAccessor accessor) => null; protected virtual Error? TryParseError(RequestDefinition requestDefinition, KeyValuePair<string, string[]>[] responseHeaders, IMessageAccessor accessor) => null;
/// <summary> /// <summary>
/// Can be used to indicate that a request should be retried. Defaults to false. Make sure to retry a max number of times (based on the the tries parameter) or the request will retry forever. /// Can be used to indicate that a request should be retried. Defaults to false. Make sure to retry a max number of times (based on the the tries parameter) or the request will retry forever.
@ -634,7 +637,7 @@ public abstract class RestApiClient : BaseApiClient, IRestApiClient
{ {
// Handle retry after header // Handle retry after header
var retryAfterHeader = responseHeaders.SingleOrDefault(r => r.Key.Equals("Retry-After", StringComparison.InvariantCultureIgnoreCase)); var retryAfterHeader = responseHeaders.SingleOrDefault(r => r.Key.Equals("Retry-After", StringComparison.InvariantCultureIgnoreCase));
if (retryAfterHeader.Value?.Any() != true) if (!(retryAfterHeader.Value.Length > 0))
return new ServerRateLimitError(); return new ServerRateLimitError();
var value = retryAfterHeader.Value.First(); var value = retryAfterHeader.Value.First();

View File

@ -839,8 +839,11 @@ public abstract class SocketApiClient : BaseApiClient, ISocketApiClient
/// <summary> /// <summary>
/// Dispose the client /// Dispose the client
/// </summary> /// </summary>
public override void Dispose() public override void Dispose(bool disposing)
{ {
if (disposing)
return;
_disposing = true; _disposing = true;
var tasks = new List<Task>(); var tasks = new List<Task>();
{ {
@ -855,7 +858,7 @@ public abstract class SocketApiClient : BaseApiClient, ISocketApiClient
} }
semaphoreSlim?.Dispose(); semaphoreSlim?.Dispose();
base.Dispose(); base.Dispose(disposing);
} }
/// <summary> /// <summary>

View File

@ -6,9 +6,9 @@
<PackageId>CryptoExchange.Net</PackageId> <PackageId>CryptoExchange.Net</PackageId>
<Authors>JKorf</Authors> <Authors>JKorf</Authors>
<Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description> <Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description>
<PackageVersion>9.5.0</PackageVersion> <PackageVersion>9.6.0</PackageVersion>
<AssemblyVersion>9.5.0</AssemblyVersion> <AssemblyVersion>9.6.0</AssemblyVersion>
<FileVersion>9.5.0</FileVersion> <FileVersion>9.6.0</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;CryptoExchange.Net</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;CryptoExchange.Net</PackageTags>
<RepositoryType>git</RepositoryType> <RepositoryType>git</RepositoryType>

View File

@ -117,7 +117,19 @@ public class AsyncResetEvent : IDisposable
/// Dispose /// Dispose
/// </summary> /// </summary>
public void Dispose() public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Dispose
/// </summary>
public void Dispose(bool disposing)
{
if (disposing)
{ {
_waits.Clear(); _waits.Clear();
} }
} }
}

View File

@ -62,6 +62,11 @@ public class RequestDefinition
/// </summary> /// </summary>
public bool PreventCaching { get; set; } public bool PreventCaching { get; set; }
/// <summary>
/// Whether the response to this requests should attempted to be parsed even when the status indicates failure
/// </summary>
public bool TryParseOnNonSuccess { get; set; }
/// <summary> /// <summary>
/// Connection id /// Connection id
/// </summary> /// </summary>

View File

@ -46,6 +46,7 @@ public class RequestDefinitionCache
/// <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> /// <param name="preventCaching">Prevent request caching</param>
/// <param name="tryParseOnNonSuccess">Try parse the response even when status is not success</param>
/// <returns></returns> /// <returns></returns>
public RequestDefinition GetOrCreate( public RequestDefinition GetOrCreate(
HttpMethod method, HttpMethod method,
@ -57,8 +58,9 @@ public class RequestDefinitionCache
RequestBodyFormat? requestBodyFormat = null, RequestBodyFormat? requestBodyFormat = null,
HttpMethodParameterPosition? parameterPosition = null, HttpMethodParameterPosition? parameterPosition = null,
ArrayParametersSerialization? arraySerialization = null, ArrayParametersSerialization? arraySerialization = null,
bool? preventCaching = null) bool? preventCaching = null,
=> GetOrCreate(method + path, method, path, rateLimitGate, weight, authenticated, limitGuard, requestBodyFormat, parameterPosition, arraySerialization, preventCaching); bool? tryParseOnNonSuccess = null)
=> GetOrCreate(method + path, method, path, rateLimitGate, weight, authenticated, limitGuard, requestBodyFormat, parameterPosition, arraySerialization, preventCaching, tryParseOnNonSuccess);
/// <summary> /// <summary>
/// Get a definition if it is already in the cache or create a new definition and add it to the cache /// Get a definition if it is already in the cache or create a new definition and add it to the cache
@ -74,6 +76,7 @@ public class RequestDefinitionCache
/// <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> /// <param name="preventCaching">Prevent request caching</param>
/// <param name="tryParseOnNonSuccess">Try parse the response even when status is not success</param>
/// <returns></returns> /// <returns></returns>
public RequestDefinition GetOrCreate( public RequestDefinition GetOrCreate(
string identifier, string identifier,
@ -86,7 +89,8 @@ public class RequestDefinitionCache
RequestBodyFormat? requestBodyFormat = null, RequestBodyFormat? requestBodyFormat = null,
HttpMethodParameterPosition? parameterPosition = null, HttpMethodParameterPosition? parameterPosition = null,
ArrayParametersSerialization? arraySerialization = null, ArrayParametersSerialization? arraySerialization = null,
bool? preventCaching = null) bool? preventCaching = null,
bool? tryParseOnNonSuccess = null)
{ {
if (!_definitions.TryGetValue(identifier, out var def)) if (!_definitions.TryGetValue(identifier, out var def))
@ -100,7 +104,8 @@ public class RequestDefinitionCache
ArraySerialization = arraySerialization, ArraySerialization = arraySerialization,
RequestBodyFormat = requestBodyFormat, RequestBodyFormat = requestBodyFormat,
ParameterPosition = parameterPosition, ParameterPosition = parameterPosition,
PreventCaching = preventCaching ?? false PreventCaching = preventCaching ?? false,
TryParseOnNonSuccess = tryParseOnNonSuccess ?? false
}; };
_definitions.TryAdd(identifier, def); _definitions.TryAdd(identifier, def);
} }

View File

@ -22,8 +22,22 @@ public class TraceLoggerProvider : ILoggerProvider
/// <inheritdoc /> /// <inheritdoc />
public ILogger CreateLogger(string categoryName) => new TraceLogger(categoryName, _logLevel); public ILogger CreateLogger(string categoryName) => new TraceLogger(categoryName, _logLevel);
/// <inheritdoc />
public void Dispose() { } /// <summary>
/// Dispose
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Dispose
/// </summary>
public static void Dispose(bool disposing)
{
}
} }
/// <summary> /// <summary>

View File

@ -577,6 +577,8 @@ public abstract class SymbolOrderBook : ISymbolOrderBook, IDisposable
/// </summary> /// </summary>
/// <param name="disposing"></param> /// <param name="disposing"></param>
protected virtual void Dispose(bool disposing) protected virtual void Dispose(bool disposing)
{
if (disposing)
{ {
Status = OrderBookStatus.Disposing; Status = OrderBookStatus.Disposing;
@ -594,6 +596,7 @@ public abstract class SymbolOrderBook : ISymbolOrderBook, IDisposable
Status = OrderBookStatus.Disposed; Status = OrderBookStatus.Disposed;
} }
}
/// <summary> /// <summary>
/// String representation of the top 3 entries /// String representation of the top 3 entries

View File

@ -488,9 +488,18 @@ public class CryptoExchangeWebSocketClient : IWebsocket
} }
/// <summary> /// <summary>
/// Dispose the socket /// Dispose
/// </summary> /// </summary>
public void Dispose() public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Dispose the socket
/// </summary>
public void Dispose(bool disposing)
{ {
if (_disposed) if (_disposed)
return; return;

View File

@ -169,7 +169,6 @@ public abstract class Query : IMessageProcessor, IDisposable
{ {
if (disposing) if (disposing)
{ {
// TODO: dispose managed state (managed objects)
_cts?.Dispose(); _cts?.Dispose();
_event.Dispose(); _event.Dispose();
} }

View File

@ -709,15 +709,30 @@ public class SocketConnection : IDisposable
} }
/// <summary> /// <summary>
/// Dispose the connection /// Dispose
/// </summary> /// </summary>
public void Dispose() protected virtual void Dispose(bool disposing)
{
if (Status != SocketStatus.Disposed)
{
if (disposing)
{ {
Status = SocketStatus.Disposed; Status = SocketStatus.Disposed;
periodicEvent?.Set(); periodicEvent?.Set();
periodicEvent?.Dispose(); periodicEvent?.Dispose();
_socket.Dispose(); _socket.Dispose();
} }
}
}
/// <summary>
/// Dispose
/// </summary>
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
/// <summary> /// <summary>
/// Whether or not a new subscription can be added to this connection /// Whether or not a new subscription can be added to this connection

View File

@ -23,6 +23,8 @@ public class RestRequestValidator<TClient> where TClient : BaseRestClient
private readonly string _baseAddress; private readonly string _baseAddress;
private readonly string? _nestedPropertyForCompare; private readonly string? _nestedPropertyForCompare;
private static readonly char[] _paramSeparator = new char[] { '?' };
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
@ -117,8 +119,8 @@ public class RestRequestValidator<TClient> where TClient : BaseRestClient
throw new Exception(name + $" authentication not matched. Expected: {expectedAuth}, Actual: {_isAuthenticated(result.AsDataless())}"); throw new Exception(name + $" authentication not matched. Expected: {expectedAuth}, Actual: {_isAuthenticated(result.AsDataless())}");
if (result.RequestMethod != new HttpMethod(expectedMethod!)) if (result.RequestMethod != new HttpMethod(expectedMethod!))
throw new Exception(name + $" http method not matched. Expected {expectedMethod}, Actual: {result.RequestMethod}"); throw new Exception(name + $" http method not matched. Expected {expectedMethod}, Actual: {result.RequestMethod}");
if (expectedPath != result.RequestUrl!.Replace(_baseAddress, "").Split(new char[] { '?' })[0]) if (expectedPath != result.RequestUrl!.Replace(_baseAddress, "").Split(_paramSeparator)[0])
throw new Exception(name + $" path not matched. Expected: {expectedPath}, Actual: {result.RequestUrl!.Replace(_baseAddress, "").Split(new char[] { '?' })[0]}"); throw new Exception(name + $" path not matched. Expected: {expectedPath}, Actual: {result.RequestUrl!.Replace(_baseAddress, "").Split(_paramSeparator)[0]}");
if (!skipResponseValidation) if (!skipResponseValidation)
{ {
@ -176,8 +178,8 @@ public class RestRequestValidator<TClient> where TClient : BaseRestClient
throw new Exception(name + $" authentication not matched. Expected: {expectedAuth}, Actual: {_isAuthenticated(result)}"); throw new Exception(name + $" authentication not matched. Expected: {expectedAuth}, Actual: {_isAuthenticated(result)}");
if (result.RequestMethod != new HttpMethod(expectedMethod!)) if (result.RequestMethod != new HttpMethod(expectedMethod!))
throw new Exception(name + $" http method not matched. Expected {expectedMethod}, Actual: {result.RequestMethod}"); throw new Exception(name + $" http method not matched. Expected {expectedMethod}, Actual: {result.RequestMethod}");
if (expectedPath != result.RequestUrl!.Replace(_baseAddress, "").Split(new char[] { '?' })[0]) if (expectedPath != result.RequestUrl!.Replace(_baseAddress, "").Split(_paramSeparator)[0])
throw new Exception(name + $" path not matched. Expected: {expectedPath}, Actual: {result.RequestUrl!.Replace(_baseAddress, "").Split(new char[] { '?' })[0]}"); throw new Exception(name + $" path not matched. Expected: {expectedPath}, Actual: {result.RequestUrl!.Replace(_baseAddress, "").Split(_paramSeparator)[0]}");
Trace.Listeners.Remove(listener); Trace.Listeners.Remove(listener);
} }

View File

@ -59,6 +59,12 @@ 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 9.6.0 - 25 Aug 2025
* Added support for parsing REST response even though status indicates error
* Added better support for subscriptions without subscribe confirmation
* Added check in websocket for receiving 401 unauthorized http response status when 101 was expected
* Removed obsolete attribute on Error.Code property, updated the description
* Version 9.5.0 - 19 Aug 2025 * Version 9.5.0 - 19 Aug 2025
* Added better error handling support * Added better error handling support
* Added ErrorDescription, ErrorType and IsTransient to Error object * Added ErrorDescription, ErrorType and IsTransient to Error object