1
0
mirror of https://github.com/JKorf/CryptoExchange.Net.git synced 2026-10-16 16:10:53 +00:00

Merge branch 'master' into feature/shared-apis-v2

This commit is contained in:
Jkorf
2026-09-01 09:58:01 +02:00
7 changed files with 166 additions and 6 deletions
@@ -0,0 +1,130 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Sockets;
using CryptoExchange.Net.Sockets.Default;
using CryptoExchange.Net.Sockets.Default.Routing;
using CryptoExchange.Net.UnitTests.Implementations;
using Microsoft.Extensions.Logging.Abstractions;
using NUnit.Framework;
using System;
namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
{
[TestFixture]
public class SubscriptionTests
{
[Test]
public void Handle_Should_OnlyCompleteSubscriptionQuery_ForMatchingTopic()
{
// arrange
var topicASubscription = new TopicSubscription("topic-a");
var topicBSubscription = new TopicSubscription("topic-b");
var topicAQuery = topicASubscription.CreateSubscriptionQuery(null!)!;
var topicBQuery = topicBSubscription.CreateSubscriptionQuery(null!)!;
// act
var topicAHandled = topicASubscription.Handle("type", "topic-a", null!, DateTime.UtcNow, "original", "data");
var topicBHandled = topicBSubscription.Handle("type", "topic-a", null!, DateTime.UtcNow, "original", "data");
// assert
Assert.That(topicAHandled, Is.True);
Assert.That(topicBHandled, Is.False);
Assert.That(topicAQuery.Completed, Is.True);
Assert.That(topicAQuery.Success, Is.True);
Assert.That(topicBQuery.Completed, Is.False);
}
[Test]
public void Handle_Should_CompleteSubscriptionQuery_BeforeInvokingMatchingHandler()
{
// arrange
Query? query = null;
var queryCompletedWhenHandlerInvoked = false;
var subscription = new TopicSubscription("topic", () => queryCompletedWhenHandlerInvoked = query!.Completed);
query = subscription.CreateSubscriptionQuery(null!)!;
// act
subscription.Handle("type", "topic", null!, DateTime.UtcNow, "original", "data");
// assert
Assert.That(queryCompletedWhenHandlerInvoked, Is.True);
}
[TestCase(null)]
[TestCase("topic")]
public void Handle_Should_CompleteSubscriptionQuery_ForUnfilteredRoute(string? topicFilter)
{
// arrange
var subscription = new TopicSubscription(topic: null);
var query = subscription.CreateSubscriptionQuery(null!)!;
// act
var handled = subscription.Handle("type", topicFilter, null!, DateTime.UtcNow, "original", "data");
// assert
Assert.That(handled, Is.True);
Assert.That(query.Completed, Is.True);
Assert.That(query.Success, Is.True);
}
[Test]
public void Handle_Should_TreatEmptyTopicFilterAsUnfilteredRoute()
{
// arrange
var subscription = new TopicSubscription(string.Empty);
var query = subscription.CreateSubscriptionQuery(null!)!;
// act
var handled = subscription.Handle("type", "topic", null!, DateTime.UtcNow, "original", "data");
// assert
Assert.That(handled, Is.True);
Assert.That(query.Completed, Is.True);
Assert.That(query.Success, Is.True);
}
[Test]
public void Handle_Should_CompleteSubscriptionQuery_ForAnyMatchingTopic()
{
// arrange
var subscription = new TopicSubscription(["topic-a", "topic-b"]);
var query = subscription.CreateSubscriptionQuery(null!)!;
// act
var handled = subscription.Handle("type", "topic-b", null!, DateTime.UtcNow, "original", "data");
// assert
Assert.That(handled, Is.True);
Assert.That(query.Completed, Is.True);
Assert.That(query.Success, Is.True);
}
private sealed class TopicSubscription : Subscription
{
public TopicSubscription(string? topic, Action? handler = null)
: base(NullLogger.Instance, false)
{
MessageRouter = MessageRouter.CreateForEvent<string>("type", topic, (_, _, _, _) =>
{
handler?.Invoke();
return CallResult.Ok();
});
}
public TopicSubscription(string[] topics)
: base(NullLogger.Instance, false)
{
MessageRouter = MessageRouter.CreateForEvent<string>("type", topics, (_, _, _, _) => CallResult.Ok());
}
protected override Query? GetSubQuery(SocketConnection connection)
{
return new TestQuery(new TestSocketMessage { Id = 1, Data = "Sub" }, false)
{
TimeoutBehavior = TimeoutBehavior.Succeed
};
}
protected override Query? GetUnsubQuery(SocketConnection connection) => null;
}
}
}
+3 -3
View File
@@ -6,9 +6,9 @@
<PackageId>CryptoExchange.Net</PackageId> <PackageId>CryptoExchange.Net</PackageId>
<Authors>JKorf</Authors> <Authors>JKorf</Authors>
<Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description> <Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description>
<PackageVersion>12.5.0</PackageVersion> <PackageVersion>12.5.1</PackageVersion>
<AssemblyVersion>12.5.0</AssemblyVersion> <AssemblyVersion>12.5.1</AssemblyVersion>
<FileVersion>12.5.0</FileVersion> <FileVersion>12.5.1</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>
@@ -25,6 +25,9 @@ namespace CryptoExchange.Net.Sockets.Default.Routing
Routes = routes; Routes = routes;
} }
private RouteCollection? GetRoutes(string typeIdentifier)
=> (_routingTable ?? throw new NullReferenceException("Routing table not build before handling")).GetRoutes(typeIdentifier);
/// <summary> /// <summary>
/// Build the route mapping /// Build the route mapping
/// </summary> /// </summary>
@@ -46,13 +49,21 @@ namespace CryptoExchange.Net.Sockets.Default.Routing
/// </summary> /// </summary>
public bool Handle(string typeIdentifier, string? topicFilter, SocketConnection connection, DateTime receiveTime, string? originalData, object data, out CallResult? result) public bool Handle(string typeIdentifier, string? topicFilter, SocketConnection connection, DateTime receiveTime, string? originalData, object data, out CallResult? result)
{ {
var routeCollection = (_routingTable ?? throw new NullReferenceException("Routing table not build before handling")).GetRoutes(typeIdentifier); var routeCollection = GetRoutes(typeIdentifier);
if (routeCollection == null) if (routeCollection == null)
throw new InvalidOperationException($"No routes for {typeIdentifier} message type"); throw new InvalidOperationException($"No routes for {typeIdentifier} message type");
return routeCollection.Handle(topicFilter, connection, receiveTime, originalData, data, out result); return routeCollection.Handle(topicFilter, connection, receiveTime, originalData, data, out result);
} }
/// <summary>
/// Whether the router has a matching route for the specified message type and topic
/// </summary>
public bool CanHandle(string typeIdentifier, string? topicFilter)
{
return GetRoutes(typeIdentifier)?.CanHandle(topicFilter) == true;
}
/// <summary> /// <summary>
/// Create a void handler /// Create a void handler
/// </summary> /// </summary>
@@ -60,6 +60,15 @@ namespace CryptoExchange.Net.Sockets.Default.Routing
return matchingTopicRoutes; return matchingTopicRoutes;
} }
public bool CanHandle(string? topicFilter)
{
if (_routesWithoutTopicFilter.Count > 0)
return true;
return topicFilter != null
&& GetRoutesWithMatchingTopicFilter(topicFilter) != null;
}
public abstract bool Handle(string? topicFilter, SocketConnection connection, DateTime receiveTime, string? originalData, object data, out CallResult? result); public abstract bool Handle(string? topicFilter, SocketConnection connection, DateTime receiveTime, string? originalData, object data, out CallResult? result);
} }
} }
@@ -219,9 +219,13 @@ namespace CryptoExchange.Net.Sockets.Default
ConnectionInvocations++; ConnectionInvocations++;
TotalInvocations++; TotalInvocations++;
if (SubscriptionQuery != null && !SubscriptionQuery.Completed && SubscriptionQuery.TimeoutBehavior == TimeoutBehavior.Succeed) if (SubscriptionQuery != null
&& !SubscriptionQuery.Completed
&& SubscriptionQuery.TimeoutBehavior == TimeoutBehavior.Succeed
&& MessageRouter.CanHandle(typeIdentifier, topicFilter))
{ {
// The subscription query is one where it is successful if there is no error returned // The subscription query is one where it is successful if there is no error returned
// The connection routes by type, so only a matching subscription route proves the query was successful
// Since we've received a data update for the subscription we can assume the subscribe query was successful // Since we've received a data update for the subscription we can assume the subscribe query was successful
// Call timeout to complete // Call timeout to complete
SubscriptionQuery.Timeout(); SubscriptionQuery.Timeout();
+6
View File
@@ -128,6 +128,12 @@ Various:
* PlatformInfo now required support environment names in the constructor * PlatformInfo now required support environment names in the constructor
## Release notes ## Release notes
* Version 12.5.1 - 01 Sep 2026
* Fixed KlineTracker reporting incorrect High/Low price on GetStats result
* Fixed caching issue for auth requests
* Fixed failed ISymbolOrderBook start potentially leaving a pending background task
* Fixed subscription query completion potentially reporting success too early for topic-filtered routes
* Version 12.5.0 - 21 Aug 2026 * Version 12.5.0 - 21 Aug 2026
* Shared APIs * Shared APIs
* Added ISpotOrderManagementSocketClient for placing/canceling Spot orders via Shared websocket implementation * Added ISpotOrderManagementSocketClient for placing/canceling Spot orders via Shared websocket implementation
+1 -1
View File
@@ -2,7 +2,7 @@
> Base C#/.NET library for cryptocurrency exchange API client implementations. Provides a standardized abstraction (REST, WebSocket, authentication, rate limiting, error handling, order book management, shared cross-exchange interfaces) that 28+ exchange-specific libraries are built on top of. > Base C#/.NET library for cryptocurrency exchange API client implementations. Provides a standardized abstraction (REST, WebSocket, authentication, rate limiting, error handling, order book management, shared cross-exchange interfaces) that 28+ exchange-specific libraries are built on top of.
CryptoExchange.Net itself is not used directly — install one of the exchange-specific libraries (Binance.Net, Bybit.Net, OKX.Net, Kraken.Net, Coinbase.Net, etc.) or `CryptoClients.Net` to access all exchanges via a single bundle. The base library is what makes the entire ecosystem feel consistent: same `HttpResult<T>` REST result pattern, same `WebSocketResult<UpdateSubscription>` websocket subscription pattern, same DI registration, same shared interfaces across all exchanges. Current version: 12.5.0. Targets netstandard2.0, netstandard2.1, net8.0, net9.0, net10.0. Native AOT supported. CryptoExchange.Net itself is not used directly — install one of the exchange-specific libraries (Binance.Net, Bybit.Net, OKX.Net, Kraken.Net, Coinbase.Net, etc.) or `CryptoClients.Net` to access all exchanges via a single bundle. The base library is what makes the entire ecosystem feel consistent: same `HttpResult<T>` REST result pattern, same `WebSocketResult<UpdateSubscription>` websocket subscription pattern, same DI registration, same shared interfaces across all exchanges. Current version: 12.5.1. Targets netstandard2.0, netstandard2.1, net8.0, net9.0, net10.0. Native AOT supported.
The standout feature for cross-exchange code is `CryptoExchange.Net.SharedApis` — a set of interfaces (`ISpotTickerRestClient`, `ISpotOrderRestClient`, `IBalanceRestClient`, etc.) implemented by every exchange library. Same call signature works against any exchange. The standout feature for cross-exchange code is `CryptoExchange.Net.SharedApis` — a set of interfaces (`ISpotTickerRestClient`, `ISpotOrderRestClient`, `IBalanceRestClient`, etc.) implemented by every exchange library. Same call signature works against any exchange.