1
0
mirror of https://github.com/JKorf/CryptoExchange.Net.git synced 2026-08-11 16:32:57 +00:00

Compare commits

..

122 Commits

Author SHA1 Message Date
Jkorf 204bda8622 Updated to version 10.7.1 2026-02-25 11:00:43 +01:00
Jkorf 78e3523a4f Updated SocketConnection internal locking to fix potential deadlock 2026-02-25 09:26:12 +01:00
Jkorf 89a73747b0 Fix for test socket not working correctly with read/write socket connection lock 2026-02-24 13:15:08 +01:00
Jkorf 02b70398b3 Updated lock call SocketConnection to outside the try 2026-02-24 13:14:48 +01:00
Jkorf 73fcb47b17 Updated to version 10.7.0 2026-02-24 11:51:37 +01:00
Jkorf d41ca3459e Updated internal lock for subscription to ReaderWriterLockSlim on SocketConnection 2026-02-24 11:32:35 +01:00
Jkorf bea2b2bd7b Added Http options to Rest client options 2026-02-24 09:58:48 +01:00
Jan Korf b29cdc41f3 Shared interfaces pagination update (#274)
Updated INextPageToken parameter on Shared interfaces to PageRequest type, functionality unchanged
Added SupportsAscending and SupportsDescending properties to PaginatedEndpointOptions to expose supported data directions
Added MaxAge property to PaginatedEndpointOptions to expose the max age of data that can be requested
Added Direction property to Shared interfaces paginated requests to configure pagination data direction 
Removed PaginationSupport property from PaginatedEndpointOptions, replaced by above new properties
Updated Shared GetTradeHistoryRequest EndTime property to be optional
Updated I(Futures/Spot)OrderRestClient.GetClosed(Futures/Spot)OrdersOptions from PaginatedEndpointOptions<GetClosedOrdersRequest> to GetClosedOrdersOptions 
Updated I(Futures/Spot)OrderRestClient.Get(Futures/Spot)UserTradesOptions from PaginatedEndpointOptions<GetUserTradesRequest> to GetUserTradesOptions
Updated rate limiting PathStartFilter to ignore added or missing slash before the path
Fixed KlineTracker throwing exception if there is no data in the initial snapshot
2026-02-23 14:53:38 +01:00
JKorf 0ce2e778f4 Added check for invalid json in JsonSocketMessageHandler and virtual GetTypeIdentifierNonJson for handling non-json messages 2026-02-22 16:29:05 +01:00
JKorf 36c2411d46 Added parsing of REST response data up to 128 characters for error responses 2026-02-22 16:05:49 +01:00
Jkorf 6d3e72745a Removed check for OnlyTrackProvidedSymbols and empty initial tracking list 2026-02-17 14:54:57 +01:00
Jkorf 51c74baa26 Updated to version 10.6.2 2026-02-17 14:39:06 +01:00
Jkorf 419e01d009 Fix for websocket queries which don't expects response getting stuck in subscribing state 2026-02-17 14:35:02 +01:00
Jkorf 297eee0e1f Updated to version 10.6.1 2026-02-16 15:42:33 +01:00
Jkorf 6a9231b1e3 Fixed exception when stopping SymbolOrderBook instance when update is received while closing 2026-02-16 15:39:36 +01:00
Jkorf e40f2a15b6 Updated to version 10.6.0 2026-02-16 09:38:55 +01:00
Jkorf b94085a27a Added some code comments 2026-02-16 09:36:44 +01:00
Jkorf 5e083811df Added check SymbolOrderBook is still alive when trying to add updates to prevent unnoticed growing in the background when subscription isn't closed while book is 2026-02-16 09:33:39 +01:00
Jkorf 7dcf5cd6ea Added AddTrackedSymbolsAsync and RemoveTrackedSymbolAsync methods to UserDataTracker, updated minimal fromTime filter in UserDataTracker to 5 seconds 2026-02-16 09:11:01 +01:00
Jkorf 1471a4733f Updated symbol tracking logic on UserDataTracker, added check for startTime filter for polling being to close to current time 2026-02-13 10:12:43 +01:00
Jkorf f39d9f7cfb Updated to version 10.5.4 2026-02-12 11:43:20 +01:00
Jkorf 9fab8faa45 Fixed bug in polling time filter for UserDataTracker items 2026-02-12 11:35:04 +01:00
Jkorf 226f175343 Fixed type check ExchangeParameters GetValue 2026-02-11 14:50:39 +01:00
Jkorf 813bd9f5a1 Updated to version 10.5.3 2026-02-11 13:10:08 +01:00
Jkorf c8d2b4f09d Added check EnumConverter to detect undefined int value parsing 2026-02-11 12:57:03 +01:00
Jkorf 6560b82a3e Fixed orders getting incorrectly set to canceled state for UserDataTracker spot and futures orders 2026-02-11 11:44:32 +01:00
JKorf e151af8f37 Updated client versions examples 2026-02-10 18:38:45 +01:00
Jkorf bdf7a07c6f Updated to version 10.5.2 2026-02-10 16:04:17 +01:00
Jkorf a8ffe90bf2 Added call to ApiClient.HandleUnhandledMessage when no websocket message processor is found based on topic to allow additional processing 2026-02-10 15:59:35 +01:00
Jkorf 3372b9eb44 Set query completed after setting Result 2026-02-10 15:40:43 +01:00
Jkorf df25221960 Combined subscribe and re-subscribe logic 2026-02-10 15:40:13 +01:00
Jkorf 7c67a014f5 Added check for subscribe queries with TimeoutBehavior.Success to complete when subscription has received update 2026-02-10 14:31:42 +01:00
Jkorf 65291b3195 Updated to version 10.5.1 2026-02-10 11:53:23 +01:00
Jkorf ece6a9d27d Fixed trading mode selection for futures listen key method in FuturesUserDataTracker 2026-02-10 11:51:19 +01:00
Jkorf 1ed29b0474 Updated to version 10.5.0 2026-02-10 10:22:05 +01:00
Jkorf be2eb01353 Merge branch 'master' of https://github.com/JKorf/CryptoExchange.Net 2026-02-10 10:15:46 +01:00
JKorf 39cd66596d Added some tests 2026-02-09 21:49:07 +01:00
Jkorf 63f51811a9 Updated logging unmatched websocket message 2026-02-09 16:29:24 +01:00
Jkorf abda065237 Updated websocket message forwarding logic 2026-02-09 12:57:52 +01:00
Jkorf 759c8b9a58 Fixed bug in UserDataTracker orders logic incorrectly setting order to canceled status 2026-02-09 09:07:17 +01:00
JKorf 8e18482781 Added keep alive for listenkeys to UserDataTracker 2026-02-08 17:02:09 +01:00
Jkorf 87dd2d9d40 Updated to version 10.4.1 2026-02-06 17:19:08 +01:00
Jkorf f2b0cb2f0d Updated UserDataTracker to only track symbol when position size > 0, update log verbosity 2026-02-06 17:16:31 +01:00
Jkorf de0a954a91 Updated to version 10.4.0 2026-02-06 12:00:50 +01:00
Jkorf 4a79ce22ec Added timeout/ct task cleanup AsyncResetEvent 2026-02-06 11:57:56 +01:00
JKorf 40d480e1fc Added AutoTimestamp option for socket client, fixed socket client timestamp offset bug 2026-02-05 21:54:16 +01:00
Jan Korf 74e5cf6fc9 Feature/userdata tracker (#271)
Added user data tracking logic
Added LastReceiveTime, SocketStatus and SubscriptionStatus properties to UpdateSubscription
Added SharedTransferStatus Enum and property to SharedDeposit
Added PositionMode property to SharedPosition model
Added IsZero property to SharedQuantity
Renamed IWebSocket LastActionTime to LastReceiveTime
Updated CryptoExchangeWebsocketClient LastReceiveTime logic
Updated Subscription status change event handler to run sync instead of separate task
2026-02-05 16:05:13 +01:00
Jkorf 2fd3912795 Merge branch 'master' of https://github.com/JKorf/CryptoExchange.Net 2026-02-03 08:47:26 +01:00
JKorf ec44307a0c Updated AsyncResetEvent implementation 2026-02-02 21:01:35 +01:00
Jkorf ba55705385 Added Disposed property on BaseClient and IRestClient/ISocketClient interfaces 2026-02-02 09:33:47 +01:00
Jkorf 2c63a83117 Merge branch 'master' of https://github.com/JKorf/CryptoExchange.Net 2026-02-02 08:42:53 +01:00
JKorf 71b1e5e906 Added additional methods for requesting supported symbols to Shared ISpotSymbolRestClient/IFuturesSymbolRestClient interfaces 2026-02-01 15:02:48 +01:00
Jkorf eaeba6f27e Made Interval property of Kline tracker public 2026-01-28 10:31:44 +01:00
Jkorf 913bdaa855 Updated to version 10.3.1 2026-01-27 14:01:26 +01:00
Jkorf 5aa5790d0a Docs 2026-01-23 15:19:33 +01:00
Jkorf a8321e083e Fixed potential collection modified exception upon logging message not handled in websocket message handling 2026-01-22 09:57:02 +01:00
Jkorf ce3fa5f186 Updated to version 10.3.0 2026-01-22 09:33:47 +01:00
Jkorf fff70a9c65 Updated some testing 2026-01-21 15:38:22 +01:00
Jkorf cff33bb5ac Added handling for setting AutoTimestamp in client options when not implemented 2026-01-21 15:37:59 +01:00
Jkorf 21c8133292 Fixed HandleSubQueryResponse not getting called 2026-01-21 11:16:57 +01:00
Jkorf 76772e91ba Remove legacy websocket message handling 2026-01-21 11:14:06 +01:00
Jkorf 218e0260ce Added PlatformInfo class for specifying platform information 2026-01-21 10:14:00 +01:00
Jkorf 96b3904266 Fixed state handling for subscriptions where queries do not get a response 2026-01-20 15:19:42 +01:00
Jkorf bc8faf9822 Updated to version 10.2.5 2026-01-19 10:26:04 +01:00
JKorf 90c1b89ceb Added GetRestOffsets and GetWebsocketOffsets to TimeOffsetManager 2026-01-18 17:46:42 +01:00
JKorf 21206ffb25 Updated SymbolOrderBook.WaitUntilFirstUpdateBufferedAsync 2026-01-18 17:36:18 +01:00
JKorf 5942423bfb Updated to version 10.2.4 2026-01-17 16:29:56 +01:00
JKorf dc4abc42a7 Added WaitUntilFirstUpdateBufferedAsync method on SymbolOrderBook, fixed sequencen validation bug SymbolOrderBook 2026-01-17 16:26:25 +01:00
Jkorf c71a81e686 Added some util methods, Added CommaSplitStringConverter 2026-01-16 16:39:21 +01:00
Jkorf 550c0eabf1 Updated to version 10.2.3 2026-01-14 08:46:07 +01:00
JKorf 28a2a0c7fd Fixed semaphore exception when creating a new REST client while time sync is in progress on another client 2026-01-13 22:11:18 +01:00
JKorf 7dd1cd5bbd Added HandleUnhandledMessage virtual method to SocketApiClient to allow some processing for messages which couldn't be mapped via the normal way 2026-01-13 21:21:30 +01:00
Jkorf a7ff4416bd Updated to version 10.2.2 2026-01-13 11:18:16 +01:00
Jkorf 669d1f7c9e Allow the same websocket connection sequence number to be recorded multiple times 2026-01-13 11:16:37 +01:00
Jkorf 34ee2d3690 Updated to version 10.2.1 2026-01-13 09:29:04 +01:00
Jkorf 005fb7875d Fixed parameter URL creation for array values with ArrayParametersSerialization.MultipleValues 2026-01-13 09:25:35 +01:00
Jkorf fa9300ce97 Removed duplicate logging for rest responses in Trace verbosity 2026-01-13 09:25:07 +01:00
Jkorf fc2d3fc2d2 Updated to version 10.2.0 2026-01-12 14:30:38 +01:00
Jkorf 187ca6a4ef Fixed warning 2026-01-12 14:30:09 +01:00
Jan Korf 3b2a85d210 Feature/websocket sequencing (#267)
Added EnforceSequenceNumbers property on SocketApiClient to configure whether websocket message contain sequence numbers and if these should be checked to be sequential
Added fallback to existing websocket connection if no dedicated request connection was found
Added IntBoolConverter base class for arbitrary int value to bool mapping
Added SequenceNumber property to DataEvent object
Added _skipSequenceCheckFirstUpdateAfterSnapshotSet property for SymbolOrderBook implementations
Updated SymbolOrderBook sequenceNumber validation
Updated SymbolOrderBook log verbosities
Renamed SetInitialOrderBook to SetSnapshot in SymbolOrderBook
Renamed updateId references to sequenceNumber in SymbolOrderBook
2026-01-12 14:26:50 +01:00
Jkorf c512bee825 Updated examples 2026-01-07 14:56:18 +01:00
Jkorf 0943b052b9 Updated to version 10.1.0 2026-01-07 10:03:45 +01:00
Jan Korf a896fffdb3 Time offset management (#266)
Updated time sync / time offset management for REST API's
Added time offset tracking for WebSocket API's
Added GetAuthenticationQuery virtual method on AuthenticationProvider
Updated AuthenticationProvider GetTimestamp methods to include a one second offset by default
Added AuthenticationProvider GetTimestamp methods for SocketApiClient instances
Added ClientName property on BaseApiClient, resolving to the type name
Added ObjectOrArrayConverter JsonConverterFactory implementation for resolving json data which might be returned as object or array
Added UpdateServerTime, UpdateLocalTime and DataAge properties to (I)SymbolOrderBook
Added OutputToConsoleAsync method to (I)SymbolOrderBook
Updated SymbolOrderBook string representation
Added DataTimeLocal and DataAge properties to DataEvent object
Added SocketConnection parameter to subscription HandleSubQueryResponse and HandleUnsubQueryResponse methods
2026-01-07 10:00:14 +01:00
JKorf 177daf903b Added some utils methods 2025-12-30 09:53:30 +01:00
Jkorf aa1ebdc4ed Updated CryptoExchange.Net to version 10.0.2 2025-12-19 11:34:26 +01:00
Jkorf 38058c4a70 Updated to version 10.0.2 2025-12-19 10:14:59 +01:00
Jkorf a7eb483479 Added exception handlers for REST response processing 2025-12-19 10:09:14 +01:00
Jkorf c76931a3b4 Fixed duplicate subscription check with updated deserialization 2025-12-19 09:58:22 +01:00
Jkorf b90b7e9e0c Updated CryptoExchange.Net to 10.0.1 2025-12-18 11:13:56 +01:00
Jkorf beda53d36d Updated to version 10.0.1 2025-12-18 10:56:10 +01:00
Jkorf 0668f669c1 Fixed query parameter array serialization 2025-12-18 10:36:22 +01:00
Jkorf 64250e13db Updated examples 2025-12-17 10:55:48 +01:00
Jkorf 451d38d5e7 Updated to version 10.0.1 2025-12-16 11:54:37 +01:00
Jkorf e11e437bbb Fixed CryptoExchange.Net reference 2025-12-16 11:54:08 +01:00
Jkorf b8a1ad798d Updated to version 10.0.0 2025-12-16 11:49:45 +01:00
Jkorf 4a851c44f2 Updated to version 10.0.0 2025-12-16 11:31:42 +01:00
Jan Korf d079796020 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
2025-12-16 11:27:49 +01:00
Jonnern f125bc88b0 Pass CancellationToken to Content.ReadAsStreamAsync (#262) 2025-12-15 16:33:06 +01:00
Jkorf f3d535f286 Fixed incorrect check for TimeFilterSupport in combination with StartTime parameter for some Shared endpoints 2025-11-20 14:03:59 +01:00
Jkorf 6e8c6feec2 Added dotnet 10 target framework, updated package reference versions 2025-11-13 10:10:55 +01:00
Jkorf 84b0444caf Added implementation for async processing of (websocket) updates 2025-11-12 11:19:40 +01:00
Jkorf 4be986ebe7 Added resolver name to datetime/bool parser warnings 2025-11-11 08:57:57 +01:00
Jkorf 21872f818a Updated to version 9.13.0 2025-11-10 13:30:33 +01:00
Jkorf 4e7c45d758 Updated CryptoExchange.Net to version 9.13.0 2025-11-10 13:27:44 +01:00
Jkorf 0e55e5f065 Updated to version 9.13.0 2025-11-10 13:15:48 +01:00
Jkorf b1c5cf318a Cleanup 2025-11-10 13:12:16 +01:00
Jkorf 9d94a24862 Added IExchangeService interface 2025-11-10 09:15:29 +01:00
Jkorf 6cf9684f55 Added SharedTickerType for defining time used for ticker calculations by the API 2025-11-10 09:15:20 +01:00
Jkorf 8043a48c49 Fixed incorrect exchange name in static logger when using multiple libraries 2025-11-10 09:14:14 +01:00
Jkorf 7d657dd533 Updated examples 2025-11-03 15:29:19 +01:00
Jkorf 1bfdec1484 Added SharedSymbolModel base class to SharedFuturesKline, SharedKline, SharedTrade models 2025-11-03 11:25:10 +01:00
Jkorf 8d5b6a53f3 Updated to version 9.12.0 2025-11-03 10:39:05 +01:00
Jkorf ad97102e7c Updated CryptoExchange.Net version 2025-11-03 10:38:53 +01:00
Jkorf d8dc121386 Updated to version 9.12.0 2025-11-03 09:48:29 +01:00
Jkorf 10c3868c00 Revert "Updated to version 9.11.0"
This reverts commit 411ba00a82.
2025-11-03 09:46:55 +01:00
Jkorf 411ba00a82 Updated to version 9.11.0 2025-11-03 09:45:41 +01:00
JKorf c1b2b62dbc Added AliasType to specify only one way conversion for AssetAliases 2025-11-02 15:17:44 +01:00
JKorf 3960cab7a7 Merge branch 'master' of https://github.com/JKorf/CryptoExchange.Net 2025-11-02 11:41:30 +01:00
JKorf 34e9447e55 Added constant for selecting a USD asset for use in a Shared API/SharedSymbol 2025-11-02 11:41:26 +01:00
Jkorf 9d3295acc7 Removed some unhelpful verbose logs 2025-10-31 15:08:14 +01:00
Jkorf 995cd3d84c Updated to version 9.11.1 2025-10-30 14:33:26 +01:00
Jkorf 919cdf0075 Updated CryptoExchange.Net version 2025-10-30 14:32:41 +01:00
311 changed files with 12630 additions and 4107 deletions
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v1
with:
dotnet-version: 9.0.x
dotnet-version: 10.0.x
- name: Restore dependencies
run: dotnet restore
- name: Build
@@ -1,14 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1;net8.0;net9.0</TargetFrameworks>
<TargetFrameworks>netstandard2.0;netstandard2.1;net8.0;net9.0;net10.0</TargetFrameworks>
</PropertyGroup>
<PropertyGroup>
<PackageId>CryptoExchange.Net.Protobuf</PackageId>
<Authors>JKorf</Authors>
<Description>Protobuf support for CryptoExchange.Net</Description>
<PackageVersion>9.11.0</PackageVersion>
<AssemblyVersion>9.11.0</AssemblyVersion>
<FileVersion>9.11.0</FileVersion>
<PackageVersion>10.0.1</PackageVersion>
<AssemblyVersion>10.0.1</AssemblyVersion>
<FileVersion>10.0.1</FileVersion>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<PackageTags>CryptoExchange;CryptoExchange.Net</PackageTags>
<RepositoryType>git</RepositoryType>
@@ -41,7 +41,7 @@
<DocumentationFile>CryptoExchange.Net.Protobuf.xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CryptoExchange.Net" Version="9.10.0" />
<PackageReference Include="CryptoExchange.Net" Version="10.0.2" />
<PackageReference Include="protobuf-net" Version="3.2.56" />
</ItemGroup>
</Project>
+15
View File
@@ -5,6 +5,21 @@
Protobuf support for CryptoExchange.Net.
## Release notes
* Version 10.0.1 - 16 Dec 2025
* Updated CryptoExchange.Net version to 10.0.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
* Version 10.0.0 - 16 Dec 2025
* Updated CryptoExchange.Net version to 10.0.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
* Version 9.13.0 - 10 Nov 2025
* Updated CryptoExchange.Net version to 9.13.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
* Version 9.12.0 - 03 Nov 2025
* Updated CryptoExchange.Net version to 9.12.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
* Version 9.11.1 - 30 Oct 2025
* Updated CryptoExchange.Net version to 9.11.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
* Version 9.11.0 - 30 Oct 2025
* Updated CryptoExchange.Net version to 9.11.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
@@ -4,7 +4,7 @@ using NUnit.Framework.Legacy;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.UnitTests
@@ -140,5 +140,17 @@ namespace CryptoExchange.Net.UnitTests
ClassicAssert.False(result1);
}
[Test]
public async Task CancellingWait_Should_ReturnFalse()
{
var evnt = new AsyncResetEvent(false, true);
var waiter1 = evnt.WaitAsync(ct: new CancellationTokenSource(50).Token);
var result1 = await waiter1;
ClassicAssert.False(result1);
}
}
}
@@ -1,11 +1,5 @@
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.UnitTests.TestImplementations;
using Microsoft.Extensions.Logging;
using NUnit.Framework;
using NUnit.Framework;
using NUnit.Framework.Legacy;
using System;
using System.Collections.Generic;
namespace CryptoExchange.Net.UnitTests
{
@@ -25,20 +19,6 @@ namespace CryptoExchange.Net.UnitTests
Assert.That(result.Success);
}
[TestCase]
public void DeserializingInvalidJson_Should_GiveErrorResult()
{
// arrange
var client = new TestBaseClient();
// act
var result = client.SubClient.Deserialize<object>("{\"testProperty\": 123");
// assert
ClassicAssert.IsFalse(result.Success);
Assert.That(result.Error != null);
}
[TestCase("https://api.test.com/api", new[] { "path1", "path2" }, "https://api.test.com/api/path1/path2")]
[TestCase("https://api.test.com/api", new[] { "path1", "/path2" }, "https://api.test.com/api/path1/path2")]
[TestCase("https://api.test.com/api", new[] { "path1/", "path2" }, "https://api.test.com/api/path1/path2")]
@@ -4,11 +4,8 @@ using NUnit.Framework;
using NUnit.Framework.Legacy;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
namespace CryptoExchange.Net.UnitTests
{
@@ -115,7 +112,7 @@ namespace CryptoExchange.Net.UnitTests
var result = new WebCallResult<TestObjectResult>(
System.Net.HttpStatusCode.OK,
HttpVersion.Version11,
new KeyValuePair<string, string[]>[0],
new HttpResponseMessage().Headers,
TimeSpan.FromSeconds(1),
null,
"{}",
@@ -123,7 +120,7 @@ namespace CryptoExchange.Net.UnitTests
"https://test.com/api",
null,
HttpMethod.Get,
new KeyValuePair<string, string[]>[0],
new HttpRequestMessage().Headers,
ResultDataSource.Server,
new TestObjectResult(),
null);
@@ -146,7 +143,7 @@ namespace CryptoExchange.Net.UnitTests
var result = new WebCallResult<TestObjectResult>(
System.Net.HttpStatusCode.OK,
HttpVersion.Version11,
new KeyValuePair<string, string[]>[0],
new HttpResponseMessage().Headers,
TimeSpan.FromSeconds(1),
null,
"{}",
@@ -154,7 +151,7 @@ namespace CryptoExchange.Net.UnitTests
"https://test.com/api",
null,
HttpMethod.Get,
new KeyValuePair<string, string[]>[0],
new HttpRequestMessage().Headers,
ResultDataSource.Server,
new TestObjectResult(),
null);
@@ -1,15 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.0"></PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1"></PackageReference>
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="NUnit" Version="4.4.0"></PackageReference>
<PackageReference Include="NUnit3TestAdapter" Version="5.2.0"></PackageReference>
<PackageReference Include="NUnit3TestAdapter" Version="6.0.1"></PackageReference>
</ItemGroup>
<ItemGroup>
@@ -1,7 +1,5 @@
using CryptoExchange.Net.Objects;
using NUnit.Framework;
using NUnit.Framework.Legacy;
using System.Diagnostics;
using System.Globalization;
namespace CryptoExchange.Net.UnitTests
@@ -0,0 +1,465 @@
using CryptoExchange.Net.SharedApis;
using NUnit.Framework;
using System;
using System.Linq;
namespace CryptoExchange.Net.UnitTests
{
[TestFixture()]
public class ExchangeSymbolCacheTests
{
private SharedSpotSymbol[] CreateTestSymbols()
{
return new[]
{
new SharedSpotSymbol("BTC", "USDT", "BTCUSDT", true, TradingMode.Spot),
new SharedSpotSymbol("ETH", "USDT", "ETHUSDT", true, TradingMode.Spot),
new SharedSpotSymbol("BTC", "EUR", "BTCEUR", true, TradingMode.Spot),
new SharedSpotSymbol("ETH", "BTC", "ETHBTC", true, TradingMode.Spot),
new SharedSpotSymbol("XRP", "USDT", "XRPUSDT", false, TradingMode.Spot)
};
}
private SharedSpotSymbol[] CreateFuturesSymbols()
{
return new[]
{
new SharedSpotSymbol("BTC", "USDT", "BTCUSDT-PERP", true, TradingMode.PerpetualLinear),
new SharedSpotSymbol("ETH", "USDT", "ETHUSDT-PERP", true, TradingMode.PerpetualLinear)
};
}
[Test]
public void UpdateSymbolInfo_NewTopic_Should_AddToCache()
{
// arrange
var topicId = "NewExchange";
var symbols = CreateTestSymbols();
// act
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
var hasCached = ExchangeSymbolCache.HasCached(topicId);
// assert
Assert.That(hasCached, Is.True);
}
[Test]
public void UpdateSymbolInfo_Should_StoreAllSymbols()
{
// arrange
var topicId = "ExchangeWithSymbols";
var symbols = CreateTestSymbols();
// act
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
// assert
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "BTCUSDT"), Is.True);
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "ETHUSDT"), Is.True);
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "BTCEUR"), Is.True);
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "ETHBTC"), Is.True);
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "XRPUSDT"), Is.True);
}
[Test]
public void UpdateSymbolInfo_CalledTwiceWithinAnHour_Should_NotUpdate()
{
// arrange
var topicId = "ExchangeNoUpdate";
var initialSymbols = new[]
{
new SharedSpotSymbol("BTC", "USDT", "BTCUSDT", true, TradingMode.Spot)
};
var updatedSymbols = new[]
{
new SharedSpotSymbol("BTC", "USDT", "BTCUSDT", true, TradingMode.Spot),
new SharedSpotSymbol("ETH", "USDT", "ETHUSDT", true, TradingMode.Spot)
};
// act
ExchangeSymbolCache.UpdateSymbolInfo(topicId, initialSymbols);
ExchangeSymbolCache.UpdateSymbolInfo(topicId, updatedSymbols);
// assert - should still have only the initial symbol since less than 60 minutes passed
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "BTCUSDT"), Is.True);
// The second update should not have been applied
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "ETHUSDT"), Is.False);
}
[Test]
public void UpdateSymbolInfo_WithEmptyArray_Should_CreateEmptyCache()
{
// arrange
var topicId = "EmptyExchange";
var symbols = Array.Empty<SharedSpotSymbol>();
// act
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
var hasCached = ExchangeSymbolCache.HasCached(topicId);
// assert
Assert.That(hasCached, Is.False);
}
[Test]
public void HasCached_NonExistentTopic_Should_ReturnFalse()
{
// arrange
var nonExistentTopic = "NonExistent_" + Guid.NewGuid();
// act
var result = ExchangeSymbolCache.HasCached(nonExistentTopic);
// assert
Assert.That(result, Is.False);
}
[Test]
public void HasCached_ExistingTopicWithSymbols_Should_ReturnTrue()
{
// arrange
var topicId = "ExchangeWithData";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
// act
var result = ExchangeSymbolCache.HasCached(topicId);
// assert
Assert.That(result, Is.True);
}
[Test]
public void HasCached_ExistingTopicWithNoSymbols_Should_ReturnFalse()
{
// arrange
var topicId = "ExchangeNoData";
ExchangeSymbolCache.UpdateSymbolInfo(topicId, Array.Empty<SharedSpotSymbol>());
// act
var result = ExchangeSymbolCache.HasCached(topicId);
// assert
Assert.That(result, Is.False);
}
[Test]
public void SupportsSymbol_ByName_ExistingSymbol_Should_ReturnTrue()
{
// arrange
var topicId = "ExchangeSupports";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
// act
var result = ExchangeSymbolCache.SupportsSymbol(topicId, "BTCUSDT");
// assert
Assert.That(result, Is.True);
}
[Test]
public void SupportsSymbol_ByName_NonExistingSymbol_Should_ReturnFalse()
{
// arrange
var topicId = "ExchangeNoSupport";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
// act
var result = ExchangeSymbolCache.SupportsSymbol(topicId, "LINKUSDT");
// assert
Assert.That(result, Is.False);
}
[Test]
public void SupportsSymbol_ByName_NonExistentTopic_Should_ReturnFalse()
{
// arrange
var nonExistentTopic = "NonExistent_" + Guid.NewGuid();
// act
var result = ExchangeSymbolCache.SupportsSymbol(nonExistentTopic, "BTCUSDT");
// assert
Assert.That(result, Is.False);
}
[Test]
public void SupportsSymbol_BySharedSymbol_ExistingSymbol_Should_ReturnTrue()
{
// arrange
var topicId = "ExchangeSharedSymbol";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
var sharedSymbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
// act
var result = ExchangeSymbolCache.SupportsSymbol(topicId, sharedSymbol);
// assert
Assert.That(result, Is.True);
}
[Test]
public void SupportsSymbol_BySharedSymbol_NonExistingSymbol_Should_ReturnFalse()
{
// arrange
var topicId = "ExchangeNoSharedSymbol";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
var sharedSymbol = new SharedSymbol(TradingMode.Spot, "LINK", "USDT");
// act
var result = ExchangeSymbolCache.SupportsSymbol(topicId, sharedSymbol);
// assert
Assert.That(result, Is.False);
}
[Test]
public void SupportsSymbol_BySharedSymbol_DifferentTradingMode_Should_ReturnFalse()
{
// arrange
var topicId = "ExchangeDifferentMode";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
var sharedSymbol = new SharedSymbol(TradingMode.PerpetualLinear, "BTC", "USDT");
// act
var result = ExchangeSymbolCache.SupportsSymbol(topicId, sharedSymbol);
// assert
Assert.That(result, Is.False);
}
[Test]
public void SupportsSymbol_BySharedSymbol_NonExistentTopic_Should_ReturnFalse()
{
// arrange
var nonExistentTopic = "NonExistent_" + Guid.NewGuid();
var sharedSymbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
// act
var result = ExchangeSymbolCache.SupportsSymbol(nonExistentTopic, sharedSymbol);
// assert
Assert.That(result, Is.False);
}
[Test]
public void GetSymbolsForBaseAsset_ExistingBaseAsset_Should_ReturnMatchingSymbols()
{
// arrange
var topicId = "ExchangeBaseAsset";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
// act
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "BTC");
// assert
Assert.That(result, Is.Not.Null);
Assert.That(result.Length, Is.EqualTo(2));
Assert.That(result.Any(x => x.QuoteAsset == "USDT"), Is.True);
Assert.That(result.Any(x => x.QuoteAsset == "EUR"), Is.True);
}
[Test]
public void GetSymbolsForBaseAsset_CaseInsensitive_Should_ReturnMatchingSymbols()
{
// arrange
var topicId = "ExchangeCaseInsensitive";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
// act
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "btc");
// assert
Assert.That(result, Is.Not.Null);
Assert.That(result.Length, Is.EqualTo(2));
}
[Test]
public void GetSymbolsForBaseAsset_NonExistingBaseAsset_Should_ReturnEmptyArray()
{
// arrange
var topicId = "ExchangeNoBaseAsset";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
// act
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "LINK");
// assert
Assert.That(result, Is.Not.Null);
Assert.That(result.Length, Is.EqualTo(0));
}
[Test]
public void GetSymbolsForBaseAsset_NonExistentTopic_Should_ReturnEmptyArray()
{
// arrange
var nonExistentTopic = "NonExistent_" + Guid.NewGuid();
// act
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(nonExistentTopic, "BTC");
// assert
Assert.That(result, Is.Not.Null);
Assert.That(result.Length, Is.EqualTo(0));
}
[Test]
public void ParseSymbol_ExistingSymbol_Should_ReturnSharedSymbol()
{
// arrange
var topicId = "ExchangeParse";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
// act
var result = ExchangeSymbolCache.ParseSymbol(topicId, "BTCUSDT");
// assert
Assert.That(result, Is.Not.Null);
Assert.That(result.BaseAsset, Is.EqualTo("BTC"));
Assert.That(result.QuoteAsset, Is.EqualTo("USDT"));
Assert.That(result.TradingMode, Is.EqualTo(TradingMode.Spot));
Assert.That(result.SymbolName, Is.EqualTo("BTCUSDT"));
}
[Test]
public void ParseSymbol_NonExistingSymbol_Should_ReturnNull()
{
// arrange
var topicId = "ExchangeNoParse";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
// act
var result = ExchangeSymbolCache.ParseSymbol(topicId, "LINKUSDT");
// assert
Assert.That(result, Is.Null);
}
[Test]
public void ParseSymbol_NullSymbolName_Should_ReturnNull()
{
// arrange
var topicId = "ExchangeNullSymbol";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
// act
var result = ExchangeSymbolCache.ParseSymbol(topicId, null);
// assert
Assert.That(result, Is.Null);
}
[Test]
public void ParseSymbol_NonExistentTopic_Should_ReturnNull()
{
// arrange
var nonExistentTopic = "NonExistent_" + Guid.NewGuid();
// act
var result = ExchangeSymbolCache.ParseSymbol(nonExistentTopic, "BTCUSDT");
// assert
Assert.That(result, Is.Null);
}
[Test]
public void MultipleTopics_Should_MaintainSeparateData()
{
// arrange
var topic1 = "Exchange1";
var topic2 = "Exchange2";
var symbols1 = new[]
{
new SharedSpotSymbol("BTC", "USDT", "BTCUSDT", true, TradingMode.Spot)
};
var symbols2 = new[]
{
new SharedSpotSymbol("ETH", "USDT", "ETHUSDT", true, TradingMode.Spot)
};
// act
ExchangeSymbolCache.UpdateSymbolInfo(topic1, symbols1);
ExchangeSymbolCache.UpdateSymbolInfo(topic2, symbols2);
// assert
Assert.That(ExchangeSymbolCache.SupportsSymbol(topic1, "BTCUSDT"), Is.True);
Assert.That(ExchangeSymbolCache.SupportsSymbol(topic1, "ETHUSDT"), Is.False);
Assert.That(ExchangeSymbolCache.SupportsSymbol(topic2, "ETHUSDT"), Is.True);
Assert.That(ExchangeSymbolCache.SupportsSymbol(topic2, "BTCUSDT"), Is.False);
}
[Test]
public void UpdateSymbolInfo_WithDifferentTradingModes_Should_StoreCorrectly()
{
// arrange
var topicId = "ExchangeMixedModes";
var spotSymbols = CreateTestSymbols();
var futuresSymbols = CreateFuturesSymbols();
var allSymbols = spotSymbols.Concat(futuresSymbols).ToArray();
// act
ExchangeSymbolCache.UpdateSymbolInfo(topicId, allSymbols);
// assert
var spotSymbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
var futuresSymbol = new SharedSymbol(TradingMode.PerpetualLinear, "BTC", "USDT");
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, spotSymbol), Is.True);
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, futuresSymbol), Is.True);
}
[Test]
public void GetSymbolsForBaseAsset_Should_ReturnAllTradingModes()
{
// arrange
var topicId = "ExchangeAllModes";
var spotSymbols = CreateTestSymbols();
var futuresSymbols = CreateFuturesSymbols();
var allSymbols = spotSymbols.Concat(futuresSymbols).ToArray();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, allSymbols);
// act
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "BTC");
// assert
Assert.That(result.Length, Is.GreaterThanOrEqualTo(2));
Assert.That(result.Any(x => x.TradingMode == TradingMode.Spot), Is.True);
Assert.That(result.Any(x => x.TradingMode == TradingMode.PerpetualLinear), Is.True);
}
[Test]
public void GetSymbolsForBaseAsset_WithMultipleMatchingSymbols_Should_ReturnAll()
{
// arrange
var topicId = "ExchangeMultiple";
var symbols = new[]
{
new SharedSpotSymbol("ETH", "USDT", "ETHUSDT", true, TradingMode.Spot),
new SharedSpotSymbol("ETH", "BTC", "ETHBTC", true, TradingMode.Spot),
new SharedSpotSymbol("ETH", "EUR", "ETHEUR", true, TradingMode.Spot)
};
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
// act
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "ETH");
// assert
Assert.That(result.Length, Is.EqualTo(3));
Assert.That(result.All(x => x.BaseAsset == "ETH"), Is.True);
}
}
}
@@ -1,15 +1,8 @@
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.UnitTests.TestImplementations;
using Microsoft.Extensions.Logging;
using NUnit.Framework;
using NUnit.Framework.Legacy;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CryptoExchange.Net.UnitTests
{
@@ -9,7 +9,6 @@ using System.Threading.Tasks;
using System.Threading;
using NUnit.Framework.Legacy;
using CryptoExchange.Net.RateLimiting;
using System.Net;
using CryptoExchange.Net.RateLimiting.Guards;
using CryptoExchange.Net.RateLimiting.Filters;
using CryptoExchange.Net.RateLimiting.Interfaces;
@@ -82,6 +81,25 @@ namespace CryptoExchange.Net.UnitTests
Assert.That(result.Error is ServerError);
}
[TestCase]
public async Task ReceivingErrorAndNotParsingErrorAndInvalidJson_Should_ContainData()
{
// arrange
var client = new TestRestClient();
var response = "<html>...</html>";
client.SetErrorWithResponse(response, System.Net.HttpStatusCode.BadRequest);
// act
var result = await client.Api1.Request<TestObject>();
// assert
ClassicAssert.IsFalse(result.Success);
Assert.That(result.Error != null);
Assert.That(result.Error is DeserializeError);
Assert.That(result.Error.Message.Contains(response));
}
[TestCase]
public async Task ReceivingErrorAndParsingError_Should_ResultInParsedError()
{
@@ -0,0 +1,633 @@
using CryptoExchange.Net.SharedApis;
using NUnit.Framework;
using System;
namespace CryptoExchange.Net.UnitTests
{
[TestFixture()]
public class SharedQuantityTests
{
[Test]
public void SharedQuantityReference_IsZero_AllNull_Should_ReturnTrue()
{
// arrange
var quantity = new SharedOrderQuantity(null, null, null);
// act & assert
Assert.That(quantity.IsZero, Is.True);
}
[Test]
public void SharedQuantityReference_IsZero_AllZero_Should_ReturnTrue()
{
// arrange
var quantity = new SharedOrderQuantity(0, 0, 0);
// act & assert
Assert.That(quantity.IsZero, Is.True);
}
[Test]
public void SharedQuantityReference_IsZero_BaseAssetSet_Should_ReturnFalse()
{
// arrange
var quantity = new SharedOrderQuantity(1.5m, null, null);
// act & assert
Assert.That(quantity.IsZero, Is.False);
}
[Test]
public void SharedQuantityReference_IsZero_QuoteAssetSet_Should_ReturnFalse()
{
// arrange
var quantity = new SharedOrderQuantity(null, 100m, null);
// act & assert
Assert.That(quantity.IsZero, Is.False);
}
[Test]
public void SharedQuantityReference_IsZero_ContractsSet_Should_ReturnFalse()
{
// arrange
var quantity = new SharedOrderQuantity(null, null, 10m);
// act & assert
Assert.That(quantity.IsZero, Is.False);
}
[Test]
public void SharedQuantityReference_IsZero_NegativeValue_Should_ReturnTrue()
{
// arrange
var quantity = new SharedOrderQuantity(-1m, 0, 0);
// act & assert
Assert.That(quantity.IsZero, Is.True);
}
[Test]
public void SharedQuantity_DefaultConstructor_Should_SetAllPropertiesToNull()
{
// arrange & act
var quantity = new SharedQuantity();
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.Null);
Assert.That(quantity.QuantityInQuoteAsset, Is.Null);
Assert.That(quantity.QuantityInContracts, Is.Null);
Assert.That(quantity.IsZero, Is.True);
}
[Test]
public void SharedQuantity_Base_Should_SetBaseAssetQuantity()
{
// arrange
var expectedQuantity = 1.5m;
// act
var quantity = SharedQuantity.Base(expectedQuantity);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(expectedQuantity));
Assert.That(quantity.QuantityInQuoteAsset, Is.Null);
Assert.That(quantity.QuantityInContracts, Is.Null);
}
[Test]
public void SharedQuantity_Base_WithZero_Should_SetZeroQuantity()
{
// arrange & act
var quantity = SharedQuantity.Base(0m);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(0m));
Assert.That(quantity.IsZero, Is.True);
}
[Test]
public void SharedQuantity_Base_WithLargeValue_Should_SetCorrectly()
{
// arrange
var largeValue = 999999.123456789m;
// act
var quantity = SharedQuantity.Base(largeValue);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(largeValue));
}
[Test]
public void SharedQuantity_Quote_Should_SetQuoteAssetQuantity()
{
// arrange
var expectedQuantity = 100m;
// act
var quantity = SharedQuantity.Quote(expectedQuantity);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.Null);
Assert.That(quantity.QuantityInQuoteAsset, Is.EqualTo(expectedQuantity));
Assert.That(quantity.QuantityInContracts, Is.Null);
}
[Test]
public void SharedQuantity_Quote_WithDecimal_Should_PreserveDecimals()
{
// arrange
var expectedQuantity = 50.123456m;
// act
var quantity = SharedQuantity.Quote(expectedQuantity);
// assert
Assert.That(quantity.QuantityInQuoteAsset, Is.EqualTo(expectedQuantity));
}
[Test]
public void SharedQuantity_Contracts_Should_SetContractQuantity()
{
// arrange
var expectedQuantity = 10m;
// act
var quantity = SharedQuantity.Contracts(expectedQuantity);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.Null);
Assert.That(quantity.QuantityInQuoteAsset, Is.Null);
Assert.That(quantity.QuantityInContracts, Is.EqualTo(expectedQuantity));
}
[Test]
public void SharedQuantity_Contracts_WithFractionalValue_Should_SetCorrectly()
{
// arrange
var expectedQuantity = 2.5m;
// act
var quantity = SharedQuantity.Contracts(expectedQuantity);
// assert
Assert.That(quantity.QuantityInContracts, Is.EqualTo(expectedQuantity));
}
[Test]
public void SharedQuantity_BaseFromQuote_Should_CalculateCorrectly()
{
// arrange
var quoteQuantity = 100m;
var price = 50m;
var expectedBase = 2m; // 100 / 50 = 2
// act
var quantity = SharedQuantity.BaseFromQuote(quoteQuantity, price);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(expectedBase));
Assert.That(quantity.QuantityInQuoteAsset, Is.Null);
Assert.That(quantity.QuantityInContracts, Is.Null);
}
[Test]
public void SharedQuantity_BaseFromQuote_WithCustomDecimals_Should_RoundCorrectly()
{
// arrange
var quoteQuantity = 100m;
var price = 3m;
var decimalPlaces = 2;
// act
var quantity = SharedQuantity.BaseFromQuote(quoteQuantity, price, decimalPlaces);
// assert
// 100 / 3 = 33.333... should round to 33.33
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(33.33m));
}
[Test]
public void SharedQuantity_BaseFromQuote_WithLotSize_Should_AdjustToLotSize()
{
// arrange
var quoteQuantity = 100m;
var price = 7m;
var lotSize = 0.1m;
// act
var quantity = SharedQuantity.BaseFromQuote(quoteQuantity, price, 8, lotSize);
// assert
// 100 / 7 = 14.285714... should adjust to nearest 0.1 = 14.3
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(14.3m));
}
[Test]
public void SharedQuantity_BaseFromQuote_WithHighPrecision_Should_HandleCorrectly()
{
// arrange
var quoteQuantity = 1000m;
var price = 0.00001m;
var decimalPlaces = 8;
// act
var quantity = SharedQuantity.BaseFromQuote(quoteQuantity, price, decimalPlaces);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.GreaterThan(0));
}
[Test]
public void SharedQuantity_QuoteFromBase_Should_CalculateCorrectly()
{
// arrange
var baseQuantity = 2m;
var price = 50m;
var expectedQuote = 100m; // 2 * 50 = 100
// act
var quantity = SharedQuantity.QuoteFromBase(baseQuantity, price);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(expectedQuote));
Assert.That(quantity.QuantityInQuoteAsset, Is.Null);
Assert.That(quantity.QuantityInContracts, Is.Null);
}
[Test]
public void SharedQuantity_QuoteFromBase_WithCustomDecimals_Should_RoundCorrectly()
{
// arrange
var baseQuantity = 1.234567m;
var price = 10m;
var decimalPlaces = 2;
// act
var quantity = SharedQuantity.QuoteFromBase(baseQuantity, price, decimalPlaces);
// assert
// 1.234567 * 10 = 12.34567 should round to 12.35
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(12.35m));
}
[Test]
public void SharedQuantity_QuoteFromBase_WithLotSize_Should_AdjustToLotSize()
{
// arrange
var baseQuantity = 3.456m;
var price = 10m;
var lotSize = 1m;
// act
var quantity = SharedQuantity.QuoteFromBase(baseQuantity, price, 8, lotSize);
// assert
// 3.456 * 10 = 34.56 should adjust to nearest 1 = 35
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(35m));
}
[Test]
public void SharedQuantity_QuoteFromBase_WithSmallValues_Should_HandleCorrectly()
{
// arrange
var baseQuantity = 0.001m;
var price = 0.1m;
var decimalPlaces = 8;
// act
var quantity = SharedQuantity.QuoteFromBase(baseQuantity, price, decimalPlaces);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(0.0001m));
}
[Test]
public void SharedQuantity_ContractsFromBase_Should_CalculateCorrectly()
{
// arrange
var baseQuantity = 100m;
var contractSize = 10m;
var expectedContracts = 10m; // 100 / 10 = 10
// act
var quantity = SharedQuantity.ContractsFromBase(baseQuantity, contractSize);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(expectedContracts));
Assert.That(quantity.QuantityInQuoteAsset, Is.Null);
Assert.That(quantity.QuantityInContracts, Is.Null);
}
[Test]
public void SharedQuantity_ContractsFromBase_WithCustomDecimals_Should_RoundCorrectly()
{
// arrange
var baseQuantity = 100m;
var contractSize = 3m;
var decimalPlaces = 2;
// act
var quantity = SharedQuantity.ContractsFromBase(baseQuantity, contractSize, decimalPlaces);
// assert
// 100 / 3 = 33.333... should round to 33.33
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(33.33m));
}
[Test]
public void SharedQuantity_ContractsFromBase_WithLotSize_Should_AdjustToLotSize()
{
// arrange
var baseQuantity = 100m;
var contractSize = 7m;
var lotSize = 0.5m;
// act
var quantity = SharedQuantity.ContractsFromBase(baseQuantity, contractSize, 8, lotSize);
// assert
// 100 / 7 = 14.285714... should adjust to nearest 0.5 = 14.5
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(14.5m));
}
[Test]
public void SharedQuantity_ContractsFromBase_WithFractionalContract_Should_HandleCorrectly()
{
// arrange
var baseQuantity = 1m;
var contractSize = 0.1m;
// act
var quantity = SharedQuantity.ContractsFromBase(baseQuantity, contractSize);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(10m));
}
[Test]
public void SharedQuantity_ContractsFromQuote_Should_CalculateCorrectly()
{
// arrange
var quoteQuantity = 1000m;
var contractSize = 10m;
var price = 50m;
var expectedContracts = 2m; // 1000 / 50 / 10 = 2
// act
var quantity = SharedQuantity.ContractsFromQuote(quoteQuantity, contractSize, price);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(expectedContracts));
Assert.That(quantity.QuantityInQuoteAsset, Is.Null);
Assert.That(quantity.QuantityInContracts, Is.Null);
}
[Test]
public void SharedQuantity_ContractsFromQuote_WithCustomDecimals_Should_RoundCorrectly()
{
// arrange
var quoteQuantity = 100m;
var contractSize = 3m;
var price = 7m;
var decimalPlaces = 2;
// act
var quantity = SharedQuantity.ContractsFromQuote(quoteQuantity, contractSize, price, decimalPlaces);
// assert
// 100 / 7 / 3 = 4.761904... should round to 4.76
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(4.76m));
}
[Test]
public void SharedQuantity_ContractsFromQuote_WithLotSize_Should_AdjustToLotSize()
{
// arrange
var quoteQuantity = 1000m;
var contractSize = 7m;
var price = 13m;
var lotSize = 0.5m;
// act
var quantity = SharedQuantity.ContractsFromQuote(quoteQuantity, contractSize, price, 8, lotSize);
// assert
// 1000 / 13 / 7 = 10.989... should adjust to nearest 0.5 = 11.0
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(11.0m));
}
[Test]
public void SharedQuantity_ContractsFromQuote_WithComplexValues_Should_CalculateCorrectly()
{
// arrange
var quoteQuantity = 5000m;
var contractSize = 0.01m;
var price = 25000m;
var decimalPlaces = 4;
// act
var quantity = SharedQuantity.ContractsFromQuote(quoteQuantity, contractSize, price, decimalPlaces);
// assert
// 5000 / 25000 / 0.01 = 20
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(20m));
}
[Test]
public void SharedOrderQuantity_DefaultConstructor_Should_SetAllPropertiesToNull()
{
// arrange & act
var quantity = new SharedOrderQuantity();
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.Null);
Assert.That(quantity.QuantityInQuoteAsset, Is.Null);
Assert.That(quantity.QuantityInContracts, Is.Null);
Assert.That(quantity.IsZero, Is.True);
}
[Test]
public void SharedOrderQuantity_ParameterizedConstructor_Should_SetBaseAsset()
{
// arrange
var baseAsset = 5m;
// act
var quantity = new SharedOrderQuantity(baseAssetQuantity: baseAsset);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(baseAsset));
Assert.That(quantity.QuantityInQuoteAsset, Is.Null);
Assert.That(quantity.QuantityInContracts, Is.Null);
}
[Test]
public void SharedOrderQuantity_ParameterizedConstructor_Should_SetQuoteAsset()
{
// arrange
var quoteAsset = 100m;
// act
var quantity = new SharedOrderQuantity(quoteAssetQuantity: quoteAsset);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.Null);
Assert.That(quantity.QuantityInQuoteAsset, Is.EqualTo(quoteAsset));
Assert.That(quantity.QuantityInContracts, Is.Null);
}
[Test]
public void SharedOrderQuantity_ParameterizedConstructor_Should_SetContracts()
{
// arrange
var contracts = 10m;
// act
var quantity = new SharedOrderQuantity(contractQuantity: contracts);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.Null);
Assert.That(quantity.QuantityInQuoteAsset, Is.Null);
Assert.That(quantity.QuantityInContracts, Is.EqualTo(contracts));
}
[Test]
public void SharedOrderQuantity_ParameterizedConstructor_Should_SetAllValues()
{
// arrange
var baseAsset = 1m;
var quoteAsset = 50m;
var contracts = 5m;
// act
var quantity = new SharedOrderQuantity(baseAsset, quoteAsset, contracts);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(baseAsset));
Assert.That(quantity.QuantityInQuoteAsset, Is.EqualTo(quoteAsset));
Assert.That(quantity.QuantityInContracts, Is.EqualTo(contracts));
Assert.That(quantity.IsZero, Is.False);
}
[Test]
public void SharedOrderQuantity_ParameterizedConstructor_WithNullValues_Should_HandleCorrectly()
{
// arrange & act
var quantity = new SharedOrderQuantity(null, null, null);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.Null);
Assert.That(quantity.QuantityInQuoteAsset, Is.Null);
Assert.That(quantity.QuantityInContracts, Is.Null);
Assert.That(quantity.IsZero, Is.True);
}
[Test]
public void SharedQuantity_RecordEquality_SameValues_Should_BeEqual()
{
// arrange
var quantity1 = SharedQuantity.Base(10m);
var quantity2 = SharedQuantity.Base(10m);
// act & assert
Assert.That(quantity1, Is.EqualTo(quantity2));
}
[Test]
public void SharedQuantity_RecordEquality_DifferentValues_Should_NotBeEqual()
{
// arrange
var quantity1 = SharedQuantity.Base(10m);
var quantity2 = SharedQuantity.Base(20m);
// act & assert
Assert.That(quantity1, Is.Not.EqualTo(quantity2));
}
[Test]
public void SharedQuantity_RecordEquality_DifferentTypes_Should_NotBeEqual()
{
// arrange
var quantity1 = SharedQuantity.Base(10m);
var quantity2 = SharedQuantity.Quote(10m);
// act & assert
Assert.That(quantity1, Is.Not.EqualTo(quantity2));
}
[Test]
public void SharedOrderQuantity_RecordEquality_SameValues_Should_BeEqual()
{
// arrange
var quantity1 = new SharedOrderQuantity(5m, 100m, 2m);
var quantity2 = new SharedOrderQuantity(5m, 100m, 2m);
// act & assert
Assert.That(quantity1, Is.EqualTo(quantity2));
}
[Test]
public void SharedQuantity_BaseFromQuote_WithDefaultParameters_Should_UseDefaults()
{
// arrange
var quoteQuantity = 100m;
var price = 3m;
// act
var quantity = SharedQuantity.BaseFromQuote(quoteQuantity, price);
// assert
// Default decimalPlaces = 8, default lotSize = 0.00000001
Assert.That(quantity.QuantityInBaseAsset, Is.Not.Null);
Assert.That(quantity.QuantityInBaseAsset, Is.GreaterThan(0));
}
[Test]
public void SharedQuantity_QuoteFromBase_WithDefaultParameters_Should_UseDefaults()
{
// arrange
var baseQuantity = 1.234567m;
var price = 10m;
// act
var quantity = SharedQuantity.QuoteFromBase(baseQuantity, price);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.Not.Null);
Assert.That(quantity.QuantityInBaseAsset, Is.GreaterThan(0));
}
[Test]
public void SharedQuantity_ContractsFromBase_WithDefaultParameters_Should_UseDefaults()
{
// arrange
var baseQuantity = 100m;
var contractSize = 3m;
// act
var quantity = SharedQuantity.ContractsFromBase(baseQuantity, contractSize);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.Not.Null);
Assert.That(quantity.QuantityInBaseAsset, Is.GreaterThan(0));
}
[Test]
public void SharedQuantity_ContractsFromQuote_WithDefaultParameters_Should_UseDefaults()
{
// arrange
var quoteQuantity = 1000m;
var contractSize = 10m;
var price = 50m;
// act
var quantity = SharedQuantity.ContractsFromQuote(quoteQuantity, contractSize, price);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.Not.Null);
Assert.That(quantity.QuantityInBaseAsset, Is.GreaterThan(0));
}
}
}
@@ -0,0 +1,400 @@
using CryptoExchange.Net.SharedApis;
using NUnit.Framework;
using System;
namespace CryptoExchange.Net.UnitTests
{
[TestFixture()]
public class SharedSymbolTests
{
[Test]
public void SharedSymbol_Constructor_Should_SetAllProperties()
{
// arrange
var tradingMode = TradingMode.Spot;
var baseAsset = "BTC";
var quoteAsset = "USDT";
// act
var symbol = new SharedSymbol(tradingMode, baseAsset, quoteAsset);
// assert
Assert.That(symbol.TradingMode, Is.EqualTo(tradingMode));
Assert.That(symbol.BaseAsset, Is.EqualTo(baseAsset));
Assert.That(symbol.QuoteAsset, Is.EqualTo(quoteAsset));
Assert.That(symbol.DeliverTime, Is.Null);
Assert.That(symbol.SymbolName, Is.Null);
}
[Test]
public void SharedSymbol_Constructor_WithDeliveryTime_Should_SetDeliveryTime()
{
// arrange
var tradingMode = TradingMode.DeliveryLinear;
var baseAsset = "BTC";
var quoteAsset = "USDT";
var deliveryTime = new DateTime(2026, 6, 25, 0, 0, 0, DateTimeKind.Utc);
// act
var symbol = new SharedSymbol(tradingMode, baseAsset, quoteAsset, deliveryTime);
// assert
Assert.That(symbol.TradingMode, Is.EqualTo(tradingMode));
Assert.That(symbol.BaseAsset, Is.EqualTo(baseAsset));
Assert.That(symbol.QuoteAsset, Is.EqualTo(quoteAsset));
Assert.That(symbol.DeliverTime, Is.EqualTo(deliveryTime));
Assert.That(symbol.SymbolName, Is.Null);
}
[Test]
public void SharedSymbol_Constructor_WithNullDeliveryTime_Should_SetToNull()
{
// arrange
var tradingMode = TradingMode.Spot;
var baseAsset = "ETH";
var quoteAsset = "BTC";
// act
var symbol = new SharedSymbol(tradingMode, baseAsset, quoteAsset, deliverTime: null);
// assert
Assert.That(symbol.DeliverTime, Is.Null);
}
[TestCase(TradingMode.Spot)]
[TestCase(TradingMode.PerpetualLinear)]
[TestCase(TradingMode.PerpetualInverse)]
[TestCase(TradingMode.DeliveryLinear)]
[TestCase(TradingMode.DeliveryInverse)]
public void SharedSymbol_Constructor_WithDifferentTradingModes_Should_SetCorrectly(TradingMode tradingMode)
{
// arrange
var baseAsset = "BTC";
var quoteAsset = "USDT";
// act
var symbol = new SharedSymbol(tradingMode, baseAsset, quoteAsset);
// assert
Assert.That(symbol.TradingMode, Is.EqualTo(tradingMode));
}
[Test]
public void SharedSymbol_ConstructorWithSymbolName_Should_SetSymbolName()
{
// arrange
var tradingMode = TradingMode.Spot;
var baseAsset = "BTC";
var quoteAsset = "USDT";
var symbolName = "BTC-USDT";
// act
var symbol = new SharedSymbol(tradingMode, baseAsset, quoteAsset, symbolName);
// assert
Assert.That(symbol.TradingMode, Is.EqualTo(tradingMode));
Assert.That(symbol.BaseAsset, Is.EqualTo(baseAsset));
Assert.That(symbol.QuoteAsset, Is.EqualTo(quoteAsset));
Assert.That(symbol.SymbolName, Is.EqualTo(symbolName));
Assert.That(symbol.DeliverTime, Is.Null);
}
[Test]
public void SharedSymbol_ConstructorWithSymbolName_WithCustomFormat_Should_SetCorrectly()
{
// arrange
var tradingMode = TradingMode.PerpetualLinear;
var baseAsset = "ETH";
var quoteAsset = "USDT";
var symbolName = "ETHUSDT-PERP";
// act
var symbol = new SharedSymbol(tradingMode, baseAsset, quoteAsset, symbolName);
// assert
Assert.That(symbol.SymbolName, Is.EqualTo(symbolName));
}
[Test]
public void SharedSymbol_ConstructorWithSymbolName_WithEmptyString_Should_SetEmptyString()
{
// arrange
var tradingMode = TradingMode.Spot;
var baseAsset = "BTC";
var quoteAsset = "USDT";
var symbolName = "";
// act
var symbol = new SharedSymbol(tradingMode, baseAsset, quoteAsset, symbolName);
// assert
Assert.That(symbol.SymbolName, Is.EqualTo(""));
}
[Test]
public void GetSymbol_WithSymbolNameSet_Should_ReturnSymbolName()
{
// arrange
var symbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT", "CUSTOM-BTC-USDT");
var formatFunc = new Func<string, string, TradingMode, DateTime?, string>(
(b, q, t, d) => $"{b}{q}");
// act
var result = symbol.GetSymbol(formatFunc);
// assert
Assert.That(result, Is.EqualTo("CUSTOM-BTC-USDT"));
}
[Test]
public void GetSymbol_WithSymbolNameNull_Should_UseFormatFunction()
{
// arrange
var symbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
var formatFunc = new Func<string, string, TradingMode, DateTime?, string>(
(b, q, t, d) => $"{b}/{q}");
// act
var result = symbol.GetSymbol(formatFunc);
// assert
Assert.That(result, Is.EqualTo("BTC/USDT"));
}
[Test]
public void GetSymbol_WithComplexFormatFunction_Should_ApplyCorrectly()
{
// arrange
var symbol = new SharedSymbol(TradingMode.PerpetualLinear, "ETH", "USDT");
var formatFunc = new Func<string, string, TradingMode, DateTime?, string>(
(b, q, t, d) => t == TradingMode.PerpetualLinear ? $"{b}{q}-PERP" : $"{b}{q}");
// act
var result = symbol.GetSymbol(formatFunc);
// assert
Assert.That(result, Is.EqualTo("ETHUSDT-PERP"));
}
[Test]
public void GetSymbol_WithDeliveryTime_Should_PassDeliveryTimeToFormatter()
{
// arrange
var deliveryTime = new DateTime(2026, 6, 25);
var symbol = new SharedSymbol(TradingMode.DeliveryLinear, "BTC", "USDT", deliveryTime);
var formatFunc = new Func<string, string, TradingMode, DateTime?, string>(
(b, q, t, d) => d.HasValue ? $"{b}{q}_{d.Value:yyyyMMdd}" : $"{b}{q}");
// act
var result = symbol.GetSymbol(formatFunc);
// assert
Assert.That(result, Is.EqualTo("BTCUSDT_20260625"));
}
[Test]
public void GetSymbol_WithTradingMode_Should_PassTradingModeToFormatter()
{
// arrange
var symbol = new SharedSymbol(TradingMode.PerpetualInverse, "BTC", "USD");
var formatFunc = new Func<string, string, TradingMode, DateTime?, string>(
(b, q, t, d) =>
{
return t switch
{
TradingMode.Spot => $"{b}{q}",
TradingMode.PerpetualLinear => $"{b}{q}-PERP",
TradingMode.PerpetualInverse => $"{b}{q}I-PERP",
_ => $"{b}{q}"
};
});
// act
var result = symbol.GetSymbol(formatFunc);
// assert
Assert.That(result, Is.EqualTo("BTCUSDI-PERP"));
}
[Test]
public void GetSymbol_WithEmptySymbolName_Should_UseFormatFunction()
{
// arrange
var symbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT", "");
var formatFunc = new Func<string, string, TradingMode, DateTime?, string>(
(b, q, t, d) => $"{b}-{q}");
// act
var result = symbol.GetSymbol(formatFunc);
// assert
Assert.That(result, Is.EqualTo("BTC-USDT"));
}
[Test]
public void GetSymbol_WithWhitespaceSymbolName_Should_ReturnWhitespace()
{
// arrange
var symbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT", " ");
var formatFunc = new Func<string, string, TradingMode, DateTime?, string>(
(b, q, t, d) => $"{b}-{q}");
// act
var result = symbol.GetSymbol(formatFunc);
// assert
Assert.That(result, Is.EqualTo(" "));
}
[Test]
public void SharedSymbol_RecordEquality_SameValues_Should_BeEqual()
{
// arrange
var symbol1 = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
var symbol2 = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
// act & assert
Assert.That(symbol1, Is.EqualTo(symbol2));
}
[Test]
public void SharedSymbol_RecordEquality_DifferentBaseAsset_Should_NotBeEqual()
{
// arrange
var symbol1 = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
var symbol2 = new SharedSymbol(TradingMode.Spot, "ETH", "USDT");
// act & assert
Assert.That(symbol1, Is.Not.EqualTo(symbol2));
}
[Test]
public void SharedSymbol_RecordEquality_DifferentQuoteAsset_Should_NotBeEqual()
{
// arrange
var symbol1 = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
var symbol2 = new SharedSymbol(TradingMode.Spot, "BTC", "EUR");
// act & assert
Assert.That(symbol1, Is.Not.EqualTo(symbol2));
}
[Test]
public void SharedSymbol_RecordEquality_DifferentTradingMode_Should_NotBeEqual()
{
// arrange
var symbol1 = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
var symbol2 = new SharedSymbol(TradingMode.PerpetualLinear, "BTC", "USDT");
// act & assert
Assert.That(symbol1, Is.Not.EqualTo(symbol2));
}
[Test]
public void SharedSymbol_RecordEquality_DifferentDeliveryTime_Should_NotBeEqual()
{
// arrange
var deliveryTime1 = new DateTime(2026, 6, 25);
var deliveryTime2 = new DateTime(2026, 9, 25);
var symbol1 = new SharedSymbol(TradingMode.DeliveryLinear, "BTC", "USDT", deliveryTime1);
var symbol2 = new SharedSymbol(TradingMode.DeliveryLinear, "BTC", "USDT", deliveryTime2);
// act & assert
Assert.That(symbol1, Is.Not.EqualTo(symbol2));
}
[Test]
public void SharedSymbol_RecordEquality_DifferentSymbolName_Should_NotBeEqual()
{
// arrange
var symbol1 = new SharedSymbol(TradingMode.Spot, "BTC", "USDT", "BTCUSDT");
var symbol2 = new SharedSymbol(TradingMode.Spot, "BTC", "USDT", "BTC-USDT");
// act & assert
Assert.That(symbol1, Is.Not.EqualTo(symbol2));
}
[Test]
public void SharedSymbol_RecordEquality_OneWithSymbolNameOneWithout_Should_NotBeEqual()
{
// NOTE; although this should probably be equal it's considered not because the SymbolName property isn't equal
// Overridding equality to ignore SymbolName would be possible but would break the default record equality behavior and cause confusion
// arrange
var symbol1 = new SharedSymbol(TradingMode.Spot, "BTC", "USDT", "BTCUSDT");
var symbol2 = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
// act & assert
Assert.That(symbol1, Is.Not.EqualTo(symbol2));
}
[Test]
public void SharedSymbol_RecordEquality_WithAllPropertiesSet_Should_BeEqual()
{
// arrange
var deliveryTime = new DateTime(2026, 6, 25);
var symbol1 = new SharedSymbol(TradingMode.DeliveryLinear, "BTC", "USDT", deliveryTime)
{
SymbolName = "BTCUSDT-0625"
};
var symbol2 = new SharedSymbol(TradingMode.DeliveryLinear, "BTC", "USDT", deliveryTime)
{
SymbolName = "BTCUSDT-0625"
};
// act & assert
Assert.That(symbol1, Is.EqualTo(symbol2));
}
[Test]
public void SharedSymbol_Properties_Should_BeSettable()
{
// arrange
var symbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
// act
symbol.BaseAsset = "ETH";
symbol.QuoteAsset = "EUR";
symbol.TradingMode = TradingMode.PerpetualLinear;
symbol.SymbolName = "CUSTOM";
symbol.DeliverTime = DateTime.UtcNow;
// assert
Assert.That(symbol.BaseAsset, Is.EqualTo("ETH"));
Assert.That(symbol.QuoteAsset, Is.EqualTo("EUR"));
Assert.That(symbol.TradingMode, Is.EqualTo(TradingMode.PerpetualLinear));
Assert.That(symbol.SymbolName, Is.EqualTo("CUSTOM"));
Assert.That(symbol.DeliverTime, Is.Not.Null);
}
[Test]
public void SharedSymbol_WithSpecialCharactersInAssets_Should_HandleCorrectly()
{
// arrange
var baseAsset = "BTC-123";
var quoteAsset = "USDT_2.0";
// act
var symbol = new SharedSymbol(TradingMode.Spot, baseAsset, quoteAsset);
// assert
Assert.That(symbol.BaseAsset, Is.EqualTo(baseAsset));
Assert.That(symbol.QuoteAsset, Is.EqualTo(quoteAsset));
}
[Test]
public void SharedSymbol_WithLongAssetNames_Should_HandleCorrectly()
{
// arrange
var baseAsset = "VERYLONGASSETNAMEFORTESTING";
var quoteAsset = "ANOTHERVERYLONGASSETNAME";
// act
var symbol = new SharedSymbol(TradingMode.Spot, baseAsset, quoteAsset);
// assert
Assert.That(symbol.BaseAsset, Is.EqualTo(baseAsset));
Assert.That(symbol.QuoteAsset, Is.EqualTo(quoteAsset));
}
}
}
+203 -200
View File
@@ -1,231 +1,234 @@
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.Sockets;
using CryptoExchange.Net.UnitTests.TestImplementations;
using CryptoExchange.Net.UnitTests.TestImplementations.Sockets;
using Microsoft.Extensions.Logging;
using Moq;
using NUnit.Framework;
using NUnit.Framework.Legacy;
//using CryptoExchange.Net.Objects;
//using CryptoExchange.Net.Objects.Sockets;
//using CryptoExchange.Net.Sockets;
//using CryptoExchange.Net.Testing.Implementations;
//using CryptoExchange.Net.UnitTests.TestImplementations;
//using CryptoExchange.Net.UnitTests.TestImplementations.Sockets;
//using Microsoft.Extensions.Logging;
//using Moq;
//using NUnit.Framework;
//using NUnit.Framework.Legacy;
//using System;
//using System.Collections.Generic;
//using System.Net.Sockets;
//using System.Text.Json;
//using System.Threading;
//using System.Threading.Tasks;
namespace CryptoExchange.Net.UnitTests
{
[TestFixture]
public class SocketClientTests
{
[TestCase]
public void SettingOptions_Should_ResultInOptionsSet()
{
//arrange
//act
var client = new TestSocketClient(options =>
{
options.SubOptions.ApiCredentials = new Authentication.ApiCredentials("1", "2");
options.SubOptions.MaxSocketConnections = 1;
});
//namespace CryptoExchange.Net.UnitTests
//{
// [TestFixture]
// public class SocketClientTests
// {
// [TestCase]
// public void SettingOptions_Should_ResultInOptionsSet()
// {
// //arrange
// //act
// var client = new TestSocketClient(options =>
// {
// options.SubOptions.ApiCredentials = new Authentication.ApiCredentials("1", "2");
// options.SubOptions.MaxSocketConnections = 1;
// });
//assert
ClassicAssert.NotNull(client.SubClient.ApiOptions.ApiCredentials);
Assert.That(1 == client.SubClient.ApiOptions.MaxSocketConnections);
}
// //assert
// ClassicAssert.NotNull(client.SubClient.ApiOptions.ApiCredentials);
// Assert.That(1 == client.SubClient.ApiOptions.MaxSocketConnections);
// }
[TestCase(true)]
[TestCase(false)]
public void ConnectSocket_Should_ReturnConnectionResult(bool canConnect)
{
//arrange
var client = new TestSocketClient();
var socket = client.CreateSocket();
socket.CanConnect = canConnect;
// [TestCase(true)]
// [TestCase(false)]
// public void ConnectSocket_Should_ReturnConnectionResult(bool canConnect)
// {
// //arrange
// var client = new TestSocketClient();
// var socket = client.CreateSocket();
// socket.CanConnect = canConnect;
//act
var connectResult = client.SubClient.ConnectSocketSub(new SocketConnection(new TraceLogger(), client.SubClient, socket, null));
// //act
// var connectResult = client.SubClient.ConnectSocketSub(
// new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, ""));
//assert
Assert.That(connectResult.Success == canConnect);
}
// //assert
// Assert.That(connectResult.Success == canConnect);
// }
[TestCase]
public void SocketMessages_Should_BeProcessedInDataHandlers()
{
// arrange
var client = new TestSocketClient(options => {
options.ReconnectInterval = TimeSpan.Zero;
});
var socket = client.CreateSocket();
socket.CanConnect = true;
var sub = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
var rstEvent = new ManualResetEvent(false);
Dictionary<string, string> result = null;
// [TestCase]
// public void SocketMessages_Should_BeProcessedInDataHandlers()
// {
// // arrange
// var client = new TestSocketClient(options => {
// options.ReconnectInterval = TimeSpan.Zero;
// });
// var socket = client.CreateSocket();
// socket.CanConnect = true;
// var sub = new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, "");
// var rstEvent = new ManualResetEvent(false);
// Dictionary<string, string> result = null;
client.SubClient.ConnectSocketSub(sub);
// client.SubClient.ConnectSocketSub(sub);
var subObj = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) =>
{
result = messageEvent.Data;
rstEvent.Set();
});
sub.AddSubscription(subObj);
// var subObj = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) =>
// {
// result = messageEvent.Data;
// rstEvent.Set();
// });
// sub.AddSubscription(subObj);
// act
socket.InvokeMessage("{\"property\": \"123\", \"action\": \"update\", \"topic\": \"topic\"}");
rstEvent.WaitOne(1000);
// // act
// socket.InvokeMessage("{\"property\": \"123\", \"action\": \"update\", \"topic\": \"topic\"}");
// rstEvent.WaitOne(1000);
// assert
Assert.That(result["property"] == "123");
}
// // assert
// Assert.That(result["property"] == "123");
// }
[TestCase(false)]
[TestCase(true)]
public void SocketMessages_Should_ContainOriginalDataIfEnabled(bool enabled)
{
// arrange
var client = new TestSocketClient(options =>
{
options.ReconnectInterval = TimeSpan.Zero;
options.SubOptions.OutputOriginalData = enabled;
});
var socket = client.CreateSocket();
socket.CanConnect = true;
var sub = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
var rstEvent = new ManualResetEvent(false);
string original = null;
// [TestCase(false)]
// [TestCase(true)]
// public void SocketMessages_Should_ContainOriginalDataIfEnabled(bool enabled)
// {
// // arrange
// var client = new TestSocketClient(options =>
// {
// options.ReconnectInterval = TimeSpan.Zero;
// options.SubOptions.OutputOriginalData = enabled;
// });
// var socket = client.CreateSocket();
// socket.CanConnect = true;
// var sub = new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, "");
// var rstEvent = new ManualResetEvent(false);
// string original = null;
client.SubClient.ConnectSocketSub(sub);
var subObj = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) =>
{
original = messageEvent.OriginalData;
rstEvent.Set();
});
sub.AddSubscription(subObj);
var msgToSend = JsonSerializer.Serialize(new { topic = "topic", action = "update", property = "123" });
// client.SubClient.ConnectSocketSub(sub);
// var subObj = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) =>
// {
// original = messageEvent.OriginalData;
// rstEvent.Set();
// });
// sub.AddSubscription(subObj);
// var msgToSend = JsonSerializer.Serialize(new { topic = "topic", action = "update", property = "123" });
// act
socket.InvokeMessage(msgToSend);
rstEvent.WaitOne(1000);
// // act
// socket.InvokeMessage(msgToSend);
// rstEvent.WaitOne(1000);
// assert
Assert.That(original == (enabled ? msgToSend : null));
}
// // assert
// Assert.That(original == (enabled ? msgToSend : null));
// }
[TestCase()]
public void UnsubscribingStream_Should_CloseTheSocket()
{
// arrange
var client = new TestSocketClient(options =>
{
options.ReconnectInterval = TimeSpan.Zero;
});
var socket = client.CreateSocket();
socket.CanConnect = true;
var sub = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
client.SubClient.ConnectSocketSub(sub);
// [TestCase()]
// public void UnsubscribingStream_Should_CloseTheSocket()
// {
// // arrange
// var client = new TestSocketClient(options =>
// {
// options.ReconnectInterval = TimeSpan.Zero;
// });
// var socket = client.CreateSocket();
// socket.CanConnect = true;
// var sub = new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, "");
// client.SubClient.ConnectSocketSub(sub);
var subscription = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) => { });
var ups = new UpdateSubscription(sub, subscription);
sub.AddSubscription(subscription);
// var subscription = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) => { });
// var ups = new UpdateSubscription(sub, subscription);
// sub.AddSubscription(subscription);
// act
client.UnsubscribeAsync(ups).Wait();
// // act
// client.UnsubscribeAsync(ups).Wait();
// assert
Assert.That(socket.Connected == false);
}
// // assert
// Assert.That(socket.Connected == false);
// }
[TestCase()]
public void UnsubscribingAll_Should_CloseAllSockets()
{
// arrange
var client = new TestSocketClient(options => { options.ReconnectInterval = TimeSpan.Zero; });
var socket1 = client.CreateSocket();
var socket2 = client.CreateSocket();
socket1.CanConnect = true;
socket2.CanConnect = true;
var sub1 = new SocketConnection(new TraceLogger(), client.SubClient, socket1, null);
var sub2 = new SocketConnection(new TraceLogger(), client.SubClient, socket2, null);
client.SubClient.ConnectSocketSub(sub1);
client.SubClient.ConnectSocketSub(sub2);
var subscription1 = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) => { });
var subscription2 = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) => { });
// [TestCase()]
// public void UnsubscribingAll_Should_CloseAllSockets()
// {
// // arrange
// var client = new TestSocketClient(options => { options.ReconnectInterval = TimeSpan.Zero; });
// var socket1 = client.CreateSocket();
// var socket2 = client.CreateSocket();
// socket1.CanConnect = true;
// socket2.CanConnect = true;
// var sub1 = new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket1), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, "");
// var sub2 = new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket2), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, "");
// client.SubClient.ConnectSocketSub(sub1);
// client.SubClient.ConnectSocketSub(sub2);
// var subscription1 = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) => { });
// var subscription2 = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) => { });
sub1.AddSubscription(subscription1);
sub2.AddSubscription(subscription2);
var ups1 = new UpdateSubscription(sub1, subscription1);
var ups2 = new UpdateSubscription(sub2, subscription2);
// sub1.AddSubscription(subscription1);
// sub2.AddSubscription(subscription2);
// var ups1 = new UpdateSubscription(sub1, subscription1);
// var ups2 = new UpdateSubscription(sub2, subscription2);
// act
client.UnsubscribeAllAsync().Wait();
// // act
// client.UnsubscribeAllAsync().Wait();
// assert
Assert.That(socket1.Connected == false);
Assert.That(socket2.Connected == false);
}
// // assert
// Assert.That(socket1.Connected == false);
// Assert.That(socket2.Connected == false);
// }
[TestCase()]
public void FailingToConnectSocket_Should_ReturnError()
{
// arrange
var client = new TestSocketClient(options => { options.ReconnectInterval = TimeSpan.Zero; });
var socket = client.CreateSocket();
socket.CanConnect = false;
var sub1 = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
// [TestCase()]
// public void FailingToConnectSocket_Should_ReturnError()
// {
// // arrange
// var client = new TestSocketClient(options => { options.ReconnectInterval = TimeSpan.Zero; });
// var socket = client.CreateSocket();
// socket.CanConnect = false;
// var sub1 = new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, "");
// act
var connectResult = client.SubClient.ConnectSocketSub(sub1);
// // act
// var connectResult = client.SubClient.ConnectSocketSub(sub1);
// assert
ClassicAssert.IsFalse(connectResult.Success);
}
// // assert
// ClassicAssert.IsFalse(connectResult.Success);
// }
[TestCase()]
public async Task ErrorResponse_ShouldNot_ConfirmSubscription()
{
// arrange
var channel = "trade_btcusd";
var client = new TestSocketClient(opt =>
{
opt.OutputOriginalData = true;
opt.SocketSubscriptionsCombineTarget = 1;
});
var socket = client.CreateSocket();
socket.CanConnect = true;
client.SubClient.ConnectSocketSub(new SocketConnection(new TraceLogger(), client.SubClient, socket, "https://test.test"));
// [TestCase()]
// public async Task ErrorResponse_ShouldNot_ConfirmSubscription()
// {
// // arrange
// var channel = "trade_btcusd";
// var client = new TestSocketClient(opt =>
// {
// opt.OutputOriginalData = true;
// opt.SocketSubscriptionsCombineTarget = 1;
// });
// var socket = client.CreateSocket();
// socket.CanConnect = true;
// client.SubClient.ConnectSocketSub(new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, ""));
// act
var sub = client.SubClient.SubscribeToSomethingAsync(channel, onUpdate => {}, ct: default);
socket.InvokeMessage(JsonSerializer.Serialize(new { channel, action = "subscribe", status = "error" }));
await sub;
// // act
// var sub = client.SubClient.SubscribeToSomethingAsync(channel, onUpdate => {}, ct: default);
// socket.InvokeMessage(JsonSerializer.Serialize(new { channel, action = "subscribe", status = "error" }));
// await sub;
// assert
ClassicAssert.IsTrue(client.SubClient.TestSubscription.Status != SubscriptionStatus.Subscribed);
}
// // assert
// ClassicAssert.IsTrue(client.SubClient.TestSubscription.Status != SubscriptionStatus.Subscribed);
// }
[TestCase()]
public async Task SuccessResponse_Should_ConfirmSubscription()
{
// arrange
var channel = "trade_btcusd";
var client = new TestSocketClient(opt =>
{
opt.OutputOriginalData = true;
opt.SocketSubscriptionsCombineTarget = 1;
});
var socket = client.CreateSocket();
socket.CanConnect = true;
client.SubClient.ConnectSocketSub(new SocketConnection(new TraceLogger(), client.SubClient, socket, "https://test.test"));
// [TestCase()]
// public async Task SuccessResponse_Should_ConfirmSubscription()
// {
// // arrange
// var channel = "trade_btcusd";
// var client = new TestSocketClient(opt =>
// {
// opt.OutputOriginalData = true;
// opt.SocketSubscriptionsCombineTarget = 1;
// });
// var socket = client.CreateSocket();
// socket.CanConnect = true;
// client.SubClient.ConnectSocketSub(new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, ""));
// act
var sub = client.SubClient.SubscribeToSomethingAsync(channel, onUpdate => {}, ct: default);
socket.InvokeMessage(JsonSerializer.Serialize(new { channel, action = "subscribe", status = "confirmed" }));
await sub;
// // act
// var sub = client.SubClient.SubscribeToSomethingAsync(channel, onUpdate => {}, ct: default);
// socket.InvokeMessage(JsonSerializer.Serialize(new { channel, action = "subscribe", status = "confirmed" }));
// await sub;
// assert
Assert.That(client.SubClient.TestSubscription.Status == SubscriptionStatus.Subscribed);
}
}
}
// // assert
// Assert.That(client.SubClient.TestSubscription.Status == SubscriptionStatus.Subscribed);
// }
// }
//}
@@ -4,9 +4,7 @@ using System.Text.Json;
using NUnit.Framework;
using System;
using System.Text.Json.Serialization;
using NUnit.Framework.Legacy;
using CryptoExchange.Net.Converters;
using CryptoExchange.Net.Testing.Comparers;
using CryptoExchange.Net.SharedApis;
namespace CryptoExchange.Net.UnitTests
@@ -1,50 +0,0 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Errors;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.Sockets;
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
{
internal class SubResponse
{
[JsonPropertyName("action")]
public string Action { get; set; } = null!;
[JsonPropertyName("channel")]
public string Channel { get; set; } = null!;
[JsonPropertyName("status")]
public string Status { get; set; } = null!;
}
internal class UnsubResponse
{
[JsonPropertyName("action")]
public string Action { get; set; } = null!;
[JsonPropertyName("status")]
public string Status { get; set; } = null!;
}
internal class TestChannelQuery : Query<SubResponse>
{
public TestChannelQuery(string channel, string request, bool authenticated, int weight = 1) : base(request, authenticated, weight)
{
MessageMatcher = MessageMatcher.Create<SubResponse>(request + "-" + channel, HandleMessage);
}
public CallResult<SubResponse> HandleMessage(SocketConnection connection, DataEvent<SubResponse> message)
{
if (!message.Data.Status.Equals("confirmed", StringComparison.OrdinalIgnoreCase))
{
return new CallResult<SubResponse>(new ServerError(ErrorInfo.Unknown with { Message = message.Data.Status }));
}
return message.ToCallResult();
}
}
}
@@ -1,17 +0,0 @@
using CryptoExchange.Net.Sockets;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
{
internal class TestQuery : Query<object>
{
public TestQuery(string identifier, object request, bool authenticated, int weight = 1) : base(request, authenticated, weight)
{
MessageMatcher = MessageMatcher.Create<object>(identifier);
}
}
}
@@ -1,34 +0,0 @@
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.Sockets;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
{
internal class TestSubscription<T> : Subscription<object, object>
{
private readonly Action<DataEvent<T>> _handler;
public TestSubscription(ILogger logger, Action<DataEvent<T>> handler) : base(logger, false)
{
_handler = handler;
MessageMatcher = MessageMatcher.Create<T>("update-topic", DoHandleMessage);
}
public CallResult DoHandleMessage(SocketConnection connection, DataEvent<T> message)
{
_handler.Invoke(message);
return new CallResult(null);
}
protected override Query GetSubQuery(SocketConnection connection) => new TestQuery("sub", new object(), false, 1);
protected override Query GetUnsubQuery(SocketConnection connection) => new TestQuery("unsub", new object(), false, 1);
}
}
@@ -1,34 +0,0 @@
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.Sockets;
using Microsoft.Extensions.Logging;
using Moq;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
{
internal class TestSubscriptionWithResponseCheck<T> : Subscription<SubResponse, UnsubResponse>
{
private readonly Action<DataEvent<T>> _handler;
private readonly string _channel;
public TestSubscriptionWithResponseCheck(string channel, Action<DataEvent<T>> handler) : base(Mock.Of<ILogger>(), false)
{
MessageMatcher = MessageMatcher.Create<T>(channel, DoHandleMessage);
_handler = handler;
_channel = channel;
}
public CallResult DoHandleMessage(SocketConnection connection, DataEvent<T> message)
{
_handler.Invoke(message);
return new CallResult(null);
}
protected override Query GetSubQuery(SocketConnection connection) => new TestChannelQuery(_channel, "subscribe", false, 1);
protected override Query GetUnsubQuery(SocketConnection connection) => new TestChannelQuery(_channel, "unsubscribe", false, 1);
}
}
@@ -1,19 +1,17 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Text;
using System.Text.Json.Serialization;
using System.Text.Json;
using System.Threading.Tasks;
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Clients;
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
using CryptoExchange.Net.Converters.SystemTextJson;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Errors;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.SharedApis;
using CryptoExchange.Net.UnitTests.TestImplementations;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
@@ -46,27 +44,19 @@ namespace CryptoExchange.Net.UnitTests
public class TestSubClient : RestApiClient
{
protected override IRestMessageHandler MessageHandler => throw new NotImplementedException();
public TestSubClient(RestExchangeOptions<TestEnvironment> options, RestApiOptions apiOptions) : base(new TraceLogger(), null, "https://localhost:123", options, apiOptions)
{
}
public CallResult<T> Deserialize<T>(string data)
{
var stream = new MemoryStream(Encoding.UTF8.GetBytes(data));
var accessor = CreateAccessor();
var valid = accessor.Read(stream, true).Result;
if (!valid)
return new CallResult<T>(new ServerError(ErrorInfo.Unknown with { Message = data }));
var deserializeResult = accessor.Deserialize<T>();
return deserializeResult;
return new CallResult<T>(JsonSerializer.Deserialize<T>(data));
}
/// <inheritdoc />
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
public override TimeSpan? GetTimeOffset() => null;
public override TimeSyncInfo GetTimeSyncInfo() => null;
protected override IStreamMessageAccessor CreateAccessor() => new SystemTextJsonStreamMessageAccessor(new System.Text.Json.JsonSerializerOptions());
protected override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(new System.Text.Json.JsonSerializerOptions());
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials) => throw new NotImplementedException();
protected override Task<WebCallResult<DateTime>> GetServerTimestampAsync() => throw new NotImplementedException();
@@ -74,6 +64,8 @@ namespace CryptoExchange.Net.UnitTests
public class TestAuthProvider : AuthenticationProvider
{
public override ApiCredentialsType[] SupportedCredentialTypes => [ApiCredentialsType.Hmac];
public TestAuthProvider(ApiCredentials credentials) : base(credentials)
{
}
@@ -85,4 +77,14 @@ namespace CryptoExchange.Net.UnitTests
public string GetKey() => _credentials.Key;
public string GetSecret() => _credentials.Secret;
}
public class TestEnvironment : TradeEnvironment
{
public string TestAddress { get; }
public TestEnvironment(string name, string url) : base(name)
{
TestAddress = url;
}
}
}
@@ -13,12 +13,13 @@ using CryptoExchange.Net.Authentication;
using System.Collections.Generic;
using Microsoft.Extensions.Logging;
using CryptoExchange.Net.Clients;
using CryptoExchange.Net.SharedApis;
using Microsoft.Extensions.Options;
using System.Linq;
using CryptoExchange.Net.Converters.SystemTextJson;
using System.Text.Json.Serialization;
using CryptoExchange.Net.Objects.Errors;
using System.Net.Http.Headers;
using CryptoExchange.Net.SharedApis;
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
namespace CryptoExchange.Net.UnitTests.TestImplementations
{
@@ -49,15 +50,15 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
var response = new Mock<IResponse>();
response.Setup(c => c.IsSuccessStatusCode).Returns(true);
response.Setup(c => c.GetResponseStreamAsync()).Returns(Task.FromResult((Stream)responseStream));
response.Setup(c => c.GetResponseStreamAsync(It.IsAny<CancellationToken>())).Returns(Task.FromResult((Stream)responseStream));
var headers = new Dictionary<string, string[]>();
var headers = new HttpRequestMessage().Headers;
var request = new Mock<IRequest>();
request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com"));
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Returns(Task.FromResult(response.Object));
request.Setup(c => c.SetContent(It.IsAny<string>(), It.IsAny<string>())).Callback(new Action<string, string>((content, type) => { request.Setup(r => r.Content).Returns(content); }));
request.Setup(c => c.AddHeader(It.IsAny<string>(), It.IsAny<string>())).Callback<string, string>((key, val) => headers.Add(key, new string[] { val }));
request.Setup(c => c.GetHeaders()).Returns(() => headers.ToArray());
request.Setup(c => c.GetHeaders()).Returns(() => headers);
var factory = Mock.Get(Api1.RequestFactory);
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
@@ -86,7 +87,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
var request = new Mock<IRequest>();
request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com"));
request.Setup(c => c.GetHeaders()).Returns(new KeyValuePair<string, string[]>[0]);
request.Setup(c => c.GetHeaders()).Returns(new HttpRequestMessage().Headers);
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Throws(we);
var factory = Mock.Get(Api1.RequestFactory);
@@ -108,14 +109,14 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
var response = new Mock<IResponse>();
response.Setup(c => c.IsSuccessStatusCode).Returns(false);
response.Setup(c => c.GetResponseStreamAsync()).Returns(Task.FromResult((Stream)responseStream));
response.Setup(c => c.GetResponseStreamAsync(It.IsAny<CancellationToken>())).Returns(Task.FromResult((Stream)responseStream));
var headers = new List<KeyValuePair<string, string[]>>();
var request = new Mock<IRequest>();
request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com"));
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Returns(Task.FromResult(response.Object));
request.Setup(c => c.AddHeader(It.IsAny<string>(), It.IsAny<string>())).Callback<string, string>((key, val) => headers.Add(new KeyValuePair<string, string[]>(key, new string[] { val })));
request.Setup(c => c.GetHeaders()).Returns(headers.ToArray());
request.Setup(c => c.GetHeaders()).Returns(new HttpRequestMessage().Headers);
var factory = Mock.Get(Api1.RequestFactory);
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
@@ -131,6 +132,8 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
public class TestRestApi1Client : RestApiClient
{
protected override IRestMessageHandler MessageHandler { get; } = new TestRestMessageHandler();
public TestRestApi1Client(TestClientOptions options) : base(new TraceLogger(), null, "https://localhost:123", options, options.Api1Options)
{
RequestFactory = new Mock<IRequestFactory>().Object;
@@ -139,7 +142,6 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
/// <inheritdoc />
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
protected override IStreamMessageAccessor CreateAccessor() => new SystemTextJsonStreamMessageAccessor(new System.Text.Json.JsonSerializerOptions() { TypeInfoResolver = new TestSerializerContext() });
protected override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(new System.Text.Json.JsonSerializerOptions());
public async Task<CallResult<T>> Request<T>(CancellationToken ct = default) where T : class
@@ -157,11 +159,6 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
ParameterPositions[method] = position;
}
public override TimeSpan? GetTimeOffset()
{
throw new NotImplementedException();
}
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials)
=> new TestAuthProvider(credentials);
@@ -169,21 +166,17 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
{
throw new NotImplementedException();
}
public override TimeSyncInfo GetTimeSyncInfo()
{
throw new NotImplementedException();
}
}
public class TestRestApi2Client : RestApiClient
{
protected override IRestMessageHandler MessageHandler { get; } = new TestRestMessageHandler();
public TestRestApi2Client(TestClientOptions options) : base(new TraceLogger(), null, "https://localhost:123", options, options.Api2Options)
{
RequestFactory = new Mock<IRequestFactory>().Object;
}
protected override IStreamMessageAccessor CreateAccessor() => new SystemTextJsonStreamMessageAccessor(new System.Text.Json.JsonSerializerOptions());
protected override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(new System.Text.Json.JsonSerializerOptions());
/// <inheritdoc />
@@ -193,19 +186,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
{
return await SendAsync<T>("http://www.test.com", new RequestDefinition("/", HttpMethod.Get) { Weight = 0 }, null, ct);
}
protected override Error ParseErrorResponse(int httpStatusCode, KeyValuePair<string, string[]>[] responseHeaders, IMessageAccessor accessor, Exception exception)
{
var errorData = accessor.Deserialize<TestError>();
return new ServerError(errorData.Data.ErrorCode, GetErrorInfo(errorData.Data.ErrorCode, errorData.Data.ErrorMessage));
}
public override TimeSpan? GetTimeOffset()
{
throw new NotImplementedException();
}
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials)
=> new TestAuthProvider(credentials);
@@ -214,10 +195,6 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
throw new NotImplementedException();
}
public override TimeSyncInfo GetTimeSyncInfo()
{
throw new NotImplementedException();
}
}
public class TestError
@@ -0,0 +1,32 @@
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
using CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Errors;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.UnitTests.TestImplementations
{
internal class TestRestMessageHandler : JsonRestMessageHandler
{
private ErrorMapping _errorMapping = new ErrorMapping([]);
public override JsonSerializerOptions Options => new JsonSerializerOptions();
public override async ValueTask<Error> ParseErrorResponse(int httpStatusCode, HttpResponseHeaders responseHeaders, Stream responseStream)
{
var result = await GetJsonDocument(responseStream).ConfigureAwait(false);
if (result.Item1 != null)
return result.Item1;
var errorData = result.Item2.Deserialize<TestError>();
return new ServerError(errorData.ErrorCode, _errorMapping.GetErrorInfo(errorData.ErrorCode.ToString(), errorData.ErrorMessage));
}
}
}
@@ -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;
// }
//}
@@ -1,140 +0,0 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Clients;
using CryptoExchange.Net.Converters.MessageParsing;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.Sockets;
using CryptoExchange.Net.UnitTests.TestImplementations.Sockets;
using Microsoft.Extensions.Logging;
using Moq;
using CryptoExchange.Net.Testing.Implementations;
using CryptoExchange.Net.SharedApis;
using Microsoft.Extensions.Options;
using CryptoExchange.Net.Converters.SystemTextJson;
using System.Net.WebSockets;
namespace CryptoExchange.Net.UnitTests.TestImplementations
{
internal class TestSocketClient: BaseSocketClient
{
public TestSubSocketClient SubClient { get; }
/// <summary>
/// Create a new instance of KucoinSocketClient
/// </summary>
/// <param name="optionsFunc">Configure the options to use for this client</param>
public TestSocketClient(Action<TestSocketOptions> optionsDelegate = null)
: this(Options.Create(ApplyOptionsDelegate(optionsDelegate)), null)
{
}
public TestSocketClient(IOptions<TestSocketOptions> options, ILoggerFactory loggerFactory = null) : base(loggerFactory, "Test")
{
Initialize(options.Value);
SubClient = AddApiClient(new TestSubSocketClient(options.Value, options.Value.SubOptions));
SubClient.SocketFactory = new Mock<IWebsocketFactory>().Object;
Mock.Get(SubClient.SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<ILogger>(), It.IsAny<WebSocketParameters>())).Returns(new TestSocket("https://test.com"));
}
public TestSocket CreateSocket()
{
Mock.Get(SubClient.SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<ILogger>(), It.IsAny<WebSocketParameters>())).Returns(new TestSocket("https://test.com"));
return (TestSocket)SubClient.CreateSocketInternal("https://localhost:123/");
}
}
public class TestEnvironment : TradeEnvironment
{
public string TestAddress { get; }
public TestEnvironment(string name, string url) : base(name)
{
TestAddress = url;
}
}
public class TestSocketOptions: SocketExchangeOptions<TestEnvironment>
{
public static TestSocketOptions Default = new TestSocketOptions
{
Environment = new TestEnvironment("Live", "https://test.test")
};
/// <summary>
/// ctor
/// </summary>
public TestSocketOptions()
{
Default?.Set(this);
}
public SocketApiOptions SubOptions { get; set; } = new SocketApiOptions();
internal TestSocketOptions Set(TestSocketOptions targetOptions)
{
targetOptions = base.Set<TestSocketOptions>(targetOptions);
targetOptions.SubOptions = SubOptions.Set(targetOptions.SubOptions);
return targetOptions;
}
}
public class TestSubSocketClient : SocketApiClient
{
private MessagePath _channelPath = MessagePath.Get().Property("channel");
private MessagePath _actionPath = MessagePath.Get().Property("action");
private MessagePath _topicPath = MessagePath.Get().Property("topic");
public Subscription TestSubscription { get; private set; } = null;
public TestSubSocketClient(TestSocketOptions options, SocketApiOptions apiOptions) : base(new TraceLogger(), options.Environment.TestAddress, options, apiOptions)
{
}
protected internal override IByteMessageAccessor CreateAccessor(WebSocketMessageType type) => new SystemTextJsonByteMessageAccessor(new System.Text.Json.JsonSerializerOptions());
protected internal override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(new System.Text.Json.JsonSerializerOptions());
/// <inheritdoc />
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
internal IWebsocket CreateSocketInternal(string address)
{
return CreateSocket(address);
}
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials)
=> new TestAuthProvider(credentials);
public CallResult ConnectSocketSub(SocketConnection sub)
{
return ConnectSocketAsync(sub, default).Result;
}
public override string GetListenerIdentifier(IMessageAccessor message)
{
if (!message.IsValid)
{
return "topic";
}
var id = message.GetValue<string>(_channelPath);
id ??= message.GetValue<string>(_topicPath);
return message.GetValue<string>(_actionPath) + "-" + id;
}
public Task<CallResult<UpdateSubscription>> SubscribeToSomethingAsync(string channel, Action<DataEvent<string>> onUpdate, CancellationToken ct)
{
TestSubscription = new TestSubscriptionWithResponseCheck<string>(channel, onUpdate);
return SubscribeAsync(TestSubscription, ct);
}
}
}
@@ -1,10 +1,6 @@
using CryptoExchange.Net.UnitTests.TestImplementations;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace CryptoExchange.Net.UnitTests
{
@@ -1,7 +1,6 @@
using System;
using System.IO;
using CryptoExchange.Net.Converters.SystemTextJson;
using CryptoExchange.Net.Converters.MessageParsing;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Authentication
{
@@ -48,6 +47,48 @@ namespace CryptoExchange.Net.Authentication
Pass = pass;
}
/// <summary>
/// Create API credentials using an API key and secret generated by the server
/// </summary>
public static ApiCredentials HmacCredentials(string apiKey, string apiSecret, string? pass)
{
return new ApiCredentials(apiKey, apiSecret, pass, ApiCredentialsType.Hmac);
}
/// <summary>
/// Create API credentials using an API key and an RSA private key in PEM format
/// </summary>
public static ApiCredentials RsaPemCredentials(string apiKey, string privateKey)
{
return new ApiCredentials(apiKey, privateKey, credentialType: ApiCredentialsType.RsaPem);
}
/// <summary>
/// Create API credentials using an API key and an RSA private key in XML format
/// </summary>
public static ApiCredentials RsaXmlCredentials(string apiKey, string privateKey)
{
return new ApiCredentials(apiKey, privateKey, credentialType: ApiCredentialsType.RsaXml);
}
/// <summary>
/// Create API credentials using an API key and an Ed25519 private key
/// </summary>
public static ApiCredentials Ed25519Credentials(string apiKey, string privateKey)
{
return new ApiCredentials(apiKey, privateKey, credentialType: ApiCredentialsType.Ed25519);
}
/// <summary>
/// Load a key from a file
/// </summary>
public static string ReadFromFile(string path)
{
using var fileStream = File.OpenRead(path);
using var streamReader = new StreamReader(fileStream);
return streamReader.ReadToEnd();
}
/// <summary>
/// Copy the credentials
/// </summary>
@@ -16,6 +16,10 @@
/// <summary>
/// Rsa keys credentials in pem/base64 format. Only available for .NetStandard 2.1 and up, use xml format for lower.
/// </summary>
RsaPem
RsaPem,
/// <summary>
/// Ed25519 keys credentials
/// </summary>
Ed25519
}
}
@@ -2,12 +2,17 @@
using CryptoExchange.Net.Converters.SystemTextJson;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
#if NET8_0_OR_GREATER
using NSec.Cryptography;
#endif
using System;
using System.Collections.Generic;
using System.Linq;
using System.Globalization;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using CryptoExchange.Net.Sockets;
using CryptoExchange.Net.Sockets.Default;
namespace CryptoExchange.Net.Authentication
{
@@ -18,6 +23,11 @@ namespace CryptoExchange.Net.Authentication
{
internal IAuthTimeProvider TimeProvider { get; set; } = new AuthTimeProvider();
/// <summary>
/// The supported credential types
/// </summary>
public abstract ApiCredentialsType[] SupportedCredentialTypes { get; }
/// <summary>
/// Provided credentials
/// </summary>
@@ -28,6 +38,13 @@ namespace CryptoExchange.Net.Authentication
/// </summary>
protected byte[] _sBytes;
#if NET8_0_OR_GREATER
/// <summary>
/// The Ed25519 private key
/// </summary>
protected Key? Ed25519Key;
#endif
/// <summary>
/// Get the API key of the current credentials
/// </summary>
@@ -46,17 +63,35 @@ namespace CryptoExchange.Net.Authentication
if (credentials.Key == null || credentials.Secret == null)
throw new ArgumentException("ApiKey/Secret needed");
if (!SupportedCredentialTypes.Any(x => x == credentials.CredentialType))
throw new ArgumentException($"Credential type {credentials.CredentialType} not supported");
if (credentials.CredentialType == ApiCredentialsType.Ed25519)
{
#if !NET8_0_OR_GREATER
throw new ArgumentException($"Credential type Ed25519 only supported on Net8.0 or newer");
#endif
}
_credentials = credentials;
_sBytes = Encoding.UTF8.GetBytes(credentials.Secret);
}
/// <summary>
/// Authenticate a request
/// Authenticate a REST request
/// </summary>
/// <param name="apiClient">The Api client sending the request</param>
/// <param name="apiClient">The API client sending the request</param>
/// <param name="requestConfig">The request configuration</param>
public abstract void ProcessRequest(RestApiClient apiClient, RestRequestConfiguration requestConfig);
/// <summary>
/// Get an authentication query for a websocket
/// </summary>
/// <param name="apiClient">The API client sending the request</param>
/// <param name="connection">The connection to authenticate</param>
/// <param name="context">Optional context required for creating the authentication query</param>
public virtual Query? GetAuthenticationQuery(SocketApiClient apiClient, SocketConnection connection, Dictionary<string, object?>? context = null) => null;
/// <summary>
/// SHA256 sign the data and return the bytes
/// </summary>
@@ -349,6 +384,36 @@ namespace CryptoExchange.Net.Authentication
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
}
/// <summary>
/// Ed25519 sign the data
/// </summary>
public string SignEd25519(string data, SignOutputType? outputType = null)
=> SignEd25519(Encoding.ASCII.GetBytes(data), outputType);
/// <summary>
/// Ed25519 sign the data
/// </summary>
public string SignEd25519(byte[] data, SignOutputType? outputType = null)
{
#if NET8_0_OR_GREATER
if (Ed25519Key == null)
{
var key = _credentials.Secret!
.Replace("\n", "")
.Replace("-----BEGIN PRIVATE KEY-----", "")
.Replace("-----END PRIVATE KEY-----", "")
.Trim();
var keyBytes = Convert.FromBase64String(key);
Ed25519Key = Key.Import(SignatureAlgorithm.Ed25519, keyBytes, KeyBlobFormat.PkixPrivateKey);
}
var resultBytes = SignatureAlgorithm.Ed25519.Sign(Ed25519Key, data);
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
#else
throw new InvalidOperationException();
#endif
}
private RSA CreateRSA()
{
var rsa = RSA.Create();
@@ -387,6 +452,14 @@ namespace CryptoExchange.Net.Authentication
/// <param name="buff"></param>
/// <returns></returns>
protected static string BytesToHexString(byte[] buff)
=> BytesToHexString(new ArraySegment<byte>(buff));
/// <summary>
/// Convert byte array to hex string
/// </summary>
/// <param name="buff"></param>
/// <returns></returns>
protected static string BytesToHexString(ArraySegment<byte> buff)
{
#if NET9_0_OR_GREATER
return Convert.ToHexString(buff);
@@ -398,6 +471,26 @@ namespace CryptoExchange.Net.Authentication
#endif
}
/// <summary>
/// Convert a hex encoded string to byte array
/// </summary>
/// <param name="hexString"></param>
/// <returns></returns>
protected static byte[] HexToBytesString(string hexString)
{
if (hexString.StartsWith("0x"))
hexString = hexString.Substring(2);
byte[] bytes = new byte[hexString.Length / 2];
for (int i = 0; i < hexString.Length; i += 2)
{
string hexSubstring = hexString.Substring(i, 2);
bytes[i / 2] = Convert.ToByte(hexSubstring, 16);
}
return bytes;
}
/// <summary>
/// Convert byte array to base64 string
/// </summary>
@@ -411,32 +504,53 @@ namespace CryptoExchange.Net.Authentication
/// <summary>
/// Get current timestamp including the time sync offset from the api client
/// </summary>
/// <param name="apiClient"></param>
/// <returns></returns>
protected DateTime GetTimestamp(RestApiClient apiClient)
protected DateTime GetTimestamp(RestApiClient apiClient, bool includeOneSecondOffset = true)
{
return TimeProvider.GetTime().Add(apiClient.GetTimeOffset() ?? TimeSpan.Zero)!;
var result = TimeProvider.GetTime().Add(TimeOffsetManager.GetRestOffset(apiClient.ClientName) ?? TimeSpan.Zero)!;
if (includeOneSecondOffset)
result = result.AddSeconds(-1);
return result;
}
/// <summary>
/// Get current timestamp including the time sync offset from the api client
/// </summary>
protected DateTime GetTimestamp(SocketApiClient apiClient, bool includeOneSecondOffset = true)
{
var timestamp = TimeProvider.GetTime();
if(apiClient.ApiOptions.AutoTimestamp ?? apiClient.ClientOptions.AutoTimestamp)
timestamp = timestamp.Add(-TimeOffsetManager.GetSocketOffset(apiClient.ClientName) ?? TimeSpan.Zero)!;
if (includeOneSecondOffset)
timestamp = timestamp.AddSeconds(-1);
return timestamp;
}
/// <summary>
/// Get millisecond timestamp as a string including the time sync offset from the api client
/// </summary>
/// <param name="apiClient"></param>
/// <returns></returns>
protected string GetMillisecondTimestamp(RestApiClient apiClient)
{
return DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient)).Value.ToString(CultureInfo.InvariantCulture);
}
protected string GetMillisecondTimestamp(RestApiClient apiClient, bool includeOneSecondOffset = true)
=> DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient, includeOneSecondOffset)).Value.ToString(CultureInfo.InvariantCulture);
/// <summary>
/// Get millisecond timestamp as a string including the time sync offset from the api client
/// </summary>
protected string GetMillisecondTimestamp(SocketApiClient apiClient, bool includeOneSecondOffset = true)
=> DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient, includeOneSecondOffset)).Value.ToString(CultureInfo.InvariantCulture);
/// <summary>
/// Get millisecond timestamp as a long including the time sync offset from the api client
/// </summary>
/// <param name="apiClient"></param>
/// <returns></returns>
protected long GetMillisecondTimestampLong(RestApiClient apiClient)
{
return DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient)).Value;
}
protected long GetMillisecondTimestampLong(RestApiClient apiClient, bool includeOneSecondOffset = true)
=> DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient, includeOneSecondOffset)).Value;
/// <summary>
/// Get millisecond timestamp as a long including the time sync offset from the api client
/// </summary>
protected long GetMillisecondTimestampLong(SocketApiClient apiClient, bool includeOneSecondOffset = true)
=> DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient, includeOneSecondOffset)).Value;
/// <summary>
/// Return the serialized request body
@@ -449,7 +563,7 @@ namespace CryptoExchange.Net.Authentication
if (serializer is not IStringMessageSerializer stringSerializer)
throw new InvalidOperationException("Non-string message serializer can't get serialized request body");
if (parameters.Count == 1 && parameters.TryGetValue(Constants.BodyPlaceHolderKey, out object? value))
if (parameters?.Count == 1 && parameters.TryGetValue(Constants.BodyPlaceHolderKey, out object? value))
return stringSerializer.Serialize(value);
else
return stringSerializer.Serialize(parameters);
@@ -1,13 +1,18 @@
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Threading;
namespace CryptoExchange.Net.Caching
{
internal class MemoryCache
{
private readonly ConcurrentDictionary<string, CacheItem> _cache = new ConcurrentDictionary<string, CacheItem>();
#if NET9_0_OR_GREATER
private readonly Lock _lock = new Lock();
#else
private readonly object _lock = new object();
#endif
/// <summary>
/// Add a new cache entry. Will override an existing entry if it already exists
+21 -3
View File
@@ -1,8 +1,6 @@
using System;
using System.Collections.Generic;
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Interfaces.Clients;
using CryptoExchange.Net.Objects.Errors;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.SharedApis;
@@ -15,6 +13,11 @@ namespace CryptoExchange.Net.Clients
/// </summary>
public abstract class BaseApiClient : IDisposable, IBaseApiClient
{
/// <summary>
/// Client name
/// </summary>
protected string? _clientName;
/// <summary>
/// Logger
/// </summary>
@@ -25,6 +28,21 @@ namespace CryptoExchange.Net.Clients
/// </summary>
protected bool _disposing;
/// <summary>
/// Name of the client
/// </summary>
protected internal string ClientName
{
get
{
if (_clientName != null)
return _clientName;
_clientName = GetType().Name;
return _clientName;
}
}
/// <summary>
/// The authentication provider for this API client. (null if no credentials are set)
/// </summary>
+12 -3
View File
@@ -1,9 +1,9 @@
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Objects.Options;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using System;
using System.Collections.Generic;
using System.Threading;
namespace CryptoExchange.Net.Clients
{
@@ -39,6 +39,11 @@ namespace CryptoExchange.Net.Clients
/// </summary>
public string Exchange { get; }
/// <summary>
/// Whether client is disposed
/// </summary>
public bool Disposed { get; private set; }
/// <summary>
/// Api clients in this client
/// </summary>
@@ -49,7 +54,11 @@ namespace CryptoExchange.Net.Clients
/// </summary>
protected internal ILogger _logger;
#if NET9_0_OR_GREATER
private readonly Lock _versionLock = new Lock();
#else
private readonly object _versionLock = new object();
#endif
private Version _exchangeVersion;
/// <summary>
@@ -102,7 +111,6 @@ namespace CryptoExchange.Net.Clients
if (ClientOptions == null)
throw new InvalidOperationException("Client should have called Initialize before adding API clients");
_logger.Log(LogLevel.Trace, $" {apiClient.GetType().Name}, base address: {apiClient.BaseAddress}");
ApiClients.Add(apiClient);
return apiClient;
}
@@ -122,7 +130,8 @@ namespace CryptoExchange.Net.Clients
/// </summary>
public virtual void Dispose()
{
_logger.Log(LogLevel.Debug, "Disposing client");
Disposed = true;
foreach (var client in ApiClients)
client.Dispose();
}
+2 -2
View File
@@ -1,5 +1,5 @@
using System.Linq;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Interfaces.Clients;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
@@ -22,7 +22,7 @@ namespace CryptoExchange.Net.Clients
{
_logger = loggerFactory?.CreateLogger(name + ".RestClient") ?? NullLoggerFactory.Instance.CreateLogger(name);
LibraryHelpers.StaticLogger = loggerFactory?.CreateLogger(name);
LibraryHelpers.StaticLogger = loggerFactory?.CreateLogger("CryptoExchange");
}
}
}
+11 -8
View File
@@ -1,14 +1,14 @@
using System;
using CryptoExchange.Net.Interfaces.Clients;
using CryptoExchange.Net.Logging.Extensions;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.Objects.Sockets;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Linq;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Logging.Extensions;
using CryptoExchange.Net.Objects.Sockets;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
namespace CryptoExchange.Net.Clients
{
@@ -30,6 +30,9 @@ namespace CryptoExchange.Net.Clients
public int CurrentSubscriptions => ApiClients.OfType<SocketApiClient>().Sum(s => s.CurrentSubscriptions);
/// <inheritdoc />
public double IncomingKbps => ApiClients.OfType<SocketApiClient>().Sum(s => s.IncomingKbps);
/// <inheritdoc />
public new SocketExchangeOptions ClientOptions => (SocketExchangeOptions)base.ClientOptions;
#endregion
/// <summary>
@@ -41,7 +44,7 @@ namespace CryptoExchange.Net.Clients
{
_logger = loggerFactory?.CreateLogger(name + ".SocketClient") ?? NullLoggerFactory.Instance.CreateLogger(name);
LibraryHelpers.StaticLogger = loggerFactory?.CreateLogger(name);
LibraryHelpers.StaticLogger = loggerFactory?.CreateLogger("CryptoExchange");
}
/// <summary>
@@ -1,8 +1,5 @@
using CryptoExchange.Net.Interfaces;
using Microsoft.Extensions.DependencyInjection;
using CryptoExchange.Net.Interfaces.Clients;
using System;
using System.Collections.Generic;
using System.Linq;
namespace CryptoExchange.Net.Clients
{
@@ -1,4 +1,4 @@
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Interfaces.Clients;
using System;
namespace CryptoExchange.Net.Clients
+209 -174
View File
@@ -1,14 +1,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using CryptoExchange.Net.Caching;
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Interfaces.Clients;
using CryptoExchange.Net.Logging.Extensions;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Errors;
@@ -17,6 +10,17 @@ using CryptoExchange.Net.RateLimiting;
using CryptoExchange.Net.RateLimiting.Interfaces;
using CryptoExchange.Net.Requests;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Clients
{
@@ -28,12 +32,6 @@ namespace CryptoExchange.Net.Clients
/// <inheritdoc />
public IRequestFactory RequestFactory { get; set; } = new RequestFactory();
/// <inheritdoc />
public abstract TimeSyncInfo? GetTimeSyncInfo();
/// <inheritdoc />
public abstract TimeSpan? GetTimeOffset();
/// <inheritdoc />
public int TotalRequestsMade { get; set; }
@@ -90,6 +88,11 @@ namespace CryptoExchange.Net.Clients
/// </summary>
private readonly static MemoryCache _cache = new MemoryCache();
/// <summary>
/// The message handler
/// </summary>
protected abstract IRestMessageHandler MessageHandler { get; }
/// <summary>
/// ctor
/// </summary>
@@ -106,15 +109,11 @@ namespace CryptoExchange.Net.Clients
options,
apiOptions)
{
TimeOffsetManager.RegisterRestApi(ClientName);
RequestFactory.Configure(options, httpClient);
}
/// <summary>
/// Create a message accessor instance
/// </summary>
/// <returns></returns>
protected abstract IStreamMessageAccessor CreateAccessor();
/// <summary>
/// Create a serializer instance
/// </summary>
@@ -204,6 +203,13 @@ namespace CryptoExchange.Net.Clients
int? weightSingleLimiter = null,
string? rateLimitKeySuffix = null)
{
var requestId = ExchangeHelpers.NextId();
if (definition.Authenticated && AuthenticationProvider == null)
{
_logger.RestApiNoApiCredentials(requestId, definition.Path);
return new WebCallResult<T>(new NoApiCredentialsError());
}
string? cacheKey = null;
if (ShouldCache(definition))
{
@@ -224,11 +230,19 @@ namespace CryptoExchange.Net.Clients
while (true)
{
currentTry++;
var requestId = ExchangeHelpers.NextId();
var prepareResult = await PrepareAsync(requestId, baseAddress, definition, cancellationToken, additionalHeaders, weight, weightSingleLimiter, rateLimitKeySuffix).ConfigureAwait(false);
if (!prepareResult)
return new WebCallResult<T>(prepareResult.Error!);
await CheckTimeSync(requestId, definition).ConfigureAwait(false);
var error = await RateLimitAsync(
baseAddress,
requestId,
definition,
weight ?? definition.Weight,
cancellationToken,
weightSingleLimiter,
rateLimitKeySuffix).ConfigureAwait(false);
if (error != null)
return new WebCallResult<T>(error);
var request = CreateRequest(
requestId,
@@ -237,16 +251,24 @@ namespace CryptoExchange.Net.Clients
uriParameters,
bodyParameters,
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)}]")));
if (_logger.IsEnabled(LogLevel.Debug))
_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++;
var result = await GetResponseAsync<T>(definition, request, definition.RateLimitGate, cancellationToken).ConfigureAwait(false);
var result = await GetResponseAsync2<T>(definition, request, definition.RateLimitGate, cancellationToken).ConfigureAwait(false);
if (result.Error is not CancellationRequestedError)
{
var originalData = OutputOriginalData ? result.OriginalData : "[Data only available when OutputOriginal = true]";
if (!result)
{
_logger.RestApiErrorReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), result.Error?.ToString(), originalData, result.Error?.Exception);
}
else
_logger.RestApiResponseReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), originalData);
{
if (_logger.IsEnabled(LogLevel.Debug))
_logger.RestApiResponseReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), originalData);
}
}
else
{
@@ -267,54 +289,19 @@ namespace CryptoExchange.Net.Clients
}
/// <summary>
/// Prepare before sending a request. Sync time between client and server and check rate limits
/// Check rate limits for the request
/// </summary>
/// <param name="requestId">Request id</param>
/// <param name="baseAddress">Host and schema</param>
/// <param name="definition">Request definition</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <param name="additionalHeaders">Additional headers for this request</param>
/// <param name="weight">Override the request weight for this request</param>
/// <param name="weightSingleLimiter">Specify the weight to apply to the individual rate limit guard for this request</param>
/// <param name="rateLimitKeySuffix">An additional optional suffix for the key selector</param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
protected virtual async Task<CallResult> PrepareAsync(
protected virtual async ValueTask<Error?> RateLimitAsync(
string host,
int requestId,
string baseAddress,
RequestDefinition definition,
int weight,
CancellationToken cancellationToken,
Dictionary<string, string>? additionalHeaders = null,
int? weight = null,
int? weightSingleLimiter = null,
string? rateLimitKeySuffix = null)
{
// Time sync
if (definition.Authenticated)
{
if (AuthenticationProvider == null)
{
_logger.RestApiNoApiCredentials(requestId, definition.Path);
return new CallResult<IRequest>(new NoApiCredentialsError());
}
var syncTask = SyncTimeAsync();
var timeSyncInfo = GetTimeSyncInfo();
if (timeSyncInfo != null && timeSyncInfo.TimeSyncState.LastSyncTime == default)
{
// Initially with first request we'll need to wait for the time syncing, if it's not the first request we can just continue
var syncTimeResult = await syncTask.ConfigureAwait(false);
if (!syncTimeResult)
{
_logger.RestApiFailedToSyncTime(requestId, syncTimeResult.Error!.ToString());
return syncTimeResult.AsDataless();
}
}
}
// Rate limiting
var requestWeight = weight ?? definition.Weight;
var requestWeight = weight;
if (requestWeight != 0)
{
if (definition.RateLimitGate == null)
@@ -322,9 +309,9 @@ namespace CryptoExchange.Net.Clients
if (ClientOptions.RateLimiterEnabled)
{
var limitResult = await definition.RateLimitGate.ProcessAsync(_logger, requestId, RateLimitItemType.Request, definition, baseAddress, AuthenticationProvider?._credentials.Key, requestWeight, ClientOptions.RateLimitingBehaviour, rateLimitKeySuffix, cancellationToken).ConfigureAwait(false);
var limitResult = await definition.RateLimitGate.ProcessAsync(_logger, requestId, RateLimitItemType.Request, definition, host, AuthenticationProvider?._credentials.Key, requestWeight, ClientOptions.RateLimitingBehaviour, rateLimitKeySuffix, cancellationToken).ConfigureAwait(false);
if (!limitResult)
return new CallResult(limitResult.Error!);
return limitResult.Error!;
}
}
@@ -337,13 +324,13 @@ namespace CryptoExchange.Net.Clients
if (ClientOptions.RateLimiterEnabled)
{
var singleRequestWeight = weightSingleLimiter ?? 1;
var limitResult = await definition.RateLimitGate.ProcessSingleAsync(_logger, requestId, definition.LimitGuard, RateLimitItemType.Request, definition, baseAddress, AuthenticationProvider?._credentials.Key, singleRequestWeight, ClientOptions.RateLimitingBehaviour, rateLimitKeySuffix, cancellationToken).ConfigureAwait(false);
var limitResult = await definition.RateLimitGate.ProcessSingleAsync(_logger, requestId, definition.LimitGuard, RateLimitItemType.Request, definition, host, AuthenticationProvider?._credentials.Key, singleRequestWeight, ClientOptions.RateLimitingBehaviour, rateLimitKeySuffix, cancellationToken).ConfigureAwait(false);
if (!limitResult)
return new CallResult(limitResult.Error!);
return limitResult.Error!;
}
}
return CallResult.SuccessResult;
return null;
}
/// <summary>
@@ -367,9 +354,9 @@ namespace CryptoExchange.Net.Clients
var requestConfiguration = new RestRequestConfiguration(
definition,
baseAddress,
uriParameters == null ? new Dictionary<string, object>() : CreateParameterDictionary(uriParameters),
bodyParameters == null ? new Dictionary<string, object>() : CreateParameterDictionary(bodyParameters),
new Dictionary<string, string>(additionalHeaders ?? []),
uriParameters == null ? null : CreateParameterDictionary(uriParameters),
bodyParameters == null ? null : CreateParameterDictionary(bodyParameters),
additionalHeaders,
definition.ArraySerialization ?? ArraySerialization,
definition.ParameterPosition ?? ParameterPositions[definition.Method],
definition.RequestBodyFormat ?? RequestBodyFormat);
@@ -389,14 +376,18 @@ namespace CryptoExchange.Net.Clients
var uri = new Uri(baseAddress.AppendPath(definition.Path) + queryString);
var request = RequestFactory.Create(ClientOptions.HttpVersion, definition.Method, uri, requestId);
request.Accept = Constants.JsonContentHeader;
request.Accept = MessageHandler.AcceptHeader;
foreach (var header in requestConfiguration.Headers)
request.AddHeader(header.Key, header.Value);
if (requestConfiguration.Headers != null)
{
foreach (var header in requestConfiguration.Headers)
request.AddHeader(header.Key, header.Value);
}
foreach (var header in StandardRequestHeaders)
{
// Only add it if it isn't overwritten
requestConfiguration.Headers ??= new Dictionary<string, string>();
if (!requestConfiguration.Headers.ContainsKey(header.Key))
request.AddHeader(header.Key, header.Value);
}
@@ -429,7 +420,7 @@ namespace CryptoExchange.Net.Clients
/// <param name="gate">The ratelimit gate used</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns></returns>
protected virtual async Task<WebCallResult<T>> GetResponseAsync<T>(
protected virtual async Task<WebCallResult<T>> GetResponseAsync2<T>(
RequestDefinition requestDefinition,
IRequest request,
IRateLimitGate? gate,
@@ -438,24 +429,41 @@ namespace CryptoExchange.Net.Clients
var sw = Stopwatch.StartNew();
Stream? responseStream = null;
IResponse? response = null;
IStreamMessageAccessor? accessor = null;
try
{
response = await request.GetResponseAsync(cancellationToken).ConfigureAwait(false);
sw.Stop();
responseStream = await response.GetResponseStreamAsync().ConfigureAwait(false);
responseStream = await response.GetResponseStreamAsync(cancellationToken).ConfigureAwait(false);
string? originalData = null;
var outputOriginalData = ApiOptions.OutputOriginalData ?? ClientOptions.OutputOriginalData;
if (outputOriginalData || MessageHandler.RequiresSeekableStream || !response.IsSuccessStatusCode)
{
// Create a seekable stream from the response stream if:
// 1. We need to output the original data
// 2. The message handler requires a seekable stream
// 3. The response indicates error and we want to output (part of) the returned data
responseStream = await CopyStreamAsync(responseStream).ConfigureAwait(false);
using var reader = new StreamReader(responseStream, Encoding.UTF8, false, 4096, true);
if (outputOriginalData)
{
originalData = await reader.ReadToEndAsync().ConfigureAwait(false);
responseStream.Position = 0;
}
}
accessor = CreateAccessor();
if (!response.IsSuccessStatusCode && !requestDefinition.TryParseOnNonSuccess)
{
// Error response
var readResult = await accessor.Read(responseStream, true).ConfigureAwait(false);
// If the response status is not success it is an error by definition
Error error;
if (response.StatusCode == (HttpStatusCode)418 || response.StatusCode == (HttpStatusCode)429)
{
var rateError = ParseRateLimitResponse((int)response.StatusCode, response.ResponseHeaders, accessor);
// Specifically handle rate limit errors
var rateError = await MessageHandler.ParseErrorRateLimitResponse(
(int)response.StatusCode,
response.ResponseHeaders,
responseStream).ConfigureAwait(false);
if (rateError.RetryAfter != null && gate != null && ClientOptions.RateLimiterEnabled)
{
_logger.RestApiRateLimitPauseUntil(request.RequestId, rateError.RetryAfter.Value);
@@ -466,28 +474,34 @@ namespace CryptoExchange.Net.Clients
}
else
{
error = ParseErrorResponse((int)response.StatusCode, response.ResponseHeaders, accessor, readResult.Error?.Exception);
// Handle a 'normal' error response. Can still be either a json error message or some random HTML or other string
try
{
error = await MessageHandler.ParseErrorResponse(
(int)response.StatusCode,
response.ResponseHeaders,
responseStream).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogError(ex, "Unhandled exception when parsing error response: {Message}", ex.Message);
var errorResult = new ServerError(ErrorInfo.Unknown with { Message = ex.Message });
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, errorResult);
}
}
if (error.Code == null || error.Code == 0)
error.Code = (int)response.StatusCode;
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error!);
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
}
var valid = await accessor.Read(responseStream, outputOriginalData).ConfigureAwait(false);
if (typeof(T) == typeof(object))
// Success status code and expected empty response, assume it's correct
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, 0, accessor.OriginalDataAvailable ? accessor.GetOriginalString() : "[Data only available when OutputOriginal = true in client options]", request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, null);
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, 0, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, null);
if (!valid)
{
// Invalid json
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, valid.Error);
}
// Json response received
var parsedError = TryParseError(requestDefinition, response.ResponseHeaders, accessor);
// Data response received, inspect the message and check if it is an error or not
var parsedError = await MessageHandler.CheckForErrorResponse(
requestDefinition,
response.ResponseHeaders,
responseStream).ConfigureAwait(false);
if (parsedError != null)
{
if (parsedError is ServerRateLimitError rateError)
@@ -500,11 +514,33 @@ namespace CryptoExchange.Net.Clients
}
// Success status code, but TryParseError determined it was an error response
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, parsedError);
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, parsedError);
}
var deserializeResult = accessor.Deserialize<T>();
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult.Data, deserializeResult.Error);
if (MessageHandler.RequiresSeekableStream)
// Reset stream read position as it might not be at the start if `CheckForErrorResponse` has read from it
responseStream.Position = 0;
// Try deserialization into the expected type
var (deserializeResult, deserializeError) = await MessageHandler.TryDeserializeAsync<T>(responseStream, cancellationToken).ConfigureAwait(false);
if (deserializeError != null)
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult, deserializeError); ;
try
{
// Check the deserialized response to see if it's an error or not
var responseError = MessageHandler.CheckDeserializedResponse(response.ResponseHeaders, deserializeResult);
if (responseError != null)
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult, responseError);
}
catch (Exception ex)
{
_logger.LogError(ex, "Unhandled exception when checking deserialized response: {Message}", ex.Message);
var error = new ServerError(ErrorInfo.Unknown with { Message = ex.Message });
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult, error);
}
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult, null);
}
catch (HttpRequestException requestException)
{
@@ -551,23 +587,11 @@ namespace CryptoExchange.Net.Clients
}
finally
{
accessor?.Clear();
responseStream?.Close();
response?.Close();
}
}
/// <summary>
/// Can be used to parse an error even though response status indicates success. Some apis always return 200 OK, even though there is an error.
/// 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
/// </summary>
/// <param name="requestDefinition">Request definition</param>
/// <param name="accessor">Data accessor</param>
/// <param name="responseHeaders">The response headers</param>
/// <returns>Null if not an error, Error otherwise</returns>
protected virtual Error? TryParseError(RequestDefinition requestDefinition, KeyValuePair<string, string[]>[] responseHeaders, IMessageAccessor accessor) => null;
/// <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.
/// Note that this is always called; even when the request might be successful
@@ -577,7 +601,7 @@ namespace CryptoExchange.Net.Clients
/// <param name="callResult">The result of the call</param>
/// <param name="tries">The current try number</param>
/// <returns>True if call should retry, false if the call should return</returns>
protected virtual async Task<bool> ShouldRetryRequestAsync<T>(IRateLimitGate? gate, WebCallResult<T> callResult, int tries)
protected virtual async ValueTask<bool> ShouldRetryRequestAsync<T>(IRateLimitGate? gate, WebCallResult<T> callResult, int tries)
{
if (tries >= 2)
// Only retry once
@@ -632,43 +656,6 @@ namespace CryptoExchange.Net.Clients
}
}
/// <summary>
/// Parse an error response from the server. Only used when server returns a status other than Success(200) or ratelimit error (429 or 418)
/// </summary>
/// <param name="httpStatusCode">The response status code</param>
/// <param name="responseHeaders">The response headers</param>
/// <param name="accessor">Data accessor</param>
/// <param name="exception">Exception</param>
/// <returns></returns>
protected virtual Error ParseErrorResponse(int httpStatusCode, KeyValuePair<string, string[]>[] responseHeaders, IMessageAccessor accessor, Exception? exception)
{
return new ServerError(ErrorInfo.Unknown, exception);
}
/// <summary>
/// Parse a rate limit error response from the server. Only used when server returns http status 429 or 418
/// </summary>
/// <param name="httpStatusCode">The response status code</param>
/// <param name="responseHeaders">The response headers</param>
/// <param name="accessor">Data accessor</param>
/// <returns></returns>
protected virtual ServerRateLimitError ParseRateLimitResponse(int httpStatusCode, KeyValuePair<string, string[]>[] responseHeaders, IMessageAccessor accessor)
{
// Handle retry after header
var retryAfterHeader = responseHeaders.SingleOrDefault(r => r.Key.Equals("Retry-After", StringComparison.InvariantCultureIgnoreCase));
if (retryAfterHeader.Value?.Any() != true)
return new ServerRateLimitError();
var value = retryAfterHeader.Value.First();
if (int.TryParse(value, out var seconds))
return new ServerRateLimitError() { RetryAfter = DateTime.UtcNow.AddSeconds(seconds) };
if (DateTime.TryParse(value, out var datetime))
return new ServerRateLimitError() { RetryAfter = datetime };
return new ServerRateLimitError();
}
/// <summary>
/// Create the parameter IDictionary
/// </summary>
@@ -696,26 +683,53 @@ namespace CryptoExchange.Net.Clients
RequestFactory.UpdateSettings(options.Proxy, options.RequestTimeout ?? ClientOptions.RequestTimeout, ClientOptions.HttpKeepAliveInterval);
}
internal async Task<WebCallResult<bool>> SyncTimeAsync()
private async ValueTask CheckTimeSync(int requestId, RequestDefinition definition)
{
var timeSyncParams = GetTimeSyncInfo();
if (timeSyncParams == null)
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null);
if (!definition.Authenticated)
return;
if (await timeSyncParams.TimeSyncState.Semaphore.WaitAsync(0).ConfigureAwait(false))
var lastUpdateTime = TimeOffsetManager.GetRestLastUpdateTime(ClientName);
var syncTask = CheckTimeOffsetAsync();
if (lastUpdateTime == null)
{
if (!timeSyncParams.SyncTime || DateTime.UtcNow - timeSyncParams.TimeSyncState.LastSyncTime < timeSyncParams.RecalculationInterval)
{
timeSyncParams.TimeSyncState.Semaphore.Release();
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null);
}
// Initially with first request we'll need to wait for the time syncing before making the actual request.
// If it's not the first request we can just continue and let it complete in the background
await syncTask.ConfigureAwait(false);
}
return;
}
internal async ValueTask CheckTimeOffsetAsync()
{
if (!(ApiOptions.AutoTimestamp ?? ClientOptions.AutoTimestamp))
// Time syncing not enabled
return;
await TimeOffsetManager.EnterAsync(ClientName).ConfigureAwait(false);
try
{
var lastUpdateTime = TimeOffsetManager.GetRestLastUpdateTime(ClientName);
if (DateTime.UtcNow - lastUpdateTime < (ApiOptions.TimestampRecalculationInterval ?? ClientOptions.TimestampRecalculationInterval))
// Time syncing was recently done
return;
var localTime = DateTime.UtcNow;
var result = await GetServerTimestampAsync().ConfigureAwait(false);
WebCallResult<DateTime> result;
try
{
result = await GetServerTimestampAsync().ConfigureAwait(false);
}
catch (NotImplementedException)
{
throw new ArgumentException("AutoTimestamp is not available for this API");
}
if (!result)
{
timeSyncParams.TimeSyncState.Semaphore.Release();
return result.As(false);
_logger.LogWarning("Failed to determine time offset between client and server, timestamping might fail");
return;
}
if (TotalRequestsMade == 1)
@@ -725,23 +739,44 @@ namespace CryptoExchange.Net.Clients
result = await GetServerTimestampAsync().ConfigureAwait(false);
if (!result)
{
timeSyncParams.TimeSyncState.Semaphore.Release();
return result.As(false);
_logger.LogWarning("Failed to determine time offset between client and server, timestamping might fail");
return;
}
}
// Calculate time offset between local and server
// Estimate the offset as the round trip time / 2
var offset = result.Data - localTime.AddMilliseconds(result.ResponseTime!.Value.TotalMilliseconds / 2);
timeSyncParams.UpdateTimeOffset(offset);
timeSyncParams.TimeSyncState.Semaphore.Release();
}
if (offset.TotalMilliseconds > 0 && offset.TotalMilliseconds < 500)
{
_logger.LogInformation("{ClientName} Time offset within limits ({Offset}ms), set offset to 0ms", ClientName, Math.Round(offset.TotalMilliseconds));
offset = TimeSpan.Zero;
}
else
{
_logger.LogInformation("{ClientName} Time offset set to {Offset}ms", ClientName, Math.Round(offset.TotalMilliseconds));
}
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null);
TimeOffsetManager.UpdateRestOffset(ClientName, offset.TotalMilliseconds);
}
finally
{
TimeOffsetManager.Release(ClientName);
}
}
private async Task<Stream> CopyStreamAsync(Stream responseStream)
{
var memoryStream = new MemoryStream();
await responseStream.CopyToAsync(memoryStream).ConfigureAwait(false);
responseStream.Close();
memoryStream.Position = 0;
return memoryStream;
}
private bool ShouldCache(RequestDefinition definition)
=> ClientOptions.CachingEnabled
&& definition.Method == HttpMethod.Get
&& !definition.PreventCaching;
}
}
+258 -116
View File
@@ -1,4 +1,6 @@
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Interfaces.Clients;
using CryptoExchange.Net.Logging.Extensions;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Errors;
@@ -7,6 +9,11 @@ using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.RateLimiting;
using CryptoExchange.Net.RateLimiting.Interfaces;
using CryptoExchange.Net.Sockets;
using CryptoExchange.Net.Sockets.Default;
using CryptoExchange.Net.Sockets.Default.Interfaces;
using CryptoExchange.Net.Sockets.HighPerf;
using CryptoExchange.Net.Sockets.HighPerf.Interfaces;
using CryptoExchange.Net.Sockets.Interfaces;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Concurrent;
@@ -25,13 +32,22 @@ namespace CryptoExchange.Net.Clients
public abstract class SocketApiClient : BaseApiClient, ISocketApiClient
{
#region Fields
/// <inheritdoc/>
public IWebsocketFactory SocketFactory { get; set; } = new WebsocketFactory();
/// <inheritdoc/>
public IHighPerfConnectionFactory? HighPerfConnectionFactory { get; set; }
/// <summary>
/// List of socket connections currently connecting/connected
/// </summary>
protected internal ConcurrentDictionary<int, SocketConnection> socketConnections = new();
protected internal ConcurrentDictionary<int, SocketConnection> _socketConnections = new();
/// <summary>
/// List of HighPerf socket connections currently connecting/connected
/// </summary>
protected internal ConcurrentDictionary<int, HighPerfSocketConnection> _highPerfSocketConnections = new();
/// <summary>
/// Semaphore used while creating sockets
@@ -72,7 +88,7 @@ namespace CryptoExchange.Net.Clients
/// Periodic task registrations
/// </summary>
protected List<PeriodicTaskRegistration> PeriodicTaskRegistrations { get; set; } = new List<PeriodicTaskRegistration>();
/// <summary>
/// List of address to keep an alive connection to
/// </summary>
@@ -83,35 +99,30 @@ namespace CryptoExchange.Net.Clients
/// </summary>
protected bool AllowTopicsOnTheSameConnection { get; set; } = true;
/// <summary>
/// Whether to continue processing and forward unparsable messages to handlers
/// </summary>
protected internal bool ProcessUnparsableMessages { get; set; } = false;
/// <inheritdoc />
public double IncomingKbps
{
get
{
if (socketConnections.IsEmpty)
if (_socketConnections.IsEmpty)
return 0;
return socketConnections.Sum(s => s.Value.IncomingKbps);
return _socketConnections.Sum(s => s.Value.IncomingKbps);
}
}
/// <inheritdoc />
public int CurrentConnections => socketConnections.Count;
public int CurrentConnections => _socketConnections.Count;
/// <inheritdoc />
public int CurrentSubscriptions
{
get
{
if (socketConnections.IsEmpty)
if (_socketConnections.IsEmpty)
return 0;
return socketConnections.Sum(s => s.Value.UserSubscriptionCount);
return _socketConnections.Sum(s => s.Value.UserSubscriptionCount);
}
}
@@ -121,6 +132,15 @@ namespace CryptoExchange.Net.Clients
/// <inheritdoc />
public new SocketApiOptions ApiOptions => (SocketApiOptions)base.ApiOptions;
/// <summary>
/// The max number of individual subscriptions on a single connection
/// </summary>
public int? MaxIndividualSubscriptionsPerConnection { get; set; }
/// <summary>
/// Whether or not to enforce that sequence number updates are always (lastSequenceNumber + 1)
/// </summary>
public bool EnforceSequenceNumbers { get; set; }
#endregion
/// <summary>
@@ -140,12 +160,6 @@ namespace CryptoExchange.Net.Clients
{
}
/// <summary>
/// Create a message accessor instance
/// </summary>
/// <returns></returns>
protected internal abstract IByteMessageAccessor CreateAccessor(WebSocketMessageType messageType);
/// <summary>
/// Create a serializer instance
/// </summary>
@@ -162,6 +176,24 @@ namespace CryptoExchange.Net.Clients
DedicatedConnectionConfigs.Add(new DedicatedConnectionConfig() { SocketAddress = url, Authenticated = auth });
}
/// <summary>
/// Update the timestamp offset between client and server based on the timestamp
/// </summary>
/// <param name="timestamp">Timestamp received from the server</param>
public virtual void UpdateTimeOffset(DateTime timestamp)
{
if (timestamp == default)
return;
TimeOffsetManager.UpdateSocketOffset(ClientName, (DateTime.UtcNow - timestamp).TotalMilliseconds);
}
/// <summary>
/// Get the time offset between client and server
/// </summary>
/// <returns></returns>
public virtual TimeSpan? GetTimeOffset() => TimeOffsetManager.GetSocketOffset(ClientName);
/// <summary>
/// Add a query to periodically send on each connection
/// </summary>
@@ -169,7 +201,7 @@ namespace CryptoExchange.Net.Clients
/// <param name="interval"></param>
/// <param name="queryDelegate"></param>
/// <param name="callback"></param>
protected virtual void RegisterPeriodicQuery(string identifier, TimeSpan interval, Func<SocketConnection, Query> queryDelegate, Action<SocketConnection, CallResult>? callback)
protected virtual void RegisterPeriodicQuery(string identifier, TimeSpan interval, Func<ISocketConnection, Query> queryDelegate, Action<SocketConnection, CallResult>? callback)
{
PeriodicTaskRegistrations.Add(new PeriodicTaskRegistration
{
@@ -209,6 +241,9 @@ namespace CryptoExchange.Net.Clients
return new CallResult<UpdateSubscription>(new NoApiCredentialsError());
}
if (subscription.IndividualSubscriptionCount > MaxIndividualSubscriptionsPerConnection)
return new CallResult<UpdateSubscription>(ArgumentError.Invalid("subscriptions", $"Max number of subscriptions in a single call is {MaxIndividualSubscriptionsPerConnection}"));
SocketConnection socketConnection;
var released = false;
// Wait for a semaphore here, so we only connect 1 socket at a time.
@@ -227,7 +262,7 @@ namespace CryptoExchange.Net.Clients
while (true)
{
// Get a new or existing socket connection
var socketResult = await GetSocketConnection(url, subscription.Authenticated, false, ct, subscription.Topic).ConfigureAwait(false);
var socketResult = await GetSocketConnection(url, subscription.Authenticated, false, ct, subscription.Topic, subscription.IndividualSubscriptionCount).ConfigureAwait(false);
if (!socketResult)
return socketResult.As<UpdateSubscription>(null);
@@ -269,47 +304,108 @@ namespace CryptoExchange.Net.Clients
return new CallResult<UpdateSubscription>(new ServerError(new ErrorInfo(ErrorType.WebsocketPaused, "Socket is paused")));
}
subscription.Status = SubscriptionStatus.Subscribing;
var waitEvent = new AsyncResetEvent(false);
var subQuery = subscription.CreateSubscriptionQuery(socketConnection);
if (subQuery != null)
{
// Send the request and wait for answer
var subResult = await socketConnection.SendAndWaitQueryAsync(subQuery, waitEvent, ct).ConfigureAwait(false);
if (!subResult)
{
waitEvent?.Set();
var isTimeout = subResult.Error is CancellationRequestedError;
if (isTimeout && subscription.Status == SubscriptionStatus.Subscribed)
{
// No response received, but the subscription did receive updates. We'll assume success
}
else
{
_logger.FailedToSubscribe(socketConnection.SocketId, subResult.Error?.ToString());
// If this was a timeout we still need to send an unsubscribe to prevent messages coming in later
subscription.Status = SubscriptionStatus.Pending;
await socketConnection.CloseAsync(subscription).ConfigureAwait(false);
return new CallResult<UpdateSubscription>(subResult.Error!);
}
}
var subscribeResult = await socketConnection.TrySubscribeAsync(subscription, true, ct).ConfigureAwait(false);
if (!subscribeResult)
return new CallResult<UpdateSubscription>(subscribeResult.Error!);
subscription.HandleSubQueryResponse(subQuery.Response!);
_logger.SubscriptionCompletedSuccessfully(socketConnection.SocketId, subscription.Id);
return new CallResult<UpdateSubscription>(new UpdateSubscription(socketConnection, subscription));
}
/// <summary>
/// Connect to an url and listen for data
/// </summary>
/// <param name="url">The URL to connect to</param>
/// <param name="subscription">The subscription</param>
/// <param name="connectionFactory">The factory for creating a socket connection</param>
/// <param name="ct">Cancellation token for closing this subscription</param>
/// <returns></returns>
protected virtual async Task<CallResult<HighPerfUpdateSubscription>> SubscribeHighPerfAsync<TUpdateType>(
string url,
HighPerfSubscription<TUpdateType> subscription,
IHighPerfConnectionFactory connectionFactory,
CancellationToken ct)
{
if (_disposing)
return new CallResult<HighPerfUpdateSubscription>(new InvalidOperationError("Client disposed, can't subscribe"));
HighPerfSocketConnection<TUpdateType> socketConnection;
var released = false;
// Wait for a semaphore here, so we only connect 1 socket at a time.
// This is necessary for being able to see if connections can be combined
try
{
await semaphoreSlim.WaitAsync(ct).ConfigureAwait(false);
}
catch (OperationCanceledException tce)
{
return new CallResult<HighPerfUpdateSubscription>(new CancellationRequestedError(tce));
}
try
{
while (true)
{
// Get a new or existing socket connection
var socketResult = await GetHighPerfSocketConnection<TUpdateType>(url, connectionFactory, ct).ConfigureAwait(false);
if (!socketResult)
return socketResult.As<HighPerfUpdateSubscription>(null);
socketConnection = socketResult.Data;
// Add a subscription on the socket connection
var success = socketConnection.AddSubscription(subscription);
if (!success)
{
_logger.FailedToAddSubscriptionRetryOnDifferentConnection(socketConnection.SocketId);
continue;
}
if (ClientOptions.SocketSubscriptionsCombineTarget == 1)
{
// Only 1 subscription per connection, so no need to wait for connection since a new subscription will create a new connection anyway
semaphoreSlim.Release();
released = true;
}
var needsConnecting = !socketConnection.Connected;
var connectResult = await ConnectIfNeededAsync(socketConnection, false, ct).ConfigureAwait(false);
if (!connectResult)
return new CallResult<HighPerfUpdateSubscription>(connectResult.Error!);
break;
}
}
finally
{
if (!released)
semaphoreSlim.Release();
}
var subRequest = subscription.CreateSubscriptionQuery(socketConnection);
if (subRequest != null)
{
// Send the request and wait for answer
var sendResult = await socketConnection.SendAsync(subRequest).ConfigureAwait(false);
if (!sendResult)
{
await socketConnection.CloseAsync().ConfigureAwait(false);
return new CallResult<HighPerfUpdateSubscription>(sendResult.Error!);
}
}
subscription.Status = SubscriptionStatus.Subscribed;
if (ct != default)
{
subscription.CancellationTokenRegistration = ct.Register(async () =>
{
_logger.CancellationTokenSetClosingSubscription(socketConnection.SocketId, subscription.Id);
await socketConnection.CloseAsync(subscription).ConfigureAwait(false);
await socketConnection.CloseAsync().ConfigureAwait(false);
}, false);
}
waitEvent?.Set();
_logger.SubscriptionCompletedSuccessfully(socketConnection.SocketId, subscription.Id);
return new CallResult<UpdateSubscription>(new UpdateSubscription(socketConnection, subscription));
return new CallResult<HighPerfUpdateSubscription>(new HighPerfUpdateSubscription(socketConnection, subscription));
}
/// <summary>
@@ -377,7 +473,7 @@ namespace CryptoExchange.Net.Clients
if (ct.IsCancellationRequested)
return new CallResult<THandlerResponse>(new CancellationRequestedError());
return await socketConnection.SendAndWaitQueryAsync(query, null, ct).ConfigureAwait(false);
return await socketConnection.SendAndWaitQueryAsync(query, ct).ConfigureAwait(false);
}
/// <summary>
@@ -387,7 +483,7 @@ namespace CryptoExchange.Net.Clients
/// <param name="authenticated">Whether the socket should authenticated</param>
/// <param name="ct">Cancellation token</param>
/// <returns></returns>
protected virtual async Task<CallResult> ConnectIfNeededAsync(SocketConnection socket, bool authenticated, CancellationToken ct)
protected virtual async Task<CallResult> ConnectIfNeededAsync(ISocketConnection socket, bool authenticated, CancellationToken ct)
{
if (socket.Connected)
return CallResult.SuccessResult;
@@ -402,7 +498,10 @@ namespace CryptoExchange.Net.Clients
if (!authenticated || socket.Authenticated)
return CallResult.SuccessResult;
var result = await AuthenticateSocketAsync(socket).ConfigureAwait(false);
if (socket is not SocketConnection sc)
throw new InvalidOperationException("HighPerfSocketConnection not supported for authentication");
var result = await AuthenticateSocketAsync(sc).ConfigureAwait(false);
if (!result)
await socket.CloseAsync().ConfigureAwait(false);
@@ -446,7 +545,8 @@ namespace CryptoExchange.Net.Clients
/// Should return the request which can be used to authenticate a socket connection
/// </summary>
/// <returns></returns>
protected internal virtual Task<Query?> GetAuthenticationRequestAsync(SocketConnection connection) => throw new NotImplementedException();
protected internal virtual Task<Query?> GetAuthenticationRequestAsync(SocketConnection connection) =>
Task.FromResult(AuthenticationProvider!.GetAuthenticationQuery(this, connection));
/// <summary>
/// Adds a system subscription. Used for example to reply to ping requests
@@ -455,7 +555,7 @@ namespace CryptoExchange.Net.Clients
protected void AddSystemSubscription(SystemSubscription systemSubscription)
{
systemSubscriptions.Add(systemSubscription);
foreach (var connection in socketConnections.Values)
foreach (var connection in _socketConnections.Values)
connection.AddSubscription(systemSubscription);
}
@@ -475,7 +575,7 @@ namespace CryptoExchange.Net.Clients
/// </summary>
/// <param name="connection"></param>
/// <returns></returns>
protected internal virtual Task<Uri?> GetReconnectUriAsync(SocketConnection connection)
protected internal virtual Task<Uri?> GetReconnectUriAsync(ISocketConnection connection)
{
return Task.FromResult<Uri?>(connection.ConnectionUri);
}
@@ -498,10 +598,17 @@ namespace CryptoExchange.Net.Clients
/// <param name="dedicatedRequestConnection">Whether a dedicated request connection should be returned</param>
/// <param name="ct">Cancellation token</param>
/// <param name="topic">The subscription topic, can be provided when multiple of the same topics are not allowed on a connection</param>
/// <param name="individualSubscriptionCount">The number of individual subscriptions in this subscribe request</param>
/// <returns></returns>
protected virtual async Task<CallResult<SocketConnection>> GetSocketConnection(string address, bool authenticated, bool dedicatedRequestConnection, CancellationToken ct, string? topic = null)
protected virtual async Task<CallResult<SocketConnection>> GetSocketConnection(
string address,
bool authenticated,
bool dedicatedRequestConnection,
CancellationToken ct,
string? topic = null,
int individualSubscriptionCount = 1)
{
var socketQuery = socketConnections.Where(s => s.Value.Tag.TrimEnd('/') == address.TrimEnd('/')
var socketQuery = _socketConnections.Where(s => s.Value.Tag.TrimEnd('/') == address.TrimEnd('/')
&& s.Value.ApiClient.GetType() == GetType()
&& (AllowTopicsOnTheSameConnection || !s.Value.Topics.Contains(topic)))
.Select(x => x.Value)
@@ -510,11 +617,11 @@ namespace CryptoExchange.Net.Clients
// If all current socket connections are reconnecting or resubscribing wait for that to finish as we can probably use the existing connection
var delayStart = DateTime.UtcNow;
var delayed = false;
while (socketQuery.Count >= 1 && socketQuery.All(x => x.Status == SocketConnection.SocketStatus.Reconnecting || x.Status == SocketConnection.SocketStatus.Resubscribing))
while (socketQuery.Count >= 1 && socketQuery.All(x => x.Status == SocketStatus.Reconnecting || x.Status == SocketStatus.Resubscribing))
{
if (DateTime.UtcNow - delayStart > TimeSpan.FromSeconds(10))
{
if (socketQuery.Count >= 1 && socketQuery.All(x => x.Status == SocketConnection.SocketStatus.Reconnecting || x.Status == SocketConnection.SocketStatus.Resubscribing))
if (socketQuery.Count >= 1 && socketQuery.All(x => x.Status == SocketStatus.Reconnecting || x.Status == SocketStatus.Resubscribing))
{
// If after this time we still trying to reconnect/reprocess there is some issue in the connection
_logger.TimeoutWaitingForReconnectingSocket();
@@ -534,7 +641,7 @@ namespace CryptoExchange.Net.Clients
if (delayed)
_logger.WaitedForReconnectingSocket((long)(DateTime.UtcNow - delayStart).TotalMilliseconds);
socketQuery = socketQuery.Where(s => (s.Status == SocketConnection.SocketStatus.None || s.Status == SocketConnection.SocketStatus.Connected)
socketQuery = socketQuery.Where(s => (s.Status == SocketStatus.None || s.Status == SocketStatus.Connected)
&& (s.Authenticated == authenticated || !authenticated)
&& s.Connected).ToList();
@@ -549,18 +656,35 @@ namespace CryptoExchange.Net.Clients
if (connection != null && !connection.DedicatedRequestConnection.Authenticated)
// Mark dedicated request connection as authenticated if the request is authenticated
connection.DedicatedRequestConnection.Authenticated = authenticated;
if (connection == null)
// Fall back to an existing connection if there is no dedicated request connection available
connection = socketQuery.OrderBy(s => s.UserSubscriptionCount).FirstOrDefault();
}
bool maxConnectionsReached = _socketConnections.Count >= (ApiOptions.MaxSocketConnections ?? ClientOptions.MaxSocketConnections);
if (connection != null)
{
if (connection.UserSubscriptionCount < ClientOptions.SocketSubscriptionsCombineTarget
|| (socketConnections.Count >= (ApiOptions.MaxSocketConnections ?? ClientOptions.MaxSocketConnections) && socketConnections.All(s => s.Value.UserSubscriptionCount >= ClientOptions.SocketSubscriptionsCombineTarget)))
bool lessThanBatchSubCombineTarget = connection.UserSubscriptionCount < ClientOptions.SocketSubscriptionsCombineTarget;
bool lessThanIndividualSubCombineTarget = connection.Subscriptions.Sum(x => x.IndividualSubscriptionCount) < ClientOptions.SocketIndividualSubscriptionCombineTarget;
if ((lessThanBatchSubCombineTarget && lessThanIndividualSubCombineTarget)
|| maxConnectionsReached)
{
// 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>(connection);
// If there is a max subscriptions per connection limit also only use existing if the new subscription doesn't go over the limit
if (MaxIndividualSubscriptionsPerConnection == null)
return new CallResult<SocketConnection>(connection);
var currentCount = connection.Subscriptions.Sum(x => x.IndividualSubscriptionCount);
if (currentCount + individualSubscriptionCount <= MaxIndividualSubscriptionsPerConnection)
return new CallResult<SocketConnection>(connection);
}
}
if (maxConnectionsReached)
return new CallResult<SocketConnection>(new InvalidOperationError("Max amount of socket connections reached"));
var connectionAddress = await GetConnectionUrlAsync(address, authenticated).ConfigureAwait(false);
if (!connectionAddress)
{
@@ -571,10 +695,8 @@ namespace CryptoExchange.Net.Clients
if (connectionAddress.Data != address)
_logger.ConnectionAddressSetTo(connectionAddress.Data!);
// Create new socket
var socket = CreateSocket(connectionAddress.Data!);
var socketConnection = new SocketConnection(_logger, this, socket, address);
socketConnection.UnhandledMessage += HandleUnhandledMessage;
// Create new socket connection
var socketConnection = new SocketConnection(_logger, SocketFactory, GetWebSocketParameters(connectionAddress.Data!), this, address);
socketConnection.ConnectRateLimitedAsync += HandleConnectRateLimitedAsync;
if (dedicatedRequestConnection)
{
@@ -594,13 +716,44 @@ namespace CryptoExchange.Net.Clients
return new CallResult<SocketConnection>(socketConnection);
}
/// <summary>
/// Gets a connection for a new subscription or query. Can be an existing if there are open position or a new one.
/// </summary>
/// <param name="address">The address the socket is for</param>
/// <param name="connectionFactory">The factory for creating a socket connection</param>
/// <param name="ct">Cancellation token</param>
/// <returns></returns>
protected virtual async Task<CallResult<HighPerfSocketConnection<TUpdateType>>> GetHighPerfSocketConnection<TUpdateType>(
string address,
IHighPerfConnectionFactory connectionFactory,
CancellationToken ct)
{
var connectionAddress = await GetConnectionUrlAsync(address, false).ConfigureAwait(false);
if (!connectionAddress)
{
_logger.FailedToDetermineConnectionUrl(connectionAddress.Error?.ToString());
return connectionAddress.As<HighPerfSocketConnection<TUpdateType>>(null);
}
if (connectionAddress.Data != address)
_logger.ConnectionAddressSetTo(connectionAddress.Data!);
// Create new socket connection
var socketConnection = connectionFactory.CreateHighPerfConnection<TUpdateType>(_logger, SocketFactory, GetWebSocketParameters(connectionAddress.Data!), this, address);
foreach (var ptg in PeriodicTaskRegistrations)
socketConnection.QueryPeriodic(ptg.Identifier, ptg.Interval, (con) => ptg.QueryDelegate(con).Request);
return new CallResult<HighPerfSocketConnection<TUpdateType>>(socketConnection);
}
/// <summary>
/// Process an unhandled message
/// </summary>
/// <param name="message">The message that wasn't processed</param>
protected virtual void HandleUnhandledMessage(IMessageAccessor message)
{
}
/// <param name="connection">The socket connection</param>
/// <param name="typeIdentifier">The type as identified</param>
/// <param name="data">The data</param>
protected internal virtual bool HandleUnhandledMessage(SocketConnection connection, string typeIdentifier, ReadOnlySpan<byte> data) => false;
/// <summary>
/// Process connect rate limited
@@ -622,12 +775,15 @@ namespace CryptoExchange.Net.Clients
/// <param name="socketConnection">The socket to connect</param>
/// <param name="ct">Cancellation token</param>
/// <returns></returns>
protected virtual async Task<CallResult> ConnectSocketAsync(SocketConnection socketConnection, CancellationToken ct)
protected virtual async Task<CallResult> ConnectSocketAsync(ISocketConnection socketConnection, CancellationToken ct)
{
var connectResult = await socketConnection.ConnectAsync(ct).ConfigureAwait(false);
if (connectResult)
{
socketConnections.TryAdd(socketConnection.SocketId, socketConnection);
if (socketConnection is SocketConnection sc)
_socketConnections.TryAdd(socketConnection.SocketId, sc);
else if (socketConnection is HighPerfSocketConnection hsc)
_highPerfSocketConnections.TryAdd(socketConnection.SocketId, hsc);
return connectResult;
}
@@ -653,18 +809,6 @@ namespace CryptoExchange.Net.Clients
ReceiveBufferSize = ClientOptions.ReceiveBufferSize,
};
/// <summary>
/// Create a socket for an address
/// </summary>
/// <param name="address">The address the socket should connect to</param>
/// <returns></returns>
protected virtual IWebsocket CreateSocket(string address)
{
var socket = SocketFactory.CreateWebsocket(_logger, GetWebSocketParameters(address));
_logger.SocketCreatedForAddress(socket.Id, address);
return socket;
}
/// <summary>
/// Unsubscribe an update subscription
/// </summary>
@@ -674,7 +818,7 @@ namespace CryptoExchange.Net.Clients
{
Subscription? subscription = null;
SocketConnection? connection = null;
foreach (var socket in socketConnections.Values.ToList())
foreach (var socket in _socketConnections.Values.ToList())
{
subscription = socket.GetSubscription(subscriptionId);
if (subscription != null)
@@ -712,21 +856,25 @@ namespace CryptoExchange.Net.Clients
/// <returns></returns>
public virtual async Task UnsubscribeAllAsync()
{
var sum = socketConnections.Sum(s => s.Value.UserSubscriptionCount);
var sum = _socketConnections.Sum(s => s.Value.UserSubscriptionCount) + _highPerfSocketConnections.Sum(s => s.Value.UserSubscriptionCount);
if (sum == 0)
return;
_logger.UnsubscribingAll(socketConnections.Sum(s => s.Value.UserSubscriptionCount));
_logger.UnsubscribingAll(sum);
var tasks = new List<Task>();
var socketList = _socketConnections.Values;
foreach (var connection in socketList)
{
var socketList = socketConnections.Values;
foreach (var connection in socketList)
{
foreach(var subscription in connection.Subscriptions.Where(x => x.UserSubscription))
tasks.Add(connection.CloseAsync(subscription));
}
foreach(var subscription in connection.Subscriptions.Where(x => x.UserSubscription))
tasks.Add(connection.CloseAsync(subscription));
}
var highPerfSocketList = _highPerfSocketConnections.Values;
foreach (var connection in highPerfSocketList)
tasks.Add(connection.CloseAsync());
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
}
@@ -736,10 +884,10 @@ namespace CryptoExchange.Net.Clients
/// <returns></returns>
public virtual async Task ReconnectAsync()
{
_logger.ReconnectingAllConnections(socketConnections.Count);
_logger.ReconnectingAllConnections(_socketConnections.Count);
var tasks = new List<Task>();
{
var socketList = socketConnections.Values;
var socketList = _socketConnections.Values;
foreach (var sub in socketList)
tasks.Add(sub.TriggerReconnectAsync());
}
@@ -771,7 +919,7 @@ namespace CryptoExchange.Net.Clients
base.SetOptions(options);
if ((!previousProxyIsSet && options.Proxy == null)
|| socketConnections.IsEmpty)
|| _socketConnections.IsEmpty)
{
return;
}
@@ -779,7 +927,7 @@ namespace CryptoExchange.Net.Clients
_logger.LogInformation("Reconnecting websockets to apply proxy");
// Update proxy, also triggers reconnect
foreach (var connection in socketConnections)
foreach (var connection in _socketConnections)
_ = connection.Value.UpdateProxy(options.Proxy);
}
@@ -798,15 +946,15 @@ namespace CryptoExchange.Net.Clients
/// <returns></returns>
public SocketApiClientState GetState(bool includeSubDetails = true)
{
var connectionStates = new List<SocketConnection.SocketConnectionState>();
foreach (var socketIdAndConnection in socketConnections)
var connectionStates = new List<SocketConnectionState>();
foreach (var socketIdAndConnection in _socketConnections)
{
SocketConnection connection = socketIdAndConnection.Value;
SocketConnection.SocketConnectionState connectionState = connection.GetState(includeSubDetails);
SocketConnectionState connectionState = connection.GetState(includeSubDetails);
connectionStates.Add(connectionState);
}
return new SocketApiClientState(socketConnections.Count, CurrentSubscriptions, IncomingKbps, connectionStates);
return new SocketApiClientState(_socketConnections.Count, CurrentSubscriptions, IncomingKbps, connectionStates);
}
/// <summary>
@@ -820,7 +968,7 @@ namespace CryptoExchange.Net.Clients
int Connections,
int Subscriptions,
double DownloadSpeed,
List<SocketConnection.SocketConnectionState> ConnectionStates)
List<SocketConnectionState> ConnectionStates)
{
/// <summary>
/// Print the state of the client
@@ -851,7 +999,6 @@ namespace CryptoExchange.Net.Clients
sb.AppendLine($"\t\t\tId: {subState.Id}");
sb.AppendLine($"\t\t\tStatus: {subState.Status}");
sb.AppendLine($"\t\t\tInvocations: {subState.Invocations}");
sb.AppendLine($"\t\t\tIdentifiers: [{subState.ListenMatcher.ToString()}]");
});
}
});
@@ -868,7 +1015,7 @@ namespace CryptoExchange.Net.Clients
_disposing = true;
var tasks = new List<Task>();
{
var socketList = socketConnections.Values.Where(x => x.UserSubscriptionCount > 0 || x.Connected);
var socketList = _socketConnections.Values.Where(x => x.UserSubscriptionCount > 0 || x.Connected);
if (socketList.Any())
_logger.DisposingSocketClient();
@@ -882,20 +1029,15 @@ namespace CryptoExchange.Net.Clients
base.Dispose();
}
/// <summary>
/// Get the listener identifier for the message
/// </summary>
/// <param name="messageAccessor"></param>
/// <returns></returns>
public abstract string? GetListenerIdentifier(IMessageAccessor messageAccessor);
/// <summary>
/// Preprocess a stream message
/// </summary>
/// <param name="connection"></param>
/// <param name="type"></param>
/// <param name="data"></param>
public virtual ReadOnlySpan<byte> PreprocessStreamMessage(SocketConnection connection, WebSocketMessageType type, ReadOnlySpan<byte> data) => data;
/// <summary>
/// Create a new message converter instance
/// </summary>
/// <returns></returns>
public virtual ReadOnlyMemory<byte> PreprocessStreamMessage(SocketConnection connection, WebSocketMessageType type, ReadOnlyMemory<byte> data) => data;
public abstract ISocketMessageHandler CreateMessageConverter(WebSocketMessageType messageType);
}
}
@@ -1,7 +1,5 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Text;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters
@@ -0,0 +1,64 @@
using CryptoExchange.Net.Objects;
using System.IO;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
{
/// <summary>
/// REST message handler
/// </summary>
public interface IRestMessageHandler
{
/// <summary>
/// The `accept` HTTP response header for the request
/// </summary>
MediaTypeWithQualityHeaderValue AcceptHeader { get; }
/// <summary>
/// Whether a seekable stream is required
/// </summary>
bool RequiresSeekableStream { get; }
/// <summary>
/// Parse the response when the HTTP response status indicated an error
/// </summary>
ValueTask<Error> ParseErrorResponse(
int httpStatusCode,
HttpResponseHeaders responseHeaders,
Stream responseStream);
/// <summary>
/// Parse the response when the HTTP response status indicated a rate limit error
/// </summary>
ValueTask<ServerRateLimitError> ParseErrorRateLimitResponse(
int httpStatusCode,
HttpResponseHeaders responseHeaders,
Stream responseStream);
/// <summary>
/// Check if the response is an error response; if so return the error.<br />
/// Note that if the API returns a standard result wrapper, something like this:
/// <code>{ "code": 400, "msg": "error", "data": {} }</code>
/// then the `CheckDeserializedResponse` method should be used for checking the result
/// </summary>
ValueTask<Error?> CheckForErrorResponse(
RequestDefinition request,
HttpResponseHeaders responseHeaders,
Stream responseStream);
/// <summary>
/// Deserialize the response stream
/// </summary>
ValueTask<(T? Result, Error? Error)> TryDeserializeAsync<T>(
Stream responseStream,
CancellationToken ct);
/// <summary>
/// Check whether the resulting T object indicates an error or not
/// </summary>
Error? CheckDeserializedResponse<T>(HttpResponseHeaders responseHeaders, T result);
}
}
@@ -0,0 +1,27 @@
using System;
using System.Net.WebSockets;
namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
{
/// <summary>
/// WebSocket message handler
/// </summary>
public interface ISocketMessageHandler
{
/// <summary>
/// Get an identifier for the message which can be used to determine the type of the message
/// </summary>
string? GetTypeIdentifier(ReadOnlySpan<byte> data, WebSocketMessageType? webSocketMessageType);
/// <summary>
/// Get optional topic filter, for example a symbol name
/// </summary>
string? GetTopicFilter(object deserializedObject);
/// <summary>
/// Deserialize to the provided type
/// </summary>
object Deserialize(ReadOnlySpan<byte> data, Type type);
}
}
@@ -0,0 +1,46 @@
using System;
namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
{
/// <summary>
/// Message type definition
/// </summary>
public class MessageTypeDefinition
{
/// <summary>
/// Whether to immediately select the definition when it is matched. Can only be used when the evaluator has a single unique field to look for
/// </summary>
public bool ForceIfFound { get; set; }
/// <summary>
/// The fields a message needs to contain for this definition
/// </summary>
public MessageFieldReference[] Fields { get; set; } = [];
/// <summary>
/// The callback for getting the identifier string
/// </summary>
public Func<SearchResult, string>? TypeIdentifierCallback { get; set; }
/// <summary>
/// The static identifier string to return when this evaluator is matched
/// </summary>
public string? StaticIdentifier { get; set; }
internal string? GetMessageType(SearchResult result)
{
if (StaticIdentifier != null)
return StaticIdentifier;
return TypeIdentifierCallback!(result);
}
internal bool Satisfied(SearchResult result)
{
foreach(var field in Fields)
{
if (!result.Contains(field))
return false;
}
return true;
}
}
}
@@ -0,0 +1,15 @@
namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
{
internal class MessageEvalutorFieldReference
{
public bool SkipReading { get; set; }
public bool OverlappingField { get; set; }
public MessageFieldReference Field { get; set; }
public MessageTypeDefinition? ForceEvaluator { get; set; }
public MessageEvalutorFieldReference(MessageFieldReference field)
{
Field = field;
}
}
}
@@ -0,0 +1,152 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
{
/// <summary>
/// Reference to a message field
/// </summary>
public abstract class MessageFieldReference
{
/// <summary>
/// The name for this search field
/// </summary>
public string SearchName { get; set; }
/// <summary>
/// The depth at which to look for this field
/// </summary>
public int Depth { get; set; } = 1;
/// <summary>
/// Callback to check if the field value matches an expected constraint
/// </summary>
public Func<string?, bool>? Constraint { get; private set; }
/// <summary>
/// Check whether the value is one of the string values in the set
/// </summary>
public MessageFieldReference WithFilterConstraint(HashSet<string?> set)
{
Constraint = set.Contains;
return this;
}
/// <summary>
/// Check whether the value is equal to a string
/// </summary>
public MessageFieldReference WithEqualConstraint(string compare)
{
Constraint = x => x != null && x.Equals(compare, StringComparison.Ordinal);
return this;
}
/// <summary>
/// Check whether the value is not equal to a string
/// </summary>
public MessageFieldReference WithNotEqualConstraint(string compare)
{
Constraint = x => x == null || !x.Equals(compare, StringComparison.Ordinal);
return this;
}
/// <summary>
/// Check whether the value is not null
/// </summary>
public MessageFieldReference WithNotNullConstraint()
{
Constraint = x => x != null;
return this;
}
/// <summary>
/// Check whether the value starts with a certain string
/// </summary>
public MessageFieldReference WithStartsWithConstraint(string start)
{
Constraint = x => x != null && x.StartsWith(start, StringComparison.Ordinal);
return this;
}
/// <summary>
/// Check whether the value starts with a certain string
/// </summary>
public MessageFieldReference WithStartsWithConstraints(params string[] startValues)
{
Constraint = x =>
{
if (x == null)
return false;
foreach (var item in startValues)
{
if (x!.StartsWith(item, StringComparison.Ordinal))
return true;
}
return false;
};
return this;
}
/// <summary>
/// Check whether the value starts with a certain string
/// </summary>
public MessageFieldReference WithCustomConstraint(Func<string?, bool> constraint)
{
Constraint = constraint;
return this;
}
/// <summary>
/// ctor
/// </summary>
public MessageFieldReference(string searchName)
{
SearchName = searchName;
}
}
/// <summary>
/// Reference to a property message field
/// </summary>
public class PropertyFieldReference : MessageFieldReference
{
/// <summary>
/// The property name in the JSON
/// </summary>
public byte[] PropertyName { get; set; }
/// <summary>
/// Whether the property value is array values
/// </summary>
public bool ArrayValues { get; set; }
/// <summary>
/// ctor
/// </summary>
public PropertyFieldReference(string propertyName) : base(propertyName)
{
PropertyName = Encoding.UTF8.GetBytes(propertyName);
}
}
/// <summary>
/// Reference to an array message field
/// </summary>
public class ArrayFieldReference : MessageFieldReference
{
/// <summary>
/// The index in the array
/// </summary>
public int ArrayIndex { get; set; }
/// <summary>
/// ctor
/// </summary>
public ArrayFieldReference(string searchName, int depth, int index) : base(searchName)
{
Depth = depth;
ArrayIndex = index;
}
}
}
@@ -0,0 +1,60 @@
using System;
using System.Collections.Generic;
namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
{
/// <summary>
/// The results of a search for fields in a JSON message
/// </summary>
public class SearchResult
{
private List<SearchResultItem> _items = new List<SearchResultItem>();
/// <summary>
/// Get the value of a field
/// </summary>
public string? FieldValue(string searchName)
{
foreach (var item in _items)
{
if (item.Field.SearchName.Equals(searchName, StringComparison.Ordinal))
return item.Value;
}
throw new Exception($"No field value found for {searchName}");
}
/// <summary>
/// The number of found search field values
/// </summary>
public int Count => _items.Count;
/// <summary>
/// Clear the search result
/// </summary>
public void Clear() => _items.Clear();
/// <summary>
/// Whether the value for a specific field was found
/// </summary>
public bool Contains(MessageFieldReference field)
{
foreach (var item in _items)
{
if (item.Field == field)
return true;
}
return false;
}
/// <summary>
/// Write a value to the result
/// </summary>
public void Write(MessageFieldReference field, string? value) => _items.Add(new SearchResultItem
{
Field = field,
Value = value
});
}
}
@@ -0,0 +1,17 @@
namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
{
/// <summary>
/// Search result value
/// </summary>
public struct SearchResultItem
{
/// <summary>
/// The field the values is for
/// </summary>
public MessageFieldReference Field { get; set; }
/// <summary>
/// The value of the field
/// </summary>
public string? Value { get; set; }
}
}
@@ -1,49 +0,0 @@
namespace CryptoExchange.Net.Converters.MessageParsing
{
/// <summary>
/// Node accessor
/// </summary>
public readonly struct NodeAccessor
{
/// <summary>
/// Index
/// </summary>
public int? Index { get; }
/// <summary>
/// Property name
/// </summary>
public string? Property { get; }
/// <summary>
/// Type (0 = int, 1 = string, 2 = prop name)
/// </summary>
public int Type { get; }
private NodeAccessor(int? index, string? property, int type)
{
Index = index;
Property = property;
Type = type;
}
/// <summary>
/// Create an int node accessor
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public static NodeAccessor Int(int value) { return new NodeAccessor(value, null, 0); }
/// <summary>
/// Create a string node accessor
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public static NodeAccessor String(string value) { return new NodeAccessor(null, value, 1); }
/// <summary>
/// Create a property name node accessor
/// </summary>
/// <returns></returns>
public static NodeAccessor PropertyName() { return new NodeAccessor(null, null, 2); }
}
}
@@ -1,50 +0,0 @@
using System.Collections;
using System.Collections.Generic;
namespace CryptoExchange.Net.Converters.MessageParsing
{
/// <summary>
/// Message access definition
/// </summary>
public readonly struct MessagePath : IEnumerable<NodeAccessor>
{
private readonly List<NodeAccessor> _path;
internal void Add(NodeAccessor node)
{
_path.Add(node);
}
/// <summary>
/// ctor
/// </summary>
public MessagePath()
{
_path = new List<NodeAccessor>();
}
/// <summary>
/// Create a new message path
/// </summary>
/// <returns></returns>
public static MessagePath Get()
{
return new MessagePath();
}
/// <summary>
/// IEnumerable implementation
/// </summary>
/// <returns></returns>
public IEnumerator<NodeAccessor> GetEnumerator()
{
for (var i = 0; i < _path.Count; i++)
yield return _path[i];
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
}
@@ -1,43 +0,0 @@
namespace CryptoExchange.Net.Converters.MessageParsing
{
/// <summary>
/// Message path extension methods
/// </summary>
public static class MessagePathExtension
{
/// <summary>
/// Add a string node accessor
/// </summary>
/// <param name="path"></param>
/// <param name="propName"></param>
/// <returns></returns>
public static MessagePath Property(this MessagePath path, string propName)
{
path.Add(NodeAccessor.String(propName));
return path;
}
/// <summary>
/// Add a property name node accessor
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
public static MessagePath PropertyName(this MessagePath path)
{
path.Add(NodeAccessor.PropertyName());
return path;
}
/// <summary>
/// Add a int node accessor
/// </summary>
/// <param name="path"></param>
/// <param name="index"></param>
/// <returns></returns>
public static MessagePath Index(this MessagePath path, int index)
{
path.Add(NodeAccessor.Int(index));
return path;
}
}
}
@@ -1,21 +0,0 @@
namespace CryptoExchange.Net.Converters.MessageParsing
{
/// <summary>
/// Message node type
/// </summary>
public enum NodeType
{
/// <summary>
/// Array node
/// </summary>
Array,
/// <summary>
/// Object node
/// </summary>
Object,
/// <summary>
/// Value node
/// </summary>
Value
}
}
@@ -1,15 +1,13 @@
using System;
using System.Collections.Concurrent;
using CryptoExchange.Net.Exceptions;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Text.Json.Serialization;
using System.Text.Json;
using CryptoExchange.Net.Attributes;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
using System.Threading;
using System.Diagnostics;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
@@ -23,8 +21,8 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
public class ArrayConverter<T> : JsonConverter<T> where T : new()
#endif
{
private static readonly Lazy<List<ArrayPropertyInfo>> _typePropertyInfo = new Lazy<List<ArrayPropertyInfo>>(CacheTypeAttributes, LazyThreadSafetyMode.PublicationOnly);
private static SortedDictionary<int, List<ArrayPropertyInfo>>? _typePropertyInfo;
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
@@ -38,54 +36,59 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
return;
}
if (_typePropertyInfo == null)
_typePropertyInfo = CacheTypeAttributes();
writer.WriteStartArray();
var ordered = _typePropertyInfo.Value.Where(x => x.ArrayProperty != null).OrderBy(p => p.ArrayProperty.Index);
var last = -1;
foreach (var prop in ordered)
foreach (var indexProps in _typePropertyInfo)
{
if (prop.ArrayProperty.Index == last)
continue;
while (prop.ArrayProperty.Index != last + 1)
foreach (var prop in indexProps.Value)
{
writer.WriteNullValue();
last += 1;
}
if (prop.ArrayProperty.Index == last)
// Don't write the same index twice
continue;
last = prop.ArrayProperty.Index;
var objValue = prop.PropertyInfo.GetValue(value);
if (objValue == null)
{
writer.WriteNullValue();
continue;
}
JsonSerializerOptions? typeOptions = null;
if (prop.JsonConverter != null)
{
typeOptions = new JsonSerializerOptions
while (prop.ArrayProperty.Index != last + 1)
{
NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals,
PropertyNameCaseInsensitive = false,
TypeInfoResolver = options.TypeInfoResolver,
};
typeOptions.Converters.Add(prop.JsonConverter);
}
writer.WriteNullValue();
last += 1;
}
if (prop.JsonConverter == null && IsSimple(prop.PropertyInfo.PropertyType))
{
if (prop.TargetType == typeof(string))
writer.WriteStringValue(Convert.ToString(objValue, CultureInfo.InvariantCulture));
else if (prop.TargetType == typeof(bool))
writer.WriteBooleanValue((bool)objValue);
last = prop.ArrayProperty.Index;
var objValue = prop.PropertyInfo.GetValue(value);
if (objValue == null)
{
writer.WriteNullValue();
continue;
}
JsonSerializerOptions? typeOptions = null;
if (prop.JsonConverter != null)
{
typeOptions = new JsonSerializerOptions
{
NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals,
PropertyNameCaseInsensitive = false,
TypeInfoResolver = options.TypeInfoResolver,
};
typeOptions.Converters.Add(prop.JsonConverter);
}
if (prop.JsonConverter == null && IsSimple(prop.PropertyInfo.PropertyType))
{
if (prop.TargetType == typeof(string))
writer.WriteStringValue(Convert.ToString(objValue, CultureInfo.InvariantCulture));
else if (prop.TargetType == typeof(bool))
writer.WriteBooleanValue((bool)objValue);
else
writer.WriteRawValue(Convert.ToString(objValue, CultureInfo.InvariantCulture)!);
}
else
writer.WriteRawValue(Convert.ToString(objValue, CultureInfo.InvariantCulture)!);
}
else
{
JsonSerializer.Serialize(writer, objValue, typeOptions ?? options);
{
JsonSerializer.Serialize(writer, objValue, typeOptions ?? options);
}
}
}
@@ -112,7 +115,11 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
#endif
{
if (reader.TokenType != JsonTokenType.StartArray)
throw new Exception("Not an array");
throw new CeDeserializationException("Not an array");
if (_typePropertyInfo == null)
_typePropertyInfo = CacheTypeAttributes();
int index = 0;
while (reader.Read())
@@ -120,8 +127,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
if (reader.TokenType == JsonTokenType.EndArray)
break;
var indexAttributes = _typePropertyInfo.Value.Where(a => a.ArrayProperty.Index == index);
if (!indexAttributes.Any())
if(!_typePropertyInfo.TryGetValue(index, out var indexAttributes))
{
index++;
continue;
@@ -161,7 +167,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
JsonTokenType.String => reader.GetString(),
JsonTokenType.Number => reader.GetDecimal(),
JsonTokenType.StartObject => JsonSerializer.Deserialize(ref reader, attribute.TargetType, options),
_ => throw new NotImplementedException($"Array deserialization of type {reader.TokenType} not supported"),
_ => throw new CeDeserializationException($"Array deserialization of type {reader.TokenType} not supported"),
};
}
@@ -193,12 +199,12 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
private static List<ArrayPropertyInfo> CacheTypeAttributes()
private static SortedDictionary<int, List<ArrayPropertyInfo>> CacheTypeAttributes()
#else
private static List<ArrayPropertyInfo> CacheTypeAttributes()
private static SortedDictionary<int, List<ArrayPropertyInfo>> CacheTypeAttributes()
#endif
{
var attributes = new List<ArrayPropertyInfo>();
var result = new SortedDictionary<int, List<ArrayPropertyInfo>>();
var properties = typeof(T).GetProperties();
foreach (var property in properties)
{
@@ -208,7 +214,13 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
var targetType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType;
var converterType = property.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType ?? targetType.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType;
attributes.Add(new ArrayPropertyInfo
if (!result.TryGetValue(att.Index, out var indexList))
{
indexList = new List<ArrayPropertyInfo>();
result[att.Index] = indexList;
}
indexList.Add(new ArrayPropertyInfo
{
ArrayProperty = att,
PropertyInfo = property,
@@ -218,7 +230,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
});
}
return attributes;
return result;
}
private class ArrayPropertyInfo
@@ -1,6 +1,5 @@
using Microsoft.Extensions.Logging;
using System;
using System.Diagnostics;
using System.Runtime.Serialization;
using System.Text.Json;
using System.Text.Json.Serialization;
@@ -21,58 +20,15 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
/// <inheritdoc />
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
return typeToConvert == typeof(bool) ? new BoolConverterInner<bool>() : new BoolConverterInner<bool?>();
return typeToConvert == typeof(bool) ? new BoolConverterInner() : new BoolConverterInnerNullable();
}
private class BoolConverterInner<T> : JsonConverter<T>
private class BoolConverterInnerNullable : JsonConverter<bool?>
{
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> (T)((object?)ReadBool(ref reader, typeToConvert, options) ?? default(T))!;
public override bool? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> ReadBool(ref reader, typeToConvert, options);
public bool? ReadBool(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.True)
return true;
if (reader.TokenType == JsonTokenType.False)
return false;
var value = reader.TokenType switch
{
JsonTokenType.String => reader.GetString(),
JsonTokenType.Number => reader.GetInt16().ToString(),
_ => null
};
value = value?.ToLowerInvariant().Trim();
if (string.IsNullOrEmpty(value))
{
if (typeToConvert == typeof(bool))
LibraryHelpers.StaticLogger?.LogWarning("Received null bool value, but property type is not a nullable bool");
return default;
}
switch (value)
{
case "true":
case "yes":
case "y":
case "1":
case "on":
return true;
case "false":
case "no":
case "n":
case "0":
case "off":
case "-1":
return false;
}
throw new SerializationException($"Can't convert bool value {value}");
}
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
public override void Write(Utf8JsonWriter writer, bool? value, JsonSerializerOptions options)
{
if (value is bool boolVal)
writer.WriteBooleanValue(boolVal);
@@ -81,5 +37,59 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
}
}
private class BoolConverterInner : JsonConverter<bool>
{
public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> ReadBool(ref reader, typeToConvert, options) ?? false;
public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options)
{
writer.WriteBooleanValue(value);
}
}
private static bool? ReadBool(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.True)
return true;
if (reader.TokenType == JsonTokenType.False)
return false;
var value = reader.TokenType switch
{
JsonTokenType.String => reader.GetString(),
JsonTokenType.Number => reader.GetInt16().ToString(),
_ => null
};
value = value?.ToLowerInvariant().Trim();
if (string.IsNullOrEmpty(value))
{
if (typeToConvert == typeof(bool))
LibraryHelpers.StaticLogger?.LogWarning("Received null or empty bool value, but property type is not a nullable bool. Resolver: {Resolver}", options.TypeInfoResolver?.GetType()?.Name);
return default;
}
switch (value)
{
case "true":
case "yes":
case "y":
case "1":
case "on":
return true;
case "false":
case "no":
case "n":
case "0":
case "off":
case "-1":
return false;
}
throw new SerializationException($"Can't convert bool value {value}");
}
}
}
@@ -1,8 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
@@ -0,0 +1,30 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
/// Converter for comma separated string values
/// </summary>
public class CommaSplitStringConverter : JsonConverter<string[]>
{
/// <inheritdoc />
public override string[]? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var str = reader.GetString();
if (string.IsNullOrEmpty(str))
return [];
return str!.Split(',').ToArray() ?? [];
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, string[] value, JsonSerializerOptions options)
{
writer.WriteStringValue(string.Join(",", value));
}
}
}
@@ -1,6 +1,5 @@
using Microsoft.Extensions.Logging;
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Text.Json;
@@ -27,64 +26,77 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
/// <inheritdoc />
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
return typeToConvert == typeof(DateTime) ? new DateTimeConverterInner<DateTime>() : new DateTimeConverterInner<DateTime?>();
return typeToConvert == typeof(DateTime) ? new DateTimeConverterInner() : new NullableDateTimeConverterInner();
}
private class DateTimeConverterInner<T> : JsonConverter<T>
private class NullableDateTimeConverterInner : JsonConverter<DateTime?>
{
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> (T)((object?)ReadDateTime(ref reader, typeToConvert, options) ?? default(T))!;
public override DateTime? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> ReadDateTime(ref reader, typeToConvert, options);
private DateTime? ReadDateTime(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null)
{
if (typeToConvert == typeof(DateTime))
LibraryHelpers.StaticLogger?.LogWarning("DateTime value of null, but property is not nullable");
return default;
}
if (reader.TokenType is JsonTokenType.Number)
{
var decValue = reader.GetDecimal();
if (decValue == 0 || decValue < 0)
return default;
return ParseFromDecimal(decValue);
}
else if (reader.TokenType is JsonTokenType.String)
{
var stringValue = reader.GetString();
if (string.IsNullOrWhiteSpace(stringValue)
|| stringValue == "-1"
|| stringValue == "0001-01-01T00:00:00Z"
|| decimal.TryParse(stringValue, out var decVal) && decVal == 0)
{
return default;
}
return ParseFromString(stringValue!);
}
else
{
return reader.GetDateTime();
}
}
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
public override void Write(Utf8JsonWriter writer, DateTime? value, JsonSerializerOptions options)
{
if (value == null)
{
writer.WriteNullValue();
return;
}
if (value.Value == default)
writer.WriteStringValue(default(DateTime));
else
writer.WriteNumberValue((long)Math.Round((value.Value - new DateTime(1970, 1, 1)).TotalMilliseconds));
}
}
private class DateTimeConverterInner : JsonConverter<DateTime>
{
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> ReadDateTime(ref reader, typeToConvert, options) ?? default;
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
{
var dtValue = value;
if (dtValue == default)
writer.WriteStringValue(default(DateTime));
else
writer.WriteNumberValue((long)Math.Round((dtValue - new DateTime(1970, 1, 1)).TotalMilliseconds));
}
}
private static DateTime? ReadDateTime(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null)
{
if (typeToConvert == typeof(DateTime))
LibraryHelpers.StaticLogger?.LogWarning("DateTime value of null, but property is not nullable. Resolver: {Resolver}", options.TypeInfoResolver?.GetType()?.Name);
return default;
}
if (reader.TokenType is JsonTokenType.Number)
{
var decValue = reader.GetDecimal();
if (decValue == 0 || decValue < 0)
return default;
return ParseFromDecimal(decValue);
}
else if (reader.TokenType is JsonTokenType.String)
{
var stringValue = reader.GetString();
if (string.IsNullOrWhiteSpace(stringValue)
|| stringValue!.Equals("-1", StringComparison.Ordinal)
|| stringValue!.Equals("0001-01-01T00:00:00Z", StringComparison.OrdinalIgnoreCase)
|| decimal.TryParse(stringValue, out var decVal) && decVal == 0)
{
var dtValue = (DateTime)(object)value;
if (dtValue == default)
writer.WriteStringValue(default(DateTime));
else
writer.WriteNumberValue((long)Math.Round((dtValue - new DateTime(1970, 1, 1)).TotalMilliseconds));
return default;
}
return ParseFromString(stringValue!, options.TypeInfoResolver?.GetType()?.Name);
}
else
{
return reader.GetDateTime();
}
}
@@ -112,11 +124,9 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
/// <summary>
/// Parse a string value to datetime
/// </summary>
/// <param name="stringValue"></param>
/// <returns></returns>
public static DateTime ParseFromString(string stringValue)
public static DateTime ParseFromString(string stringValue, string? resolverName)
{
if (stringValue!.Length == 12 && stringValue.StartsWith("202"))
if (stringValue!.Length == 12 && stringValue.StartsWith("202", StringComparison.OrdinalIgnoreCase))
{
// Parse 202303261200 format
if (!int.TryParse(stringValue.Substring(0, 4), out var year)
@@ -125,7 +135,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|| !int.TryParse(stringValue.Substring(8, 2), out var hour)
|| !int.TryParse(stringValue.Substring(10, 2), out var minute))
{
LibraryHelpers.StaticLogger?.LogWarning("Unknown DateTime format: " + stringValue);
LibraryHelpers.StaticLogger?.LogWarning("Unknown DateTime format: {Value}. Resolver: {Resolver}", stringValue, resolverName);
return default;
}
return new DateTime(year, month, day, hour, minute, 0, DateTimeKind.Utc);
@@ -138,7 +148,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|| !int.TryParse(stringValue.Substring(4, 2), out var month)
|| !int.TryParse(stringValue.Substring(6, 2), out var day))
{
LibraryHelpers.StaticLogger?.LogWarning("Unknown DateTime format: " + stringValue);
LibraryHelpers.StaticLogger?.LogWarning("Unknown DateTime format: {Value}. Resolver: {Resolver}", stringValue, resolverName);
return default;
}
return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc);
@@ -151,7 +161,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|| !int.TryParse(stringValue.Substring(2, 2), out var month)
|| !int.TryParse(stringValue.Substring(4, 2), out var day))
{
LibraryHelpers.StaticLogger?.LogWarning("Unknown DateTime format: " + stringValue);
LibraryHelpers.StaticLogger?.LogWarning("Unknown DateTime format: {Value}. Resolver: {Resolver}", stringValue, resolverName);
return default;
}
return new DateTime(year + 2000, month, day, 0, 0, 0, DateTimeKind.Utc);
@@ -180,7 +190,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|| !int.TryParse(values[1], out var month)
|| !int.TryParse(values[2], out var day))
{
LibraryHelpers.StaticLogger?.LogWarning("Unknown DateTime format: " + stringValue);
LibraryHelpers.StaticLogger?.LogWarning("Unknown DateTime format: {Value}. Resolver: {Resolver}", stringValue, resolverName);
return default;
}
@@ -1,5 +1,4 @@
using System;
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
@@ -1,10 +1,11 @@
using CryptoExchange.Net.Attributes;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using System;
using System.Collections.Concurrent;
#if NET8_0_OR_GREATER
using System.Collections.Frozen;
#endif
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
@@ -66,7 +67,25 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
#endif
: JsonConverter<T>, INullableConverterFactory where T : struct, Enum
{
private static List<KeyValuePair<T, string>>? _mapping = null;
class EnumMapping
{
public T Value { get; set; }
public string StringValue { get; set; }
public EnumMapping(T value, string stringValue)
{
Value = value;
StringValue = stringValue;
}
}
#if NET8_0_OR_GREATER
private static FrozenSet<EnumMapping>? _mappingToEnum = null;
private static FrozenDictionary<T, string>? _mappingToString = null;
#else
private static List<EnumMapping>? _mappingToEnum = null;
private static Dictionary<T, string>? _mappingToString = null;
#endif
private NullableEnumConverter? _nullableEnumConverter = null;
private static ConcurrentBag<string> _unknownValuesWarned = new ConcurrentBag<string>();
@@ -106,7 +125,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
if (isEmptyString && !_unknownValuesWarned.Contains(null))
{
// We received an empty string and have no mapping for it, and the property isn't nullable
LibraryHelpers.StaticLogger?.LogWarning($"Received null or empty enum value, but property type is not a nullable enum. EnumType: {typeof(T).Name}. If you think {typeof(T).Name} should be nullable please open an issue on the Github repo");
LibraryHelpers.StaticLogger?.LogWarning($"Received null or empty enum value, but property type is not a nullable enum. EnumType: {typeof(T).FullName}. If you think {typeof(T).FullName} should be nullable please open an issue on the Github repo");
}
return new T(); // return default value
@@ -121,8 +140,8 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
{
isEmptyString = false;
var enumType = typeof(T);
if (_mapping == null)
_mapping = AddMapping();
if (_mappingToEnum == null)
CreateMapping();
var stringValue = reader.TokenType switch
{
@@ -149,7 +168,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
if (!_unknownValuesWarned.Contains(stringValue))
{
_unknownValuesWarned.Add(stringValue!);
LibraryHelpers.StaticLogger?.LogWarning($"Cannot map enum value. EnumType: {enumType.Name}, Value: {stringValue}, Known values: {string.Join(", ", _mapping.Select(m => m.Value))}. If you think {stringValue} should added please open an issue on the Github repo");
LibraryHelpers.StaticLogger?.LogWarning($"Cannot map enum value. EnumType: {enumType.FullName}, Value: {stringValue}, Known values: [{string.Join(", ", _mappingToEnum!.Select(m => $"{m.StringValue}: {m.Value}"))}]. If you think {stringValue} should added please open an issue on the Github repo");
}
}
@@ -168,16 +187,35 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
private static bool GetValue(Type objectType, string value, out T? result)
{
if (_mapping != null)
if (_mappingToEnum != null)
{
// Check for exact match first, then if not found fallback to a case insensitive match
var mapping = _mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCulture));
if (mapping.Equals(default(KeyValuePair<T, string>)))
mapping = _mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase));
if (!mapping.Equals(default(KeyValuePair<T, string>)))
EnumMapping? mapping = null;
// Try match on full equals
foreach (var item in _mappingToEnum)
{
result = mapping.Key;
if (item.StringValue.Equals(value, StringComparison.Ordinal))
{
mapping = item;
break;
}
}
// If not found, try matching ignoring case
if (mapping == null)
{
foreach (var item in _mappingToEnum)
{
if (item.StringValue.Equals(value, StringComparison.OrdinalIgnoreCase))
{
mapping = item;
break;
}
}
}
if (mapping != null)
{
result = mapping.Value;
return true;
}
}
@@ -208,6 +246,12 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
{
// If no explicit mapping is found try to parse string
result = (T)Enum.Parse(objectType, value, true);
if (!Enum.IsDefined(objectType, result))
{
result = default;
return false;
}
return true;
}
catch (Exception)
@@ -217,9 +261,11 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
}
}
private static List<KeyValuePair<T, string>> AddMapping()
private static void CreateMapping()
{
var mapping = new List<KeyValuePair<T, string>>();
var mappingToEnum = new List<EnumMapping>();
var mappingToString = new Dictionary<T, string>();
var enumType = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
var enumMembers = enumType.GetFields();
foreach (var member in enumMembers)
@@ -228,12 +274,22 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
foreach (MapAttribute attribute in maps)
{
foreach (var value in attribute.Values)
mapping.Add(new KeyValuePair<T, string>((T)Enum.Parse(enumType, member.Name), value));
{
var enumVal = (T)Enum.Parse(enumType, member.Name);
mappingToEnum.Add(new EnumMapping(enumVal, value));
if (!mappingToString.ContainsKey(enumVal))
mappingToString.Add(enumVal, value);
}
}
}
_mapping = mapping;
return mapping;
#if NET8_0_OR_GREATER
_mappingToEnum = mappingToEnum.ToFrozenSet();
_mappingToString = mappingToString.ToFrozenDictionary();
#else
_mappingToEnum = mappingToEnum;
_mappingToString = mappingToString;
#endif
}
/// <summary>
@@ -244,10 +300,10 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
[return: NotNullIfNotNull("enumValue")]
public static string? GetString(T? enumValue)
{
if (_mapping == null)
_mapping = AddMapping();
if (_mappingToString == null)
CreateMapping();
return enumValue == null ? null : (_mapping.FirstOrDefault(v => v.Key.Equals(enumValue)).Value ?? enumValue.ToString());
return enumValue == null ? null : (_mappingToString!.TryGetValue(enumValue.Value, out var str) ? str : enumValue.ToString());
}
/// <summary>
@@ -258,15 +314,35 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
public static T? ParseString(string value)
{
var type = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
if (_mapping == null)
_mapping = AddMapping();
if (_mappingToEnum == null)
CreateMapping();
var mapping = _mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCulture));
if (mapping.Equals(default(KeyValuePair<T, string>)))
mapping = _mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase));
EnumMapping? mapping = null;
// Try match on full equals
foreach(var item in _mappingToEnum!)
{
if (item.StringValue.Equals(value, StringComparison.Ordinal))
{
mapping = item;
break;
}
}
if (!mapping.Equals(default(KeyValuePair<T, string>)))
return mapping.Key;
// If not found, try matching ignoring case
if (mapping == null)
{
foreach (var item in _mappingToEnum)
{
if (item.StringValue.Equals(value, StringComparison.OrdinalIgnoreCase))
{
mapping = item;
break;
}
}
}
if (mapping != null)
return mapping.Value;
try
{
@@ -1,5 +1,4 @@
using System;
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
@@ -0,0 +1,38 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
/// Bool converter
/// </summary>
public class IntBoolConverter : JsonConverter<bool>
{
private readonly int _trueValue;
/// <summary>
/// ctor
/// </summary>
/// <param name="trueValue">The int value representing the true value</param>
public IntBoolConverter(int trueValue)
{
_trueValue = trueValue;
}
/// <inheritdoc />
public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.Number)
return false;
return reader.GetDecimal() == _trueValue;
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options)
{
writer.WriteNumberValue(_trueValue);
}
}
}
@@ -0,0 +1,131 @@
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Errors;
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Net.Http.Headers;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers
{
/// <summary>
/// JSON REST message handler
/// </summary>
public abstract class JsonRestMessageHandler : IRestMessageHandler
{
private static MediaTypeWithQualityHeaderValue _acceptJsonContent = new MediaTypeWithQualityHeaderValue(Constants.JsonContentHeader);
private const int _errorResponseSnippetLimit = 128;
/// <summary>
/// Empty rate limit error
/// </summary>
protected static readonly ServerRateLimitError _emptyRateLimitError = new ServerRateLimitError();
/// <inheritdoc />
public virtual bool RequiresSeekableStream => false;
/// <summary>
/// The serializer options to use
/// </summary>
public abstract JsonSerializerOptions Options { get; }
/// <inheritdoc />
public MediaTypeWithQualityHeaderValue AcceptHeader => _acceptJsonContent;
/// <inheritdoc />
public virtual ValueTask<ServerRateLimitError> ParseErrorRateLimitResponse(
int httpStatusCode,
HttpResponseHeaders responseHeaders,
Stream responseStream)
{
// Handle retry after header
var retryAfterHeader = responseHeaders.SingleOrDefault(r => r.Key.Equals("Retry-After", StringComparison.InvariantCultureIgnoreCase));
if (retryAfterHeader.Value?.Any() != true)
return new ValueTask<ServerRateLimitError>(_emptyRateLimitError);
var value = retryAfterHeader.Value.First();
if (int.TryParse(value, out var seconds))
return new ValueTask<ServerRateLimitError>(new ServerRateLimitError() { RetryAfter = DateTime.UtcNow.AddSeconds(seconds) });
if (DateTime.TryParse(value, out var datetime))
return new ValueTask<ServerRateLimitError>(new ServerRateLimitError() { RetryAfter = datetime });
return new ValueTask<ServerRateLimitError>(_emptyRateLimitError);
}
/// <inheritdoc />
public abstract ValueTask<Error> ParseErrorResponse(
int httpStatusCode,
HttpResponseHeaders responseHeaders,
Stream responseStream);
/// <inheritdoc />
public virtual ValueTask<Error?> CheckForErrorResponse(
RequestDefinition request,
HttpResponseHeaders responseHeaders,
Stream responseStream) => new ValueTask<Error?>((Error?)null);
/// <summary>
/// Read the response into a JsonDocument object
/// </summary>
protected virtual async ValueTask<(Error?, JsonDocument?)> GetJsonDocument(Stream stream)
{
try
{
var document = await JsonDocument.ParseAsync(stream).ConfigureAwait(false);
return (null, document);
}
catch (Exception ex)
{
var errorMsg = "Deserialization failed, invalid JSON";
if (stream.CanSeek)
{
var dataSnippet = new char[_errorResponseSnippetLimit];
stream.Seek(0, SeekOrigin.Begin);
var written = new StreamReader(stream).ReadBlock(dataSnippet, 0, _errorResponseSnippetLimit);
var data = new string(dataSnippet, 0, written);
errorMsg += $": {data}";
if (data.Length == _errorResponseSnippetLimit)
errorMsg += " (truncated)";
}
var error = new DeserializeError(errorMsg, ex);
return (error, null);
}
}
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
public async ValueTask<(T? Result, Error? Error)> TryDeserializeAsync<T>(Stream responseStream, CancellationToken cancellationToken)
{
try
{
#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
#pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
var result = await JsonSerializer.DeserializeAsync<T>(responseStream, Options)!.ConfigureAwait(false)!;
#pragma warning restore IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
#pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
return (result, null);
}
catch (JsonException ex)
{
var info = $"Json deserialization failed: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}";
return (default, new DeserializeError(info, ex));
}
catch (Exception ex)
{
return (default, new DeserializeError($"Json deserialization failed: {ex.Message}", ex));
}
}
/// <inheritdoc />
public virtual Error? CheckDeserializedResponse<T>(HttpResponseHeaders responseHeaders, T result) => null;
}
}
@@ -0,0 +1,362 @@
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Net.WebSockets;
using System.Text;
using System.Text.Json;
namespace CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers
{
/// <summary>
/// JSON WebSocket message handler, sequentially read the JSON and looks for specific predefined fields to identify the message
/// </summary>
public abstract class JsonSocketMessageHandler : ISocketMessageHandler
{
/// <summary>
/// The serializer options to use
/// </summary>
public abstract JsonSerializerOptions Options { get; }
/// <summary>
/// Message evaluators
/// </summary>
protected abstract MessageTypeDefinition[] TypeEvaluators { get; }
private readonly SearchResult _searchResult = new();
private bool _hasArraySearches;
private bool _initialized;
private int _maxSearchDepth;
private MessageTypeDefinition? _topEvaluator;
private List<MessageEvalutorFieldReference>? _searchFields;
private Dictionary<Type, Func<object, string?>>? _baseTypeMapping;
private Dictionary<Type, Func<object, string?>>? _mapping;
/// <summary>
/// Add a mapping of a specific object of a type to a specific topic
/// </summary>
/// <typeparam name="T">Type to get topic for</typeparam>
/// <param name="mapping">The topic retrieve delegate</param>
protected void AddTopicMapping<T>(Func<T, string?> mapping)
{
_mapping ??= new Dictionary<Type, Func<object, string?>>();
_mapping.Add(typeof(T), x => mapping((T)x));
}
private void InitializeConverter()
{
if (_initialized)
return;
_maxSearchDepth = int.MinValue;
_searchFields = new List<MessageEvalutorFieldReference>();
foreach (var evaluator in TypeEvaluators)
{
_topEvaluator ??= evaluator;
foreach (var field in evaluator.Fields)
{
var overlapping = _searchFields.Where(otherField =>
{
if (field is PropertyFieldReference propRef
&& otherField.Field is PropertyFieldReference otherPropRef)
{
return field.Depth == otherPropRef.Depth && propRef.PropertyName.SequenceEqual(otherPropRef.PropertyName);
}
else if (field is ArrayFieldReference arrayRef
&& otherField.Field is ArrayFieldReference otherArrayPropRef)
{
return field.Depth == otherArrayPropRef.Depth && arrayRef.ArrayIndex == otherArrayPropRef.ArrayIndex;
}
return false;
}).ToList();
if (overlapping.Any())
{
foreach (var overlap in overlapping)
overlap.OverlappingField = true;
}
List<MessageEvalutorFieldReference>? existingSameSearchField = new();
if (field is ArrayFieldReference arrayField)
{
_hasArraySearches = true;
existingSameSearchField = _searchFields.Where(x =>
x.Field is ArrayFieldReference arrayFieldRef
&& arrayFieldRef.ArrayIndex == arrayField.ArrayIndex
&& arrayFieldRef.Depth == arrayField.Depth
&& arrayFieldRef.Constraint == null && arrayField.Constraint == null).ToList();
}
else if (field is PropertyFieldReference propField)
{
existingSameSearchField = _searchFields.Where(x =>
x.Field is PropertyFieldReference propFieldRef
&& propFieldRef.PropertyName.SequenceEqual(propField.PropertyName)
&& propFieldRef.Depth == propField.Depth
&& propFieldRef.Constraint == null && propFieldRef.Constraint == null).ToList();
}
foreach(var sameSearchField in existingSameSearchField)
{
if (sameSearchField.SkipReading == true
&& (evaluator.TypeIdentifierCallback != null || field.Constraint != null))
{
sameSearchField.SkipReading = false;
}
if (evaluator.ForceIfFound)
{
if (evaluator.Fields.Length > 1 || sameSearchField.ForceEvaluator != null)
throw new Exception("Invalid config");
//sameSearchField.ForceEvaluator = evaluator;
}
}
_searchFields.Add(new MessageEvalutorFieldReference(field)
{
SkipReading = evaluator.TypeIdentifierCallback == null && field.Constraint == null,
ForceEvaluator = !existingSameSearchField.Any() ? evaluator.ForceIfFound ? evaluator : null : null,
OverlappingField = overlapping.Any()
});
if (field.Depth > _maxSearchDepth)
_maxSearchDepth = field.Depth;
}
}
_initialized = true;
}
/// <inheritdoc />
public virtual string? GetTopicFilter(object deserializedObject)
{
if (_mapping == null)
return null;
// Cache the found type for future
var currentType = deserializedObject.GetType();
if (_baseTypeMapping != null)
{
if (_baseTypeMapping.TryGetValue(currentType, out var typeMapping))
return typeMapping(deserializedObject);
}
var mappedBase = false;
while (currentType != null)
{
if (_mapping.TryGetValue(currentType, out var mapping))
{
if (mappedBase)
{
_baseTypeMapping ??= new Dictionary<Type, Func<object, string?>>();
_baseTypeMapping.Add(deserializedObject.GetType(), mapping);
}
return mapping(deserializedObject);
}
mappedBase = true;
currentType = currentType.BaseType;
}
return null;
}
/// <summary>
/// Return type identifier for non-json messages
/// </summary>
protected virtual string? GetTypeIdentifierNonJson(ReadOnlySpan<byte> data, WebSocketMessageType? webSocketMessageType)
{
return null;
}
/// <inheritdoc />
public virtual string? GetTypeIdentifier(ReadOnlySpan<byte> data, WebSocketMessageType? webSocketMessageType)
{
InitializeConverter();
int? arrayIndex = null;
_searchResult.Clear();
if (data[0] != 0x5B && data[0] != 0x7B)
{
// Message doesn't start with `{` or `[`, not valid for processing as json
return GetTypeIdentifierNonJson(data, webSocketMessageType);
}
var reader = new Utf8JsonReader(data);
while (reader.Read())
{
if ((reader.TokenType == JsonTokenType.StartArray
|| reader.TokenType == JsonTokenType.StartObject)
&& reader.CurrentDepth == _maxSearchDepth)
{
// There is no field we need to search for on a depth deeper than this, skip
reader.Skip();
continue;
}
if (reader.TokenType == JsonTokenType.StartArray)
arrayIndex = -1;
else if (reader.TokenType == JsonTokenType.EndArray)
arrayIndex = null;
else if (arrayIndex != null)
arrayIndex++;
if (reader.TokenType == JsonTokenType.PropertyName
|| arrayIndex != null && _hasArraySearches)
{
bool written = false;
string? value = null;
byte[]? propName = null;
foreach (var field in _searchFields!)
{
if (field.Field.Depth != reader.CurrentDepth)
continue;
bool readArrayValues = false;
if (field.Field is PropertyFieldReference propFieldRef)
{
if (propName == null)
{
if (reader.TokenType != JsonTokenType.PropertyName)
continue;
if (!reader.ValueTextEquals(propFieldRef.PropertyName))
continue;
propName = propFieldRef.PropertyName;
readArrayValues = propFieldRef.ArrayValues;
reader.Read();
}
else if (!propFieldRef.PropertyName.SequenceEqual(propName))
{
continue;
}
}
else if (field.Field is ArrayFieldReference arrayFieldRef)
{
if (propName != null)
continue;
if (reader.TokenType == JsonTokenType.PropertyName)
continue;
if (arrayFieldRef.ArrayIndex != arrayIndex)
continue;
}
if (!field.SkipReading)
{
if (value == null)
{
if (readArrayValues)
{
if (reader.TokenType != JsonTokenType.StartArray)
// error
return null;
var sb = new StringBuilder();
reader.Read();// Read start array
bool first = true;
while(reader.TokenType != JsonTokenType.EndArray)
{
if (!first)
sb.Append(",");
first = false;
sb.Append(reader.GetString());
reader.Read();
}
value = first ? null : sb.ToString();
}
else
{
switch (reader.TokenType)
{
case JsonTokenType.Number:
value = reader.GetDecimal().ToString();
break;
case JsonTokenType.String:
value = reader.GetString()!;
break;
case JsonTokenType.True:
case JsonTokenType.False:
value = reader.GetBoolean().ToString()!;
break;
case JsonTokenType.Null:
value = null;
break;
case JsonTokenType.StartObject:
case JsonTokenType.StartArray:
value = null;
break;
default:
continue;
}
}
}
if (field.Field.Constraint != null
&& !field.Field.Constraint(value))
{
continue;
}
}
_searchResult.Write(field.Field, value);
if (field.ForceEvaluator != null)
{
if (field.ForceEvaluator.StaticIdentifier != null)
return field.ForceEvaluator.StaticIdentifier;
// Force the immediate return upon encountering this field
return field.ForceEvaluator.GetMessageType(_searchResult);
}
written = true;
if (!field.OverlappingField)
break;
}
if (!written)
continue;
if (_topEvaluator!.Satisfied(_searchResult))
return _topEvaluator.GetMessageType(_searchResult);
if (_searchFields.Count == _searchResult.Count)
break;
}
}
foreach (var evaluator in TypeEvaluators)
{
if (evaluator.Satisfied(_searchResult))
return evaluator.GetMessageType(_searchResult);
}
return null;
}
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
public virtual object Deserialize(ReadOnlySpan<byte> data, Type type)
{
#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
#pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
return JsonSerializer.Deserialize(data, type, Options)!;
#pragma warning restore IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
#pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
}
}
}
@@ -0,0 +1,63 @@
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
using System;
using System.Net.WebSockets;
using System.Text.Json;
namespace CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers
{
/// <summary>
/// JSON WebSocket message handler, reads the json data info a JsonDocument after which the data can be inspected to identify the message
/// </summary>
public abstract class JsonSocketPreloadMessageHandler : ISocketMessageHandler
{
/// <summary>
/// The serializer options to use
/// </summary>
public abstract JsonSerializerOptions Options { get; }
/// <inheritdoc />
public virtual string? GetTypeIdentifier(ReadOnlySpan<byte> data, WebSocketMessageType? webSocketMessageType)
{
var reader = new Utf8JsonReader(data);
var jsonDocument = JsonDocument.ParseValue(ref reader);
return GetTypeIdentifier(jsonDocument);
}
/// <summary>
/// Get the message identifier for this document
/// </summary>
protected abstract string? GetTypeIdentifier(JsonDocument document);
/// <summary>
/// Get optional topic filter, for example a symbol name
/// </summary>
public virtual string? GetTopicFilter(object deserializedObject) => null;
/// <inheritdoc />
public virtual object Deserialize(ReadOnlySpan<byte> data, Type type)
{
#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
#pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
return JsonSerializer.Deserialize(data, type, Options)!;
#pragma warning restore IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
#pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
}
/// <summary>
/// Get the string value for a path, or an emtpy string if not found
/// </summary>
protected string StringOrEmpty(JsonDocument document, string path)
{
if (!document.RootElement.TryGetProperty(path, out var element))
return string.Empty;
if (element.ValueKind == JsonValueKind.String)
return element.GetString() ?? string.Empty;
else if (element.ValueKind == JsonValueKind.Number)
return element.GetDecimal().ToString();
return string.Empty;
}
}
}
@@ -1,6 +1,4 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Text.Json.Serialization.Metadata;
using System.Text.Json;
using System.Text.Json.Serialization;
@@ -0,0 +1,58 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
#pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
/// Converter for parsing object or array responses
/// </summary>
public class ObjectOrArrayConverter : JsonConverterFactory
{
/// <inheritdoc />
public override bool CanConvert(Type typeToConvert) => true;
/// <inheritdoc />
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
var type = typeof(InternalObjectOrArrayConverter<>).MakeGenericType(typeToConvert);
return (JsonConverter)Activator.CreateInstance(type)!;
}
private class InternalObjectOrArrayConverter<T> : JsonConverter<T>
{
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.StartObject && !typeToConvert.IsArray)
{
// Object to object
return JsonDocument.ParseValue(ref reader).Deserialize<T>(options);
}
else if (reader.TokenType == JsonTokenType.StartArray && typeToConvert.IsArray)
{
// Array to array
return JsonDocument.ParseValue(ref reader).Deserialize<T>(options);
}
else if (reader.TokenType == JsonTokenType.StartArray)
{
// Array to object
JsonDocument.ParseValue(ref reader).Deserialize<T[]>(options);
return default;
}
else
{
// Object to array
JsonDocument.ParseValue(ref reader);
return default;
}
}
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
JsonSerializer.Serialize(writer, value, options);
}
}
}
}
@@ -1,6 +1,4 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
@@ -1,7 +1,5 @@
using CryptoExchange.Net.SharedApis;
using System;
using System.Collections.Generic;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
@@ -1,7 +1,5 @@
using CryptoExchange.Net.SharedApis;
using System;
using System.Collections.Generic;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
@@ -1,374 +0,0 @@
using CryptoExchange.Net.Converters.MessageParsing;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
/// System.Text.Json message accessor
/// </summary>
public abstract class SystemTextJsonMessageAccessor : IMessageAccessor
{
/// <summary>
/// The JsonDocument loaded
/// </summary>
protected JsonDocument? _document;
private readonly JsonSerializerOptions? _customSerializerOptions;
/// <inheritdoc />
public bool IsValid { get; set; }
/// <inheritdoc />
public abstract bool OriginalDataAvailable { get; }
/// <inheritdoc />
public object? Underlying => throw new NotImplementedException();
/// <summary>
/// ctor
/// </summary>
public SystemTextJsonMessageAccessor(JsonSerializerOptions options)
{
_customSerializerOptions = options;
}
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
public CallResult<object> Deserialize(Type type, MessagePath? path = null)
{
if (!IsValid)
return new CallResult<object>(GetOriginalString());
if (_document == null)
throw new InvalidOperationException("No json document loaded");
try
{
var result = _document.Deserialize(type, _customSerializerOptions);
return new CallResult<object>(result!);
}
catch (JsonException ex)
{
var info = $"Json deserialization failed: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}";
return new CallResult<object>(new DeserializeError(info, ex));
}
catch (Exception ex)
{
return new CallResult<object>(new DeserializeError($"Json deserialization failed: {ex.Message}", ex));
}
}
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
public CallResult<T> Deserialize<T>(MessagePath? path = null)
{
if (_document == null)
throw new InvalidOperationException("No json document loaded");
try
{
var result = _document.Deserialize<T>(_customSerializerOptions);
return new CallResult<T>(result!);
}
catch (JsonException ex)
{
var info = $"Json deserialization failed: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}";
return new CallResult<T>(new DeserializeError(info, ex));
}
catch (Exception ex)
{
return new CallResult<T>(new DeserializeError($"Json deserialization failed: {ex.Message}", ex));
}
}
/// <inheritdoc />
public NodeType? GetNodeType()
{
if (!IsValid)
throw new InvalidOperationException("Can't access json data on non-json message");
if (_document == null)
throw new InvalidOperationException("No json document loaded");
return _document.RootElement.ValueKind switch
{
JsonValueKind.Object => NodeType.Object,
JsonValueKind.Array => NodeType.Array,
_ => NodeType.Value
};
}
/// <inheritdoc />
public NodeType? GetNodeType(MessagePath path)
{
if (!IsValid)
throw new InvalidOperationException("Can't access json data on non-json message");
var node = GetPathNode(path);
if (!node.HasValue)
return null;
return node.Value.ValueKind switch
{
JsonValueKind.Object => NodeType.Object,
JsonValueKind.Array => NodeType.Array,
_ => NodeType.Value
};
}
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
public T? GetValue<T>(MessagePath path)
{
if (!IsValid)
throw new InvalidOperationException("Can't access json data on non-json message");
var value = GetPathNode(path);
if (value == null)
return default;
if (value.Value.ValueKind == JsonValueKind.Object || value.Value.ValueKind == JsonValueKind.Array)
{
try
{
return value.Value.Deserialize<T>(_customSerializerOptions);
}
catch { }
return default;
}
if (typeof(T) == typeof(string))
{
if (value.Value.ValueKind == JsonValueKind.Number)
return (T)(object)value.Value.GetInt64().ToString();
}
return value.Value.Deserialize<T>(_customSerializerOptions);
}
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
public T?[]? GetValues<T>(MessagePath path)
{
if (!IsValid)
throw new InvalidOperationException("Can't access json data on non-json message");
var value = GetPathNode(path);
if (value == null)
return default;
if (value.Value.ValueKind != JsonValueKind.Array)
return default;
return value.Value.Deserialize<T[]>(_customSerializerOptions)!;
}
private JsonElement? GetPathNode(MessagePath path)
{
if (!IsValid)
throw new InvalidOperationException("Can't access json data on non-json message");
if (_document == null)
throw new InvalidOperationException("No json document loaded");
JsonElement? currentToken = _document.RootElement;
foreach (var node in path)
{
if (node.Type == 0)
{
// Int value
var val = node.Index!.Value;
if (currentToken!.Value.ValueKind != JsonValueKind.Array || currentToken.Value.GetArrayLength() <= val)
return null;
currentToken = currentToken.Value[val];
}
else if (node.Type == 1)
{
// String value
if (currentToken!.Value.ValueKind != JsonValueKind.Object)
return null;
if (!currentToken.Value.TryGetProperty(node.Property!, out var token))
return null;
currentToken = token;
}
else
{
// Property name
if (currentToken!.Value.ValueKind != JsonValueKind.Object)
return null;
throw new NotImplementedException();
}
if (currentToken == null)
return null;
}
return currentToken;
}
/// <inheritdoc />
public abstract string GetOriginalString();
/// <inheritdoc />
public abstract void Clear();
}
/// <summary>
/// System.Text.Json stream message accessor
/// </summary>
public class SystemTextJsonStreamMessageAccessor : SystemTextJsonMessageAccessor, IStreamMessageAccessor
{
private Stream? _stream;
/// <inheritdoc />
public override bool OriginalDataAvailable => _stream?.CanSeek == true;
/// <summary>
/// ctor
/// </summary>
public SystemTextJsonStreamMessageAccessor(JsonSerializerOptions options): base(options)
{
}
/// <inheritdoc />
public async Task<CallResult> Read(Stream stream, bool bufferStream)
{
if (bufferStream && stream is not MemoryStream)
{
// We need to be buffer the stream, and it's not currently a seekable stream, so copy it to a new memory stream
_stream = new MemoryStream();
stream.CopyTo(_stream);
_stream.Position = 0;
}
else if (bufferStream)
{
// We need to buffer the stream, and the current stream is seekable, store as is
_stream = stream;
}
else
{
// We don't need to buffer the stream, so don't bother keeping the reference
}
try
{
_document = await JsonDocument.ParseAsync(_stream ?? stream).ConfigureAwait(false);
IsValid = true;
return CallResult.SuccessResult;
}
catch (Exception ex)
{
// Not a json message
IsValid = false;
return new CallResult(new DeserializeError($"Json deserialization failed: {ex.Message}", ex));
}
}
/// <inheritdoc />
public override string GetOriginalString()
{
if (_stream is null)
throw new NullReferenceException("Stream not initialized");
_stream.Position = 0;
using var textReader = new StreamReader(_stream, Encoding.UTF8, false, 1024, true);
return textReader.ReadToEnd();
}
/// <inheritdoc />
public override void Clear()
{
_stream?.Dispose();
_stream = null;
_document?.Dispose();
_document = null;
}
}
/// <summary>
/// System.Text.Json byte message accessor
/// </summary>
public class SystemTextJsonByteMessageAccessor : SystemTextJsonMessageAccessor, IByteMessageAccessor
{
private ReadOnlyMemory<byte> _bytes;
/// <summary>
/// ctor
/// </summary>
public SystemTextJsonByteMessageAccessor(JsonSerializerOptions options) : base(options)
{
}
/// <inheritdoc />
public CallResult Read(ReadOnlyMemory<byte> data)
{
_bytes = data;
try
{
var firstByte = data.Span[0];
if (firstByte != 0x7b && firstByte != 0x5b)
{
// Value doesn't start with `{` or `[`, prevent deserialization attempt as it's slow
IsValid = false;
return new CallResult(new DeserializeError("Not a json value"));
}
_document = JsonDocument.Parse(data);
IsValid = true;
return CallResult.SuccessResult;
}
catch (Exception ex)
{
// Not a json message
IsValid = false;
return new CallResult(new DeserializeError($"Json deserialization failed: {ex.Message}", ex));
}
}
/// <inheritdoc />
public override string GetOriginalString() =>
// NetStandard 2.0 doesn't support GetString from a ReadonlySpan<byte>, so use ToArray there instead
#if NETSTANDARD2_0
Encoding.UTF8.GetString(_bytes.ToArray());
#else
Encoding.UTF8.GetString(_bytes.Span);
#endif
/// <inheritdoc />
public override bool OriginalDataAvailable => true;
/// <inheritdoc />
public override void Clear()
{
_bytes = null;
_document?.Dispose();
_document = null;
}
}
}
@@ -1,8 +1,6 @@
using CryptoExchange.Net.Interfaces;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
+12 -10
View File
@@ -1,14 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1;net8.0;net9.0</TargetFrameworks>
<TargetFrameworks>netstandard2.0;netstandard2.1;net8.0;net9.0;net10.0</TargetFrameworks>
</PropertyGroup>
<PropertyGroup>
<PackageId>CryptoExchange.Net</PackageId>
<Authors>JKorf</Authors>
<Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description>
<PackageVersion>9.11.0</PackageVersion>
<AssemblyVersion>9.11.0</AssemblyVersion>
<FileVersion>9.11.0</FileVersion>
<PackageVersion>10.7.1</PackageVersion>
<AssemblyVersion>10.7.1</AssemblyVersion>
<FileVersion>10.7.1</FileVersion>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange;CryptoExchange.Net</PackageTags>
<RepositoryType>git</RepositoryType>
@@ -20,7 +20,7 @@
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageReleaseNotes>https://github.com/JKorf/CryptoExchange.Net?tab=readme-ov-file#release-notes</PackageReleaseNotes>
<Nullable>enable</Nullable>
<LangVersion>12.0</LangVersion>
<LangVersion>latest</LangVersion>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
</PropertyGroup>
<ItemGroup>
@@ -45,17 +45,19 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0">
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="10.0.101">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.10" />
<PackageReference Include="System.Text.Json" Version="9.0.10" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.1" />
<PackageReference Include="System.Text.Json" Version="10.0.1" />
<PackageReference Include="NSec.Cryptography" Version="25.4.0" Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0'))" />
</ItemGroup>
<ItemGroup Label="Transitive Client Packages">
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.10" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.10" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.1" />
<PackageReference Include="System.Threading.Channels" Version="10.0.1" />
</ItemGroup>
</Project>
@@ -0,0 +1,24 @@
using System;
namespace CryptoExchange.Net.Exceptions
{
/// <summary>
/// Exception during deserialization
/// </summary>
public class CeDeserializationException : Exception
{
/// <summary>
/// ctor
/// </summary>
public CeDeserializationException(string message) : base(message)
{
}
/// <summary>
/// ctor
/// </summary>
public CeDeserializationException(string message, Exception innerException) : base(message, innerException)
{
}
}
}
+153 -5
View File
@@ -1,8 +1,10 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.SharedApis;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Threading;
@@ -241,8 +243,7 @@ namespace CryptoExchange.Net
/// <summary>
/// Generate a long value
/// </summary>
/// <param name="maxLength">Max character length</param>
/// <returns></returns>
/// <param name="maxLength">Max number of digits</param>
public static long RandomLong(int maxLength)
{
#if NETSTANDARD2_1_OR_GREATER || NET9_0_OR_GREATER
@@ -258,6 +259,25 @@ namespace CryptoExchange.Net
return value;
}
/// <summary>
/// Generate a long value between two values
/// </summary>
/// <param name="minValue">Min value</param>
/// <param name="maxValue">Max value</param>
/// <returns></returns>
public static long RandomLong(long minValue, long maxValue)
{
#if NET8_0_OR_GREATER
var buf = RandomNumberGenerator.GetBytes(8);
#else
byte[] buf = new byte[8];
var random = new Random();
random.NextBytes(buf);
#endif
long longRand = BitConverter.ToInt64(buf, 0);
return (Math.Abs(longRand % (maxValue - minValue)) + minValue);
}
/// <summary>
/// Generate a random string of specified length
/// </summary>
@@ -291,11 +311,11 @@ namespace CryptoExchange.Net
/// <param name="request">The request parameters</param>
/// <param name="ct">Cancellation token</param>
/// <returns></returns>
public static async IAsyncEnumerable<ExchangeWebResult<T[]>> ExecutePages<T, U>(Func<U, INextPageToken?, CancellationToken, Task<ExchangeWebResult<T[]>>> paginatedFunc, U request, [EnumeratorCancellation]CancellationToken ct = default)
public static async IAsyncEnumerable<ExchangeWebResult<T[]>> ExecutePages<T, U>(Func<U, PageRequest?, CancellationToken, Task<ExchangeWebResult<T[]>>> paginatedFunc, U request, [EnumeratorCancellation]CancellationToken ct = default)
{
var result = new List<T>();
ExchangeWebResult<T[]> batch;
INextPageToken? nextPageToken = null;
PageRequest? nextPageToken = null;
while (true)
{
batch = await paginatedFunc(request, nextPageToken, ct).ConfigureAwait(false);
@@ -304,12 +324,42 @@ namespace CryptoExchange.Net
break;
result.AddRange(batch.Data);
nextPageToken = batch.NextPageToken;
nextPageToken = batch.NextPageRequest;
if (nextPageToken == null)
break;
}
}
/// <summary>
/// Apply filters to the data set
/// </summary>
/// <typeparam name="T">Type</typeparam>
/// <param name="data">Data set</param>
/// <param name="timeSelector">Time selector for the data</param>
/// <param name="startTime">Start time filter</param>
/// <param name="endTime">End time filter</param>
/// <param name="direction">Data direction</param>
public static IEnumerable<T> ApplyFilter<T>(
IEnumerable<T> data,
Func<T, DateTime> timeSelector,
DateTime? startTime,
DateTime? endTime,
DataDirection direction)
{
if (direction == DataDirection.Ascending)
data = data.OrderBy(timeSelector);
else
data = data.OrderByDescending(timeSelector);
if (startTime != null)
data = data.Where(x => timeSelector(x) >= startTime.Value);
if (endTime != null)
data = data.Where(x => timeSelector(x) < endTime.Value);
return data;
}
/// <summary>
/// Apply the rules (price and quantity step size and decimals precision, min/max quantity) from the symbol to the quantity and price
/// </summary>
@@ -341,6 +391,104 @@ namespace CryptoExchange.Net
}
/// <summary>
/// Queue updates received from a websocket subscriptions and process them async
/// </summary>
/// <typeparam name="T">The queued update type</typeparam>
/// <param name="subscribeCall">The subscribe call</param>
/// <param name="asyncHandler">The async update handler</param>
/// <param name="maxQueuedItems">The max number of updates 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 updates. If no max is set this setting is ignored</param>
public static async Task<CallResult<UpdateSubscription>> ProcessQueuedAsync<T>(
Func<Action<DataEvent<T>>, Task<CallResult<UpdateSubscription>>> subscribeCall,
Func<DataEvent<T>, Task> asyncHandler,
int? maxQueuedItems = null,
QueueFullBehavior? fullBehavior = null)
{
var processor = new ProcessQueue<DataEvent<T>>(asyncHandler, maxQueuedItems, fullBehavior);
await processor.StartAsync().ConfigureAwait(false);
var result = await subscribeCall(upd => processor.Write(upd)).ConfigureAwait(false);
if (!result)
{
await processor.StopAsync().ConfigureAwait(false);
return result;
}
processor.Exception += result.Data._subscription.InvokeExceptionHandler;
result.Data.SubscriptionStatusChanged += (upd) =>
{
if (upd == CryptoExchange.Net.Objects.SubscriptionStatus.Closed)
_ = processor.StopAsync(true);
};
return result;
}
/// <summary>
/// Queue updates and process them async
/// </summary>
/// <typeparam name="T">The queued update type</typeparam>
/// <param name="subscribeCall">The subscribe call</param>
/// <param name="asyncHandler">The async update handler</param>
/// <param name="maxQueuedItems">The max number of updates 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 updates. If no max is set this setting is ignored</param>
/// <param name="ct">Cancellation token to stop the processing</param>
public static async Task ProcessQueuedAsync<T>(
Func<Action<T>, Task> subscribeCall,
Func<T, Task> asyncHandler,
CancellationToken ct,
int? maxQueuedItems = null,
QueueFullBehavior? fullBehavior = null)
{
var processor = new ProcessQueue<T>(asyncHandler, maxQueuedItems, fullBehavior);
await processor.StartAsync().ConfigureAwait(false);
ct.Register(async () =>
{
await processor.StopAsync().ConfigureAwait(false);
});
await subscribeCall(upd => processor.Write(upd)).ConfigureAwait(false);
}
/// <summary>
/// Queue updates received from a websocket subscriptions and process them async
/// </summary>
/// <typeparam name="TEventType">The type of the queued item</typeparam>
/// <typeparam name="TOutputType">The type of the item to pass to the processor</typeparam>
/// <param name="subscribeCall">The subscribe call</param>
/// <param name="mapper">The mapper function to go from <see>TEventType</see> to <see>TOutputType</see></param>
/// <param name="asyncHandler">The async update handler</param>
/// <param name="maxQueuedItems">The max number of updates 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 updates. If no max is set this setting is ignored</param>
public static async Task<CallResult<UpdateSubscription>> ProcessQueuedAsync<TEventType, TOutputType>(
Func<ProcessQueue<DataEvent<TEventType>>, Task<CallResult<UpdateSubscription>>> subscribeCall,
Func<DataEvent<TEventType>, DataEvent<TOutputType>> mapper,
Func<DataEvent<TOutputType>, Task> asyncHandler,
int? maxQueuedItems = null,
QueueFullBehavior? fullBehavior = null
)
{
var processor = new ProcessQueue<DataEvent<TEventType>>((update) => {
return asyncHandler.Invoke(mapper.Invoke(update));
}, maxQueuedItems, fullBehavior);
await processor.StartAsync().ConfigureAwait(false);
var result = await subscribeCall(processor).ConfigureAwait(false);
if (!result)
{
await processor.StopAsync().ConfigureAwait(false);
return result;
}
processor.Exception += result.Data._subscription.InvokeExceptionHandler;
result.Data.SubscriptionStatusChanged += (upd) =>
{
if (upd == SubscriptionStatus.Closed)
_ = processor.StopAsync(true);
};
return result;
}
/// <summary>
/// Parse a decimal value from a string
/// </summary>
+60 -1
View File
@@ -3,7 +3,6 @@ using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace CryptoExchange.Net
{
@@ -33,6 +32,66 @@ namespace CryptoExchange.Net
_symbolInfos[topicId] = new ExchangeInfo(DateTime.UtcNow, updateData.ToDictionary(x => x.Name, x => x.SharedSymbol));
}
/// <summary>
/// Whether the specific topic has been cached
/// </summary>
/// <param name="topicId">Id</param>
public static bool HasCached(string topicId)
{
if (!_symbolInfos.TryGetValue(topicId, out var exchangeInfo))
return false;
return exchangeInfo.Symbols.Count > 0;
}
/// <summary>
/// Whether a specific exchange(topic) support the provided symbol
/// </summary>
/// <param name="topicId">Id for the provided data</param>
/// <param name="symbolName">The symbol name</param>
public static bool SupportsSymbol(string topicId, string symbolName)
{
if (!_symbolInfos.TryGetValue(topicId, out var exchangeInfo))
return false;
if (!exchangeInfo.Symbols.TryGetValue(symbolName, out var symbolInfo))
return false;
return true;
}
/// <summary>
/// Whether a specific exchange(topic) support the provided symbol
/// </summary>
/// <param name="topicId">Id for the provided data</param>
/// <param name="symbol">The symbol info</param>
public static bool SupportsSymbol(string topicId, SharedSymbol symbol)
{
if (!_symbolInfos.TryGetValue(topicId, out var exchangeInfo))
return false;
return exchangeInfo.Symbols.Any(x =>
x.Value.TradingMode == symbol.TradingMode
&& x.Value.BaseAsset == symbol.BaseAsset
&& x.Value.QuoteAsset == symbol.QuoteAsset);
}
/// <summary>
/// Get all symbols for a specific base asset
/// </summary>
/// <param name="topicId">Id for the provided data</param>
/// <param name="baseAsset">Base asset name</param>
public static SharedSymbol[] GetSymbolsForBaseAsset(string topicId, string baseAsset)
{
if (!_symbolInfos.TryGetValue(topicId, out var exchangeInfo))
return [];
return exchangeInfo.Symbols
.Where(x => x.Value.BaseAsset.Equals(baseAsset, StringComparison.InvariantCultureIgnoreCase))
.Select(x => x.Value)
.ToArray();
}
/// <summary>
/// Parse a symbol name to a SharedSymbol
/// </summary>
+107 -168
View File
@@ -1,18 +1,15 @@
using System;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.SharedApis;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.IO.Compression;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Web;
using CryptoExchange.Net.Objects;
using System.Globalization;
using Microsoft.Extensions.DependencyInjection;
using CryptoExchange.Net.SharedApis;
using System.Text.Json.Serialization.Metadata;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net
{
@@ -64,30 +61,80 @@ namespace CryptoExchange.Net
/// <returns></returns>
public static string CreateParamString(this IDictionary<string, object> parameters, bool urlEncodeValues, ArrayParametersSerialization serializationType)
{
var uriString = string.Empty;
var arraysParameters = parameters.Where(p => p.Value.GetType().IsArray).ToList();
foreach (var arrayEntry in arraysParameters)
var uriString = new StringBuilder();
bool first = true;
foreach(var parameter in parameters)
{
if (serializationType == ArrayParametersSerialization.Array)
if (!first)
uriString.Append("&");
first = false;
if (parameter.Value.GetType().IsArray)
{
uriString += $"{string.Join("&", ((object[])(urlEncodeValues ? Uri.EscapeDataString(arrayEntry.Value.ToString()!) : arrayEntry.Value)).Select(v => $"{arrayEntry.Key}[]={string.Format(CultureInfo.InvariantCulture, "{0}", v)}"))}&";
if (serializationType == ArrayParametersSerialization.Array)
{
bool firstArrayValue = true;
foreach (var entry in (object[])parameter.Value)
{
if (!firstArrayValue)
uriString.Append('&');
firstArrayValue = false;
uriString.Append(parameter.Key);
uriString.Append("[]=");
if (urlEncodeValues)
uriString.Append(Uri.EscapeDataString(string.Format(CultureInfo.InvariantCulture, "{0}", entry)));
else
uriString.Append(string.Format(CultureInfo.InvariantCulture, "{0}", entry));
}
}
else if (serializationType == ArrayParametersSerialization.MultipleValues)
{
bool firstArrayValue = true;
foreach (var entry in (object[])parameter.Value)
{
if (!firstArrayValue)
uriString.Append('&');
firstArrayValue = false;
uriString.Append(parameter.Key);
uriString.Append("=");
if (urlEncodeValues)
uriString.Append(Uri.EscapeDataString(string.Format(CultureInfo.InvariantCulture, "{0}", entry)));
else
uriString.Append(string.Format(CultureInfo.InvariantCulture, "{0}", entry));
}
}
else
{
uriString.Append('[');
var firstArrayEntry = true;
foreach (var entry in (object[])parameter.Value)
{
if (!firstArrayEntry)
uriString.Append(',');
firstArrayEntry = false;
if (urlEncodeValues)
uriString.Append(Uri.EscapeDataString(string.Format(CultureInfo.InvariantCulture, "{0}", entry)));
else
uriString.Append(string.Format(CultureInfo.InvariantCulture, "{0}", entry));
}
uriString.Append(']');
}
}
else if (serializationType == ArrayParametersSerialization.MultipleValues)
else
{
var array = (Array)arrayEntry.Value;
uriString += string.Join("&", array.OfType<object>().Select(a => $"{arrayEntry.Key}={Uri.EscapeDataString(string.Format(CultureInfo.InvariantCulture, "{0}", a))}"));
uriString += "&";
}
else
{
var array = (Array)arrayEntry.Value;
uriString += $"{arrayEntry.Key}=[{string.Join(",", array.OfType<object>().Select(a => string.Format(CultureInfo.InvariantCulture, "{0}", a)))}]&";
uriString.Append(parameter.Key);
uriString.Append('=');
if (urlEncodeValues)
uriString.Append(Uri.EscapeDataString(string.Format(CultureInfo.InvariantCulture, "{0}", parameter.Value)));
else
uriString.Append(string.Format(CultureInfo.InvariantCulture, "{0}", parameter.Value));
}
}
uriString += $"{string.Join("&", parameters.Where(p => !p.Value.GetType().IsArray).Select(s => $"{s.Key}={(urlEncodeValues ? Uri.EscapeDataString(string.Format(CultureInfo.InvariantCulture, "{0}", s.Value)) : string.Format(CultureInfo.InvariantCulture, "{0}", s.Value))}"))}";
uriString = uriString.TrimEnd('&');
return uriString;
return uriString.ToString();
}
/// <summary>
@@ -233,167 +280,39 @@ namespace CryptoExchange.Net
/// <summary>
/// Append a base url with provided path
/// </summary>
/// <param name="url"></param>
/// <param name="path"></param>
/// <returns></returns>
public static string AppendPath(this string url, params string[] path)
{
if (!url.EndsWith("/"))
url += "/";
foreach (var item in path)
url += item.Trim('/') + "/";
return url.TrimEnd('/');
}
/// <summary>
/// Create a new uri with the provided parameters as query
/// </summary>
/// <param name="parameters"></param>
/// <param name="baseUri"></param>
/// <param name="arraySerialization"></param>
/// <returns></returns>
public static Uri SetParameters(this Uri baseUri, IDictionary<string, object> parameters, ArrayParametersSerialization arraySerialization)
{
var uriBuilder = new UriBuilder();
uriBuilder.Scheme = baseUri.Scheme;
uriBuilder.Host = baseUri.Host;
uriBuilder.Port = baseUri.Port;
uriBuilder.Path = baseUri.AbsolutePath;
var httpValueCollection = HttpUtility.ParseQueryString(string.Empty);
foreach (var parameter in parameters)
var sb = new StringBuilder(url.TrimEnd('/'));
foreach (var subPath in path)
{
if (parameter.Value.GetType().IsArray)
{
if (arraySerialization == ArrayParametersSerialization.JsonArray)
{
httpValueCollection.Add(parameter.Key, $"[{string.Join(",", (object[])parameter.Value)}]");
}
else
{
foreach (var item in (object[])parameter.Value)
{
if (arraySerialization == ArrayParametersSerialization.Array)
{
httpValueCollection.Add(parameter.Key + "[]", item.ToString());
}
else
{
httpValueCollection.Add(parameter.Key, item.ToString());
}
}
}
}
else
{
httpValueCollection.Add(parameter.Key, parameter.Value.ToString());
}
sb.Append('/');
sb.Append(subPath.Trim('/'));
}
uriBuilder.Query = httpValueCollection.ToString();
return uriBuilder.Uri;
}
/// <summary>
/// Create a new uri with the provided parameters as query
/// </summary>
/// <param name="parameters"></param>
/// <param name="baseUri"></param>
/// <param name="arraySerialization"></param>
/// <returns></returns>
public static Uri SetParameters(this Uri baseUri, IOrderedEnumerable<KeyValuePair<string, object>> parameters, ArrayParametersSerialization arraySerialization)
{
var uriBuilder = new UriBuilder();
uriBuilder.Scheme = baseUri.Scheme;
uriBuilder.Host = baseUri.Host;
uriBuilder.Port = baseUri.Port;
uriBuilder.Path = baseUri.AbsolutePath;
var httpValueCollection = HttpUtility.ParseQueryString(string.Empty);
foreach (var parameter in parameters)
{
if (parameter.Value.GetType().IsArray)
{
if (arraySerialization == ArrayParametersSerialization.JsonArray)
{
httpValueCollection.Add(parameter.Key, $"[{string.Join(",", (object[])parameter.Value)}]");
}
else
{
foreach (var item in (object[])parameter.Value)
{
if (arraySerialization == ArrayParametersSerialization.Array)
{
httpValueCollection.Add(parameter.Key + "[]", item.ToString());
}
else
{
httpValueCollection.Add(parameter.Key, item.ToString());
}
}
}
}
else
{
httpValueCollection.Add(parameter.Key, parameter.Value.ToString());
}
}
uriBuilder.Query = httpValueCollection.ToString();
return uriBuilder.Uri;
}
/// <summary>
/// Add parameter to URI
/// </summary>
/// <param name="uri"></param>
/// <param name="name"></param>
/// <param name="value"></param>
/// <returns></returns>
public static Uri AddQueryParameter(this Uri uri, string name, string value)
{
var httpValueCollection = HttpUtility.ParseQueryString(uri.Query);
httpValueCollection.Remove(name);
httpValueCollection.Add(name, value);
var ub = new UriBuilder(uri);
ub.Query = httpValueCollection.ToString();
return ub.Uri;
return sb.ToString();
}
/// <summary>
/// Decompress using GzipStream
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
public static ReadOnlyMemory<byte> DecompressGzip(this ReadOnlyMemory<byte> data)
public static ReadOnlySpan<byte> DecompressGzip(this ReadOnlySpan<byte> data)
{
using var decompressedStream = new MemoryStream();
using var dataStream = MemoryMarshal.TryGetArray(data, out var arraySegment)
? new MemoryStream(arraySegment.Array!, arraySegment.Offset, arraySegment.Count)
: new MemoryStream(data.ToArray());
using var deflateStream = new GZipStream(new MemoryStream(data.ToArray()), CompressionMode.Decompress);
deflateStream.CopyTo(decompressedStream);
return new ReadOnlyMemory<byte>(decompressedStream.GetBuffer(), 0, (int)decompressedStream.Length);
return new ReadOnlySpan<byte>(decompressedStream.GetBuffer(), 0, (int)decompressedStream.Length);
}
/// <summary>
/// Decompress using DeflateStream
/// Decompress using GzipStream
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public static ReadOnlyMemory<byte> Decompress(this ReadOnlyMemory<byte> input)
public static ReadOnlySpan<byte> Decompress(this ReadOnlySpan<byte> input)
{
var output = new MemoryStream();
using (var compressStream = new MemoryStream(input.ToArray()))
using (var decompressor = new DeflateStream(compressStream, CompressionMode.Decompress))
decompressor.CopyTo(output);
output.Position = 0;
return new ReadOnlyMemory<byte>(output.GetBuffer(), 0, (int)output.Length);
using var output = new MemoryStream();
using var compressStream = new MemoryStream(input.ToArray());
using var decompressor = new DeflateStream(compressStream, CompressionMode.Decompress);
decompressor.CopyTo(output);
return new ReadOnlySpan<byte>(output.GetBuffer(), 0, (int)output.Length);
}
/// <summary>
@@ -551,6 +470,26 @@ namespace CryptoExchange.Net
return services;
}
/// <summary>
/// Convert a hex encoded string to byte array
/// </summary>
/// <param name="hexString"></param>
/// <returns></returns>
public static byte[] HexStringToBytes(this string hexString)
{
if (hexString.StartsWith("0x"))
hexString = hexString.Substring(2);
byte[] bytes = new byte[hexString.Length / 2];
for (int i = 0; i < hexString.Length; i += 2)
{
string hexSubstring = hexString.Substring(i, 2);
bytes[i / 2] = Convert.ToByte(hexSubstring, 16);
}
return bytes;
}
}
}
@@ -1,10 +1,9 @@
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.SharedApis;
using System;
namespace CryptoExchange.Net.Interfaces
namespace CryptoExchange.Net.Interfaces.Clients
{
/// <summary>
/// Base api client
@@ -1,6 +1,6 @@
using System;
namespace CryptoExchange.Net.Interfaces
namespace CryptoExchange.Net.Interfaces.Clients
{
/// <summary>
/// Client for accessing REST API's for different exchanges
@@ -1,6 +1,6 @@
using System;
namespace CryptoExchange.Net.Interfaces
namespace CryptoExchange.Net.Interfaces.Clients
{
/// <summary>
/// Client for accessing Websocket API's for different exchanges
@@ -1,4 +1,4 @@
namespace CryptoExchange.Net.Interfaces
namespace CryptoExchange.Net.Interfaces.Clients
{
/// <summary>
/// Base rest API client
@@ -1,7 +1,7 @@
using System;
using CryptoExchange.Net.Objects.Options;
namespace CryptoExchange.Net.Interfaces
namespace CryptoExchange.Net.Interfaces.Clients
{
/// <summary>
/// Base class for rest API implementations
@@ -22,5 +22,10 @@ namespace CryptoExchange.Net.Interfaces
/// The exchange name
/// </summary>
string Exchange { get; }
/// <summary>
/// Whether client is disposed
/// </summary>
bool Disposed { get; }
}
}
@@ -1,9 +1,11 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.Sockets.Default.Interfaces;
using CryptoExchange.Net.Sockets.HighPerf.Interfaces;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Interfaces
namespace CryptoExchange.Net.Interfaces.Clients
{
/// <summary>
/// Socket API client
@@ -27,6 +29,10 @@ namespace CryptoExchange.Net.Interfaces
/// </summary>
IWebsocketFactory SocketFactory { get; set; }
/// <summary>
/// High performance websocket factory
/// </summary>
IHighPerfConnectionFactory? HighPerfConnectionFactory { get; set; }
/// <summary>
/// Current client options
/// </summary>
SocketExchangeOptions ClientOptions { get; }
@@ -3,7 +3,7 @@ using System.Threading.Tasks;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.Objects.Sockets;
namespace CryptoExchange.Net.Interfaces
namespace CryptoExchange.Net.Interfaces.Clients
{
/// <summary>
/// Base class for socket API implementations
@@ -35,6 +35,11 @@ namespace CryptoExchange.Net.Interfaces
/// </summary>
public int CurrentSubscriptions { get; }
/// <summary>
/// Whether client is disposed
/// </summary>
bool Disposed { get; }
/// <summary>
/// Unsubscribe from a stream using the subscription id received when starting the subscription
/// </summary>
@@ -0,0 +1,13 @@
namespace CryptoExchange.Net.Interfaces
{
/// <summary>
/// Service for an exchange
/// </summary>
public interface IExchangeService
{
/// <summary>
/// The exchange the service is for
/// </summary>
public string ExchangeName { get; }
}
}
@@ -1,110 +0,0 @@
using CryptoExchange.Net.Converters.MessageParsing;
using CryptoExchange.Net.Objects;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Interfaces
{
/// <summary>
/// Message accessor
/// </summary>
public interface IMessageAccessor
{
/// <summary>
/// Is this a valid message
/// </summary>
bool IsValid { get; }
/// <summary>
/// Is the original data available for retrieval
/// </summary>
bool OriginalDataAvailable { get; }
/// <summary>
/// The underlying data object
/// </summary>
object? Underlying { get; }
/// <summary>
/// Clear internal data structure
/// </summary>
void Clear();
/// <summary>
/// Get the type of node
/// </summary>
/// <returns></returns>
NodeType? GetNodeType();
/// <summary>
/// Get the type of node
/// </summary>
/// <param name="path">Access path</param>
/// <returns></returns>
NodeType? GetNodeType(MessagePath path);
/// <summary>
/// Get the value of a path
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="path"></param>
/// <returns></returns>
T? GetValue<T>(MessagePath path);
/// <summary>
/// Get the values of an array
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="path"></param>
/// <returns></returns>
T?[]? GetValues<T>(MessagePath path);
/// <summary>
/// Deserialize the message into this type
/// </summary>
/// <param name="type"></param>
/// <param name="path"></param>
/// <returns></returns>
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
CallResult<object> Deserialize(Type type, MessagePath? path = null);
/// <summary>
/// Deserialize the message into this type
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
CallResult<T> Deserialize<T>(MessagePath? path = null);
/// <summary>
/// Get the original string value
/// </summary>
/// <returns></returns>
string GetOriginalString();
}
/// <summary>
/// Stream message accessor
/// </summary>
public interface IStreamMessageAccessor : IMessageAccessor
{
/// <summary>
/// Load a stream message
/// </summary>
/// <param name="stream"></param>
/// <param name="bufferStream"></param>
Task<CallResult> Read(Stream stream, bool bufferStream);
}
/// <summary>
/// Byte message accessor
/// </summary>
public interface IByteMessageAccessor : IMessageAccessor
{
/// <summary>
/// Load a data message
/// </summary>
/// <param name="data"></param>
CallResult Read(ReadOnlyMemory<byte> data);
}
}
@@ -1,35 +0,0 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.Sockets;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Interfaces
{
/// <summary>
/// Message processor
/// </summary>
public interface IMessageProcessor
{
/// <summary>
/// Id of the processor
/// </summary>
public int Id { get; }
/// <summary>
/// The matcher for this listener
/// </summary>
public MessageMatcher MessageMatcher { get; }
/// <summary>
/// Handle a message
/// </summary>
Task<CallResult> Handle(SocketConnection connection, DataEvent<object> message, MessageHandlerLink matchedHandler);
/// <summary>
/// Deserialize a message into object of type
/// </summary>
/// <param name="accessor"></param>
/// <param name="type"></param>
/// <returns></returns>
CallResult<object> Deserialize(IMessageAccessor accessor, Type type);
}
}
@@ -1,6 +1,4 @@
using System.Diagnostics.CodeAnalysis;
namespace CryptoExchange.Net.Interfaces
namespace CryptoExchange.Net.Interfaces
{
/// <summary>
/// Serializer interface
+3 -3
View File
@@ -1,6 +1,6 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
@@ -14,7 +14,7 @@ namespace CryptoExchange.Net.Interfaces
/// <summary>
/// Accept header
/// </summary>
string Accept { set; }
MediaTypeWithQualityHeaderValue Accept { set; }
/// <summary>
/// Content
/// </summary>
@@ -58,7 +58,7 @@ namespace CryptoExchange.Net.Interfaces
/// Get all headers
/// </summary>
/// <returns></returns>
KeyValuePair<string, string[]>[] GetHeaders();
HttpRequestHeaders GetHeaders();
/// <summary>
/// Get the response
+4 -3
View File
@@ -1,7 +1,8 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Interfaces
@@ -34,13 +35,13 @@ namespace CryptoExchange.Net.Interfaces
/// <summary>
/// The response headers
/// </summary>
KeyValuePair<string, string[]>[] ResponseHeaders { get; }
HttpResponseHeaders ResponseHeaders { get; }
/// <summary>
/// Get the response stream
/// </summary>
/// <returns></returns>
Task<Stream> GetResponseStreamAsync();
Task<Stream> GetResponseStreamAsync(CancellationToken cancellationToken);
/// <summary>
/// Close the response
@@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using CryptoExchange.Net.Objects;
@@ -48,9 +47,21 @@ namespace CryptoExchange.Net.Interfaces
/// </summary>
event Action<(ISymbolOrderBookEntry BestBid, ISymbolOrderBookEntry BestAsk)> OnBestOffersChanged;
/// <summary>
/// Timestamp of the last update
/// Timestamp of when the last update was applied to the book, local time
/// </summary>
DateTime UpdateTime { get; }
/// <summary>
/// Timestamp of the last event that was applied, server time
/// </summary>
DateTime? UpdateServerTime { get; }
/// <summary>
/// Timestamp of the last event that was applied, in local time, estimated based on timestamp difference between client and server
/// </summary>
DateTime? UpdateLocalTime { get; }
/// <summary>
/// Age of the data, in local time, estimated based on timestamp difference between client and server + the period since last update
/// </summary>
TimeSpan? DataAge { get; }
/// <summary>
/// The number of asks in the book
@@ -127,5 +138,13 @@ namespace CryptoExchange.Net.Interfaces
/// </summary>
/// <returns></returns>
string ToString(int rows);
/// <summary>
/// Output the orderbook to the console
/// </summary>
/// <param name="numberOfEntries">Number of rows to display</param>
/// <param name="refreshInterval">Refresh interval</param>
/// <param name="ct">Cancellation token</param>
Task OutputToConsoleAsync(int numberOfEntries, TimeSpan refreshInterval, CancellationToken ct = default);
}
}
@@ -1,19 +0,0 @@
using CryptoExchange.Net.Objects.Sockets;
using Microsoft.Extensions.Logging;
namespace CryptoExchange.Net.Interfaces
{
/// <summary>
/// Websocket factory interface
/// </summary>
public interface IWebsocketFactory
{
/// <summary>
/// Create a websocket for an url
/// </summary>
/// <param name="logger">The logger</param>
/// <param name="parameters">The parameters to use for the connection</param>
/// <returns></returns>
IWebsocket CreateWebsocket(ILogger logger, WebSocketParameters parameters);
}
}
+16 -11
View File
@@ -1,10 +1,10 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Text;
namespace CryptoExchange.Net
{
@@ -69,7 +69,7 @@ namespace CryptoExchange.Net
{
var reservedLength = brokerId.Length + ClientOrderIdSeparator.Length;
if ((clientOrderId?.Length + reservedLength) > maxLength)
if (clientOrderId?.Length + reservedLength > maxLength)
return clientOrderId!;
if (!string.IsNullOrEmpty(clientOrderId))
@@ -106,31 +106,36 @@ namespace CryptoExchange.Net
/// <summary>
/// Create a new HttpMessageHandler instance
/// </summary>
public static HttpMessageHandler CreateHttpClientMessageHandler(ApiProxy? proxy, TimeSpan? keepAliveInterval)
public static HttpMessageHandler CreateHttpClientMessageHandler(RestExchangeOptions options)
{
#if NET5_0_OR_GREATER
var socketHandler = new SocketsHttpHandler();
try
{
if (keepAliveInterval != null && keepAliveInterval != TimeSpan.Zero)
if (options.HttpKeepAliveInterval != null && options.HttpKeepAliveInterval != TimeSpan.Zero)
{
socketHandler.KeepAlivePingPolicy = HttpKeepAlivePingPolicy.Always;
socketHandler.KeepAlivePingDelay = keepAliveInterval.Value;
socketHandler.KeepAlivePingDelay = options.HttpKeepAliveInterval.Value;
socketHandler.KeepAlivePingTimeout = TimeSpan.FromSeconds(10);
}
socketHandler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
socketHandler.DefaultProxyCredentials = CredentialCache.DefaultCredentials;
socketHandler.EnableMultipleHttp2Connections = options.HttpEnableMultipleHttp2Connections;
socketHandler.PooledConnectionLifetime = options.HttpPooledConnectionLifetime;
socketHandler.PooledConnectionIdleTimeout = options.HttpPooledConnectionIdleTimeout;
socketHandler.MaxConnectionsPerServer = options.HttpMaxConnectionsPerServer;
}
catch (PlatformNotSupportedException) { }
catch (NotImplementedException) { } // Mono runtime throws NotImplementedException
if (proxy != null)
if (options.Proxy != null)
{
socketHandler.Proxy = new WebProxy
{
Address = new Uri($"{proxy.Host}:{proxy.Port}"),
Credentials = proxy.Password == null ? null : new NetworkCredential(proxy.Login, proxy.Password)
Address = new Uri($"{options.Proxy.Host}:{options.Proxy.Port}"),
Credentials = options.Proxy.Password == null ? null : new NetworkCredential(options.Proxy.Login, options.Proxy.Password)
};
}
return socketHandler;
@@ -144,12 +149,12 @@ namespace CryptoExchange.Net
catch (PlatformNotSupportedException) { }
catch (NotImplementedException) { } // Mono runtime throws NotImplementedException
if (proxy != null)
if (options.Proxy != null)
{
httpHandler.Proxy = new WebProxy
{
Address = new Uri($"{proxy.Host}:{proxy.Port}"),
Credentials = proxy.Password == null ? null : new NetworkCredential(proxy.Login, proxy.Password)
Address = new Uri($"{options.Proxy.Host}:{options.Proxy.Port}"),
Credentials = options.Proxy.Password == null ? null : new NetworkCredential(options.Proxy.Login, options.Proxy.Password)
};
}
return httpHandler;
@@ -89,7 +89,7 @@ namespace CryptoExchange.Net.Logging.Extensions
LogLevel.Debug,
new EventId(4012, "RestApiCancellationRequested"),
"[Req {RequestId}] Request cancelled by user");
}
public static void RestApiErrorReceived(this ILogger logger, int? requestId, HttpStatusCode? responseStatusCode, long responseTime, string? error, string? originalData, Exception? exception)
@@ -1,5 +1,6 @@
using System;
using System.Net.WebSockets;
using CryptoExchange.Net.Sockets.Default;
using Microsoft.Extensions.Logging;
namespace CryptoExchange.Net.Logging.Extensions
@@ -8,12 +9,11 @@ namespace CryptoExchange.Net.Logging.Extensions
public static class SocketConnectionLoggingExtension
{
private static readonly Action<ILogger, int, bool, Exception?> _activityPaused;
private static readonly Action<ILogger, int, Sockets.SocketConnection.SocketStatus, Sockets.SocketConnection.SocketStatus, Exception?> _socketStatusChanged;
private static readonly Action<ILogger, int, SocketStatus, SocketStatus, Exception?> _socketStatusChanged;
private static readonly Action<ILogger, int, string?, Exception?> _failedReconnectProcessing;
private static readonly Action<ILogger, int, Exception?> _unknownExceptionWhileProcessingReconnection;
private static readonly Action<ILogger, int, WebSocketError, string?, Exception?> _webSocketErrorCodeAndDetails;
private static readonly Action<ILogger, int, string?, Exception?> _webSocketError;
private static readonly Action<ILogger, int, int, Exception?> _messageSentNotPending;
private static readonly Action<ILogger, int, string, Exception?> _receivedData;
private static readonly Action<ILogger, int, string, Exception?> _failedToParse;
private static readonly Action<ILogger, int, string, Exception?> _failedToEvaluateMessage;
@@ -37,7 +37,7 @@ namespace CryptoExchange.Net.Logging.Extensions
private static readonly Action<ILogger, int, string, Exception?> _sendingPeriodic;
private static readonly Action<ILogger, int, string, string, Exception?> _periodicSendFailed;
private static readonly Action<ILogger, int, int, string, Exception?> _sendingData;
private static readonly Action<ILogger, int, string, string, Exception?> _receivedMessageNotMatchedToAnyListener;
private static readonly Action<ILogger, int, string, string, string, Exception?> _receivedMessageNotMatchedToAnyListener;
private static readonly Action<ILogger, int, int, int, Exception?> _sendingByteData;
static SocketConnectionLoggingExtension()
@@ -47,7 +47,7 @@ namespace CryptoExchange.Net.Logging.Extensions
new EventId(2000, "ActivityPaused"),
"[Sckt {SocketId}] paused activity: {Paused}");
_socketStatusChanged = LoggerMessage.Define<int, Sockets.SocketConnection.SocketStatus, Sockets.SocketConnection.SocketStatus>(
_socketStatusChanged = LoggerMessage.Define<int, SocketStatus, SocketStatus>(
LogLevel.Debug,
new EventId(2001, "SocketStatusChanged"),
"[Sckt {SocketId}] status changed from {OldStatus} to {NewStatus}");
@@ -72,11 +72,6 @@ namespace CryptoExchange.Net.Logging.Extensions
new EventId(2005, "WebSocketError"),
"[Sckt {SocketId}] error: {ErrorMessage}");
_messageSentNotPending = LoggerMessage.Define<int, int>(
LogLevel.Debug,
new EventId(2006, "MessageSentNotPending"),
"[Sckt {SocketId}] [Req {RequestId}] message sent, but not pending");
_receivedData = LoggerMessage.Define<int, string>(
LogLevel.Trace,
new EventId(2007, "ReceivedData"),
@@ -182,10 +177,10 @@ namespace CryptoExchange.Net.Logging.Extensions
new EventId(2028, "SendingData"),
"[Sckt {SocketId}] [Req {RequestId}] sending message: {Data}");
_receivedMessageNotMatchedToAnyListener = LoggerMessage.Define<int, string, string>(
_receivedMessageNotMatchedToAnyListener = LoggerMessage.Define<int, string, string, string>(
LogLevel.Warning,
new EventId(2029, "ReceivedMessageNotMatchedToAnyListener"),
"[Sckt {SocketId}] received message not matched to any listener. ListenId: {ListenId}, current listeners: [{ListenIds}]");
"[Sckt {SocketId}] received message not matched to any listener. TypeIdentifier: {TypeIdentifier}, ListenId: {ListenId}, current listeners: [{ListenIds}]");
_failedToParse = LoggerMessage.Define<int, string>(
LogLevel.Warning,
@@ -209,7 +204,7 @@ namespace CryptoExchange.Net.Logging.Extensions
_activityPaused(logger, socketId, paused, null);
}
public static void SocketStatusChanged(this ILogger logger, int socketId, Sockets.SocketConnection.SocketStatus oldStatus, Sockets.SocketConnection.SocketStatus newStatus)
public static void SocketStatusChanged(this ILogger logger, int socketId, SocketStatus oldStatus, SocketStatus newStatus)
{
_socketStatusChanged(logger, socketId, oldStatus, newStatus, null);
}
@@ -234,11 +229,6 @@ namespace CryptoExchange.Net.Logging.Extensions
_webSocketError(logger, socketId, errorMessage, e);
}
public static void MessageSentNotPending(this ILogger logger, int socketId, int requestId)
{
_messageSentNotPending(logger, socketId, requestId, null);
}
public static void ReceivedData(this ILogger logger, int socketId, string originalData)
{
_receivedData(logger, socketId, originalData, null);
@@ -336,9 +326,9 @@ namespace CryptoExchange.Net.Logging.Extensions
_sendingData(logger, socketId, requestId, data, null);
}
public static void ReceivedMessageNotMatchedToAnyListener(this ILogger logger, int socketId, string listenId, string listenIds)
public static void ReceivedMessageNotMatchedToAnyListener(this ILogger logger, int socketId, string typeIdentifier, string listenId, string listenIds)
{
_receivedMessageNotMatchedToAnyListener(logger, socketId, listenId, listenIds, null);
_receivedMessageNotMatchedToAnyListener(logger, socketId, typeIdentifier, listenId, listenIds, null);
}
public static void SendingByteData(this ILogger logger, int socketId, int requestId, int length)
@@ -22,13 +22,15 @@ namespace CryptoExchange.Net.Logging.Extensions
private static readonly Action<ILogger, string, string, Exception?> _orderBookResyncing;
private static readonly Action<ILogger, string, string, Exception?> _orderBookResynced;
private static readonly Action<ILogger, string, string, Exception?> _orderBookMessageSkippedBecauseOfResubscribing;
private static readonly Action<ILogger, string, string, long, long, long, Exception?> _orderBookDataSet;
private static readonly Action<ILogger, string, string, long, long, long?, Exception?> _orderBookDataSet;
private static readonly Action<ILogger, string, string, long, long, long, long, Exception?> _orderBookUpdateBuffered;
private static readonly Action<ILogger, string, string, decimal, decimal, Exception?> _orderBookOutOfSyncDetected;
private static readonly Action<ILogger, string, string, Exception?> _orderBookReconnectingSocket;
private static readonly Action<ILogger, string, string, long, long, Exception?> _orderBookSkippedMessage;
private static readonly Action<ILogger, string, string, long, long, Exception?> _orderBookProcessedMessage;
private static readonly Action<ILogger, string, string, long, Exception?> _orderBookProcessedMessageSingle;
private static readonly Action<ILogger, string, string, long, long, Exception?> _orderBookOutOfSync;
private static readonly Action<ILogger, string, string, long, long, long, Exception?> _orderBookUpdateSkippedStartEnd;
static SymbolOrderBookLoggingExtensions()
{
@@ -73,7 +75,7 @@ namespace CryptoExchange.Net.Logging.Extensions
"{Api} order book {Symbol} Processing {NumberBufferedUpdated} buffered updates");
_orderBookUpdateSkipped = LoggerMessage.Define<string, string, long, long>(
LogLevel.Debug,
LogLevel.Trace,
new EventId(5008, "OrderBookUpdateSkipped"),
"{Api} order book {Symbol} update skipped #{SequenceNumber}, currently at #{LastSequenceNumber}");
@@ -92,10 +94,10 @@ namespace CryptoExchange.Net.Logging.Extensions
new EventId(5011, "OrderBookMessageSkippedResubscribing"),
"{Api} order book {Symbol} Skipping message because of resubscribing");
_orderBookDataSet = LoggerMessage.Define<string, string, long, long, long>(
LogLevel.Debug,
_orderBookDataSet = LoggerMessage.Define<string, string, long, long, long?>(
LogLevel.Trace,
new EventId(5012, "OrderBookDataSet"),
"{Api} order book {Symbol} data set: {BidCount} bids, {AskCount} asks. #{EndUpdateId}");
"{Api} order book {Symbol} snapshot set: {BidCount} bids, {AskCount} asks. #{EndUpdateId}");
_orderBookUpdateBuffered = LoggerMessage.Define<string, string, long, long, long, long>(
LogLevel.Trace,
@@ -136,6 +138,17 @@ namespace CryptoExchange.Net.Logging.Extensions
LogLevel.Warning,
new EventId(5020, "OrderBookOutOfSyncChecksum"),
"{Api} order book {Symbol} out of sync. Checksum mismatch, resyncing");
_orderBookProcessedMessageSingle = LoggerMessage.Define<string, string, long>(
LogLevel.Trace,
new EventId(5021, "OrderBookProcessedMessage"),
"{Api} order book {Symbol} update processed #{UpdateId}");
_orderBookUpdateSkippedStartEnd = LoggerMessage.Define<string, string, long, long, long>(
LogLevel.Trace,
new EventId(5022, "OrderBookUpdateSkippedStartEnd"),
"{Api} order book {Symbol} update skipped #{SequenceStart}-#{SequenceEnd}, currently at #{LastSequenceNumber}");
}
public static void OrderBookStatusChanged(this ILogger logger, string api, string symbol, OrderBookStatus previousStatus, OrderBookStatus newStatus)
@@ -194,7 +207,7 @@ namespace CryptoExchange.Net.Logging.Extensions
{
_orderBookMessageSkippedBecauseOfResubscribing(logger, api, symbol, null);
}
public static void OrderBookDataSet(this ILogger logger, string api, string symbol, long bidCount, long askCount, long endUpdateId)
public static void OrderBookDataSet(this ILogger logger, string api, string symbol, long bidCount, long askCount, long? endUpdateId)
{
_orderBookDataSet(logger, api, symbol, bidCount, askCount, endUpdateId, null);
}
@@ -229,9 +242,18 @@ namespace CryptoExchange.Net.Logging.Extensions
_orderBookProcessedMessage(logger, api, symbol, firstUpdateId, lastUpdateId, null);
}
public static void OrderBookProcessedMessage(this ILogger logger, string api, string symbol, long updateId)
{
_orderBookProcessedMessageSingle(logger, api, symbol, updateId, null);
}
public static void OrderBookOutOfSyncChecksum(this ILogger logger, string api, string symbol)
{
_orderBookOutOfSyncChecksum(logger, api, symbol, null);
}
public static void OrderBookUpdateSkipped(this ILogger logger, string api, string symbol, long sequenceStart, long sequenceEnd, long lastSequenceNumber)
{
_orderBookUpdateSkippedStartEnd(logger, api, symbol, sequenceStart, sequenceEnd, lastSequenceNumber, null);
}
}
}
+22 -6
View File
@@ -1,14 +1,14 @@
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
/// </summary>
public class AssetAlias
{
/// <summary>
/// Alias type
/// </summary>
public AliasType Type { get; set; }
/// <summary>
/// The name of the asset on the exchange
/// </summary>
@@ -21,10 +21,26 @@ namespace CryptoExchange.Net.Objects
/// <summary>
/// ctor
/// </summary>
public AssetAlias(string exchangeName, string commonName)
public AssetAlias(string exchangeName, string commonName, AliasType type = AliasType.BothWays)
{
ExchangeAssetName = exchangeName;
CommonAssetName = commonName;
Type = type;
}
}
/// <summary>
/// Alias type
/// </summary>
public enum AliasType
{
/// <summary>
/// Translate both from and to exchange
/// </summary>
BothWays,
/// <summary>
/// Only translate when converting to exchange
/// </summary>
OnlyToExchange
}
}
@@ -1,7 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace CryptoExchange.Net.Objects
{
@@ -23,12 +21,23 @@ 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.SingleOrDefault(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
/// </summary>
public string ExchangeToCommonName(string exchangeName) => !AutoConvertEnabled ? exchangeName : Aliases.SingleOrDefault(x => x.ExchangeAssetName == exchangeName)?.CommonAssetName ?? exchangeName;
public string ExchangeToCommonName(string exchangeName)
{
if (!AutoConvertEnabled)
return exchangeName;
var alias = Aliases.FirstOrDefault(x => x.ExchangeAssetName.Equals(exchangeName, StringComparison.InvariantCulture));
if (alias == null || alias.Type == AliasType.OnlyToExchange)
return exchangeName;
return alias.CommonAssetName;
}
}
}
@@ -1,122 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Objects
{
/// <summary>
/// Async auto reset based on Stephen Toub`s implementation
/// https://devblogs.microsoft.com/pfxteam/building-async-coordination-primitives-part-2-asyncautoresetevent/
/// </summary>
public class AsyncResetEvent : IDisposable
{
private static readonly Task<bool> _completed = Task.FromResult(true);
private Queue<TaskCompletionSource<bool>> _waits = new Queue<TaskCompletionSource<bool>>();
private bool _signaled;
private readonly bool _reset;
/// <summary>
/// New AsyncResetEvent
/// </summary>
/// <param name="initialState"></param>
/// <param name="reset"></param>
public AsyncResetEvent(bool initialState = false, bool reset = true)
{
_signaled = initialState;
_reset = reset;
}
/// <summary>
/// Wait for the AutoResetEvent to be set
/// </summary>
/// <returns></returns>
public async Task<bool> WaitAsync(TimeSpan? timeout = null, CancellationToken ct = default)
{
CancellationTokenRegistration registration = default;
try
{
Task<bool> waiter = _completed;
lock (_waits)
{
if (_signaled)
{
if (_reset)
_signaled = false;
}
else if (!ct.IsCancellationRequested)
{
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
if (timeout.HasValue)
{
var timeoutSource = new CancellationTokenSource(timeout.Value);
var cancellationSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutSource.Token, ct);
ct = cancellationSource.Token;
}
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);
waiter = tcs.Task;
}
}
return await waiter.ConfigureAwait(false);
}
finally
{
registration.Dispose();
}
}
/// <summary>
/// Signal a waiter
/// </summary>
public void Set()
{
lock (_waits)
{
if (!_reset)
{
// Act as ManualResetEvent. Once set keep it signaled and signal everyone who is waiting
_signaled = true;
while (_waits.Count > 0)
{
var toRelease = _waits.Dequeue();
toRelease.TrySetResult(true);
}
}
else
{
// Act as AutoResetEvent. When set signal 1 waiter
if (_waits.Count > 0)
{
var toRelease = _waits.Dequeue();
toRelease.TrySetResult(true);
}
else if (!_signaled)
_signaled = true;
}
}
}
/// <summary>
/// Dispose
/// </summary>
public void Dispose()
{
_waits.Clear();
}
}
}
@@ -0,0 +1,149 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Objects
{
/// <summary>
/// Async auto/manual reset event implementation
/// </summary>
public class AsyncResetEvent
{
private readonly Queue<TaskCompletionSource<bool>> _waiters = new();
private readonly bool _autoReset;
private bool _signaled;
#if NET9_0_OR_GREATER
private readonly Lock _waitersLock = new Lock();
#else
private readonly object _waitersLock = new object();
#endif
/// <summary>
/// ctor
/// </summary>
public AsyncResetEvent(bool initialState = false, bool autoReset = true)
{
_signaled = initialState;
_autoReset = autoReset;
}
/// <summary>
/// Wait for the set event
/// </summary>
/// <returns></returns>
public async Task<bool> WaitAsync(
TimeSpan? timeout = null,
CancellationToken ct = default)
{
TaskCompletionSource<bool> tcs;
lock (_waitersLock)
{
if (_signaled)
{
// Already was signaled, can return immediately
if (_autoReset)
_signaled = false;
return true;
}
tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
_waiters.Enqueue(tcs);
}
CancellationTokenSource? delayCts = null;
try
{
if (timeout.HasValue || ct.CanBeCanceled)
{
delayCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
var delayTask = Task.Delay(
timeout ?? Timeout.InfiniteTimeSpan,
delayCts.Token);
var completedTask =
await Task.WhenAny(tcs.Task, delayTask)
.ConfigureAwait(false);
if (completedTask != tcs.Task)
{
// This was a timeout or cancellation, need to remove tcs from waiters
// if the tcs was set instead it will be removed in the Set method
if (tcs.TrySetResult(false))
{
lock (_waitersLock)
{
// Dequeue and put in the back of the queue again except for the one we need to remove
int count = _waiters.Count;
for (int i = 0; i < count; i++)
{
var w = _waiters.Dequeue();
if (w != tcs)
_waiters.Enqueue(w);
}
}
}
return false;
}
}
else
{
await tcs.Task.ConfigureAwait(false);
}
return true;
}
finally
{
// Actively stop the delay if tcs.Task won
delayCts?.Cancel();
delayCts?.Dispose();
}
}
/// <summary>
/// Signal a waiter
/// </summary>
public void Set()
{
if (!_autoReset && _signaled)
// Already signaled and not resetting
return;
lock (_waitersLock)
{
if (_autoReset)
{
while (_waiters.Count > 0)
{
// Try to dequeue and set the result
// If result setting was not successful it means timeout/cancellation happened at the same time
// If this is the case this Set isn't the one setting the result and we need to continue
var w = _waiters.Dequeue();
if (w.TrySetResult(true))
return;
}
// No queued waiters, set signaled for next waiter
_signaled = true;
}
else
{
_signaled = true;
// Signal all current waiters
while (_waiters.Count > 0)
{
var w = _waiters.Dequeue();
w.TrySetResult(true);
}
}
}
}
}
}
+15 -15
View File
@@ -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)
@@ -531,11 +531,11 @@ namespace CryptoExchange.Net.Objects
/// <param name="exchange">The exchange</param>
/// <param name="tradeMode">Trade mode the result applies to</param>
/// <param name="data">Data</param>
/// <param name="nextPageToken">Next page token</param>
/// <param name="nextPageRequest">Next page request</param>
/// <returns></returns>
public ExchangeWebResult<K> AsExchangeResult<K>(string exchange, TradingMode tradeMode, [AllowNull] K data, INextPageToken? nextPageToken = null)
public ExchangeWebResult<K> AsExchangeResult<K>(string exchange, TradingMode tradeMode, [AllowNull] K data, PageRequest? nextPageRequest = null)
{
return new ExchangeWebResult<K>(exchange, tradeMode, As<K>(data), nextPageToken);
return new ExchangeWebResult<K>(exchange, tradeMode, As<K>(data), nextPageRequest);
}
/// <summary>
@@ -545,11 +545,11 @@ namespace CryptoExchange.Net.Objects
/// <param name="exchange">The exchange</param>
/// <param name="tradeModes">Trade modes the result applies to</param>
/// <param name="data">Data</param>
/// <param name="nextPageToken">Next page token</param>
/// <param name="nextPageRequest">Next page token</param>
/// <returns></returns>
public ExchangeWebResult<K> AsExchangeResult<K>(string exchange, TradingMode[]? tradeModes, [AllowNull] K data, INextPageToken? nextPageToken = null)
public ExchangeWebResult<K> AsExchangeResult<K>(string exchange, TradingMode[]? tradeModes, [AllowNull] K data, PageRequest? nextPageRequest = null)
{
return new ExchangeWebResult<K>(exchange, tradeModes, As<K>(data), nextPageToken);
return new ExchangeWebResult<K>(exchange, tradeModes, As<K>(data), nextPageRequest);
}
/// <summary>
+47 -3
View File
@@ -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
@@ -252,6 +250,40 @@ namespace CryptoExchange.Net.Objects
DEX
}
/// <summary>
/// Type of platform
/// </summary>
public enum PlatformType
{
/// <summary>
/// Platform to trade cryptocurrency
/// </summary>
CryptoCurrencyExchange,
/// <summary>
/// Platform for trading on predictions
/// </summary>
PredictionMarket,
/// <summary>
/// Other
/// </summary>
Other
}
/// <summary>
/// Centralization type
/// </summary>
public enum CentralizationType
{
/// <summary>
/// Centralized, a person or company is in full control
/// </summary>
Centralized,
/// <summary>
/// Decentralized, governance is split over different entities with no single entity in full control
/// </summary>
Decentralized
}
/// <summary>
/// Timeout behavior for queries
/// </summary>
@@ -294,4 +326,16 @@ namespace CryptoExchange.Net.Objects
Closed
}
/// <summary>
/// Queue full behavior
/// </summary>
public enum QueueFullBehavior
{
/// <summary>Remove and ignore the newest item in the queue in order to make room for the item being written.</summary>
DropNewest,
/// <summary>Remove and ignore the oldest item in the queue in order to make room for the item being written.</summary>
DropOldest,
/// <summary>Drop the item being written.</summary>
DropWrite
}
}
+23 -2
View File
@@ -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;
}
}
@@ -198,7 +211,15 @@ namespace CryptoExchange.Net.Objects
/// <summary>
/// ctor
/// </summary>
public DeserializeError(string? message = null, Exception? exception = null) : base(null, _errorInfo with { Message = (message?.Length > 0 ? _errorInfo.Message + ": " + message : _errorInfo.Message) }, exception) { }
public DeserializeError(string? message = null, Exception? exception = null)
: base(null,
_errorInfo with
{
Message = message?.Length > 0
? message
: _errorInfo.Message
},
exception) { }
}
/// <summary>
@@ -1,6 +1,4 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.Objects.Errors
{

Some files were not shown because too many files have changed in this diff Show More