1
0
mirror of https://github.com/JKorf/CryptoExchange.Net synced 2025-06-07 16:06:15 +00:00

Compare commits

...

147 Commits

Author SHA1 Message Date
Jkorf
3e635cf0fe Updated to version 9.1.0 2025-05-28 15:03:32 +02:00
Jkorf
1425c66c69 Dependencies update
* Updated dotnet package versions from 9.0.0 to 9.0.5
* Replaced Microsoft.Extensions.Logging.Abstractions with icrosoft.Extensions.Logging
* Replaced Microsoft.Extensions.Options.ConfigurationExtensions with Microsoft.Extensions.Configuration.Binder, which includes a source generator for AOT publishing
* Removed redundant Microsoft.Extensions.DependencyInjection.Abstractions package reference
2025-05-28 14:50:23 +02:00
Jkorf
fc3b7cc75b Added JsonConverter implementation for SharedQuantity and SharedSymbol types 2025-05-28 14:26:55 +02:00
Jkorf
2cc2dc6ceb Updated to version 9.0.1 2025-05-20 14:51:19 +02:00
Jkorf
7da8cedf66 Improved response time on CancellationToken cancel during subscribing 2025-05-20 14:49:43 +02:00
Jkorf
2cf10668dd Added support for sending query without expecting a response 2025-05-19 16:06:45 +02:00
Jkorf
f1342b5ff2 Examples 2025-05-14 14:30:05 +02:00
JKorf
a04b636a11 Disable AOT warnings in testing code 2025-05-13 20:03:34 +02:00
Jkorf
e4637ad295 Disable warning for AOT in testing helpers 2025-05-13 15:53:13 +02:00
Jkorf
3a1e43dabe Fixed framework check for setting IsAotCompatible project flag 2025-05-13 15:36:16 +02:00
Jkorf
10da1a7bfe Fix documentation link 2025-05-13 15:10:15 +02:00
Jkorf
37320ca862 Docs 2025-05-13 15:08:30 +02:00
Jkorf
2074a5e26f Updated to version 9.0.0 2025-05-13 10:28:12 +02:00
Jan Korf
6b14cdbf06
Feature/9.0.0 (#236)
* Added support for Native AOT compilation
* Updated all IEnumerable response types to array response types
* Added Pass support for ApiCredentials, removing the need for most implementations to add their own ApiCredentials type
* Added KeepAliveTimeout setting setting ping frame timeouts for SocketApiClient
* Added IBookTickerRestClient Shared interface for requesting book tickers
* Added ISpotTriggerOrderRestClient Shared interface for managing spot trigger orders
* Added ISpotOrderClientIdClient Shared interface for managing spot orders by client order id
* Added IFuturesTriggerOrderRestClient Shared interface for managing futures trigger orders
* Added IFuturesOrderClientIdClient Shared interface for managing futures orders by client order id
* Added IFuturesTpSlRestClient Shared interface for setting TP/SL on open futures positions
* Added GenerateClientOrderId to ISpotOrderRestClient and IFuturesOrderRestClient interface
* Added OptionalExchangeParameters and Supported properties to EndpointOptions
* Refactor Shared interfaces quantity parameters and properties to use SharedQuantity
* Added SharedSymbol property to Shared interface models returning a symbol
* Added TriggerPrice, IsTriggerOrder, TakeProfitPrice, StopLossPrice and IsCloseOrder to SharedFuturesOrder response model
* Added MaxShortLeverage and MaxLongLeverage to SharedFuturesSymbol response model
* Added StopLossPrice and TakeProfitPrice to SharedPosition response model
* Added TriggerPrice and IsTriggerOrder to SharedSpotOrder response model
* Added QuoteVolume property to SharedSpotTicker response model
* Added AssetAlias configuration models
* Added static ExchangeSymbolCache for tracking symbol information from exchanges
* Added static CallResult.SuccessResult to be used instead of constructing success CallResult instance
* Added static ApplyRules, RandomHexString and RandomLong helper methods to ExchangeHelpers class
* Added AsErrorWithData To CallResult
* Added OriginalData property to CallResult
* Added support for adjusting the rate limit key per call, allowing for ratelimiting depending on request parameters
* Added implementation for integration testing ISymbolOrderBook instances
* Added implementation for integration testing socket subscriptions
* Added implementation for testing socket queries
* Updated request cancellation logging to Debug level
* Updated logging SourceContext to include the client type
* Updated some logging logic, errors no longer contain any data, exception are not logged as string but instead forwarded to structured logging
* Fixed warning for Enum parsing throwing exception and output warnings for each object in a response to only once to prevent slowing down execution
* Fixed memory leak in AsyncAutoRestEvent
* Fixed logging for ping frame timeout
* Fixed warning getting logged when user stops SymbolOrderBook instance
* Fixed socket client `UnsubscribeAll` not unsubscribing dedicated connections
* Fixed memory leak in Rest client cache
* Fixed integers bigger than int16 not getting correctly parsed to enums
* Fixed issue where the default options were overridden when using SetApiCredentials
* Removed Newtonsoft.Json dependency
* Removed legacy Rest client code
* Removed legacy ISpotClient and IFuturesClient support
2025-05-13 10:15:30 +02:00
Jkorf
3d6267da93 Added specific logging for user cancellation on rest requests instead of generic warning log 2025-03-05 09:02:25 +01:00
JKorf
8def7f32af Update UnsubscribeAll in socket client to also unsubscribe on a dedicated connection 2025-03-04 19:45:06 +01:00
Jkorf
ac295de9f6 Added referal link 2025-03-04 11:46:03 +01:00
Jkorf
d412e0895e Added DeepCoin reference and examples 2025-03-04 11:41:03 +01:00
Jkorf
1f9e2b4fcb Fixed websocket ping timeout not recognized for warning logging 2025-02-26 10:44:57 +01:00
James Carter
b13cff5a95
Fix memory leak in AsyncAutoResetEvent (#229)
* Fix memory leak in AsyncAutoResetEvent

CancellationTokenRegistration MUST be disposed, as the CancellationToken passed is saved for the lifetime of the Client, and registrations build up forever.
2025-02-24 08:33:26 +01:00
James Carter
4c050744ad
Allow specifying the ReceiveMessageBuffer size on Websockets (#228)
In order to support more User websocket connections, allow reducing the memory requirements for the receive buffer, keeping the default buffer.
2025-02-23 19:58:05 +01:00
JKorf
3b15c35a02 Added support for ratelimiting key suffix, allowing parameter based ratelimiting 2025-02-17 17:26:04 +01:00
Jkorf
cd78dbf575 Updated to version 8.8.0 2025-02-10 14:38:19 +01:00
Jkorf
a258532d6a Fixed DataTime copying in DataEvent 2025-02-10 14:32:25 +01:00
JKorf
d2a87a1069 Added additional enum values to default SupportIntervals for shared rest and socket kline operations 2025-02-09 21:43:18 +01:00
JKorf
e07f24ea0a Fixed various info-warnings and spelling issues 2025-02-09 21:25:26 +01:00
JKorf
024e8dcfe2 Added SharedKlineInterval values 2025-02-09 20:12:55 +01:00
JKorf
4bb5aae40a Split DataEvent.Timestamp in DataEvent.ReceivedTimestamp and 2025-02-09 16:40:28 +01:00
JKorf
dec94678ec Updated to version 8.7.4 2025-02-08 14:28:29 +01:00
JKorf
1a49fc8251 Fix exception when creating rest client for mono runtime 2025-02-08 14:25:19 +01:00
Jkorf
29b0875960 Updated examples 2025-02-07 13:50:00 +01:00
Jkorf
976ccab1da Added BitMEX reference 2025-02-07 13:20:54 +01:00
Jkorf
02bbd37bb6 Updated to version 8.7.3 2025-02-05 09:15:25 +01:00
Jkorf
1bbbec7f2b Fixed issue with serialization of nullable types in System.Text.Json ArrayConverter 2025-02-05 09:12:12 +01:00
Jkorf
0262f04913 Added handling of negative number DateTime deserialization to default 2025-02-05 08:25:59 +01:00
Jkorf
fd1ec17d72 Fix for unnecessary error message in logging when closing connection 2025-02-04 08:28:48 +01:00
Jkorf
4bdad7fe0c Updated SharedSymbol from class to record 2025-02-04 08:28:21 +01:00
Jkorf
74f73dc790 Updated to version 8.7.2 2025-01-27 13:24:08 +01:00
Jkorf
0527a8a76e Some small fixes in the System.Text.Json ArrayConverter, added support for flags in EnumConverter 2025-01-27 11:52:07 +01:00
Jkorf
c693eb8c02 Updated to version 8.7.1 2025-01-24 08:42:08 +01:00
Jkorf
3eb28c7fed Added HyperLiquid referral 2025-01-23 09:35:44 +01:00
JKorf
618c4922b9 Added Authenticated property to IBaseApiClient interface 2025-01-22 19:11:38 +01:00
Jkorf
c81b15861d Updated examples and docs with HyperLiquid references 2025-01-21 15:24:23 +01:00
Jkorf
4a5832cccd Updated to version 8.7.0 2025-01-21 14:02:42 +01:00
Jkorf
4e47c4cbdf Updated CheckForMissingInterfaces test 2025-01-21 14:00:30 +01:00
Jkorf
2af1520ecc Added PriceSignificationFigures to SharedSpotSymbol model 2025-01-21 14:00:08 +01:00
Jkorf
cf397af3ab Added GetMillisecondTimestampLong helper method to AuthenticationProvider 2025-01-21 13:47:05 +01:00
JKorf
a1479705e2 Fixed typo 2025-01-13 17:49:38 +01:00
Jkorf
175e23f110 Updated to version 8.6.1 2025-01-09 16:25:11 +01:00
Jkorf
9b7019ded2 Removed websocket Error callback when exception is expected 2025-01-09 16:23:34 +01:00
Jkorf
7904aa9ba7 Fixed websocket connection getting stuck after a ping frame timeout, removed unnecessary type restraints on RestApiClient.SendAsync methods 2025-01-09 16:18:13 +01:00
Jkorf
3fe6db589f Updated to version 8.6.0 2025-01-07 13:25:50 +01:00
Jkorf
625dccbbe4 Added ExchangeType enum, some small improvements 2025-01-07 13:22:16 +01:00
Jkorf
e650771d16 Added response headers parameter to RestApiClient.TryParseError method, added check for ServerRateLimitError on the result 2025-01-07 10:19:37 +01:00
Jkorf
3dad28b19d Added IFeeRestClient to service registration 2025-01-07 08:59:51 +01:00
Jkorf
2b9fda985e Add support for passing weight to apply to an individual ratelimit guard 2025-01-07 08:35:06 +01:00
JKorf
ff8759409b Use Convert.ToHexString if available 2025-01-06 21:38:42 +01:00
JKorf
0d9627c13f Changed socket no data reconnect message to LogLevel Warning 2024-12-23 20:03:56 +01:00
Jkorf
0179fd7e2a Fixed workflow automated tests 2024-12-23 14:43:23 +01:00
Jkorf
b8d0b0cf95 Workflow fix 2024-12-23 14:36:18 +01:00
Jkorf
73c42bd452 Updated to version 8.5.0 2024-12-23 14:25:39 +01:00
Jkorf
290be7f5e0 Added net9.0 build target, added KeepAliveTimeout for websocket connections 2024-12-23 14:14:47 +01:00
Jan Korf
0be1bb16e3
Feature/update settings (#225)
Added SetOptions method to update client settings
Added SocketConnection parameter to PeriodicQuery callback
Added setting of DefaultProxyCredentials on HttpClient instance when client is not provided by DI
Added support for overriding request time out per request
Changed max wait time for close handshake response from 5 seconds to 1 second
Fixed exception in trade tracker when there is no data in the initial snapshot
2024-12-23 08:49:58 +01:00
Jkorf
8605196390 Updated to version 8.4.5 2024-12-20 15:25:41 +01:00
Jkorf
460dd97537 Added EmptyArrayObjectConverter, added JsonSerializerOptions parameter to SystemTextJsonMessageAccessor ctor 2024-12-20 15:21:21 +01:00
JKorf
1ec5984fad Updated to version 8.4.4 2024-12-08 10:16:19 +01:00
JKorf
8260c2661d Changed JsonConverterCtorAttribute to use type parameter instead of generic parameter to support .net framework 2024-12-08 10:12:39 +01:00
Jkorf
591c1dd405 docs 2024-12-04 10:44:35 +01:00
Jkorf
0164cdfcc4 docs 2024-12-04 09:15:23 +01:00
Jkorf
23a6cfff87 docs 2024-12-04 09:07:59 +01:00
Jkorf
fdcdb90a5f Added XT reference 2024-12-04 08:58:46 +01:00
Jkorf
0b7107401f Updated to version 8.4.3 2024-12-03 09:59:36 +01:00
JKorf
06add65354 Fixed KlineTracker update handling 2024-12-02 21:46:30 +01:00
Jkorf
773d288497 Updated to version 8.4.2 2024-12-02 14:39:27 +01:00
Jkorf
fd4e8da938 Removed special characters in ClientOrderIdSeperator to adhere to field content rules 2024-12-02 14:38:22 +01:00
Jkorf
271743b669 Updated to version 8.4.1 2024-12-02 13:15:31 +01:00
Jkorf
f4797caf37 Added replace converter, added library helpers class 2024-12-02 13:13:56 +01:00
Jkorf
62c9769c72 Updated to version 8.4.0 2024-11-28 14:24:00 +01:00
Jkorf
92d7bc1e2e Added GetFeesAsync Shared REST client support, Added TimePeriodFilterSupport and MaxLimit properties to PaginatedEndpointOptions 2024-11-28 14:18:09 +01:00
Jkorf
99e4f96f63 Updated some testing code 2024-11-27 13:07:26 +01:00
Jkorf
94d8afe149 Updated package dependency versions 2024-11-27 13:07:05 +01:00
Jkorf
90ad59c63a Added comma split enum string json converter 2024-11-22 16:20:09 +01:00
Jkorf
c2273edfaa Added library options class 2024-11-20 09:52:38 +01:00
Jkorf
236283f4dd Update example-config.json 2024-11-19 14:53:58 +01:00
Jkorf
b66f12ff75 Updated to version 8.3.0 2024-11-19 11:52:46 +01:00
Jkorf
0403384beb Fixed warnings 2024-11-19 11:50:55 +01:00
Jan Korf
7d7bc35869
Client Configuration (#219)
Added support for IOptions injection, allowing options to be read from IConfiguration
Small refactor on client options internals
Updated HttpClient to be static field to be
2024-11-19 11:44:30 +01:00
Jkorf
48797038be Added rate limit update event 2024-11-13 14:29:43 +01:00
Jkorf
d21792d04c Added handling of Infinity values in decimal converter 2024-11-13 11:39:55 +01:00
Jkorf
8414e9d94f Fixed concurrency issue when unsubscribing websocket subscription during reconnection 2024-11-12 16:21:15 +01:00
Jkorf
ab0243445d Updated docs and examples, added WhiteBit reference 2024-11-07 11:39:44 +01:00
Jkorf
f2cf70b02f Updated to version 8.2.0 2024-11-06 14:00:23 +01:00
Jkorf
9ff417bba8 Changed SocketApiClient GetAuthenticationRequest to GetAuthenticationRequestAsync to allow for requesting token 2024-11-06 13:56:33 +01:00
Jkorf
6b43d08a4d Added support for not allowing duplicate subscription topics on the same websocket connection 2024-11-06 11:39:11 +01:00
Jkorf
39bf7fe9b9 Added support for object deserialization in SystemTextJsonMessageAccessor.GetValue<T> 2024-11-06 11:20:37 +01:00
Jkorf
b5893c3b60 Added PerAccount SharedLeverageSettingMode enum value, changed Side on SharedUserTrade to nullable 2024-11-06 11:20:11 +01:00
Jkorf
15657ba683 Updated to version 8.1.1 2024-11-01 10:38:30 +01:00
Jkorf
1aed9f0c67 Fixed System.Text.Json ArrayConverter not passing serializer options to nested deserialization, fixed creating new serializer options each time a JsonConverter attribute is encountered 2024-11-01 10:34:07 +01:00
Jkorf
17f1560310 Fixed socket connections trying to authenticated connection when it's marked as dedicated request connection even when no authentication is needed 2024-11-01 09:38:01 +01:00
Jkorf
41de0a3150 Update index.html 2024-10-28 16:14:54 +01:00
Jkorf
3e410be611 Update index.html 2024-10-28 16:11:01 +01:00
Jkorf
be75449e4a Updated examples, added trackers example 2024-10-28 15:38:25 +01:00
Jkorf
b1b05c8f6b Added catch around HttpClientHandler.AutomaticDecompression setting as it's not support on Blazor WASM 2024-10-28 13:41:58 +01:00
Jkorf
a0e588c3de Updated to version 8.1.0 2024-10-28 10:44:45 +01:00
Jan Korf
9e86a08327
Trackers (#218)
Fix for intermittently failing rate limiting test
Added ConnectionId to RequestDefinition to correctly handle connection and path rate limiting configuration
Added ValidateMessage method to websocket Query object to filter messages even though it is matched to the query based on the  ListenIdentifier
Added KlineTracker and TradeTracker implementation
2024-10-28 10:36:19 +01:00
Jkorf
ed007b5272 Added overload for Create method in OrderBookFactory using SharedSymbol 2024-10-23 14:04:10 +02:00
Jkorf
bdd5526244 Added Side to SharedTrade model 2024-10-23 14:01:17 +02:00
Jkorf
ce35e30688 Updated documentation and examples 2024-10-22 16:20:27 +02:00
Jkorf
b40f72b1b0 Added Crypto.com reference 2024-10-22 15:37:56 +02:00
Jkorf
31a6cf285b Doc fix 2024-10-22 11:56:59 +02:00
Jkorf
1842f4fda0 Set ApiCredentials in the client specific options to prevent unknown client credentials when using SetApiCredentials method on client 2024-10-22 10:15:17 +02:00
Jkorf
7a58902ab6 Made SharedFuturesTicker last/high/low price properties nullable 2024-10-22 09:54:44 +02:00
Jkorf
3cb91296ca Added DoHandleReset method for websocket subscriptions 2024-10-22 09:47:14 +02:00
Jkorf
130ed40580 Comment 2024-10-21 16:29:56 +02:00
Jkorf
94cb2caf0b Added System.Text.Json ArrayConverter Write implementation 2024-10-15 10:51:19 +02:00
Jkorf
917d060827 Updated to version 8.0.3 2024-10-14 14:13:38 +02:00
JKorf
c58bc2be07 Merge branch 'master' of https://github.com/JKorf/CryptoExchange.Net 2024-10-12 13:06:28 +02:00
JKorf
ff3356e2b4 Added Authenticated property on base client and shared client 2024-10-12 13:06:22 +02:00
Jkorf
79434c7be5 Implemented GetValues System.Text.Json in message accessor 2024-10-11 16:02:17 +02:00
Jkorf
168dabc11f Added fallback for unparsable value in System.Text.Json NumberStringConverter 2024-10-09 15:42:11 +02:00
Jkorf
71ee263683 Added support for duplicate array indexes in System.Text.Json ArrayConverter 2024-10-09 15:41:43 +02:00
Jkorf
7239b9c289 docs 2024-10-09 10:25:57 +02:00
Jkorf
84d36544e4 Updated to version 8.0.2 2024-10-09 08:49:19 +02:00
Jkorf
a71f57ae7f Updated dependency versions, including System.Text.Json containing a vulnerability fix 2024-10-09 08:46:15 +02:00
Jkorf
6e5bcd5e9a Some small doc fixes 2024-10-07 15:30:09 +02:00
Jkorf
4131e563c3 Added Coinbase reference, updated examples 2024-10-07 15:20:12 +02:00
Jkorf
613766dbca Updated to version 8.0.1 2024-10-07 13:03:08 +02:00
Jkorf
23b07d709e Added testing check for year 1 datetime 2024-10-07 12:57:23 +02:00
Jkorf
bbbdac2fd3 Added ToRfc3339String extension method for DateTime 2024-10-07 12:57:04 +02:00
Jkorf
c614b7869c Added check for datetime year 1 to be deserialized as null 2024-10-04 18:50:27 +02:00
Jkorf
1f31e4a9d7 Added cached lib versions properties 2024-10-04 18:49:58 +02:00
Jkorf
6cb6cd6b11 Note 2024-10-03 16:33:00 +02:00
Jkorf
17ffec329f Fixed typo 2024-09-27 15:15:09 +02:00
Jkorf
7a3927ef49 Add parameters documentation for shared clients 2024-09-27 15:13:50 +02:00
Jkorf
c1b0437c93 Updated examples 2024-09-27 13:53:33 +02:00
Jkorf
23e947f258 Updated to version 8.0.0 2024-09-27 10:55:29 +02:00
Jan Korf
b8686d60b9
Shared exchange functionality (#214) 2024-09-27 09:17:44 +02:00
Jonnern
5d3de52da6
feature: Handle error 429 when connecting websocket (#213)
* feature: Handle error 429 when connecting websocket

* Add preprocessor directive for NET6_0_OR_GREATER when checking for connection rate limit
2024-09-24 12:50:59 +02:00
Jonnern
fee18fd183
Dispose ClientWebSocket before creating a new (#212) 2024-09-17 11:59:58 +02:00
JKorf
3a43d461a3 Fixed workflow dotnet version 2024-08-29 16:45:34 +02:00
Jkorf
5f409efad3 Updated dotnet version unit tests 2024-08-29 14:01:59 +02:00
JKorf
cc1f0796fe Updated to version 7.11.2 2024-08-28 19:17:06 +02:00
JKorf
b1cd9b5412 Fixed exception being thrown when waiting was canceled during rate limiting 2024-08-28 19:10:08 +02:00
Jonnern
42003a0247
Fix issue where SemaphoreSlim is released twice in RateLimitGate (#210) 2024-08-28 12:25:09 +02:00
JKorf
d89c2bde94 Updated to version 7.11.1 2024-08-25 18:41:33 +02:00
JKorf
3e6bdaafc6 Improved closing logic websockets 2024-08-25 18:38:37 +02:00
JKorf
93e4722a81 Added testing checks for JsonInclude attribute for internal properties 2024-08-08 09:20:26 +02:00
372 changed files with 16453 additions and 4476 deletions

View File

@ -16,7 +16,7 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v1
with:
dotnet-version: 6.0.x
dotnet-version: 9.0.x
- name: Restore dependencies
run: dotnet restore
- name: Build

View File

@ -106,6 +106,7 @@ namespace CryptoExchange.Net.UnitTests
for(var i = 1; i <= 10; i++)
{
evnt.Set();
await Task.Delay(1); // Wait for the continuation.
Assert.That(10 - i == waiters.Count(w => w.Status != TaskStatus.RanToCompletion));
}

View File

@ -112,7 +112,7 @@ namespace CryptoExchange.Net.UnitTests
{
var result = new WebCallResult<TestObjectResult>(
System.Net.HttpStatusCode.OK,
new List<KeyValuePair<string, IEnumerable<string>>>(),
new KeyValuePair<string, string[]>[0],
TimeSpan.FromSeconds(1),
null,
"{}",
@ -120,7 +120,7 @@ namespace CryptoExchange.Net.UnitTests
"https://test.com/api",
null,
HttpMethod.Get,
new List<KeyValuePair<string, IEnumerable<string>>>(),
new KeyValuePair<string, string[]>[0],
ResultDataSource.Server,
new TestObjectResult(),
null);
@ -142,7 +142,7 @@ namespace CryptoExchange.Net.UnitTests
{
var result = new WebCallResult<TestObjectResult>(
System.Net.HttpStatusCode.OK,
new List<KeyValuePair<string, IEnumerable<string>>>(),
new KeyValuePair<string, string[]>[0],
TimeSpan.FromSeconds(1),
null,
"{}",
@ -150,7 +150,7 @@ namespace CryptoExchange.Net.UnitTests
"https://test.com/api",
null,
HttpMethod.Get,
new List<KeyValuePair<string, IEnumerable<string>>>(),
new KeyValuePair<string, string[]>[0],
ResultDataSource.Server,
new TestObjectResult(),
null);

View File

@ -1,15 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0"></PackageReference>
<PackageReference Include="Moq" Version="4.20.70" />
<PackageReference Include="NUnit" Version="4.1.0"></PackageReference>
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0"></PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0"></PackageReference>
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="NUnit" Version="4.2.2"></PackageReference>
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0"></PackageReference>
</ItemGroup>
<ItemGroup>

View File

@ -1,6 +1,7 @@
using CryptoExchange.Net.Objects;
using NUnit.Framework;
using NUnit.Framework.Legacy;
using System.Diagnostics;
using System.Globalization;
namespace CryptoExchange.Net.UnitTests
@ -70,5 +71,20 @@ namespace CryptoExchange.Net.UnitTests
var result = ExchangeHelpers.Normalize(input);
Assert.That(expected == result.ToString(CultureInfo.InvariantCulture));
}
[Test]
[TestCase("123", "BKR", 32, true, "BKRJK123")]
[TestCase("123", "BKR", 32, false, "123")]
[TestCase("123123123123123123123123123123", "BKR", 32, true, "123123123123123123123123123123")] // 30
[TestCase("12312312312312312312312312312", "BKR", 32, true, "12312312312312312312312312312")] // 27
[TestCase("123123123123123123123123123", "BKR", 32, true, "BKRJK123123123123123123123123123")] // 25
[TestCase(null, "BKR", 32, true, null)]
public void ApplyBrokerIdTests(string clientOrderId, string brokerId, int maxLength, bool allowValueAdjustement, string expected)
{
var result = LibraryHelpers.ApplyBrokerId(clientOrderId, brokerId, maxLength, allowValueAdjustement);
if (expected != null)
Assert.That(result, Is.EqualTo(expected));
}
}
}

View File

@ -1,248 +0,0 @@
using CryptoExchange.Net.Attributes;
using CryptoExchange.Net.Converters;
using CryptoExchange.Net.Converters.JsonNet;
using Newtonsoft.Json;
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
{
[TestFixture()]
public class JsonNetConverterTests
{
[TestCase("2021-05-12")]
[TestCase("20210512")]
[TestCase("210512")]
[TestCase("1620777600.000")]
[TestCase("1620777600000")]
[TestCase("2021-05-12T00:00:00.000Z")]
[TestCase("2021-05-12T00:00:00.000000000Z")]
[TestCase("0.000000", true)]
[TestCase("0", true)]
[TestCase("", true)]
[TestCase(" ", true)]
public void TestDateTimeConverterString(string input, bool expectNull = false)
{
var output = JsonConvert.DeserializeObject<TimeObject>($"{{ \"time\": \"{input}\" }}");
Assert.That(output.Time == (expectNull ? null: new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc)));
}
[TestCase(1620777600.000)]
[TestCase(1620777600000d)]
public void TestDateTimeConverterDouble(double input)
{
var output = JsonConvert.DeserializeObject<TimeObject>($"{{ \"time\": {input} }}");
Assert.That(output.Time == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
}
[TestCase(1620777600)]
[TestCase(1620777600000)]
[TestCase(1620777600000000)]
[TestCase(1620777600000000000)]
[TestCase(0, true)]
public void TestDateTimeConverterLong(long input, bool expectNull = false)
{
var output = JsonConvert.DeserializeObject<TimeObject>($"{{ \"time\": {input} }}");
Assert.That(output.Time == (expectNull ? null : new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc)));
}
[TestCase(1620777600)]
[TestCase(1620777600.000)]
public void TestDateTimeConverterFromSeconds(double input)
{
var output = DateTimeConverter.ConvertFromSeconds(input);
Assert.That(output == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
}
[Test]
public void TestDateTimeConverterToSeconds()
{
var output = DateTimeConverter.ConvertToSeconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
Assert.That(output == 1620777600);
}
[TestCase(1620777600000)]
[TestCase(1620777600000.000)]
public void TestDateTimeConverterFromMilliseconds(double input)
{
var output = DateTimeConverter.ConvertFromMilliseconds(input);
Assert.That(output == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
}
[Test]
public void TestDateTimeConverterToMilliseconds()
{
var output = DateTimeConverter.ConvertToMilliseconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
Assert.That(output == 1620777600000);
}
[TestCase(1620777600000000)]
public void TestDateTimeConverterFromMicroseconds(long input)
{
var output = DateTimeConverter.ConvertFromMicroseconds(input);
Assert.That(output == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
}
[Test]
public void TestDateTimeConverterToMicroseconds()
{
var output = DateTimeConverter.ConvertToMicroseconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
Assert.That(output == 1620777600000000);
}
[TestCase(1620777600000000000)]
public void TestDateTimeConverterFromNanoseconds(long input)
{
var output = DateTimeConverter.ConvertFromNanoseconds(input);
Assert.That(output == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
}
[Test]
public void TestDateTimeConverterToNanoseconds()
{
var output = DateTimeConverter.ConvertToNanoseconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
Assert.That(output == 1620777600000000000);
}
[TestCase()]
public void TestDateTimeConverterNull()
{
var output = JsonConvert.DeserializeObject<TimeObject>($"{{ \"time\": null }}");
Assert.That(output.Time == null);
}
[TestCase(TestEnum.One, "1")]
[TestCase(TestEnum.Two, "2")]
[TestCase(TestEnum.Three, "three")]
[TestCase(TestEnum.Four, "Four")]
[TestCase(null, null)]
public void TestEnumConverterNullableGetStringTests(TestEnum? value, string expected)
{
var output = EnumConverter.GetString(value);
Assert.That(output == expected);
}
[TestCase(TestEnum.One, "1")]
[TestCase(TestEnum.Two, "2")]
[TestCase(TestEnum.Three, "three")]
[TestCase(TestEnum.Four, "Four")]
public void TestEnumConverterGetStringTests(TestEnum value, string expected)
{
var output = EnumConverter.GetString(value);
Assert.That(output == expected);
}
[TestCase("1", TestEnum.One)]
[TestCase("2", TestEnum.Two)]
[TestCase("3", TestEnum.Three)]
[TestCase("three", TestEnum.Three)]
[TestCase("Four", TestEnum.Four)]
[TestCase("four", TestEnum.Four)]
[TestCase("Four1", null)]
[TestCase(null, null)]
public void TestEnumConverterNullableDeserializeTests(string value, TestEnum? expected)
{
var val = value == null ? "null" : $"\"{value}\"";
var output = JsonConvert.DeserializeObject<EnumObject>($"{{ \"Value\": {val} }}");
Assert.That(output.Value == expected);
}
[TestCase("1", TestEnum.One)]
[TestCase("2", TestEnum.Two)]
[TestCase("3", TestEnum.Three)]
[TestCase("three", TestEnum.Three)]
[TestCase("Four", TestEnum.Four)]
[TestCase("four", TestEnum.Four)]
[TestCase("Four1", TestEnum.One)]
[TestCase(null, TestEnum.One)]
public void TestEnumConverterNotNullableDeserializeTests(string value, TestEnum? expected)
{
var val = value == null ? "null" : $"\"{value}\"";
var output = JsonConvert.DeserializeObject<NotNullableEnumObject>($"{{ \"Value\": {val} }}");
Assert.That(output.Value == expected);
}
[TestCase("1", true)]
[TestCase("true", true)]
[TestCase("yes", true)]
[TestCase("y", true)]
[TestCase("on", true)]
[TestCase("-1", false)]
[TestCase("0", false)]
[TestCase("n", false)]
[TestCase("no", false)]
[TestCase("false", false)]
[TestCase("off", false)]
[TestCase("", null)]
public void TestBoolConverter(string value, bool? expected)
{
var val = value == null ? "null" : $"\"{value}\"";
var output = JsonConvert.DeserializeObject<BoolObject>($"{{ \"Value\": {val} }}");
Assert.That(output.Value == expected);
}
[TestCase("1", true)]
[TestCase("true", true)]
[TestCase("yes", true)]
[TestCase("y", true)]
[TestCase("on", true)]
[TestCase("-1", false)]
[TestCase("0", false)]
[TestCase("n", false)]
[TestCase("no", false)]
[TestCase("false", false)]
[TestCase("off", false)]
[TestCase("", false)]
public void TestBoolConverterNotNullable(string value, bool expected)
{
var val = value == null ? "null" : $"\"{value}\"";
var output = JsonConvert.DeserializeObject<NotNullableBoolObject>($"{{ \"Value\": {val} }}");
Assert.That(output.Value == expected);
}
}
public class TimeObject
{
[JsonConverter(typeof(DateTimeConverter))]
public DateTime? Time { get; set; }
}
public class EnumObject
{
public TestEnum? Value { get; set; }
}
public class NotNullableEnumObject
{
public TestEnum Value { get; set; }
}
public class BoolObject
{
[JsonConverter(typeof(BoolConverter))]
public bool? Value { get; set; }
}
public class NotNullableBoolObject
{
[JsonConverter(typeof(BoolConverter))]
public bool Value { get; set; }
}
[JsonConverter(typeof(EnumConverter))]
public enum TestEnum
{
[Map("1")]
One,
[Map("2")]
Two,
[Map("three", "3")]
Three,
Four
}
}

View File

@ -100,6 +100,10 @@ namespace CryptoExchange.Net.UnitTests
Assert.That(authProvider1.GetSecret() == "222");
Assert.That(authProvider2.GetKey() == "123");
Assert.That(authProvider2.GetSecret() == "456");
// Cleanup static values
TestClientOptions.Default.ApiCredentials = null;
TestClientOptions.Default.Api1Options.ApiCredentials = null;
}
[Test]
@ -121,6 +125,10 @@ namespace CryptoExchange.Net.UnitTests
Assert.That(authProvider2.GetKey() == "123");
Assert.That(authProvider2.GetSecret() == "456");
Assert.That(client.Api2.BaseAddress == "https://localhost:123");
// Cleanup static values
TestClientOptions.Default.ApiCredentials = null;
TestClientOptions.Default.Api1Options.ApiCredentials = null;
}
}
@ -134,6 +142,14 @@ namespace CryptoExchange.Net.UnitTests
Environment = new TestEnvironment("test", "https://test.com")
};
/// <summary>
/// ctor
/// </summary>
public TestClientOptions()
{
Default?.Set(this);
}
/// <summary>
/// The default receive window for requests
/// </summary>
@ -143,12 +159,12 @@ namespace CryptoExchange.Net.UnitTests
public RestApiOptions Api2Options { get; set; } = new RestApiOptions();
internal TestClientOptions Copy()
internal TestClientOptions Set(TestClientOptions targetOptions)
{
var options = Copy<TestClientOptions>();
options.Api1Options = Api1Options.Copy<RestApiOptions>();
options.Api2Options = Api2Options.Copy<RestApiOptions>();
return options;
targetOptions = base.Set<TestClientOptions>(targetOptions);
targetOptions.Api1Options = Api1Options.Set(targetOptions.Api1Options);
targetOptions.Api2Options = Api2Options.Set(targetOptions.Api2Options);
return targetOptions;
}
}
}

View File

@ -1,14 +1,9 @@
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.UnitTests.TestImplementations;
using Newtonsoft.Json;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using CryptoExchange.Net.Interfaces;
using Microsoft.Extensions.Logging;
using System.Net.Http;
using System.Threading.Tasks;
using System.Threading;
@ -18,6 +13,7 @@ using System.Net;
using CryptoExchange.Net.RateLimiting.Guards;
using CryptoExchange.Net.RateLimiting.Filters;
using CryptoExchange.Net.RateLimiting.Interfaces;
using System.Text.Json;
namespace CryptoExchange.Net.UnitTests
{
@ -30,7 +26,7 @@ namespace CryptoExchange.Net.UnitTests
// arrange
var client = new TestRestClient();
var expected = new TestObject() { DecimalData = 1.23M, IntData = 10, StringData = "Some data" };
client.SetResponse(JsonConvert.SerializeObject(expected), out _);
client.SetResponse(JsonSerializer.Serialize(expected, new JsonSerializerOptions { TypeInfoResolver = new TestSerializerContext() }), out _);
// act
var result = client.Api1.Request<TestObject>().Result;
@ -84,8 +80,6 @@ namespace CryptoExchange.Net.UnitTests
ClassicAssert.IsFalse(result.Success);
Assert.That(result.Error != null);
Assert.That(result.Error is ServerError);
Assert.That(result.Error.Message.Contains("Invalid request"));
Assert.That(result.Error.Message.Contains("123"));
}
[TestCase]
@ -140,7 +134,7 @@ namespace CryptoExchange.Net.UnitTests
client.SetResponse("{}", out var request);
await client.Api1.RequestWithParams<TestObject>(new HttpMethod(method), new Dictionary<string, object>
await client.Api1.RequestWithParams<TestObject>(new HttpMethod(method), new ParameterCollection
{
{ "TestParam1", "Value1" },
{ "TestParam2", 2 },
@ -176,12 +170,12 @@ namespace CryptoExchange.Net.UnitTests
for (var i = 0; i < requests + 1; i++)
{
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, default);
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
Assert.That(i == requests? triggered : !triggered);
}
triggered = false;
await Task.Delay((int)Math.Round(perSeconds * 1000) + 10);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, default);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
Assert.That(!triggered);
}
@ -201,7 +195,7 @@ namespace CryptoExchange.Net.UnitTests
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
for (var i = 0; i < 2; i++)
{
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, default);
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
bool expected = i == 1 ? (expectLimiting ? evnt.DelayTime > TimeSpan.Zero : evnt == null) : evnt == null;
Assert.That(expected);
}
@ -222,9 +216,9 @@ namespace CryptoExchange.Net.UnitTests
RateLimitEvent evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, default);
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
Assert.That(evnt == null);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, default);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
Assert.That(expectLimiting ? evnt != null : evnt == null);
}
@ -243,12 +237,12 @@ namespace CryptoExchange.Net.UnitTests
for (var i = 0; i < requests + 1; i++)
{
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, default);
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
Assert.That(i == requests ? triggered : !triggered);
}
triggered = false;
await Task.Delay((int)Math.Round(perSeconds * 1000) + 10);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, default);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
Assert.That(!triggered);
}
@ -266,7 +260,7 @@ namespace CryptoExchange.Net.UnitTests
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
for (var i = 0; i < 2; i++)
{
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, default);
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
bool expected = i == 1 ? (expectLimited ? evnt.DelayTime > TimeSpan.Zero : evnt == null) : evnt == null;
Assert.That(expected);
}
@ -286,7 +280,7 @@ namespace CryptoExchange.Net.UnitTests
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
for (var i = 0; i < 2; i++)
{
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, default);
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
bool expected = i == 1 ? (expectLimited ? evnt.DelayTime > TimeSpan.Zero : evnt == null) : evnt == null;
Assert.That(expected);
}
@ -302,16 +296,16 @@ namespace CryptoExchange.Net.UnitTests
public async Task ApiKeyRateLimiterBasics(string key1, string key2, string endpoint1, string endpoint2, bool expectLimited)
{
var rateLimiter = new RateLimitGate("Test");
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerApiKey, new AuthenticatedEndpointFilter(true), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerApiKey, new AuthenticatedEndpointFilter(true), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Sliding));
var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get) { Authenticated = key1 != null };
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get) { Authenticated = key2 != null };
RateLimitEvent evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", key1, 1, RateLimitingBehaviour.Wait, default);
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", key1, 1, RateLimitingBehaviour.Wait, null, default);
Assert.That(evnt == null);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", key2, 1, RateLimitingBehaviour.Wait, default);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", key2, 1, RateLimitingBehaviour.Wait, null, default);
Assert.That(expectLimited ? evnt != null : evnt == null);
}
@ -328,9 +322,9 @@ namespace CryptoExchange.Net.UnitTests
RateLimitEvent evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, default);
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
Assert.That(evnt == null);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", null, 1, RateLimitingBehaviour.Wait, default);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", null, 1, RateLimitingBehaviour.Wait, null, default);
Assert.That(expectLimited ? evnt != null : evnt == null);
}
@ -348,9 +342,9 @@ namespace CryptoExchange.Net.UnitTests
RateLimitEvent evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, host1, "123", 1, RateLimitingBehaviour.Wait, default);
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, host1, "123", 1, RateLimitingBehaviour.Wait, null, default);
Assert.That(evnt == null);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, host2, "123", 1, RateLimitingBehaviour.Wait, default);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, host2, "123", 1, RateLimitingBehaviour.Wait, null, default);
Assert.That(expectLimited ? evnt != null : evnt == null);
}
@ -365,10 +359,25 @@ namespace CryptoExchange.Net.UnitTests
RateLimitEvent evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), host1, "123", 1, RateLimitingBehaviour.Wait, default);
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), host1, "123", 1, RateLimitingBehaviour.Wait, null, default);
Assert.That(evnt == null);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), host2, "123", 1, RateLimitingBehaviour.Wait, default);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), host2, "123", 1, RateLimitingBehaviour.Wait, null, default);
Assert.That(expectLimited ? evnt != null : evnt == null);
}
[Test]
public async Task ConnectionRateLimiterCancel()
{
var rateLimiter = new RateLimitGate("Test");
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new LimitItemTypeFilter(RateLimitItemType.Connection), 1, TimeSpan.FromSeconds(10), RateLimitWindowType.Fixed));
RateLimitEvent evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
var ct = new CancellationTokenSource(TimeSpan.FromSeconds(0.2));
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, ct.Token);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, ct.Token);
Assert.That(result2.Error, Is.TypeOf<CancellationRequestedError>());
}
}
}

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using CryptoExchange.Net.Objects;
@ -9,7 +10,6 @@ using CryptoExchange.Net.UnitTests.TestImplementations;
using CryptoExchange.Net.UnitTests.TestImplementations.Sockets;
using Microsoft.Extensions.Logging;
using Moq;
using Newtonsoft.Json;
using NUnit.Framework;
using NUnit.Framework.Legacy;
@ -103,7 +103,7 @@ namespace CryptoExchange.Net.UnitTests
rstEvent.Set();
});
sub.AddSubscription(subObj);
var msgToSend = JsonConvert.SerializeObject(new { topic = "topic", action = "update", property = 123 });
var msgToSend = JsonSerializer.Serialize(new { topic = "topic", action = "update", property = "123" });
// act
socket.InvokeMessage(msgToSend);
@ -198,7 +198,7 @@ namespace CryptoExchange.Net.UnitTests
// act
var sub = client.SubClient.SubscribeToSomethingAsync(channel, onUpdate => {}, ct: default);
socket.InvokeMessage(JsonConvert.SerializeObject(new { channel, action = "subscribe", status = "error" }));
socket.InvokeMessage(JsonSerializer.Serialize(new { channel, action = "subscribe", status = "error" }));
await sub;
// assert
@ -221,7 +221,7 @@ namespace CryptoExchange.Net.UnitTests
// act
var sub = client.SubClient.SubscribeToSomethingAsync(channel, onUpdate => {}, ct: default);
socket.InvokeMessage(JsonConvert.SerializeObject(new { channel, action = "subscribe", status = "confirmed" }));
socket.InvokeMessage(JsonSerializer.Serialize(new { channel, action = "subscribe", status = "confirmed" }));
await sub;
// assert

View File

@ -5,6 +5,9 @@ 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
{
@ -144,7 +147,7 @@ namespace CryptoExchange.Net.UnitTests
public void TestEnumConverterNullableDeserializeTests(string value, TestEnum? expected)
{
var val = value == null ? "null" : $"\"{value}\"";
var output = JsonSerializer.Deserialize<STJEnumObject>($"{{ \"Value\": {val} }}");
var output = JsonSerializer.Deserialize<STJEnumObject>($"{{ \"Value\": {val} }}", SerializerOptions.WithConverters(new SerializationContext()));
Assert.That(output.Value == expected);
}
@ -169,8 +172,8 @@ namespace CryptoExchange.Net.UnitTests
[TestCase("three", TestEnum.Three)]
[TestCase("Four", TestEnum.Four)]
[TestCase("four", TestEnum.Four)]
[TestCase("Four1", TestEnum.One)]
[TestCase(null, TestEnum.One)]
[TestCase("Four1", null)]
[TestCase(null, null)]
public void TestEnumConverterParseStringTests(string value, TestEnum? expected)
{
var result = EnumConverter.ParseString<TestEnum>(value);
@ -192,7 +195,7 @@ namespace CryptoExchange.Net.UnitTests
public void TestBoolConverter(string value, bool? expected)
{
var val = value == null ? "null" : $"\"{value}\"";
var output = JsonSerializer.Deserialize<STJBoolObject>($"{{ \"Value\": {val} }}");
var output = JsonSerializer.Deserialize<STJBoolObject>($"{{ \"Value\": {val} }}", SerializerOptions.WithConverters(new SerializationContext()));
Assert.That(output.Value == expected);
}
@ -211,7 +214,7 @@ namespace CryptoExchange.Net.UnitTests
public void TestBoolConverterNotNullable(string value, bool expected)
{
var val = value == null ? "null" : $"\"{value}\"";
var output = JsonSerializer.Deserialize<NotNullableSTJBoolObject>($"{{ \"Value\": {val} }}");
var output = JsonSerializer.Deserialize<NotNullableSTJBoolObject>($"{{ \"Value\": {val} }}", SerializerOptions.WithConverters(new SerializationContext()));
Assert.That(output.Value == expected);
}
@ -242,6 +245,94 @@ namespace CryptoExchange.Net.UnitTests
var result = JsonSerializer.Deserialize<STJDecimalObject>("{ \"test\": " + value + "}");
Assert.That(result.Test, Is.EqualTo(expected == -999 ? decimal.MaxValue : expected));
}
[Test()]
public void TestArrayConverter()
{
var data = new Test()
{
Prop1 = 2,
Prop2 = null,
Prop3 = "123",
Prop3Again = "123",
Prop4 = null,
Prop5 = new Test2
{
Prop21 = 3,
Prop22 = "456"
},
Prop6 = new Test3
{
Prop31 = 4,
Prop32 = "789"
},
Prop7 = TestEnum.Two,
TestInternal = new Test
{
Prop1 = 10
},
Prop8 = new Test3
{
Prop31 = 5,
Prop32 = "101"
},
};
var options = new JsonSerializerOptions()
{
TypeInfoResolver = new SerializationContext()
};
var serialized = JsonSerializer.Serialize(data);
var deserialized = JsonSerializer.Deserialize<Test>(serialized);
Assert.That(deserialized.Prop1, Is.EqualTo(2));
Assert.That(deserialized.Prop2, Is.Null);
Assert.That(deserialized.Prop3, Is.EqualTo("123"));
Assert.That(deserialized.Prop3Again, Is.EqualTo("123"));
Assert.That(deserialized.Prop4, Is.Null);
Assert.That(deserialized.Prop5.Prop21, Is.EqualTo(3));
Assert.That(deserialized.Prop5.Prop22, Is.EqualTo("456"));
Assert.That(deserialized.Prop6.Prop31, Is.EqualTo(4));
Assert.That(deserialized.Prop6.Prop32, Is.EqualTo("789"));
Assert.That(deserialized.Prop7, Is.EqualTo(TestEnum.Two));
Assert.That(deserialized.TestInternal.Prop1, Is.EqualTo(10));
Assert.That(deserialized.Prop8.Prop31, Is.EqualTo(5));
Assert.That(deserialized.Prop8.Prop32, Is.EqualTo("101"));
}
[TestCase(TradingMode.Spot, "ETH", "USDT", null)]
[TestCase(TradingMode.PerpetualLinear, "ETH", "USDT", null)]
[TestCase(TradingMode.DeliveryLinear, "ETH", "USDT", 1748432430)]
public void TestSharedSymbolConversion(TradingMode tradingMode, string baseAsset, string quoteAsset, int? deliverTime)
{
DateTime? time = deliverTime == null ? null : DateTimeConverter.ParseFromDouble(deliverTime.Value);
var symbol = new SharedSymbol(tradingMode, baseAsset, quoteAsset, time);
var serialized = JsonSerializer.Serialize(symbol);
var restored = JsonSerializer.Deserialize<SharedSymbol>(serialized);
Assert.That(restored.TradingMode, Is.EqualTo(symbol.TradingMode));
Assert.That(restored.BaseAsset, Is.EqualTo(symbol.BaseAsset));
Assert.That(restored.QuoteAsset, Is.EqualTo(symbol.QuoteAsset));
Assert.That(restored.DeliverTime, Is.EqualTo(symbol.DeliverTime));
}
[TestCase(0.1, null, null)]
[TestCase(0.1, 0.1, null)]
[TestCase(0.1, 0.1, 0.1)]
[TestCase(null, 0.1, null)]
[TestCase(null, 0.1, 0.1)]
public void TestSharedQuantityConversion(double? baseQuantity, double? quoteQuantity, double? contractQuantity)
{
var symbol = new SharedOrderQuantity((decimal?)baseQuantity, (decimal?)quoteQuantity, (decimal?)contractQuantity);
var serialized = JsonSerializer.Serialize(symbol);
var restored = JsonSerializer.Deserialize<SharedOrderQuantity>(serialized);
Assert.That(restored.QuantityInBaseAsset, Is.EqualTo(symbol.QuantityInBaseAsset));
Assert.That(restored.QuantityInQuoteAsset, Is.EqualTo(symbol.QuantityInQuoteAsset));
Assert.That(restored.QuantityInContracts, Is.EqualTo(symbol.QuantityInContracts));
}
}
public class STJDecimalObject
@ -260,25 +351,88 @@ namespace CryptoExchange.Net.UnitTests
public class STJEnumObject
{
[JsonConverter(typeof(EnumConverter))]
public TestEnum? Value { get; set; }
}
public class NotNullableSTJEnumObject
{
[JsonConverter(typeof(EnumConverter))]
public TestEnum Value { get; set; }
}
public class STJBoolObject
{
[JsonConverter(typeof(BoolConverter))]
public bool? Value { get; set; }
}
public class NotNullableSTJBoolObject
{
[JsonConverter(typeof(BoolConverter))]
public bool Value { get; set; }
}
[JsonConverter(typeof(ArrayConverter<Test>))]
record Test
{
[ArrayProperty(0)]
public int Prop1 { get; set; }
[ArrayProperty(1)]
public int? Prop2 { get; set; }
[ArrayProperty(2)]
public string Prop3 { get; set; }
[ArrayProperty(2)]
public string Prop3Again { get; set; }
[ArrayProperty(3)]
public string Prop4 { get; set; }
[ArrayProperty(4)]
public Test2 Prop5 { get; set; }
[ArrayProperty(5)]
public Test3 Prop6 { get; set; }
[ArrayProperty(6), JsonConverter(typeof(EnumConverter<TestEnum>))]
public TestEnum? Prop7 { get; set; }
[ArrayProperty(7)]
public Test TestInternal { get; set; }
[ArrayProperty(8), JsonConversion]
public Test3 Prop8 { get; set; }
}
[JsonConverter(typeof(ArrayConverter<Test2>))]
record Test2
{
[ArrayProperty(0)]
public int Prop21 { get; set; }
[ArrayProperty(1)]
public string Prop22 { get; set; }
}
record Test3
{
[JsonPropertyName("prop31")]
public int Prop31 { get; set; }
[JsonPropertyName("prop32")]
public string Prop32 { get; set; }
}
[JsonConverter(typeof(EnumConverter<TestEnum>))]
public enum TestEnum
{
[Map("1")]
One,
[Map("2")]
Two,
[Map("three", "3")]
Three,
Four
}
[JsonSerializable(typeof(Test))]
[JsonSerializable(typeof(Test2))]
[JsonSerializable(typeof(Test3))]
[JsonSerializable(typeof(NotNullableSTJBoolObject))]
[JsonSerializable(typeof(STJBoolObject))]
[JsonSerializable(typeof(NotNullableSTJEnumObject))]
[JsonSerializable(typeof(STJEnumObject))]
[JsonSerializable(typeof(STJDecimalObject))]
[JsonSerializable(typeof(STJTimeObject))]
internal partial class SerializationContext : JsonSerializerContext
{
}
}

View File

@ -1,32 +1,31 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.Sockets;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
{
internal class SubResponse
{
[JsonProperty("action")]
[JsonPropertyName("action")]
public string Action { get; set; } = null!;
[JsonProperty("channel")]
[JsonPropertyName("channel")]
public string Channel { get; set; } = null!;
[JsonProperty("status")]
[JsonPropertyName("status")]
public string Status { get; set; } = null!;
}
internal class UnsubResponse
{
[JsonProperty("action")]
[JsonPropertyName("action")]
public string Action { get; set; } = null!;
[JsonProperty("status")]
[JsonPropertyName("status")]
public string Status { get; set; } = null!;
}

View File

@ -3,13 +3,18 @@ using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Text;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Clients;
using CryptoExchange.Net.Converters.SystemTextJson;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.SharedApis;
using CryptoExchange.Net.UnitTests.TestImplementations;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
namespace CryptoExchange.Net.UnitTests
{
@ -19,13 +24,15 @@ namespace CryptoExchange.Net.UnitTests
public TestBaseClient(): base(null, "Test")
{
var options = TestClientOptions.Default.Copy();
var options = new TestClientOptions();
_logger = NullLogger.Instance;
Initialize(options);
SubClient = AddApiClient(new TestSubClient(options, new RestApiOptions()));
}
public TestBaseClient(TestClientOptions exchangeOptions) : base(null, "Test")
{
_logger = NullLogger.Instance;
Initialize(exchangeOptions);
SubClient = AddApiClient(new TestSubClient(exchangeOptions, new RestApiOptions()));
}
@ -55,9 +62,11 @@ namespace CryptoExchange.Net.UnitTests
}
/// <inheritdoc />
public override string FormatSymbol(string baseAsset, string quoteAsset) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
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();
}

View File

@ -1,12 +1,14 @@
using Newtonsoft.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.UnitTests.TestImplementations
{
public class TestObject
{
[JsonProperty("other")]
[JsonPropertyName("other")]
public string StringData { get; set; }
[JsonPropertyName("intData")]
public int IntData { get; set; }
[JsonPropertyName("decimalData")]
public decimal DecimalData { get; set; }
}
}

View File

@ -1,7 +1,6 @@
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using Moq;
using Newtonsoft.Json.Linq;
using System;
using System.IO;
using System.Net;
@ -12,9 +11,13 @@ using System.Threading;
using System.Threading.Tasks;
using CryptoExchange.Net.Authentication;
using System.Collections.Generic;
using CryptoExchange.Net.Objects.Options;
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;
namespace CryptoExchange.Net.UnitTests.TestImplementations
{
@ -23,22 +26,17 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
public TestRestApi1Client Api1 { get; }
public TestRestApi2Client Api2 { get; }
public TestRestClient(Action<TestClientOptions> optionsFunc) : this(optionsFunc, null)
public TestRestClient(Action<TestClientOptions> optionsDelegate = null)
: this(null, null, Options.Create(ApplyOptionsDelegate(optionsDelegate)))
{
}
public TestRestClient(ILoggerFactory loggerFactory = null, HttpClient httpClient = null) : this((x) => { }, httpClient, loggerFactory)
public TestRestClient(HttpClient httpClient, ILoggerFactory loggerFactory, IOptions<TestClientOptions> options) : base(loggerFactory, "Test")
{
}
Initialize(options.Value);
public TestRestClient(Action<TestClientOptions> optionsFunc, HttpClient httpClient = null, ILoggerFactory loggerFactory = null) : base(loggerFactory, "Test")
{
var options = TestClientOptions.Default.Copy();
optionsFunc(options);
Initialize(options);
Api1 = new TestRestApi1Client(options);
Api2 = new TestRestApi2Client(options);
Api1 = new TestRestApi1Client(options.Value);
Api2 = new TestRestApi2Client(options.Value);
}
public void SetResponse(string responseData, out IRequest requestObj)
@ -52,13 +50,13 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
response.Setup(c => c.IsSuccessStatusCode).Returns(true);
response.Setup(c => c.GetResponseStreamAsync()).Returns(Task.FromResult((Stream)responseStream));
var headers = new Dictionary<string, IEnumerable<string>>();
var headers = new Dictionary<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.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 List<string> { val }));
request.Setup(c => c.GetHeaders()).Returns(() => headers);
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());
var factory = Mock.Get(Api1.RequestFactory);
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
@ -87,7 +85,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 Dictionary<string, IEnumerable<string>>());
request.Setup(c => c.GetHeaders()).Returns(new KeyValuePair<string, string[]>[0]);
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Throws(we);
var factory = Mock.Get(Api1.RequestFactory);
@ -111,12 +109,12 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
response.Setup(c => c.IsSuccessStatusCode).Returns(false);
response.Setup(c => c.GetResponseStreamAsync()).Returns(Task.FromResult((Stream)responseStream));
var headers = new Dictionary<string, IEnumerable<string>>();
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(key, new List<string> { val }));
request.Setup(c => c.GetHeaders()).Returns(headers);
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());
var factory = Mock.Get(Api1.RequestFactory);
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
@ -138,16 +136,19 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
}
/// <inheritdoc />
public override string FormatSymbol(string baseAsset, string quoteAsset) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
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
{
return await SendRequestAsync<T>(new Uri("http://www.test.com"), HttpMethod.Get, ct, requestWeight: 0);
return await SendAsync<T>("http://www.test.com", new RequestDefinition("/", HttpMethod.Get) { Weight = 0 }, null, ct);
}
public async Task<CallResult<T>> RequestWithParams<T>(HttpMethod method, Dictionary<string, object> parameters, Dictionary<string, string> headers) where T : class
public async Task<CallResult<T>> RequestWithParams<T>(HttpMethod method, ParameterCollection parameters, Dictionary<string, string> headers) where T : class
{
return await SendRequestAsync<T>(new Uri("http://www.test.com"), method, default, parameters, requestWeight: 0, additionalHeaders: headers);
return await SendAsync<T>("http://www.test.com", new RequestDefinition("/", method) { Weight = 0 }, parameters, default, additionalHeaders: headers);
}
public void SetParameterPosition(HttpMethod method, HttpMethodParameterPosition position)
@ -181,15 +182,18 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
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 />
public override string FormatSymbol(string baseAsset, string quoteAsset) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
public async Task<CallResult<T>> Request<T>(CancellationToken ct = default) where T : class
{
return await SendRequestAsync<T>(new Uri("http://www.test.com"), HttpMethod.Get, ct, requestWeight: 0);
return await SendAsync<T>("http://www.test.com", new RequestDefinition("/", HttpMethod.Get) { Weight = 0 }, null, ct);
}
protected override Error ParseErrorResponse(int httpStatusCode, IEnumerable<KeyValuePair<string, IEnumerable<string>>> responseHeaders, IMessageAccessor accessor)
protected override Error ParseErrorResponse(int httpStatusCode, KeyValuePair<string, string[]>[] responseHeaders, IMessageAccessor accessor, Exception exception)
{
var errorData = accessor.Deserialize<TestError>();
@ -217,7 +221,9 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
public class TestError
{
[JsonPropertyName("errorCode")]
public int ErrorCode { get; set; }
[JsonPropertyName("errorMessage")]
public string ErrorMessage { get; set; }
}

View File

@ -14,6 +14,9 @@ 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;
namespace CryptoExchange.Net.UnitTests.TestImplementations
{
@ -21,25 +24,20 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
{
public TestSubSocketClient SubClient { get; }
public TestSocketClient(ILoggerFactory loggerFactory = null) : this((x) => { }, loggerFactory)
{
}
/// <summary>
/// Create a new instance of KucoinSocketClient
/// </summary>
/// <param name="optionsFunc">Configure the options to use for this client</param>
public TestSocketClient(Action<TestSocketOptions> optionsFunc) : this(optionsFunc, null)
public TestSocketClient(Action<TestSocketOptions> optionsDelegate = null)
: this(Options.Create(ApplyOptionsDelegate(optionsDelegate)), null)
{
}
public TestSocketClient(Action<TestSocketOptions> optionsFunc, ILoggerFactory loggerFactory = null) : base(loggerFactory, "Test")
public TestSocketClient(IOptions<TestSocketOptions> options, ILoggerFactory loggerFactory = null) : base(loggerFactory, "Test")
{
var options = TestSocketOptions.Default.Copy<TestSocketOptions>();
optionsFunc(options);
Initialize(options);
Initialize(options.Value);
SubClient = AddApiClient(new TestSubSocketClient(options, options.SubOptions));
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"));
}
@ -69,7 +67,22 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
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
@ -85,8 +98,11 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
}
protected internal override IByteMessageAccessor CreateAccessor() => 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) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
internal IWebsocket CreateSocketInternal(string address)
{
@ -98,7 +114,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
public CallResult ConnectSocketSub(SocketConnection sub)
{
return ConnectSocketAsync(sub).Result;
return ConnectSocketAsync(sub, default).Result;
}
public override string GetListenerIdentifier(IMessageAccessor message)

View File

@ -0,0 +1,21 @@
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
{
[JsonSerializable(typeof(string))]
[JsonSerializable(typeof(int))]
[JsonSerializable(typeof(Dictionary<string, string>))]
[JsonSerializable(typeof(IDictionary<string, string>))]
[JsonSerializable(typeof(Dictionary<string, object>))]
[JsonSerializable(typeof(IDictionary<string, object>))]
[JsonSerializable(typeof(TestObject))]
internal partial class TestSerializerContext : JsonSerializerContext
{
}
}

View File

@ -11,7 +11,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorClient", "Examples\Bl
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{5734C2A9-F12C-4754-A8B9-640C24DC4E02}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleClient", "Examples\ConsoleClient\ConsoleClient.csproj", "{23480C58-23BF-4EBF-A173-B7F51A043A99}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleClient", "Examples\ConsoleClient\ConsoleClient.csproj", "{23480C58-23BF-4EBF-A173-B7F51A043A99}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharedClients", "Examples\SharedClients\SharedClients.csproj", "{988A87EF-EAEA-4313-A6CF-FA869813D5AB}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -35,6 +37,10 @@ Global
{23480C58-23BF-4EBF-A173-B7F51A043A99}.Debug|Any CPU.Build.0 = Debug|Any CPU
{23480C58-23BF-4EBF-A173-B7F51A043A99}.Release|Any CPU.ActiveCfg = Release|Any CPU
{23480C58-23BF-4EBF-A173-B7F51A043A99}.Release|Any CPU.Build.0 = Release|Any CPU
{988A87EF-EAEA-4313-A6CF-FA869813D5AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{988A87EF-EAEA-4313-A6CF-FA869813D5AB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{988A87EF-EAEA-4313-A6CF-FA869813D5AB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{988A87EF-EAEA-4313-A6CF-FA869813D5AB}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -42,6 +48,7 @@ Global
GlobalSection(NestedProjects) = preSolution
{AF4F5C19-162E-48F4-8B0B-BA5A2D7CE06A} = {5734C2A9-F12C-4754-A8B9-640C24DC4E02}
{23480C58-23BF-4EBF-A173-B7F51A043A99} = {5734C2A9-F12C-4754-A8B9-640C24DC4E02}
{988A87EF-EAEA-4313-A6CF-FA869813D5AB} = {5734C2A9-F12C-4754-A8B9-640C24DC4E02}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {0D1B9CE9-E0B7-4B8B-88BF-6EA2CC8CA3D7}

View File

@ -5,6 +5,7 @@ namespace CryptoExchange.Net.Attributes
/// <summary>
/// Map a enum entry to string values
/// </summary>
[AttributeUsage(AttributeTargets.Field)]
public class MapAttribute : Attribute
{
/// <summary>

View File

@ -1,4 +1,4 @@
#if !NETSTANDARD2_1
#if NETSTANDARD2_0
namespace System.Diagnostics.CodeAnalysis
{
using System;

View File

@ -11,43 +11,41 @@ namespace CryptoExchange.Net.Authentication
public class ApiCredentials
{
/// <summary>
/// The api key to authenticate requests
/// The api key / label to authenticate requests
/// </summary>
public string Key { get; }
public string Key { get; set; }
/// <summary>
/// The api secret to authenticate requests
/// The api secret or private key to authenticate requests
/// </summary>
public string Secret { get; }
public string Secret { get; set; }
/// <summary>
/// The api passphrase. Not needed on all exchanges
/// </summary>
public string? Pass { get; set; }
/// <summary>
/// Type of the credentials
/// </summary>
public ApiCredentialsType CredentialType { get; }
public ApiCredentialsType CredentialType { get; set; }
/// <summary>
/// Create Api credentials providing an api key and secret for authentication
/// </summary>
/// <param name="key">The api key used for identification</param>
/// <param name="secret">The api secret used for signing</param>
public ApiCredentials(string key, string secret) : this(key, secret, ApiCredentialsType.Hmac)
{
}
/// <summary>
/// Create Api credentials providing an api key and secret for authentication
/// </summary>
/// <param name="key">The api key used for identification</param>
/// <param name="secret">The api secret used for signing</param>
/// <param name="credentialsType">The type of credentials</param>
public ApiCredentials(string key, string secret, ApiCredentialsType credentialsType)
/// <param name="key">The api key / label used for identification</param>
/// <param name="secret">The api secret or private key used for signing</param>
/// <param name="pass">The api pass for the key. Not always needed</param>
/// <param name="credentialType">The type of credentials</param>
public ApiCredentials(string key, string secret, string? pass = null, ApiCredentialsType credentialType = ApiCredentialsType.Hmac)
{
if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(secret))
throw new ArgumentException("Key and secret can't be null/empty");
CredentialType = credentialsType;
CredentialType = credentialType;
Key = key;
Secret = secret;
Pass = pass;
}
/// <summary>
@ -56,30 +54,7 @@ namespace CryptoExchange.Net.Authentication
/// <returns></returns>
public virtual ApiCredentials Copy()
{
return new ApiCredentials(Key, Secret, CredentialType);
}
/// <summary>
/// Create Api credentials providing a stream containing json data. The json data should include two values: apiKey and apiSecret
/// </summary>
/// <param name="inputStream">The stream containing the json data</param>
/// <param name="identifierKey">A key to identify the credentials for the API. For example, when set to `binanceKey` the json data should contain a value for the property `binanceKey`. Defaults to 'apiKey'.</param>
/// <param name="identifierSecret">A key to identify the credentials for the API. For example, when set to `binanceSecret` the json data should contain a value for the property `binanceSecret`. Defaults to 'apiSecret'.</param>
public ApiCredentials(Stream inputStream, string? identifierKey = null, string? identifierSecret = null)
{
var accessor = new SystemTextJsonStreamMessageAccessor();
if (!accessor.Read(inputStream, false).Result)
throw new ArgumentException("Input stream not valid json data");
var key = accessor.GetValue<string>(MessagePath.Get().Property(identifierKey ?? "apiKey"));
var secret = accessor.GetValue<string>(MessagePath.Get().Property(identifierSecret ?? "apiSecret"));
if (key == null || secret == null)
throw new ArgumentException("apiKey or apiSecret value not found in Json credential file");
Key = key;
Secret = secret;
inputStream.Seek(0, SeekOrigin.Begin);
return new ApiCredentials(Key, Secret, Pass, CredentialType);
}
}
}

View File

@ -31,7 +31,11 @@ namespace CryptoExchange.Net.Authentication
/// <summary>
/// Get the API key of the current credentials
/// </summary>
public string ApiKey => _credentials.Key;
public string ApiKey => _credentials.Key!;
/// <summary>
/// Get the Passphrase of the current credentials
/// </summary>
public string? Pass => _credentials.Pass;
/// <summary>
/// ctor
@ -39,7 +43,7 @@ namespace CryptoExchange.Net.Authentication
/// <param name="credentials"></param>
protected AuthenticationProvider(ApiCredentials credentials)
{
if (credentials.Secret == null)
if (credentials.Key == null || credentials.Secret == null)
throw new ArgumentException("ApiKey/Secret needed");
_credentials = credentials;
@ -369,7 +373,7 @@ namespace CryptoExchange.Net.Authentication
var rsa = RSA.Create();
if (_credentials.CredentialType == ApiCredentialsType.RsaPem)
{
#if NETSTANDARD2_1_OR_GREATER
#if NETSTANDARD2_1_OR_GREATER || NET9_0_OR_GREATER
// Read from pem private key
var key = _credentials.Secret!
.Replace("\n", "")
@ -403,10 +407,14 @@ namespace CryptoExchange.Net.Authentication
/// <returns></returns>
protected static string BytesToHexString(byte[] buff)
{
#if NET9_0_OR_GREATER
return Convert.ToHexString(buff);
#else
var result = string.Empty;
foreach (var t in buff)
result += t.ToString("X2");
return result;
#endif
}
/// <summary>
@ -439,16 +447,26 @@ namespace CryptoExchange.Net.Authentication
return DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient)).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;
}
/// <summary>
/// Return the serialized request body
/// </summary>
/// <param name="serializer"></param>
/// <param name="parameters"></param>
/// <returns></returns>
protected string GetSerializedBody(IMessageSerializer serializer, IDictionary<string, object> parameters)
protected static string GetSerializedBody(IMessageSerializer serializer, IDictionary<string, object> parameters)
{
if (parameters.Count == 1 && parameters.ContainsKey(Constants.BodyPlaceHolderKey))
return serializer.Serialize(parameters[Constants.BodyPlaceHolderKey]);
if (parameters.Count == 1 && parameters.TryGetValue(Constants.BodyPlaceHolderKey, out object? value))
return serializer.Serialize(value);
else
return serializer.Serialize(parameters);
}

View File

@ -1,11 +1,13 @@
using System;
using System.Collections.Concurrent;
using System.Linq;
namespace CryptoExchange.Net.Caching
{
internal class MemoryCache
{
private readonly ConcurrentDictionary<string, CacheItem> _cache = new ConcurrentDictionary<string, CacheItem>();
private readonly object _lock = new object();
/// <summary>
/// Add a new cache entry. Will override an existing entry if it already exists
@ -26,16 +28,13 @@ namespace CryptoExchange.Net.Caching
/// <returns>Cached value if it was in cache</returns>
public object? Get(string key, TimeSpan maxAge)
{
_cache.TryGetValue(key, out CacheItem value);
foreach (var item in _cache.Where(x => DateTime.UtcNow - x.Value.CacheTime > maxAge).ToList())
_cache.TryRemove(item.Key, out _);
_cache.TryGetValue(key, out CacheItem? value);
if (value == null)
return null;
if (DateTime.UtcNow - value.CacheTime > maxAge)
{
_cache.TryRemove(key, out _);
return null;
}
return value.Value;
}

View File

@ -1,7 +1,9 @@
using System;
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.SharedApis;
using Microsoft.Extensions.Logging;
namespace CryptoExchange.Net.Clients
@ -36,6 +38,12 @@ namespace CryptoExchange.Net.Clients
/// </summary>
public bool OutputOriginalData { get; }
/// <inheritdoc />
public bool Authenticated => ApiCredentials != null;
/// <inheritdoc />
public ApiCredentials? ApiCredentials { get; set; }
/// <summary>
/// Api options
/// </summary>
@ -50,7 +58,7 @@ namespace CryptoExchange.Net.Clients
/// ctor
/// </summary>
/// <param name="logger">Logger</param>
/// <param name="outputOriginalData">Should data from this client include the orginal data in the call result</param>
/// <param name="outputOriginalData">Should data from this client include the original data in the call result</param>
/// <param name="baseAddress">Base address for this API client</param>
/// <param name="apiCredentials">Api credentials</param>
/// <param name="clientOptions">Client options</param>
@ -63,9 +71,10 @@ namespace CryptoExchange.Net.Clients
ApiOptions = apiOptions;
OutputOriginalData = outputOriginalData;
BaseAddress = baseAddress;
ApiCredentials = apiCredentials?.Copy();
if (apiCredentials != null)
AuthenticationProvider = CreateAuthenticationProvider(apiCredentials.Copy());
if (ApiCredentials != null)
AuthenticationProvider = CreateAuthenticationProvider(ApiCredentials);
}
/// <summary>
@ -76,13 +85,25 @@ namespace CryptoExchange.Net.Clients
protected abstract AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials);
/// <inheritdoc />
public abstract string FormatSymbol(string baseAsset, string quoteAsset);
public abstract string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverDate = null);
/// <inheritdoc />
public void SetApiCredentials<T>(T credentials) where T : ApiCredentials
{
if (credentials != null)
AuthenticationProvider = CreateAuthenticationProvider(credentials.Copy());
ApiCredentials = credentials?.Copy();
if (ApiCredentials != null)
AuthenticationProvider = CreateAuthenticationProvider(ApiCredentials);
}
/// <inheritdoc />
public virtual void SetOptions<T>(UpdateOptions<T> options) where T : ApiCredentials
{
ClientOptions.Proxy = options.Proxy;
ClientOptions.RequestTimeout = options.RequestTimeout ?? ClientOptions.RequestTimeout;
ApiCredentials = options.ApiCredentials?.Copy() ?? ApiCredentials;
if (ApiCredentials != null)
AuthenticationProvider = CreateAuthenticationProvider(ApiCredentials);
}
/// <summary>

View File

@ -12,6 +12,28 @@ namespace CryptoExchange.Net.Clients
/// </summary>
public abstract class BaseClient : IDisposable
{
/// <summary>
/// Version of the CryptoExchange.Net base library
/// </summary>
public Version CryptoExchangeLibVersion { get; } = typeof(BaseClient).Assembly.GetName().Version!;
/// <summary>
/// Version of the client implementation
/// </summary>
public Version ExchangeLibVersion
{
get
{
lock(_versionLock)
{
if (_exchangeVersion == null)
_exchangeVersion = GetType().Assembly.GetName().Version!;
return _exchangeVersion;
}
}
}
/// <summary>
/// The name of the API the client is for
/// </summary>
@ -27,6 +49,9 @@ namespace CryptoExchange.Net.Clients
/// </summary>
protected internal ILogger _logger;
private readonly object _versionLock = new object();
private Version _exchangeVersion;
/// <summary>
/// Provided client options
/// </summary>
@ -41,8 +66,6 @@ namespace CryptoExchange.Net.Clients
protected BaseClient(ILoggerFactory? logger, string exchange)
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
{
_logger = logger?.CreateLogger(exchange) ?? NullLoggerFactory.Instance.CreateLogger(exchange);
Exchange = exchange;
}
@ -57,7 +80,7 @@ namespace CryptoExchange.Net.Clients
throw new ArgumentNullException(nameof(options));
ClientOptions = options;
_logger.Log(LogLevel.Trace, $"Client configuration: {options}, CryptoExchange.Net: v{typeof(BaseClient).Assembly.GetName().Version}, {Exchange}.Net: v{GetType().Assembly.GetName().Version}");
_logger.Log(LogLevel.Trace, $"Client configuration: {options}, CryptoExchange.Net: v{CryptoExchangeLibVersion}, {Exchange}.Net: v{ExchangeLibVersion}");
}
/// <summary>
@ -84,6 +107,16 @@ namespace CryptoExchange.Net.Clients
return apiClient;
}
/// <summary>
/// Apply the options delegate to a new options instance
/// </summary>
protected static T ApplyOptionsDelegate<T>(Action<T>? del) where T: new()
{
var opts = new T();
del?.Invoke(opts);
return opts;
}
/// <summary>
/// Dispose
/// </summary>

View File

@ -1,6 +1,7 @@
using System.Linq;
using CryptoExchange.Net.Interfaces;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
namespace CryptoExchange.Net.Clients
{
@ -19,6 +20,7 @@ namespace CryptoExchange.Net.Clients
/// <param name="name">The name of the API this client is for</param>
protected BaseRestClient(ILoggerFactory? loggerFactory, string name) : base(loggerFactory, name)
{
_logger = loggerFactory?.CreateLogger(name + ".RestClient") ?? NullLoggerFactory.Instance.CreateLogger(name);
}
}
}

View File

@ -3,10 +3,12 @@ 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
{
@ -33,10 +35,11 @@ namespace CryptoExchange.Net.Clients
/// <summary>
/// ctor
/// </summary>
/// <param name="logger">Logger</param>
/// <param name="exchange">The name of the exchange this client is for</param>
protected BaseSocketClient(ILoggerFactory? logger, string exchange) : base(logger, exchange)
/// <param name="loggerFactory">Logger factory</param>
/// <param name="name">The name of the exchange this client is for</param>
protected BaseSocketClient(ILoggerFactory? loggerFactory, string name) : base(loggerFactory, name)
{
_logger = loggerFactory?.CreateLogger(name + ".SocketClient") ?? NullLoggerFactory.Instance.CreateLogger(name);
}
/// <summary>
@ -93,6 +96,7 @@ namespace CryptoExchange.Net.Clients
{
tasks.Add(client.ReconnectAsync());
}
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
}
@ -106,6 +110,7 @@ namespace CryptoExchange.Net.Clients
{
result.AppendLine(client.GetSubscriptionsState());
}
return result.ToString();
}
@ -120,6 +125,7 @@ namespace CryptoExchange.Net.Clients
{
result.Add(client.GetState());
}
return result;
}
}

View File

@ -9,7 +9,7 @@ namespace CryptoExchange.Net.Clients
/// </summary>
public class CryptoBaseClient : IDisposable
{
private Dictionary<Type, object> _serviceCache = new Dictionary<Type, object>();
private readonly Dictionary<Type, object> _serviceCache = new Dictionary<Type, object>();
/// <summary>
/// Service provider

View File

@ -1,5 +1,4 @@
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Interfaces.CommonClients;
using CryptoExchange.Net.Interfaces;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
@ -24,24 +23,5 @@ namespace CryptoExchange.Net.Clients
public CryptoRestClient(IServiceProvider serviceProvider) : base(serviceProvider)
{
}
/// <summary>
/// Get a list of the registered ISpotClient implementations
/// </summary>
/// <returns></returns>
public IEnumerable<ISpotClient> GetSpotClients()
{
if (_serviceProvider == null)
return new List<ISpotClient>();
return _serviceProvider.GetServices<ISpotClient>().ToList();
}
/// <summary>
/// Get an ISpotClient implementation by exchange name
/// </summary>
/// <param name="exchangeName"></param>
/// <returns></returns>
public ISpotClient? SpotClient(string exchangeName) => _serviceProvider?.GetServices<ISpotClient>()?.SingleOrDefault(s => s.ExchangeName.Equals(exchangeName, StringComparison.InvariantCultureIgnoreCase));
}
}

View File

@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Net;
@ -9,7 +8,6 @@ using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using CryptoExchange.Net.Caching;
using CryptoExchange.Net.Converters.JsonNet;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Logging.Extensions;
using CryptoExchange.Net.Objects;
@ -18,7 +16,6 @@ using CryptoExchange.Net.RateLimiting;
using CryptoExchange.Net.RateLimiting.Interfaces;
using CryptoExchange.Net.Requests;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace CryptoExchange.Net.Clients
{
@ -90,7 +87,7 @@ namespace CryptoExchange.Net.Clients
/// <summary>
/// Memory cache
/// </summary>
private static MemoryCache _cache = new MemoryCache();
private readonly static MemoryCache _cache = new MemoryCache();
/// <summary>
/// ctor
@ -115,13 +112,13 @@ namespace CryptoExchange.Net.Clients
/// Create a message accessor instance
/// </summary>
/// <returns></returns>
protected virtual IStreamMessageAccessor CreateAccessor() => new JsonNetStreamMessageAccessor();
protected abstract IStreamMessageAccessor CreateAccessor();
/// <summary>
/// Create a serializer instance
/// </summary>
/// <returns></returns>
protected virtual IMessageSerializer CreateSerializer() => new JsonNetMessageSerializer();
protected abstract IMessageSerializer CreateSerializer();
/// <summary>
/// Send a request to the base address based on the request definition
@ -155,6 +152,8 @@ namespace CryptoExchange.Net.Clients
/// <param name="cancellationToken">Cancellation token</param>
/// <param name="additionalHeaders">Additional headers for this request</param>
/// <param name="weight">Override the request weight for this request definition, for example when the weight depends on the parameters</param>
/// <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. Can be used to make rate limiting work based on parameters.</param>
/// <returns></returns>
protected virtual Task<WebCallResult<T>> SendAsync<T>(
string baseAddress,
@ -162,7 +161,9 @@ namespace CryptoExchange.Net.Clients
ParameterCollection? parameters,
CancellationToken cancellationToken,
Dictionary<string, string>? additionalHeaders = null,
int? weight = null) where T : class
int? weight = null,
int? weightSingleLimiter = null,
string? rateLimitKeySuffix = null)
{
var parameterPosition = definition.ParameterPosition ?? ParameterPositions[definition.Method];
return SendAsync<T>(
@ -172,7 +173,9 @@ namespace CryptoExchange.Net.Clients
parameterPosition == HttpMethodParameterPosition.InBody ? parameters : null,
cancellationToken,
additionalHeaders,
weight);
weight,
weightSingleLimiter,
rateLimitKeySuffix);
}
/// <summary>
@ -186,6 +189,8 @@ namespace CryptoExchange.Net.Clients
/// <param name="cancellationToken">Cancellation token</param>
/// <param name="additionalHeaders">Additional headers for this request</param>
/// <param name="weight">Override the request weight for this request definition, for example when the weight depends on the parameters</param>
/// <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. Can be used to make rate limiting work based on parameters.</param>
/// <returns></returns>
protected virtual async Task<WebCallResult<T>> SendAsync<T>(
string baseAddress,
@ -194,7 +199,9 @@ namespace CryptoExchange.Net.Clients
ParameterCollection? bodyParameters,
CancellationToken cancellationToken,
Dictionary<string, string>? additionalHeaders = null,
int? weight = null) where T : class
int? weight = null,
int? weightSingleLimiter = null,
string? rateLimitKeySuffix = null)
{
string? cacheKey = null;
if (ShouldCache(definition))
@ -218,7 +225,7 @@ namespace CryptoExchange.Net.Clients
currentTry++;
var requestId = ExchangeHelpers.NextId();
var prepareResult = await PrepareAsync(requestId, baseAddress, definition, cancellationToken, additionalHeaders, weight).ConfigureAwait(false);
var prepareResult = await PrepareAsync(requestId, baseAddress, definition, cancellationToken, additionalHeaders, weight, weightSingleLimiter, rateLimitKeySuffix).ConfigureAwait(false);
if (!prepareResult)
return new WebCallResult<T>(prepareResult.Error!);
@ -232,10 +239,18 @@ namespace CryptoExchange.Net.Clients
_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>(request, definition.RateLimitGate, cancellationToken).ConfigureAwait(false);
if (!result)
_logger.RestApiErrorReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), result.Error?.ToString());
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);
}
else
_logger.RestApiResponseReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), OutputOriginalData ? result.OriginalData : "[Data only available when OutputOriginal = true]");
{
_logger.RestApiCancellationRequested(result.RequestId);
}
if (await ShouldRetryRequestAsync(definition.RateLimitGate, result, currentTry).ConfigureAwait(false))
continue;
@ -259,6 +274,8 @@ namespace CryptoExchange.Net.Clients
/// <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(
@ -267,10 +284,10 @@ namespace CryptoExchange.Net.Clients
RequestDefinition definition,
CancellationToken cancellationToken,
Dictionary<string, string>? additionalHeaders = null,
int? weight = null)
int? weight = null,
int? weightSingleLimiter = null,
string? rateLimitKeySuffix = null)
{
var requestWeight = weight ?? definition.Weight;
// Time sync
if (definition.Authenticated)
{
@ -296,6 +313,7 @@ namespace CryptoExchange.Net.Clients
}
// Rate limiting
var requestWeight = weight ?? definition.Weight;
if (requestWeight != 0)
{
if (definition.RateLimitGate == null)
@ -303,7 +321,7 @@ 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, cancellationToken).ConfigureAwait(false);
var limitResult = await definition.RateLimitGate.ProcessAsync(_logger, requestId, RateLimitItemType.Request, definition, baseAddress, AuthenticationProvider?._credentials.Key, requestWeight, ClientOptions.RateLimitingBehaviour, rateLimitKeySuffix, cancellationToken).ConfigureAwait(false);
if (!limitResult)
return new CallResult(limitResult.Error!);
}
@ -317,13 +335,14 @@ namespace CryptoExchange.Net.Clients
if (ClientOptions.RateLimiterEnabled)
{
var limitResult = await definition.RateLimitGate.ProcessSingleAsync(_logger, requestId, definition.LimitGuard, RateLimitItemType.Request, definition, baseAddress, AuthenticationProvider?._credentials.Key, ClientOptions.RateLimitingBehaviour, cancellationToken).ConfigureAwait(false);
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);
if (!limitResult)
return new CallResult(limitResult.Error!);
}
}
return new CallResult(null);
return CallResult.SuccessResult;
}
/// <summary>
@ -417,215 +436,6 @@ namespace CryptoExchange.Net.Clients
return request;
}
/// <summary>
/// Execute a request to the uri and returns if it was successful
/// </summary>
/// <param name="uri">The uri to send the request to</param>
/// <param name="method">The method of the request</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <param name="parameters">The parameters of the request</param>
/// <param name="signed">Whether or not the request should be authenticated</param>
/// <param name="requestBodyFormat">The format of the body content</param>
/// <param name="parameterPosition">Where the parameters should be placed, overwrites the value set in the client</param>
/// <param name="arraySerialization">How array parameters should be serialized, overwrites the value set in the client</param>
/// <param name="requestWeight">Credits used for the request</param>
/// <param name="additionalHeaders">Additional headers to send with the request</param>
/// <param name="gate">The ratelimit gate to use</param>
/// <returns></returns>
[return: NotNull]
protected virtual async Task<WebCallResult> SendRequestAsync(
Uri uri,
HttpMethod method,
CancellationToken cancellationToken,
Dictionary<string, object>? parameters = null,
bool signed = false,
RequestBodyFormat? requestBodyFormat = null,
HttpMethodParameterPosition? parameterPosition = null,
ArrayParametersSerialization? arraySerialization = null,
int requestWeight = 1,
Dictionary<string, string>? additionalHeaders = null,
IRateLimitGate? gate = null)
{
int currentTry = 0;
while (true)
{
currentTry++;
var request = await PrepareRequestAsync(uri, method, cancellationToken, parameters, signed, requestBodyFormat, parameterPosition, arraySerialization, requestWeight, additionalHeaders, gate).ConfigureAwait(false);
if (!request)
return new WebCallResult(request.Error!);
var result = await GetResponseAsync<object>(request.Data, gate, cancellationToken).ConfigureAwait(false);
if (!result)
_logger.RestApiErrorReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), result.Error?.ToString());
else
_logger.RestApiResponseReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), OutputOriginalData ? result.OriginalData : "[Data only available when OutputOriginal = true]");
if (await ShouldRetryRequestAsync(gate, result, currentTry).ConfigureAwait(false))
continue;
return result.AsDataless();
}
}
/// <summary>
/// Execute a request to the uri and deserialize the response into the provided type parameter
/// </summary>
/// <typeparam name="T">The type to deserialize into</typeparam>
/// <param name="uri">The uri to send the request to</param>
/// <param name="method">The method of the request</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <param name="parameters">The parameters of the request</param>
/// <param name="signed">Whether or not the request should be authenticated</param>
/// <param name="requestBodyFormat">The format of the body content</param>
/// <param name="parameterPosition">Where the parameters should be placed, overwrites the value set in the client</param>
/// <param name="arraySerialization">How array parameters should be serialized, overwrites the value set in the client</param>
/// <param name="requestWeight">Credits used for the request</param>
/// <param name="additionalHeaders">Additional headers to send with the request</param>
/// <param name="gate">The ratelimit gate to use</param>
/// <param name="preventCaching">Whether caching should be prevented for this request</param>
/// <returns></returns>
[return: NotNull]
protected virtual async Task<WebCallResult<T>> SendRequestAsync<T>(
Uri uri,
HttpMethod method,
CancellationToken cancellationToken,
Dictionary<string, object>? parameters = null,
bool signed = false,
RequestBodyFormat? requestBodyFormat = null,
HttpMethodParameterPosition? parameterPosition = null,
ArrayParametersSerialization? arraySerialization = null,
int requestWeight = 1,
Dictionary<string, string>? additionalHeaders = null,
IRateLimitGate? gate = null,
bool preventCaching = false
) where T : class
{
var key = uri.ToString() + method + signed + parameters?.ToFormData();
if (ShouldCache(method) && !preventCaching)
{
_logger.CheckingCache(key);
var cachedValue = _cache.Get(key, ClientOptions.CachingMaxAge);
if (cachedValue != null)
{
_logger.CacheHit(key);
var original = (WebCallResult<T>)cachedValue;
return original.Cached();
}
_logger.CacheNotHit(key);
}
int currentTry = 0;
while (true)
{
currentTry++;
var request = await PrepareRequestAsync(uri, method, cancellationToken, parameters, signed, requestBodyFormat, parameterPosition, arraySerialization, requestWeight, additionalHeaders, gate).ConfigureAwait(false);
if (!request)
return new WebCallResult<T>(request.Error!);
var result = await GetResponseAsync<T>(request.Data, gate, cancellationToken).ConfigureAwait(false);
if (!result)
_logger.RestApiErrorReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), result.Error?.ToString());
else
_logger.RestApiResponseReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), OutputOriginalData ? result.OriginalData : "[Data only available when OutputOriginal = true]");
if (await ShouldRetryRequestAsync(gate, result, currentTry).ConfigureAwait(false))
continue;
if (result.Success &&
ShouldCache(method) &&
!preventCaching)
{
_cache.Add(key, result);
}
return result;
}
}
/// <summary>
/// Prepares a request to be sent to the server
/// </summary>
/// <param name="uri">The uri to send the request to</param>
/// <param name="method">The method of the request</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <param name="parameters">The parameters of the request</param>
/// <param name="signed">Whether or not the request should be authenticated</param>
/// <param name="requestBodyFormat">The format of the body content</param>
/// <param name="parameterPosition">Where the parameters should be placed, overwrites the value set in the client</param>
/// <param name="arraySerialization">How array parameters should be serialized, overwrites the value set in the client</param>
/// <param name="requestWeight">Credits used for the request</param>
/// <param name="additionalHeaders">Additional headers to send with the request</param>
/// <param name="gate">The rate limit gate to use</param>
/// <returns></returns>
protected virtual async Task<CallResult<IRequest>> PrepareRequestAsync(
Uri uri,
HttpMethod method,
CancellationToken cancellationToken,
Dictionary<string, object>? parameters = null,
bool signed = false,
RequestBodyFormat? requestBodyFormat = null,
HttpMethodParameterPosition? parameterPosition = null,
ArrayParametersSerialization? arraySerialization = null,
int requestWeight = 1,
Dictionary<string, string>? additionalHeaders = null,
IRateLimitGate? gate = null)
{
var requestId = ExchangeHelpers.NextId();
if (signed)
{
if (AuthenticationProvider == null)
{
_logger.RestApiNoApiCredentials(requestId, uri.AbsolutePath);
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.As<IRequest>(default);
}
}
}
if (requestWeight != 0)
{
if (gate == null)
throw new Exception("Ratelimit gate not set when request weight is not 0");
if (ClientOptions.RateLimiterEnabled)
{
var limitResult = await gate.ProcessAsync(_logger, requestId, RateLimitItemType.Request, new RequestDefinition(uri.AbsolutePath.TrimStart('/'), method) { Authenticated = signed }, uri.Host, AuthenticationProvider?._credentials.Key, requestWeight, ClientOptions.RateLimitingBehaviour, cancellationToken).ConfigureAwait(false);
if (!limitResult)
return new CallResult<IRequest>(limitResult.Error!);
}
}
_logger.RestApiCreatingRequest(requestId, uri);
var paramsPosition = parameterPosition ?? ParameterPositions[method];
var request = ConstructRequest(uri, method, parameters?.OrderBy(p => p.Key).ToDictionary(p => p.Key, p => p.Value), signed, paramsPosition, arraySerialization ?? ArraySerialization, requestBodyFormat ?? RequestBodyFormat, requestId, additionalHeaders);
string? paramString = "";
if (paramsPosition == HttpMethodParameterPosition.InBody)
paramString = $" with request body '{request.Content}'";
var headers = request.GetHeaders();
if (headers.Any())
paramString += " with headers " + string.Join(", ", headers.Select(h => h.Key + $"=[{string.Join(",", h.Value)}]"));
TotalRequestsMade++;
_logger.RestApiSendingRequest(requestId, method, signed ? "signed": "", request.Uri, paramString);
return new CallResult<IRequest>(request);
}
/// <summary>
/// Executes the request and returns the result deserialized into the type parameter class
/// </summary>
@ -656,7 +466,7 @@ namespace CryptoExchange.Net.Clients
if (!response.IsSuccessStatusCode)
{
// Error response
await accessor.Read(responseStream, true).ConfigureAwait(false);
var readResult = await accessor.Read(responseStream, true).ConfigureAwait(false);
Error error;
if (response.StatusCode == (HttpStatusCode)418 || response.StatusCode == (HttpStatusCode)429)
@ -672,7 +482,7 @@ namespace CryptoExchange.Net.Clients
}
else
{
error = ParseErrorResponse((int)response.StatusCode, response.ResponseHeaders, accessor);
error = ParseErrorResponse((int)response.StatusCode, response.ResponseHeaders, accessor, readResult.Error?.Exception);
}
if (error.Code == null || error.Code == 0)
@ -681,23 +491,34 @@ namespace CryptoExchange.Net.Clients
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error!);
}
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>(statusCode, headers, sw.Elapsed, 0, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, null);
return new WebCallResult<T>(statusCode, headers, 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);
var valid = await accessor.Read(responseStream, outputOriginalData).ConfigureAwait(false);
if (!valid)
{
// Invalid json
var error = new ServerError("Failed to parse response: " + valid.Error!.Message, accessor.OriginalDataAvailable ? accessor.GetOriginalString() : "[Data only available when OutputOriginal = true in client options]");
var error = new DeserializeError("Failed to parse response: " + valid.Error!.Message, valid.Error.Exception);
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
}
// Json response received
var parsedError = TryParseError(accessor);
var parsedError = TryParseError(response.ResponseHeaders, accessor);
if (parsedError != null)
{
if (parsedError is ServerRateLimitError rateError)
{
if (rateError.RetryAfter != null && gate != null && ClientOptions.RateLimiterEnabled)
{
_logger.RestApiRateLimitPauseUntil(request.RequestId, rateError.RetryAfter.Value);
await gate.SetRetryAfterGuardAsync(rateError.RetryAfter.Value).ConfigureAwait(false);
}
}
// Success status code, but TryParseError determined it was an error response
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, parsedError);
}
var deserializeResult = accessor.Deserialize<T>();
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult.Data, deserializeResult.Error);
@ -705,20 +526,19 @@ namespace CryptoExchange.Net.Clients
catch (HttpRequestException requestException)
{
// Request exception, can't reach server for instance
var exceptionInfo = requestException.ToLogString();
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new WebError(exceptionInfo));
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new WebError(requestException.Message, exception: requestException));
}
catch (OperationCanceledException canceledException)
{
if (cancellationToken != default && canceledException.CancellationToken == cancellationToken)
{
// Cancellation token canceled by caller
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new CancellationRequestedError());
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new CancellationRequestedError(canceledException));
}
else
{
// Request timed out
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new WebError($"Request timed out"));
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new WebError($"Request timed out", exception: canceledException));
}
}
finally
@ -731,12 +551,13 @@ namespace CryptoExchange.Net.Clients
/// <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.
/// When setting manualParseError to true this method will be called for each response to be able to check if the response is an error or not.
/// This method will be called for each response to be able to check if the response is an error or not.
/// If the response is an error this method should return the parsed error, else it should return null
/// </summary>
/// <param name="accessor">Data accessor</param>
/// <param name="responseHeaders">The response headers</param>
/// <returns>Null if not an error, Error otherwise</returns>
protected virtual ServerError? TryParseError(IMessageAccessor accessor) => null;
protected virtual Error? TryParseError(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.
@ -753,7 +574,7 @@ namespace CryptoExchange.Net.Clients
// Only retry once
return false;
if ((int?)callResult.ResponseStatusCode == 429
if (callResult.Error is ServerRateLimitError
&& ClientOptions.RateLimiterEnabled
&& ClientOptions.RateLimitingBehaviour != RateLimitingBehaviour.Fail
&& gate != null)
@ -772,112 +593,6 @@ namespace CryptoExchange.Net.Clients
return false;
}
/// <summary>
/// Creates a request object
/// </summary>
/// <param name="uri">The uri to send the request to</param>
/// <param name="method">The method of the request</param>
/// <param name="parameters">The parameters of the request</param>
/// <param name="signed">Whether or not the request should be authenticated</param>
/// <param name="parameterPosition">Where the parameters should be placed</param>
/// <param name="arraySerialization">How array parameters should be serialized</param>
/// <param name="bodyFormat">Format of the body content</param>
/// <param name="requestId">Unique id of a request</param>
/// <param name="additionalHeaders">Additional headers to send with the request</param>
/// <returns></returns>
protected virtual IRequest ConstructRequest(
Uri uri,
HttpMethod method,
Dictionary<string, object>? parameters,
bool signed,
HttpMethodParameterPosition parameterPosition,
ArrayParametersSerialization arraySerialization,
RequestBodyFormat bodyFormat,
int requestId,
Dictionary<string, string>? additionalHeaders)
{
parameters ??= new Dictionary<string, object>();
for (var i = 0; i < parameters.Count; i++)
{
var kvp = parameters.ElementAt(i);
if (kvp.Value is Func<object> delegateValue)
parameters[kvp.Key] = delegateValue();
}
if (parameterPosition == HttpMethodParameterPosition.InUri)
{
foreach (var parameter in parameters)
uri = uri.AddQueryParmeter(parameter.Key, parameter.Value.ToString());
}
var headers = new Dictionary<string, string>();
var uriParameters = parameterPosition == HttpMethodParameterPosition.InUri ? CreateParameterDictionary(parameters) : null;
var bodyParameters = parameterPosition == HttpMethodParameterPosition.InBody ? CreateParameterDictionary(parameters) : null;
if (AuthenticationProvider != null)
{
try
{
AuthenticationProvider.AuthenticateRequest(
this,
uri,
method,
ref uriParameters,
ref bodyParameters,
ref headers,
signed,
arraySerialization,
parameterPosition,
bodyFormat
);
}
catch (Exception ex)
{
throw new Exception("Failed to authenticate request, make sure your API credentials are correct", ex);
}
}
// Add the auth parameters to the uri, start with a new URI to be able to sort the parameters including the auth parameters
if (uriParameters != null)
uri = uri.SetParameters(uriParameters, arraySerialization);
var request = RequestFactory.Create(method, uri, requestId);
request.Accept = Constants.JsonContentHeader;
if (headers != null)
{
foreach (var header in headers)
request.AddHeader(header.Key, header.Value);
}
if (additionalHeaders != null)
{
foreach (var header in additionalHeaders)
request.AddHeader(header.Key, header.Value);
}
if (StandardRequestHeaders != null)
{
foreach (var header in StandardRequestHeaders)
{
// Only add it if it isn't overwritten
if (additionalHeaders?.ContainsKey(header.Key) != true)
request.AddHeader(header.Key, header.Value);
}
}
if (parameterPosition == HttpMethodParameterPosition.InBody)
{
var contentType = bodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader;
if (bodyParameters?.Any() == true)
WriteParamBody(request, bodyParameters, contentType);
else
request.SetContent(RequestBodyEmptyContent, contentType);
}
return request;
}
/// <summary>
/// Writes the parameters of the request to the request object body
/// </summary>
@ -890,8 +605,8 @@ namespace CryptoExchange.Net.Clients
{
// Write the parameters as json in the body
string stringData;
if (parameters.Count == 1 && parameters.ContainsKey(Constants.BodyPlaceHolderKey))
stringData = CreateSerializer().Serialize(parameters[Constants.BodyPlaceHolderKey]);
if (parameters.Count == 1 && parameters.TryGetValue(Constants.BodyPlaceHolderKey, out object? value))
stringData = CreateSerializer().Serialize(value);
else
stringData = CreateSerializer().Serialize(parameters);
request.SetContent(stringData, contentType);
@ -910,11 +625,11 @@ namespace CryptoExchange.Net.Clients
/// <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, IEnumerable<KeyValuePair<string, IEnumerable<string>>> responseHeaders, IMessageAccessor accessor)
protected virtual Error ParseErrorResponse(int httpStatusCode, KeyValuePair<string, string[]>[] responseHeaders, IMessageAccessor accessor, Exception? exception)
{
var message = accessor.OriginalDataAvailable ? accessor.GetOriginalString() : "[Error response content only available when OutputOriginal = true in client options]";
return new ServerError(message);
return new ServerError(null, "Unknown request error", exception);
}
/// <summary>
@ -924,23 +639,21 @@ namespace CryptoExchange.Net.Clients
/// <param name="responseHeaders">The response headers</param>
/// <param name="accessor">Data accessor</param>
/// <returns></returns>
protected virtual ServerRateLimitError ParseRateLimitResponse(int httpStatusCode, IEnumerable<KeyValuePair<string, IEnumerable<string>>> responseHeaders, IMessageAccessor accessor)
protected virtual ServerRateLimitError ParseRateLimitResponse(int httpStatusCode, KeyValuePair<string, string[]>[] responseHeaders, IMessageAccessor accessor)
{
var message = accessor.OriginalDataAvailable ? accessor.GetOriginalString() : "[Error response content only available when OutputOriginal = true in client options]";
// Handle retry after header
var retryAfterHeader = responseHeaders.SingleOrDefault(r => r.Key.Equals("Retry-After", StringComparison.InvariantCultureIgnoreCase));
if (retryAfterHeader.Value?.Any() != true)
return new ServerRateLimitError(message);
return new ServerRateLimitError();
var value = retryAfterHeader.Value.First();
if (int.TryParse(value, out var seconds))
return new ServerRateLimitError(message) { RetryAfter = DateTime.UtcNow.AddSeconds(seconds) };
return new ServerRateLimitError() { RetryAfter = DateTime.UtcNow.AddSeconds(seconds) };
if (DateTime.TryParse(value, out var datetime))
return new ServerRateLimitError(message) { RetryAfter = datetime };
return new ServerRateLimitError() { RetryAfter = datetime };
return new ServerRateLimitError(message);
return new ServerRateLimitError();
}
/// <summary>
@ -962,6 +675,14 @@ namespace CryptoExchange.Net.Clients
/// <returns>Server time</returns>
protected virtual Task<WebCallResult<DateTime>> GetServerTimestampAsync() => throw new NotImplementedException();
/// <inheritdoc />
public override void SetOptions<T>(UpdateOptions<T> options)
{
base.SetOptions(options);
RequestFactory.UpdateSettings(options.Proxy, options.RequestTimeout ?? ClientOptions.RequestTimeout);
}
internal async Task<WebCallResult<bool>> SyncTimeAsync()
{
var timeSyncParams = GetTimeSyncInfo();
@ -1009,9 +730,5 @@ namespace CryptoExchange.Net.Clients
=> ClientOptions.CachingEnabled
&& definition.Method == HttpMethod.Get
&& !definition.PreventCaching;
private bool ShouldCache(HttpMethod method)
=> ClientOptions.CachingEnabled
&& method == HttpMethod.Get;
}
}

View File

@ -1,14 +1,13 @@
using CryptoExchange.Net.Converters.JsonNet;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Logging.Extensions;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.RateLimiting;
using CryptoExchange.Net.RateLimiting.Interfaces;
using CryptoExchange.Net.Sockets;
using Microsoft.Extensions.Logging;
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
@ -43,6 +42,11 @@ namespace CryptoExchange.Net.Clients
/// </summary>
protected TimeSpan KeepAliveInterval { get; set; } = TimeSpan.FromSeconds(10);
/// <summary>
/// Keep alive timeout for websocket connection
/// </summary>
protected TimeSpan KeepAliveTimeout { get; set; } = TimeSpan.FromSeconds(10);
/// <summary>
/// Handlers for data from the socket which doesn't need to be forwarded to the caller. Ping or welcome messages for example.
/// </summary>
@ -73,12 +77,17 @@ namespace CryptoExchange.Net.Clients
/// </summary>
protected List<DedicatedConnectionConfig> DedicatedConnectionConfigs { get; set; } = new List<DedicatedConnectionConfig>();
/// <summary>
/// Whether to allow multiple subscriptions with the same topic on the same connection
/// </summary>
protected bool AllowTopicsOnTheSameConnection { get; set; } = true;
/// <inheritdoc />
public double IncomingKbps
{
get
{
if (!socketConnections.Any())
if (socketConnections.IsEmpty)
return 0;
return socketConnections.Sum(s => s.Value.IncomingKbps);
@ -93,7 +102,7 @@ namespace CryptoExchange.Net.Clients
{
get
{
if (!socketConnections.Any())
if (socketConnections.IsEmpty)
return 0;
return socketConnections.Sum(s => s.Value.UserSubscriptionCount);
@ -129,13 +138,13 @@ namespace CryptoExchange.Net.Clients
/// Create a message accessor instance
/// </summary>
/// <returns></returns>
protected internal virtual IByteMessageAccessor CreateAccessor() => new JsonNetByteMessageAccessor();
protected internal abstract IByteMessageAccessor CreateAccessor();
/// <summary>
/// Create a serializer instance
/// </summary>
/// <returns></returns>
protected internal virtual IMessageSerializer CreateSerializer() => new JsonNetMessageSerializer();
protected internal abstract IMessageSerializer CreateSerializer();
/// <summary>
/// Keep an open connection to this url
@ -154,7 +163,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<CallResult>? callback)
protected virtual void RegisterPeriodicQuery(string identifier, TimeSpan interval, Func<SocketConnection, Query> queryDelegate, Action<SocketConnection, CallResult>? callback)
{
PeriodicTaskRegistrations.Add(new PeriodicTaskRegistration
{
@ -202,9 +211,9 @@ namespace CryptoExchange.Net.Clients
{
await semaphoreSlim.WaitAsync(ct).ConfigureAwait(false);
}
catch (OperationCanceledException)
catch (OperationCanceledException tce)
{
return new CallResult<UpdateSubscription>(new CancellationRequestedError());
return new CallResult<UpdateSubscription>(new CancellationRequestedError(tce));
}
try
@ -212,7 +221,7 @@ namespace CryptoExchange.Net.Clients
while (true)
{
// Get a new or existing socket connection
var socketResult = await GetSocketConnection(url, subscription.Authenticated, false).ConfigureAwait(false);
var socketResult = await GetSocketConnection(url, subscription.Authenticated, false, subscription.Topic).ConfigureAwait(false);
if (!socketResult)
return socketResult.As<UpdateSubscription>(null);
@ -235,7 +244,7 @@ namespace CryptoExchange.Net.Clients
var needsConnecting = !socketConnection.Connected;
var connectResult = await ConnectIfNeededAsync(socketConnection, subscription.Authenticated).ConfigureAwait(false);
var connectResult = await ConnectIfNeededAsync(socketConnection, subscription.Authenticated, ct).ConfigureAwait(false);
if (!connectResult)
return new CallResult<UpdateSubscription>(connectResult.Error!);
@ -259,7 +268,7 @@ namespace CryptoExchange.Net.Clients
if (subQuery != null)
{
// Send the request and wait for answer
var subResult = await socketConnection.SendAndWaitQueryAsync(subQuery, waitEvent).ConfigureAwait(false);
var subResult = await socketConnection.SendAndWaitQueryAsync(subQuery, waitEvent, ct).ConfigureAwait(false);
if (!subResult)
{
waitEvent?.Set();
@ -343,7 +352,7 @@ namespace CryptoExchange.Net.Clients
released = true;
}
var connectResult = await ConnectIfNeededAsync(socketConnection, query.Authenticated).ConfigureAwait(false);
var connectResult = await ConnectIfNeededAsync(socketConnection, query.Authenticated, ct).ConfigureAwait(false);
if (!connectResult)
return new CallResult<THandlerResponse>(connectResult.Error!);
}
@ -370,13 +379,14 @@ namespace CryptoExchange.Net.Clients
/// </summary>
/// <param name="socket">The connection to check</param>
/// <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)
protected virtual async Task<CallResult> ConnectIfNeededAsync(SocketConnection socket, bool authenticated, CancellationToken ct)
{
if (socket.Connected)
return new CallResult(null);
return CallResult.SuccessResult;
var connectResult = await ConnectSocketAsync(socket).ConfigureAwait(false);
var connectResult = await ConnectSocketAsync(socket, ct).ConfigureAwait(false);
if (!connectResult)
return connectResult;
@ -384,7 +394,7 @@ namespace CryptoExchange.Net.Clients
await Task.Delay(ClientOptions.DelayAfterConnect).ConfigureAwait(false);
if (!authenticated || socket.Authenticated)
return new CallResult(null);
return CallResult.SuccessResult;
var result = await AuthenticateSocketAsync(socket).ConfigureAwait(false);
if (!result)
@ -404,7 +414,7 @@ namespace CryptoExchange.Net.Clients
return new CallResult(new NoApiCredentialsError());
_logger.AttemptingToAuthenticate(socket.SocketId);
var authRequest = GetAuthenticationRequest(socket);
var authRequest = await GetAuthenticationRequestAsync(socket).ConfigureAwait(false);
if (authRequest != null)
{
var result = await socket.SendAndWaitQueryAsync(authRequest).ConfigureAwait(false);
@ -418,18 +428,19 @@ namespace CryptoExchange.Net.Clients
result.Error!.Message = "Authentication failed: " + result.Error.Message;
return new CallResult(result.Error)!;
}
_logger.Authenticated(socket.SocketId);
}
_logger.Authenticated(socket.SocketId);
socket.Authenticated = true;
return new CallResult(null);
return CallResult.SuccessResult;
}
/// <summary>
/// Should return the request which can be used to authenticate a socket connection
/// </summary>
/// <returns></returns>
protected internal virtual Query? GetAuthenticationRequest(SocketConnection connection) => throw new NotImplementedException();
protected internal virtual Task<Query?> GetAuthenticationRequestAsync(SocketConnection connection) => throw new NotImplementedException();
/// <summary>
/// Adds a system subscription. Used for example to reply to ping requests
@ -470,7 +481,7 @@ namespace CryptoExchange.Net.Clients
/// <returns></returns>
protected internal virtual Task<CallResult> RevitalizeRequestAsync(Subscription subscription)
{
return Task.FromResult(new CallResult(null));
return Task.FromResult(CallResult.SuccessResult);
}
/// <summary>
@ -479,28 +490,33 @@ namespace CryptoExchange.Net.Clients
/// <param name="address">The address the socket is for</param>
/// <param name="authenticated">Whether the socket should be authenticated</param>
/// <param name="dedicatedRequestConnection">Whether a dedicated request connection should be returned</param>
/// <param name="topic">The subscription topic, can be provided when multiple of the same topics are not allowed on a connection</param>
/// <returns></returns>
protected virtual async Task<CallResult<SocketConnection>> GetSocketConnection(string address, bool authenticated, bool dedicatedRequestConnection)
protected virtual async Task<CallResult<SocketConnection>> GetSocketConnection(string address, bool authenticated, bool dedicatedRequestConnection, string? topic = null)
{
var socketQuery = socketConnections.Where(s => (s.Value.Status == SocketConnection.SocketStatus.None || s.Value.Status == SocketConnection.SocketStatus.Connected)
&& s.Value.Tag.TrimEnd('/') == address.TrimEnd('/')
&& s.Value.ApiClient.GetType() == GetType()
&& (s.Value.Authenticated == authenticated || !authenticated)
&& (AllowTopicsOnTheSameConnection || !s.Value.Topics.Contains(topic))
&& s.Value.Connected);
SocketConnection connection;
if (!dedicatedRequestConnection)
{
connection = socketQuery.Where(s => !s.Value.DedicatedRequestConnection).OrderBy(s => s.Value.UserSubscriptionCount).FirstOrDefault().Value;
connection = socketQuery.Where(s => !s.Value.DedicatedRequestConnection.IsDedicatedRequestConnection).OrderBy(s => s.Value.UserSubscriptionCount).FirstOrDefault().Value;
}
else
{
connection = socketQuery.Where(s => s.Value.DedicatedRequestConnection).FirstOrDefault().Value;
connection = socketQuery.Where(s => s.Value.DedicatedRequestConnection.IsDedicatedRequestConnection).FirstOrDefault().Value;
if (connection != null && !connection.DedicatedRequestConnection.Authenticated)
// Mark dedicated request connection as authenticated if the request is authenticated
connection.DedicatedRequestConnection.Authenticated = authenticated;
}
if (connection != null)
{
if (connection.UserSubscriptionCount < ClientOptions.SocketSubscriptionsCombineTarget || socketConnections.Count >= (ApiOptions.MaxSocketConnections ?? ClientOptions.MaxSocketConnections) && socketConnections.All(s => s.Value.UserSubscriptionCount >= ClientOptions.SocketSubscriptionsCombineTarget))
if (connection.UserSubscriptionCount < ClientOptions.SocketSubscriptionsCombineTarget || (socketConnections.Count >= (ApiOptions.MaxSocketConnections ?? ClientOptions.MaxSocketConnections) && socketConnections.All(s => s.Value.UserSubscriptionCount >= ClientOptions.SocketSubscriptionsCombineTarget)))
// Use existing socket if it has less than target connections OR it has the least connections and we can't make new
return new CallResult<SocketConnection>(connection);
}
@ -519,7 +535,15 @@ namespace CryptoExchange.Net.Clients
var socket = CreateSocket(connectionAddress.Data!);
var socketConnection = new SocketConnection(_logger, this, socket, address);
socketConnection.UnhandledMessage += HandleUnhandledMessage;
socketConnection.DedicatedRequestConnection = dedicatedRequestConnection;
socketConnection.ConnectRateLimitedAsync += HandleConnectRateLimitedAsync;
if (dedicatedRequestConnection)
{
socketConnection.DedicatedRequestConnection = new DedicatedConnectionState
{
IsDedicatedRequestConnection = dedicatedRequestConnection,
Authenticated = authenticated
};
}
foreach (var ptg in PeriodicTaskRegistrations)
socketConnection.QueryPeriodic(ptg.Identifier, ptg.Interval, ptg.QueryDelegate, ptg.Callback);
@ -538,14 +562,29 @@ namespace CryptoExchange.Net.Clients
{
}
/// <summary>
/// Process connect rate limited
/// </summary>
protected async virtual Task HandleConnectRateLimitedAsync()
{
if (ClientOptions.RateLimiterEnabled && ClientOptions.ConnectDelayAfterRateLimited.HasValue)
{
var retryAfter = DateTime.UtcNow.Add(ClientOptions.ConnectDelayAfterRateLimited.Value);
_logger.AddingRetryAfterGuard(retryAfter);
RateLimiter ??= new RateLimitGate("Connection");
await RateLimiter.SetRetryAfterGuardAsync(retryAfter, RateLimitItemType.Connection).ConfigureAwait(false);
}
}
/// <summary>
/// Connect a socket
/// </summary>
/// <param name="socketConnection">The socket to connect</param>
/// <param name="ct">Cancellation token</param>
/// <returns></returns>
protected virtual async Task<CallResult> ConnectSocketAsync(SocketConnection socketConnection)
protected virtual async Task<CallResult> ConnectSocketAsync(SocketConnection socketConnection, CancellationToken ct)
{
var connectResult = await socketConnection.ConnectAsync().ConfigureAwait(false);
var connectResult = await socketConnection.ConnectAsync(ct).ConfigureAwait(false);
if (connectResult)
{
socketConnections.TryAdd(socketConnection.SocketId, socketConnection);
@ -565,11 +604,13 @@ namespace CryptoExchange.Net.Clients
=> new(new Uri(address), ClientOptions.ReconnectPolicy)
{
KeepAliveInterval = KeepAliveInterval,
KeepAliveTimeout = KeepAliveTimeout,
ReconnectInterval = ClientOptions.ReconnectInterval,
RateLimiter = ClientOptions.RateLimiterEnabled ? RateLimiter : null,
RateLimitingBehaviour = ClientOptions.RateLimitingBehaviour,
RateLimitingBehavior = ClientOptions.RateLimitingBehaviour,
Proxy = ClientOptions.Proxy,
Timeout = ApiOptions.SocketNoDataTimeout ?? ClientOptions.SocketNoDataTimeout
Timeout = ApiOptions.SocketNoDataTimeout ?? ClientOptions.SocketNoDataTimeout,
ReceiveBufferSize = ClientOptions.ReceiveBufferSize,
};
/// <summary>
@ -639,8 +680,11 @@ namespace CryptoExchange.Net.Clients
var tasks = new List<Task>();
{
var socketList = socketConnections.Values;
foreach (var connection in socketList.Where(s => !s.DedicatedRequestConnection))
tasks.Add(connection.CloseAsync());
foreach (var connection in socketList)
{
foreach(var subscription in connection.Subscriptions.Where(x => x.UserSubscription))
tasks.Add(connection.CloseAsync(subscription));
}
}
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
@ -672,12 +716,31 @@ namespace CryptoExchange.Net.Clients
if (!socketResult)
return socketResult.AsDataless();
var connectResult = await ConnectIfNeededAsync(socketResult.Data, item.Authenticated).ConfigureAwait(false);
var connectResult = await ConnectIfNeededAsync(socketResult.Data, item.Authenticated, default).ConfigureAwait(false);
if (!connectResult)
return new CallResult(connectResult.Error!);
}
return new CallResult(null);
return CallResult.SuccessResult;
}
/// <inheritdoc />
public override void SetOptions<T>(UpdateOptions<T> options)
{
var previousProxyIsSet = ClientOptions.Proxy != null;
base.SetOptions(options);
if ((!previousProxyIsSet && options.Proxy == null)
|| socketConnections.IsEmpty)
{
return;
}
_logger.LogInformation("Reconnecting websockets to apply proxy");
// Update proxy, also triggers reconnect
foreach (var connection in socketConnections)
_ = connection.Value.UpdateProxy(options.Proxy);
}
/// <summary>

View File

@ -1,21 +0,0 @@
namespace CryptoExchange.Net.CommonObjects
{
/// <summary>
/// Balance data
/// </summary>
public class Balance: BaseCommonObject
{
/// <summary>
/// The asset name
/// </summary>
public string Asset { get; set; } = string.Empty;
/// <summary>
/// Quantity available
/// </summary>
public decimal? Available { get; set; }
/// <summary>
/// Total quantity
/// </summary>
public decimal? Total { get; set; }
}
}

View File

@ -1,13 +0,0 @@
namespace CryptoExchange.Net.CommonObjects
{
/// <summary>
/// Base class for common objects
/// </summary>
public class BaseCommonObject
{
/// <summary>
/// The source object the data is derived from
/// </summary>
public object SourceObject { get; set; } = null!;
}
}

View File

@ -1,73 +0,0 @@
namespace CryptoExchange.Net.CommonObjects
{
/// <summary>
/// Order type
/// </summary>
public enum CommonOrderType
{
/// <summary>
/// Limit type
/// </summary>
Limit,
/// <summary>
/// Market type
/// </summary>
Market,
/// <summary>
/// Other order type
/// </summary>
Other
}
/// <summary>
/// Order side
/// </summary>
public enum CommonOrderSide
{
/// <summary>
/// Buy order
/// </summary>
Buy,
/// <summary>
/// Sell order
/// </summary>
Sell
}
/// <summary>
/// Order status
/// </summary>
public enum CommonOrderStatus
{
/// <summary>
/// placed and not fully filled order
/// </summary>
Active,
/// <summary>
/// canceled order
/// </summary>
Canceled,
/// <summary>
/// filled order
/// </summary>
Filled
}
/// <summary>
/// Position side
/// </summary>
public enum CommonPositionSide
{
/// <summary>
/// Long position
/// </summary>
Long,
/// <summary>
/// Short position
/// </summary>
Short,
/// <summary>
/// Both
/// </summary>
Both
}
}

View File

@ -1,35 +0,0 @@
using System;
namespace CryptoExchange.Net.CommonObjects
{
/// <summary>
/// Kline data
/// </summary>
public class Kline: BaseCommonObject
{
/// <summary>
/// Opening time of the kline
/// </summary>
public DateTime OpenTime { get; set; }
/// <summary>
/// Price at the open time
/// </summary>
public decimal? OpenPrice { get; set; }
/// <summary>
/// Highest price of the kline
/// </summary>
public decimal? HighPrice { get; set; }
/// <summary>
/// Lowest price of the kline
/// </summary>
public decimal? LowPrice { get; set; }
/// <summary>
/// Close price of the kline
/// </summary>
public decimal? ClosePrice { get; set; }
/// <summary>
/// Volume of the kline
/// </summary>
public decimal? Volume { get; set; }
}
}

View File

@ -1,47 +0,0 @@
using System;
namespace CryptoExchange.Net.CommonObjects
{
/// <summary>
/// Order data
/// </summary>
public class Order: BaseCommonObject
{
/// <summary>
/// Id of the order
/// </summary>
public string Id { get; set; } = string.Empty;
/// <summary>
/// Symbol of the order
/// </summary>
public string Symbol { get; set; } = string.Empty;
/// <summary>
/// Price of the order
/// </summary>
public decimal? Price { get; set; }
/// <summary>
/// Quantity of the order
/// </summary>
public decimal? Quantity { get; set; }
/// <summary>
/// The quantity of the order which has been filled
/// </summary>
public decimal? QuantityFilled { get; set; }
/// <summary>
/// Status of the order
/// </summary>
public CommonOrderStatus Status { get; set; }
/// <summary>
/// Side of the order
/// </summary>
public CommonOrderSide Side { get; set; }
/// <summary>
/// Type of the order
/// </summary>
public CommonOrderType Type { get; set; }
/// <summary>
/// Order time
/// </summary>
public DateTime Timestamp { get; set; }
}
}

View File

@ -1,20 +0,0 @@
using System;
using System.Collections.Generic;
namespace CryptoExchange.Net.CommonObjects
{
/// <summary>
/// Order book data
/// </summary>
public class OrderBook: BaseCommonObject
{
/// <summary>
/// List of bids
/// </summary>
public IEnumerable<OrderBookEntry> Bids { get; set; } = Array.Empty<OrderBookEntry>();
/// <summary>
/// List of asks
/// </summary>
public IEnumerable<OrderBookEntry> Asks { get; set; } = Array.Empty<OrderBookEntry>();
}
}

View File

@ -1,17 +0,0 @@
namespace CryptoExchange.Net.CommonObjects
{
/// <summary>
/// Order book entry
/// </summary>
public class OrderBookEntry
{
/// <summary>
/// Quantity of the entry
/// </summary>
public decimal Quantity { get; set; }
/// <summary>
/// Price of the entry
/// </summary>
public decimal Price { get; set; }
}
}

View File

@ -1,13 +0,0 @@
namespace CryptoExchange.Net.CommonObjects
{
/// <summary>
/// Id of an order
/// </summary>
public class OrderId: BaseCommonObject
{
/// <summary>
/// Id of an order
/// </summary>
public string Id { get; set; } = string.Empty;
}
}

View File

@ -1,65 +0,0 @@
namespace CryptoExchange.Net.CommonObjects
{
/// <summary>
/// Position data
/// </summary>
public class Position: BaseCommonObject
{
/// <summary>
/// Id of the position
/// </summary>
public string? Id { get; set; }
/// <summary>
/// Symbol of the position
/// </summary>
public string Symbol { get; set; } = string.Empty;
/// <summary>
/// Leverage
/// </summary>
public decimal Leverage { get; set; }
/// <summary>
/// Position quantity
/// </summary>
public decimal Quantity { get; set; }
/// <summary>
/// Entry price
/// </summary>
public decimal? EntryPrice { get; set; }
/// <summary>
/// Liquidation price
/// </summary>
public decimal? LiquidationPrice { get; set; }
/// <summary>
/// Unrealized profit and loss
/// </summary>
public decimal? UnrealizedPnl { get; set; }
/// <summary>
/// Realized profit and loss
/// </summary>
public decimal? RealizedPnl { get; set; }
/// <summary>
/// Mark price
/// </summary>
public decimal? MarkPrice { get; set; }
/// <summary>
/// Auto adding margin
/// </summary>
public bool? AutoMargin { get; set; }
/// <summary>
/// Position margin
/// </summary>
public decimal? PositionMargin { get; set; }
/// <summary>
/// Position side
/// </summary>
public CommonPositionSide? Side { get; set; }
/// <summary>
/// Is isolated
/// </summary>
public bool? Isolated { get; set; }
/// <summary>
/// Maintenance margin
/// </summary>
public decimal? MaintananceMargin { get; set; }
}
}

View File

@ -1,33 +0,0 @@
namespace CryptoExchange.Net.CommonObjects
{
/// <summary>
/// Symbol data
/// </summary>
public class Symbol: BaseCommonObject
{
/// <summary>
/// Name of the symbol
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// Minimal quantity of an order
/// </summary>
public decimal? MinTradeQuantity { get; set; }
/// <summary>
/// Step with which the quantity should increase
/// </summary>
public decimal? QuantityStep { get; set; }
/// <summary>
/// step with which the price should increase
/// </summary>
public decimal? PriceStep { get; set; }
/// <summary>
/// The max amount of decimals for quantity
/// </summary>
public int? QuantityDecimals { get; set; }
/// <summary>
/// The max amount of decimal for price
/// </summary>
public int? PriceDecimals { get; set; }
}
}

View File

@ -1,33 +0,0 @@
namespace CryptoExchange.Net.CommonObjects
{
/// <summary>
/// Ticker data
/// </summary>
public class Ticker: BaseCommonObject
{
/// <summary>
/// Symbol
/// </summary>
public string Symbol { get; set; } = string.Empty;
/// <summary>
/// Price 24 hours ago
/// </summary>
public decimal? Price24H { get; set; }
/// <summary>
/// Last trade price
/// </summary>
public decimal? LastPrice { get; set; }
/// <summary>
/// 24 hour low price
/// </summary>
public decimal? LowPrice { get; set; }
/// <summary>
/// 24 hour high price
/// </summary>
public decimal? HighPrice { get; set; }
/// <summary>
/// 24 hour volume
/// </summary>
public decimal? Volume { get; set; }
}
}

View File

@ -1,50 +0,0 @@
using System;
namespace CryptoExchange.Net.CommonObjects
{
/// <summary>
/// Trade data
/// </summary>
public class Trade: BaseCommonObject
{
/// <summary>
/// Symbol of the trade
/// </summary>
public string Symbol { get; set; } = string.Empty;
/// <summary>
/// Price of the trade
/// </summary>
public decimal Price { get; set; }
/// <summary>
/// Quantity of the trade
/// </summary>
public decimal Quantity { get; set; }
/// <summary>
/// Timestamp of the trade
/// </summary>
public DateTime Timestamp { get; set; }
}
/// <summary>
/// User trade info
/// </summary>
public class UserTrade: Trade
{
/// <summary>
/// Id of the trade
/// </summary>
public string Id { get; set; } = string.Empty;
/// <summary>
/// Order id of the trade
/// </summary>
public string? OrderId { get; set; }
/// <summary>
/// Fee of the trade
/// </summary>
public decimal? Fee { get; set; }
/// <summary>
/// The asset the fee is paid in
/// </summary>
public string? FeeAsset { get; set; }
}
}

View File

@ -1,195 +0,0 @@
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Globalization;
using System.Linq;
using System.Reflection;
using CryptoExchange.Net.Attributes;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace CryptoExchange.Net.Converters.JsonNet
{
/// <summary>
/// Converter for arrays to objects. Can deserialize data like [0.1, 0.2, "test"] to an object. Mapping is done by marking the class with [JsonConverter(typeof(ArrayConverter))] and the properties
/// with [ArrayProperty(x)] where x is the index of the property in the array
/// </summary>
public class ArrayConverter : JsonConverter
{
private static readonly ConcurrentDictionary<(MemberInfo, Type), Attribute> _attributeByMemberInfoAndTypeCache = new ConcurrentDictionary<(MemberInfo, Type), Attribute>();
private static readonly ConcurrentDictionary<(Type, Type), Attribute> _attributeByTypeAndTypeCache = new ConcurrentDictionary<(Type, Type), Attribute>();
/// <inheritdoc />
public override bool CanConvert(Type objectType)
{
return true;
}
/// <inheritdoc />
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
return null;
if (objectType == typeof(JToken))
return JToken.Load(reader);
var result = Activator.CreateInstance(objectType);
var arr = JArray.Load(reader);
return ParseObject(arr, result, objectType);
}
private static object ParseObject(JArray arr, object result, Type objectType)
{
foreach (var property in objectType.GetProperties())
{
var attribute = GetCustomAttribute<ArrayPropertyAttribute>(property);
if (attribute == null)
continue;
if (attribute.Index >= arr.Count)
continue;
if (property.PropertyType.BaseType == typeof(Array))
{
var objType = property.PropertyType.GetElementType();
var innerArray = (JArray)arr[attribute.Index];
var count = 0;
if (innerArray.Count == 0)
{
var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { 0 });
property.SetValue(result, arrayResult);
}
else if (innerArray[0].Type == JTokenType.Array)
{
var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { innerArray.Count });
foreach (var obj in innerArray)
{
var innerObj = Activator.CreateInstance(objType!);
arrayResult[count] = ParseObject((JArray)obj, innerObj, objType!);
count++;
}
property.SetValue(result, arrayResult);
}
else
{
var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { 1 });
var innerObj = Activator.CreateInstance(objType!);
arrayResult[0] = ParseObject(innerArray, innerObj, objType!);
property.SetValue(result, arrayResult);
}
continue;
}
var converterAttribute = GetCustomAttribute<JsonConverterAttribute>(property) ?? GetCustomAttribute<JsonConverterAttribute>(property.PropertyType);
var conversionAttribute = GetCustomAttribute<JsonConversionAttribute>(property) ?? GetCustomAttribute<JsonConversionAttribute>(property.PropertyType);
object? value;
if (converterAttribute != null)
{
value = arr[attribute.Index].ToObject(property.PropertyType, new JsonSerializer {Converters = {(JsonConverter) Activator.CreateInstance(converterAttribute.ConverterType)}});
}
else if (conversionAttribute != null)
{
value = arr[attribute.Index].ToObject(property.PropertyType);
}
else
{
value = arr[attribute.Index];
}
if (value != null && property.PropertyType.IsInstanceOfType(value))
{
property.SetValue(result, value);
}
else
{
if (value is JToken token)
{
if (token.Type == JTokenType.Null)
value = null;
if (token.Type == JTokenType.Float)
value = token.Value<decimal>();
}
if (value is decimal)
{
property.SetValue(result, value);
}
else if ((property.PropertyType == typeof(decimal)
|| property.PropertyType == typeof(decimal?))
&& (value != null && value.ToString().IndexOf("e", StringComparison.OrdinalIgnoreCase) >= 0))
{
var v = value.ToString();
if (decimal.TryParse(v, NumberStyles.Float, CultureInfo.InvariantCulture, out var dec))
property.SetValue(result, dec);
}
else
{
property.SetValue(result, value == null ? null : Convert.ChangeType(value, property.PropertyType));
}
}
}
return result;
}
/// <inheritdoc />
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{
if (value == null)
return;
writer.WriteStartArray();
var props = value.GetType().GetProperties();
var ordered = props.OrderBy(p => GetCustomAttribute<ArrayPropertyAttribute>(p)?.Index);
var last = -1;
foreach (var prop in ordered)
{
var arrayProp = GetCustomAttribute<ArrayPropertyAttribute>(prop);
if (arrayProp == null)
continue;
if (arrayProp.Index == last)
continue;
while (arrayProp.Index != last + 1)
{
writer.WriteValue((string?)null);
last += 1;
}
last = arrayProp.Index;
var converterAttribute = GetCustomAttribute<JsonConverterAttribute>(prop);
if (converterAttribute != null)
writer.WriteRawValue(JsonConvert.SerializeObject(prop.GetValue(value), (JsonConverter)Activator.CreateInstance(converterAttribute.ConverterType)));
else if (!IsSimple(prop.PropertyType))
serializer.Serialize(writer, prop.GetValue(value));
else
writer.WriteValue(prop.GetValue(value));
}
writer.WriteEndArray();
}
private static bool IsSimple(Type type)
{
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
{
// nullable type, check if the nested type is simple.
return IsSimple(type.GetGenericArguments()[0]);
}
return type.IsPrimitive
|| type.IsEnum
|| type == typeof(string)
|| type == typeof(decimal);
}
private static T? GetCustomAttribute<T>(MemberInfo memberInfo) where T : Attribute =>
(T?)_attributeByMemberInfoAndTypeCache.GetOrAdd((memberInfo, typeof(T)), tuple => memberInfo.GetCustomAttribute(typeof(T)));
private static T? GetCustomAttribute<T>(Type type) where T : Attribute =>
(T?)_attributeByTypeAndTypeCache.GetOrAdd((type, typeof(T)), tuple => type.GetCustomAttribute(typeof(T)));
}
}

View File

@ -1,98 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Newtonsoft.Json;
namespace CryptoExchange.Net.Converters.JsonNet
{
/// <summary>
/// Base class for enum converters
/// </summary>
/// <typeparam name="T">Type of enum to convert</typeparam>
public abstract class BaseConverter<T>: JsonConverter where T: struct
{
/// <summary>
/// The enum->string mapping
/// </summary>
protected abstract List<KeyValuePair<T, string>> Mapping { get; }
private readonly bool _quotes;
/// <summary>
/// ctor
/// </summary>
/// <param name="useQuotes"></param>
protected BaseConverter(bool useQuotes)
{
_quotes = useQuotes;
}
/// <inheritdoc />
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{
var stringValue = value == null? null: GetValue((T) value);
if (_quotes)
writer.WriteValue(stringValue);
else
writer.WriteRawValue(stringValue);
}
/// <inheritdoc />
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
if (reader.Value == null)
return null;
var stringValue = reader.Value.ToString();
if (string.IsNullOrWhiteSpace(stringValue))
return null;
if (!GetValue(stringValue, out var result))
{
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Cannot map enum value. EnumType: {typeof(T)}, Value: {reader.Value}, Known values: {string.Join(", ", Mapping.Select(m => m.Value))}. If you think {reader.Value} should added please open an issue on the Github repo");
return null;
}
return result;
}
/// <summary>
/// Convert a string value
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
public T ReadString(string data)
{
return Mapping.FirstOrDefault(v => v.Value == data).Key;
}
/// <inheritdoc />
public override bool CanConvert(Type objectType)
{
// Check if it is type, or nullable of type
return objectType == typeof(T) || Nullable.GetUnderlyingType(objectType) == typeof(T);
}
private bool GetValue(string value, out T result)
{
// 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>)))
{
result = mapping.Key;
return true;
}
result = default;
return false;
}
private string GetValue(T value)
{
return Mapping.FirstOrDefault(v => v.Key.Equals(value)).Value;
}
}
}

View File

@ -1,62 +0,0 @@
using System;
using System.Globalization;
using Newtonsoft.Json;
namespace CryptoExchange.Net.Converters.JsonNet
{
/// <summary>
/// Decimal converter that handles overflowing decimal values (by setting it to decimal.MaxValue)
/// </summary>
public class BigDecimalConverter : JsonConverter
{
/// <inheritdoc />
public override bool CanConvert(Type objectType)
{
if (Nullable.GetUnderlyingType(objectType) != null)
return Nullable.GetUnderlyingType(objectType) == typeof(decimal);
return objectType == typeof(decimal);
}
/// <inheritdoc />
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
return null;
if (reader.TokenType == JsonToken.Float || reader.TokenType == JsonToken.Integer)
{
try
{
return decimal.Parse(reader.Value!.ToString(), NumberStyles.Float, CultureInfo.InvariantCulture);
}
catch (OverflowException)
{
// Value doesn't fit decimal; set it to max value
return decimal.MaxValue;
}
}
if (reader.TokenType == JsonToken.String)
{
try
{
var value = reader.Value!.ToString();
return decimal.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture);
}
catch (OverflowException)
{
// Value doesn't fit decimal; set it to max value
return decimal.MaxValue;
}
}
return null;
}
/// <inheritdoc />
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{
writer.WriteValue(value);
}
}
}

View File

@ -1,80 +0,0 @@
using System;
using Newtonsoft.Json;
namespace CryptoExchange.Net.Converters.JsonNet
{
/// <summary>
/// Boolean converter with support for "0"/"1" (strings)
/// </summary>
public class BoolConverter : JsonConverter
{
/// <summary>
/// Determines whether this instance can convert the specified object type.
/// </summary>
/// <param name="objectType">Type of the object.</param>
/// <returns>
/// <c>true</c> if this instance can convert the specified object type; otherwise, <c>false</c>.
/// </returns>
public override bool CanConvert(Type objectType)
{
if (Nullable.GetUnderlyingType(objectType) != null)
return Nullable.GetUnderlyingType(objectType) == typeof(bool);
return objectType == typeof(bool);
}
/// <summary>
/// Reads the JSON representation of the object.
/// </summary>
/// <param name="reader">The <see cref="T:Newtonsoft.Json.JsonReader"/> to read from.</param>
/// <param name="objectType">Type of the object.</param>
/// <param name="existingValue">The existing value of object being read.</param>
/// <param name="serializer">The calling serializer.</param>
/// <returns>
/// The object value.
/// </returns>
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
var value = reader.Value?.ToString().ToLower().Trim();
if (value == null || value == "")
{
if (Nullable.GetUnderlyingType(objectType) != null)
return null;
return false;
}
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;
}
// If we reach here, we're pretty much going to throw an error so let's let Json.NET throw it's pretty-fied error message.
return new JsonSerializer().Deserialize(reader, objectType);
}
/// <summary>
/// Specifies that this converter will not participate in writing results.
/// </summary>
public override bool CanWrite { get { return false; } }
/// <summary>
/// Writes the JSON representation of the object.
/// </summary>
/// <param name="writer">The <see cref="T:Newtonsoft.Json.JsonWriter"/> to write to.</param><param name="value">The value.</param><param name="serializer">The calling serializer.</param>
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{
}
}
}

View File

@ -1,241 +0,0 @@
using Microsoft.Extensions.Primitives;
using Newtonsoft.Json;
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
namespace CryptoExchange.Net.Converters.JsonNet
{
/// <summary>
/// Datetime converter. Supports converting from string/long/double to DateTime and back. Numbers are assumed to be the time since 1970-01-01.
/// </summary>
public class DateTimeConverter: JsonConverter
{
private static readonly DateTime _epoch = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
private const long _ticksPerSecond = TimeSpan.TicksPerMillisecond * 1000;
private const decimal _ticksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000;
private const decimal _ticksPerNanosecond = TimeSpan.TicksPerMillisecond / 1000m / 1000;
/// <inheritdoc />
public override bool CanConvert(Type objectType)
{
return objectType == typeof(DateTime) || objectType == typeof(DateTime?);
}
/// <inheritdoc />
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
if (reader.Value == null)
{
if (objectType == typeof(DateTime))
return default(DateTime);
return null;
}
if(reader.TokenType is JsonToken.Integer)
{
var longValue = (long)reader.Value;
if (longValue == 0 || longValue == -1)
return objectType == typeof(DateTime) ? default(DateTime): null;
return ParseFromLong(longValue);
}
else if (reader.TokenType is JsonToken.Float)
{
var doubleValue = (double)reader.Value;
if (doubleValue == 0 || doubleValue == -1)
return objectType == typeof(DateTime) ? default(DateTime) : null;
if (doubleValue < 19999999999)
return ConvertFromSeconds(doubleValue);
return ConvertFromMilliseconds(doubleValue);
}
else if(reader.TokenType is JsonToken.String)
{
var stringValue = (string)reader.Value;
if (string.IsNullOrWhiteSpace(stringValue)
|| stringValue == "-1"
|| (double.TryParse(stringValue, out var doubleVal) && doubleVal == 0))
{
return objectType == typeof(DateTime) ? default(DateTime) : null;
}
return ParseFromString(stringValue);
}
else if(reader.TokenType == JsonToken.Date)
{
return (DateTime)reader.Value;
}
else
{
Trace.WriteLine("{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + reader.Value);
return default;
}
}
/// <summary>
/// Parse a long value to datetime
/// </summary>
/// <param name="longValue"></param>
/// <returns></returns>
public static DateTime ParseFromLong(long longValue)
{
if (longValue < 19999999999)
return ConvertFromSeconds(longValue);
if (longValue < 19999999999999)
return ConvertFromMilliseconds(longValue);
if (longValue < 19999999999999999)
return ConvertFromMicroseconds(longValue);
return ConvertFromNanoseconds(longValue);
}
/// <summary>
/// Parse a string value to datetime
/// </summary>
/// <param name="stringValue"></param>
/// <returns></returns>
public static DateTime ParseFromString(string stringValue)
{
if (stringValue.Length == 12 && stringValue.StartsWith("202"))
{
// Parse 202303261200 format
if (!int.TryParse(stringValue.Substring(0, 4), out var year)
|| !int.TryParse(stringValue.Substring(4, 2), out var month)
|| !int.TryParse(stringValue.Substring(6, 2), out var day)
|| !int.TryParse(stringValue.Substring(8, 2), out var hour)
|| !int.TryParse(stringValue.Substring(10, 2), out var minute))
{
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
return default;
}
return new DateTime(year, month, day, hour, minute, 0, DateTimeKind.Utc);
}
if (stringValue.Length == 8)
{
// Parse 20211103 format
if (!int.TryParse(stringValue.Substring(0, 4), out var year)
|| !int.TryParse(stringValue.Substring(4, 2), out var month)
|| !int.TryParse(stringValue.Substring(6, 2), out var day))
{
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
return default;
}
return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc);
}
if (stringValue.Length == 6)
{
// Parse 211103 format
if (!int.TryParse(stringValue.Substring(0, 2), out var year)
|| !int.TryParse(stringValue.Substring(2, 2), out var month)
|| !int.TryParse(stringValue.Substring(4, 2), out var day))
{
Trace.WriteLine("{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
return default;
}
return new DateTime(year + 2000, month, day, 0, 0, 0, DateTimeKind.Utc);
}
if (double.TryParse(stringValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var doubleValue))
{
// Parse 1637745563.000 format
if (doubleValue < 19999999999)
return ConvertFromSeconds(doubleValue);
if (doubleValue < 19999999999999)
return ConvertFromMilliseconds((long)doubleValue);
if (doubleValue < 19999999999999999)
return ConvertFromMicroseconds((long)doubleValue);
return ConvertFromNanoseconds((long)doubleValue);
}
if (stringValue.Length == 10)
{
// Parse 2021-11-03 format
var values = stringValue.Split('-');
if (!int.TryParse(values[0], out var year)
|| !int.TryParse(values[1], out var month)
|| !int.TryParse(values[2], out var day))
{
Trace.WriteLine("{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
return default;
}
return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc);
}
return DateTime.Parse(stringValue, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal);
}
/// <summary>
/// Convert a seconds since epoch (01-01-1970) value to DateTime
/// </summary>
/// <param name="seconds"></param>
/// <returns></returns>
public static DateTime ConvertFromSeconds(double seconds) => _epoch.AddTicks((long)Math.Round(seconds * _ticksPerSecond));
/// <summary>
/// Convert a milliseconds since epoch (01-01-1970) value to DateTime
/// </summary>
/// <param name="milliseconds"></param>
/// <returns></returns>
public static DateTime ConvertFromMilliseconds(double milliseconds) => _epoch.AddTicks((long)Math.Round(milliseconds * TimeSpan.TicksPerMillisecond));
/// <summary>
/// Convert a microseconds since epoch (01-01-1970) value to DateTime
/// </summary>
/// <param name="microseconds"></param>
/// <returns></returns>
public static DateTime ConvertFromMicroseconds(long microseconds) => _epoch.AddTicks((long)Math.Round(microseconds * _ticksPerMicrosecond));
/// <summary>
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
/// </summary>
/// <param name="nanoseconds"></param>
/// <returns></returns>
public static DateTime ConvertFromNanoseconds(long nanoseconds) => _epoch.AddTicks((long)Math.Round(nanoseconds * _ticksPerNanosecond));
/// <summary>
/// Convert a DateTime value to seconds since epoch (01-01-1970) value
/// </summary>
/// <param name="time"></param>
/// <returns></returns>
[return: NotNullIfNotNull("time")]
public static long? ConvertToSeconds(DateTime? time) => time == null ? null: (long)Math.Round((time.Value - _epoch).TotalSeconds);
/// <summary>
/// Convert a DateTime value to milliseconds since epoch (01-01-1970) value
/// </summary>
/// <param name="time"></param>
/// <returns></returns>
[return: NotNullIfNotNull("time")]
public static long? ConvertToMilliseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).TotalMilliseconds);
/// <summary>
/// Convert a DateTime value to microseconds since epoch (01-01-1970) value
/// </summary>
/// <param name="time"></param>
/// <returns></returns>
[return: NotNullIfNotNull("time")]
public static long? ConvertToMicroseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / _ticksPerMicrosecond);
/// <summary>
/// Convert a DateTime value to nanoseconds since epoch (01-01-1970) value
/// </summary>
/// <param name="time"></param>
/// <returns></returns>
[return: NotNullIfNotNull("time")]
public static long? ConvertToNanoseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / _ticksPerNanosecond);
/// <inheritdoc />
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{
var datetimeValue = (DateTime?)value;
if (datetimeValue == null)
writer.WriteValue((DateTime?)null);
if(datetimeValue == default(DateTime))
writer.WriteValue((DateTime?)null);
else
writer.WriteValue((long)Math.Round(((DateTime)value! - new DateTime(1970, 1, 1)).TotalMilliseconds));
}
}
}

View File

@ -1,27 +0,0 @@
using Newtonsoft.Json;
using System;
using System.Globalization;
namespace CryptoExchange.Net.Converters.JsonNet
{
/// <summary>
/// Converter for serializing decimal values as string
/// </summary>
public class DecimalStringWriterConverter : JsonConverter
{
/// <inheritdoc />
public override bool CanRead => false;
/// <inheritdoc />
public override bool CanConvert(Type objectType) => objectType == typeof(decimal) || objectType == typeof(decimal?);
/// <inheritdoc />
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
throw new NotImplementedException();
}
/// <inheritdoc />
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) => writer.WriteValue(((decimal?)value)?.ToString(CultureInfo.InvariantCulture) ?? null);
}
}

View File

@ -1,176 +0,0 @@
using CryptoExchange.Net.Attributes;
using Newtonsoft.Json;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
namespace CryptoExchange.Net.Converters.JsonNet
{
/// <summary>
/// Converter for enum values. Enums entries should be noted with a MapAttribute to map the enum value to a string value
/// </summary>
public class EnumConverter : JsonConverter
{
private bool _warnOnMissingEntry = true;
private bool _writeAsInt;
/// <summary>
/// </summary>
public EnumConverter() { }
/// <summary>
/// </summary>
/// <param name="writeAsInt"></param>
/// <param name="warnOnMissingEntry"></param>
public EnumConverter(bool writeAsInt, bool warnOnMissingEntry)
{
_writeAsInt = writeAsInt;
_warnOnMissingEntry = warnOnMissingEntry;
}
private static readonly ConcurrentDictionary<Type, List<KeyValuePair<object, string>>> _mapping = new();
/// <inheritdoc />
public override bool CanConvert(Type objectType)
{
return objectType.IsEnum || Nullable.GetUnderlyingType(objectType)?.IsEnum == true;
}
/// <inheritdoc />
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
var enumType = Nullable.GetUnderlyingType(objectType) ?? objectType;
if (!_mapping.TryGetValue(enumType, out var mapping))
mapping = AddMapping(enumType);
var stringValue = reader.Value?.ToString();
if (stringValue == null || stringValue == "")
{
// Received null value
var emptyResult = GetDefaultValue(objectType, enumType);
if(emptyResult != null)
// If the property we're parsing to isn't nullable there isn't a correct way to return this as null will either throw an exception (.net framework) or the default enum value (dotnet core).
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Received null enum value, but property type is not a nullable enum. EnumType: {enumType.Name}. If you think {enumType.Name} should be nullable please open an issue on the Github repo");
return emptyResult;
}
if (!GetValue(enumType, mapping, stringValue!, out var result))
{
var defaultValue = GetDefaultValue(objectType, enumType);
if (string.IsNullOrWhiteSpace(stringValue))
{
if (defaultValue != null)
// We received an empty string and have no mapping for it, and the property isn't nullable
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Received empty string as enum value, but property type is not a nullable enum. EnumType: {enumType.Name}. If you think {enumType.Name} should be nullable please open an issue on the Github repo");
}
else
{
// We received an enum value but weren't able to parse it.
if (_warnOnMissingEntry)
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Cannot map enum value. EnumType: {enumType.Name}, Value: {reader.Value}, Known values: {string.Join(", ", mapping.Select(m => m.Value))}. If you think {reader.Value} should added please open an issue on the Github repo");
}
return defaultValue;
}
return result;
}
private static object? GetDefaultValue(Type objectType, Type enumType)
{
if (Nullable.GetUnderlyingType(objectType) != null)
return null;
return Activator.CreateInstance(enumType); // return default value
}
private static List<KeyValuePair<object, string>> AddMapping(Type objectType)
{
var mapping = new List<KeyValuePair<object, string>>();
var enumMembers = objectType.GetMembers();
foreach (var member in enumMembers)
{
var maps = member.GetCustomAttributes(typeof(MapAttribute), false);
foreach (MapAttribute attribute in maps)
{
foreach (var value in attribute.Values)
mapping.Add(new KeyValuePair<object, string>(Enum.Parse(objectType, member.Name), value));
}
}
_mapping.TryAdd(objectType, mapping);
return mapping;
}
private static bool GetValue(Type objectType, List<KeyValuePair<object, string>> enumMapping, string value, out object? result)
{
// Check for exact match first, then if not found fallback to a case insensitive match
var mapping = enumMapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCulture));
if (mapping.Equals(default(KeyValuePair<object, string>)))
mapping = enumMapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase));
if (!mapping.Equals(default(KeyValuePair<object, string>)))
{
result = mapping.Key;
return true;
}
try
{
// If no explicit mapping is found try to parse string
result = Enum.Parse(objectType, value, true);
return true;
}
catch (Exception)
{
result = default;
return false;
}
}
/// <summary>
/// Get the string value for an enum value using the MapAttribute mapping. When multiple values are mapped for a enum entry the first value will be returned
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="enumValue"></param>
/// <returns></returns>
[return: NotNullIfNotNull("enumValue")]
public static string? GetString<T>(T enumValue) => GetString(typeof(T), enumValue);
[return: NotNullIfNotNull("enumValue")]
private static string? GetString(Type objectType, object? enumValue)
{
objectType = Nullable.GetUnderlyingType(objectType) ?? objectType;
if (!_mapping.TryGetValue(objectType, out var mapping))
mapping = AddMapping(objectType);
return enumValue == null ? null : (mapping.FirstOrDefault(v => v.Key.Equals(enumValue)).Value ?? enumValue.ToString());
}
/// <inheritdoc />
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{
if (value == null)
{
writer.WriteNull();
}
else
{
if (!_writeAsInt)
{
var stringValue = GetString(value.GetType(), value);
writer.WriteValue(stringValue);
}
else
{
writer.WriteValue((int)value);
}
}
}
}
}

View File

@ -1,338 +0,0 @@
using CryptoExchange.Net.Converters.MessageParsing;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Converters.JsonNet
{
/// <summary>
/// Json.Net message accessor
/// </summary>
public abstract class JsonNetMessageAccessor : IMessageAccessor
{
/// <summary>
/// The json token loaded
/// </summary>
protected JToken? _token;
private static readonly JsonSerializer _serializer = JsonSerializer.Create(SerializerOptions.WithConverters);
/// <inheritdoc />
public bool IsJson { get; protected set; }
/// <inheritdoc />
public abstract bool OriginalDataAvailable { get; }
/// <inheritdoc />
public object? Underlying => _token;
/// <inheritdoc />
public CallResult<object> Deserialize(Type type, MessagePath? path = null)
{
if (!IsJson)
return new CallResult<object>(GetOriginalString());
var source = _token;
if (path != null)
source = GetPathNode(path.Value);
try
{
var result = source!.ToObject(type, _serializer)!;
return new CallResult<object>(result);
}
catch (JsonReaderException jre)
{
var info = $"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}";
return new CallResult<object>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
}
catch (JsonSerializationException jse)
{
var info = $"Deserialize JsonSerializationException: {jse.Message}";
return new CallResult<object>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
}
catch (Exception ex)
{
var exceptionInfo = ex.ToLogString();
var info = $"Deserialize Unknown Exception: {exceptionInfo}";
return new CallResult<object>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
}
}
/// <inheritdoc />
public CallResult<T> Deserialize<T>(MessagePath? path = null)
{
var source = _token;
if (path != null)
source = GetPathNode(path.Value);
try
{
var result = source!.ToObject<T>(_serializer)!;
return new CallResult<T>(result);
}
catch (JsonReaderException jre)
{
var info = $"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}";
return new CallResult<T>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
}
catch (JsonSerializationException jse)
{
var info = $"Deserialize JsonSerializationException: {jse.Message}";
return new CallResult<T>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
}
catch (Exception ex)
{
var exceptionInfo = ex.ToLogString();
var info = $"Deserialize Unknown Exception: {exceptionInfo}";
return new CallResult<T>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
}
}
/// <inheritdoc />
public NodeType? GetNodeType()
{
if (!IsJson)
throw new InvalidOperationException("Can't access json data on non-json message");
if (_token == null)
return null;
if (_token.Type == JTokenType.Object)
return NodeType.Object;
if (_token.Type == JTokenType.Array)
return NodeType.Array;
return NodeType.Value;
}
/// <inheritdoc />
public NodeType? GetNodeType(MessagePath path)
{
if (!IsJson)
throw new InvalidOperationException("Can't access json data on non-json message");
var node = GetPathNode(path);
if (node == null)
return null;
if (node.Type == JTokenType.Object)
return NodeType.Object;
if (node.Type == JTokenType.Array)
return NodeType.Array;
return NodeType.Value;
}
/// <inheritdoc />
public T? GetValue<T>(MessagePath path)
{
if (!IsJson)
throw new InvalidOperationException("Can't access json data on non-json message");
var value = GetPathNode(path);
if (value == null)
return default;
if (value.Type == JTokenType.Object || value.Type == JTokenType.Array)
return default;
return value!.Value<T>();
}
/// <inheritdoc />
public List<T?>? GetValues<T>(MessagePath path)
{
if (!IsJson)
throw new InvalidOperationException("Can't access json data on non-json message");
var value = GetPathNode(path);
if (value == null)
return default;
if (value.Type == JTokenType.Object)
return default;
return value!.Values<T>().ToList();
}
private JToken? GetPathNode(MessagePath path)
{
if (!IsJson)
throw new InvalidOperationException("Can't access json data on non-json message");
var currentToken = _token;
foreach (var node in path)
{
if (node.Type == 0)
{
// Int value
var val = node.Index!.Value;
if (currentToken!.Type != JTokenType.Array || ((JArray)currentToken).Count <= val)
return null;
currentToken = currentToken[val];
}
else if (node.Type == 1)
{
// String value
if (currentToken!.Type != JTokenType.Object)
return null;
currentToken = currentToken[node.Property!];
}
else
{
// Property name
if (currentToken!.Type != JTokenType.Object)
return null;
currentToken = (currentToken.First as JProperty)?.Name;
}
if (currentToken == null)
return null;
}
return currentToken;
}
/// <inheritdoc />
public abstract string GetOriginalString();
/// <inheritdoc />
public abstract void Clear();
}
/// <summary>
/// Json.Net stream message accessor
/// </summary>
public class JsonNetStreamMessageAccessor : JsonNetMessageAccessor, IStreamMessageAccessor
{
private Stream? _stream;
/// <inheritdoc />
public override bool OriginalDataAvailable => _stream?.CanSeek == true;
/// <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
}
var readStream = _stream ?? stream;
var length = readStream.CanSeek ? readStream.Length : 4096;
using var reader = new StreamReader(readStream, Encoding.UTF8, false, (int)Math.Max(2, length), true);
using var jsonTextReader = new JsonTextReader(reader);
try
{
_token = await JToken.LoadAsync(jsonTextReader).ConfigureAwait(false);
IsJson = true;
return new CallResult(null);
}
catch (Exception ex)
{
// Not a json message
IsJson = false;
return new CallResult(new ServerError("JsonError: " + ex.Message));
}
}
/// <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;
_token = null;
}
}
/// <summary>
/// Json.Net byte message accessor
/// </summary>
public class JsonNetByteMessageAccessor : JsonNetMessageAccessor, IByteMessageAccessor
{
private ReadOnlyMemory<byte> _bytes;
/// <inheritdoc />
public CallResult Read(ReadOnlyMemory<byte> data)
{
_bytes = data;
// Try getting the underlying byte[] instead of the ToArray to prevent creating a copy
using var stream = MemoryMarshal.TryGetArray(data, out var arraySegment)
? new MemoryStream(arraySegment.Array, arraySegment.Offset, arraySegment.Count)
: new MemoryStream(data.ToArray());
using var reader = new StreamReader(stream, Encoding.UTF8, false, Math.Max(2, data.Length), true);
using var jsonTextReader = new JsonTextReader(reader);
try
{
_token = JToken.Load(jsonTextReader);
IsJson = true;
return new CallResult(null);
}
catch (Exception ex)
{
// Not a json message
IsJson = false;
return new CallResult(new ServerError("JsonError: " + ex.Message));
}
}
/// <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;
_token = null;
}
}
}

View File

@ -1,12 +0,0 @@
using CryptoExchange.Net.Interfaces;
using Newtonsoft.Json;
namespace CryptoExchange.Net.Converters.JsonNet
{
/// <inheritdoc />
public class JsonNetMessageSerializer : IMessageSerializer
{
/// <inheritdoc />
public string Serialize(object message) => JsonConvert.SerializeObject(message, Formatting.None);
}
}

View File

@ -1,35 +0,0 @@
using Newtonsoft.Json;
using System.Globalization;
namespace CryptoExchange.Net.Converters.JsonNet
{
/// <summary>
/// Serializer options
/// </summary>
public static class SerializerOptions
{
/// <summary>
/// Json serializer settings which includes the EnumConverter, DateTimeConverter and BoolConverter
/// </summary>
public static JsonSerializerSettings WithConverters => new JsonSerializerSettings
{
DateTimeZoneHandling = DateTimeZoneHandling.Utc,
Culture = CultureInfo.InvariantCulture,
Converters =
{
new EnumConverter(),
new DateTimeConverter(),
new BoolConverter()
}
};
/// <summary>
/// Default json serializer settings
/// </summary>
public static JsonSerializerSettings Default => new JsonSerializerSettings
{
DateTimeZoneHandling = DateTimeZoneHandling.Utc,
Culture = CultureInfo.InvariantCulture
};
}
}

View File

@ -0,0 +1,31 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Text;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters
{
/// <summary>
/// Caching for JsonSerializerContext instances
/// </summary>
public static class JsonSerializerContextCache
{
private static ConcurrentDictionary<Type, JsonSerializerContext> _cache = new ConcurrentDictionary<Type, JsonSerializerContext>();
/// <summary>
/// Get the instance of the provided type T. It will be created if it doesn't exist yet.
/// </summary>
/// <typeparam name="T">Implementation type of the JsonSerializerContext</typeparam>
public static JsonSerializerContext GetOrCreate<T>() where T: JsonSerializerContext, new()
{
var contextType = typeof(T);
if (_cache.TryGetValue(contextType, out var context))
return context;
var instance = new T();
_cache[contextType] = instance;
return instance;
}
}
}

View File

@ -3,7 +3,7 @@
/// <summary>
/// Node accessor
/// </summary>
public struct NodeAccessor
public readonly struct NodeAccessor
{
/// <summary>
/// Index

View File

@ -6,9 +6,9 @@ namespace CryptoExchange.Net.Converters.MessageParsing
/// <summary>
/// Message access definition
/// </summary>
public struct MessagePath : IEnumerable<NodeAccessor>
public readonly struct MessagePath : IEnumerable<NodeAccessor>
{
private List<NodeAccessor> _path;
private readonly List<NodeAccessor> _path;
internal void Add(NodeAccessor node)
{

View File

@ -7,6 +7,9 @@ using System.Text.Json.Serialization;
using System.Text.Json;
using CryptoExchange.Net.Attributes;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Diagnostics;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
@ -14,106 +17,142 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
/// Converter for arrays to objects. Can deserialize data like [0.1, 0.2, "test"] to an object. Mapping is done by marking the class with [JsonConverter(typeof(ArrayConverter))] and the properties
/// with [ArrayProperty(x)] where x is the index of the property in the array
/// </summary>
public class ArrayConverter : JsonConverterFactory
#if NET5_0_OR_GREATER
public class ArrayConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T> : JsonConverter<T> where T : new()
#else
public class ArrayConverter<T> : JsonConverter<T> where T : new()
#endif
{
/// <inheritdoc />
public override bool CanConvert(Type typeToConvert) => true;
private static readonly Lazy<List<ArrayPropertyInfo>> _typePropertyInfo = new Lazy<List<ArrayPropertyInfo>>(CacheTypeAttributes, LazyThreadSafetyMode.PublicationOnly);
private static readonly ConcurrentDictionary<JsonConverter, JsonSerializerOptions> _converterOptionsCache = new ConcurrentDictionary<JsonConverter, JsonSerializerOptions>();
/// <inheritdoc />
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
#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 override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
Type converterType = typeof(ArrayConverterInner<>).MakeGenericType(typeToConvert);
return (JsonConverter)Activator.CreateInstance(converterType);
}
private class ArrayPropertyInfo
{
public PropertyInfo PropertyInfo { get; set; } = null!;
public ArrayPropertyAttribute ArrayProperty { get; set; } = null!;
public Type? JsonConverterType { get; set; }
public bool DefaultDeserialization { get; set; }
public Type TargetType { get; set; } = null!;
}
private class ArrayConverterInner<T> : JsonConverter<T>
{
private static readonly ConcurrentDictionary<Type, List<ArrayPropertyInfo>> _typeAttributesCache = new ConcurrentDictionary<Type, List<ArrayPropertyInfo>>();
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
if (value == null)
{
// TODO
throw new NotImplementedException();
writer.WriteNullValue();
return;
}
/// <inheritdoc />
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null)
return default;
writer.WriteStartArray();
var result = Activator.CreateInstance(typeToConvert);
return (T)ParseObject(ref reader, result, typeToConvert);
}
private static List<ArrayPropertyInfo> CacheTypeAttributes(Type type)
var ordered = _typePropertyInfo.Value.Where(x => x.ArrayProperty != null).OrderBy(p => p.ArrayProperty.Index);
var last = -1;
foreach (var prop in ordered)
{
var attributes = new List<ArrayPropertyInfo>();
var properties = type.GetProperties();
foreach (var property in properties)
if (prop.ArrayProperty.Index == last)
continue;
while (prop.ArrayProperty.Index != last + 1)
{
var att = property.GetCustomAttribute<ArrayPropertyAttribute>();
if (att == null)
continue;
attributes.Add(new ArrayPropertyInfo
{
ArrayProperty = att,
PropertyInfo = property,
DefaultDeserialization = property.GetCustomAttribute<JsonConversionAttribute>() != null,
JsonConverterType = property.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType,
TargetType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType
});
writer.WriteNullValue();
last += 1;
}
_typeAttributesCache.TryAdd(type, attributes);
return attributes;
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
{
JsonSerializer.Serialize(writer, objValue, typeOptions ?? options);
}
}
private static object ParseObject(ref Utf8JsonReader reader, object result, Type objectType)
writer.WriteEndArray();
}
/// <inheritdoc />
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null)
return default;
var result = Activator.CreateInstance(typeof(T))!;
return (T)ParseObject(ref reader, result, typeof(T), options);
}
#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 object ParseObject(ref Utf8JsonReader reader, object result, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type objectType, JsonSerializerOptions options)
#else
private static object ParseObject(ref Utf8JsonReader reader, object result, Type objectType, JsonSerializerOptions options)
#endif
{
if (reader.TokenType != JsonTokenType.StartArray)
throw new Exception("Not an array");
int index = 0;
while (reader.Read())
{
if (reader.TokenType != JsonTokenType.StartArray)
throw new Exception("Not an array");
if (reader.TokenType == JsonTokenType.EndArray)
break;
if (!_typeAttributesCache.TryGetValue(objectType, out var attributes))
attributes = CacheTypeAttributes(objectType);
int index = 0;
while (reader.Read())
var indexAttributes = _typePropertyInfo.Value.Where(a => a.ArrayProperty.Index == index);
if (!indexAttributes.Any())
{
if (reader.TokenType == JsonTokenType.EndArray)
break;
var attribute = attributes.SingleOrDefault(a => a.ArrayProperty.Index == index);
if (attribute == null)
{
index++;
continue;
}
index++;
continue;
}
foreach (var attribute in indexAttributes)
{
var targetType = attribute.TargetType;
object? value = null;
if (attribute.JsonConverterType != null)
if (attribute.JsonConverter != null)
{
// Has JsonConverter attribute
var options = new JsonSerializerOptions();
options.Converters.Add((JsonConverter)Activator.CreateInstance(attribute.JsonConverterType));
value = JsonDocument.ParseValue(ref reader).Deserialize(targetType, options);
if (!_converterOptionsCache.TryGetValue(attribute.JsonConverter, out var newOptions))
{
newOptions = new JsonSerializerOptions
{
NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals,
PropertyNameCaseInsensitive = false,
Converters = { attribute.JsonConverter },
TypeInfoResolver = options.TypeInfoResolver,
};
_converterOptionsCache.TryAdd(attribute.JsonConverter, newOptions);
}
var doc = JsonDocument.ParseValue(ref reader);
value = doc.Deserialize(attribute.PropertyInfo.PropertyType, newOptions);
}
else if (attribute.DefaultDeserialization)
{
// Use default deserialization
value = JsonDocument.ParseValue(ref reader).Deserialize(targetType);
value = JsonDocument.ParseValue(ref reader).Deserialize(options.GetTypeInfo(attribute.PropertyInfo.PropertyType));
}
else
{
@ -124,17 +163,74 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
JsonTokenType.True => true,
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"),
};
}
attribute.PropertyInfo.SetValue(result, value == null ? null : Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture));
index++;
if (targetType.IsAssignableFrom(value?.GetType()))
attribute.PropertyInfo.SetValue(result, value);
else
attribute.PropertyInfo.SetValue(result, value == null ? null : Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture));
}
return result;
index++;
}
return result;
}
private static bool IsSimple(Type type)
{
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
{
// nullable type, check if the nested type is simple.
return IsSimple(type.GetGenericArguments()[0]);
}
return type.IsPrimitive
|| type.IsEnum
|| type == typeof(string)
|| type == typeof(decimal);
}
#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()
#else
private static List<ArrayPropertyInfo> CacheTypeAttributes()
#endif
{
var attributes = new List<ArrayPropertyInfo>();
var properties = typeof(T).GetProperties();
foreach (var property in properties)
{
var att = property.GetCustomAttribute<ArrayPropertyAttribute>();
if (att == null)
continue;
var targetType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType;
var converterType = property.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType ?? targetType.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType;
attributes.Add(new ArrayPropertyInfo
{
ArrayProperty = att,
PropertyInfo = property,
DefaultDeserialization = property.GetCustomAttribute<CryptoExchange.Net.Attributes.JsonConversionAttribute>() != null,
JsonConverter = converterType == null ? null : (JsonConverter)Activator.CreateInstance(converterType)!,
TargetType = targetType
});
}
return attributes;
}
private class ArrayPropertyInfo
{
public PropertyInfo PropertyInfo { get; set; } = null!;
public ArrayPropertyAttribute ArrayProperty { get; set; } = null!;
public JsonConverter? JsonConverter { get; set; }
public bool DefaultDeserialization { get; set; }
public Type TargetType { get; set; } = null!;
}
}
}

View File

@ -17,7 +17,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
{
try
{
return decimal.Parse(reader.GetString(), NumberStyles.Float, CultureInfo.InvariantCulture);
return decimal.Parse(reader.GetString()!, NumberStyles.Float, CultureInfo.InvariantCulture);
}
catch(OverflowException)
{

View File

@ -20,8 +20,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
/// <inheritdoc />
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
Type converterType = typeof(BoolConverterInner<>).MakeGenericType(typeToConvert);
return (JsonConverter)Activator.CreateInstance(converterType);
return typeToConvert == typeof(bool) ? new BoolConverterInner<bool>() : new BoolConverterInner<bool?>();
}
private class BoolConverterInner<T> : JsonConverter<T>

View File

@ -0,0 +1,37 @@
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;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
/// Converter for comma separated enum values
/// </summary>
#if NET5_0_OR_GREATER
public class CommaSplitEnumConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T> : JsonConverter<T[]> where T : struct, Enum
#else
public class CommaSplitEnumConverter<T> : JsonConverter<T[]> where T : struct, Enum
#endif
{
/// <inheritdoc />
public override T[]? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var str = reader.GetString();
if (string.IsNullOrEmpty(str))
return [];
return str!.Split(',').Select(x => (T)EnumConverter.ParseString<T>(x)!).ToArray() ?? [];
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, T[] value, JsonSerializerOptions options)
{
writer.WriteStringValue(string.Join(",", value.Select(x => EnumConverter.GetString(x))));
}
}
}

View File

@ -1,5 +1,4 @@
using Microsoft.Extensions.Primitives;
using System;
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
@ -27,8 +26,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
/// <inheritdoc />
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
Type converterType = typeof(DateTimeConverterInner<>).MakeGenericType(typeToConvert);
return (JsonConverter)Activator.CreateInstance(converterType);
return typeToConvert == typeof(DateTime) ? new DateTimeConverterInner<DateTime>() : new DateTimeConverterInner<DateTime?>();
}
private class DateTimeConverterInner<T> : JsonConverter<T>
@ -48,7 +46,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
if (reader.TokenType is JsonTokenType.Number)
{
var longValue = reader.GetDouble();
if (longValue == 0 || longValue == -1)
if (longValue == 0 || longValue < 0)
return default;
return ParseFromDouble(longValue);
@ -58,6 +56,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
var stringValue = reader.GetString();
if (string.IsNullOrWhiteSpace(stringValue)
|| stringValue == "-1"
|| stringValue == "0001-01-01T00:00:00Z"
|| double.TryParse(stringValue, out var doubleVal) && doubleVal == 0)
{
return default;
@ -74,7 +73,9 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
if (value == null)
{
writer.WriteNullValue();
}
else
{
var dtValue = (DateTime)(object)value;

View File

@ -19,9 +19,13 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
if (reader.TokenType == JsonTokenType.String)
{
var value = reader.GetString();
if (string.IsNullOrEmpty(value) || string.Equals("null", value))
if (string.IsNullOrEmpty(value) || string.Equals("null", value, StringComparison.OrdinalIgnoreCase))
return null;
if (string.Equals("Infinity", value, StringComparison.Ordinal))
// Infinity returned by the server, default to max value
return decimal.MaxValue;
try
{
return decimal.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture);

View File

@ -11,127 +11,78 @@ using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
/// Static EnumConverter methods
/// </summary>
public static class EnumConverter
{
/// <summary>
/// Get the enum value from a string
/// </summary>
/// <param name="value">String value</param>
/// <returns></returns>
#if NET5_0_OR_GREATER
public static T? ParseString<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string value) where T : struct, Enum
#else
public static T? ParseString<T>(string value) where T : struct, Enum
#endif
=> EnumConverter<T>.ParseString(value);
/// <summary>
/// Get the string value for an enum value using the MapAttribute mapping. When multiple values are mapped for a enum entry the first value will be returned
/// </summary>
/// <param name="enumValue"></param>
/// <returns></returns>
#if NET5_0_OR_GREATER
public static string GetString<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(T enumValue) where T : struct, Enum
#else
public static string GetString<T>(T enumValue) where T : struct, Enum
#endif
=> EnumConverter<T>.GetString(enumValue);
/// <summary>
/// Get the string value for an enum value using the MapAttribute mapping. When multiple values are mapped for a enum entry the first value will be returned
/// </summary>
/// <param name="enumValue"></param>
/// <returns></returns>
[return: NotNullIfNotNull("enumValue")]
#if NET5_0_OR_GREATER
public static string? GetString<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(T? enumValue) where T : struct, Enum
#else
public static string? GetString<T>(T? enumValue) where T : struct, Enum
#endif
=> EnumConverter<T>.GetString(enumValue);
}
/// <summary>
/// Converter for enum values. Enums entries should be noted with a MapAttribute to map the enum value to a string value
/// </summary>
public class EnumConverter : JsonConverterFactory
#if NET5_0_OR_GREATER
public class EnumConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>
#else
public class EnumConverter<T>
#endif
: JsonConverter<T>, INullableConverterFactory where T : struct, Enum
{
private bool _warnOnMissingEntry = true;
private bool _writeAsInt;
private static readonly ConcurrentDictionary<Type, List<KeyValuePair<object, string>>> _mapping = new();
private static List<KeyValuePair<T, string>>? _mapping = null;
private NullableEnumConverter? _nullableEnumConverter = null;
/// <summary>
/// </summary>
public EnumConverter() { }
private static ConcurrentBag<string> _unknownValuesWarned = new ConcurrentBag<string>();
/// <summary>
/// </summary>
/// <param name="writeAsInt"></param>
/// <param name="warnOnMissingEntry"></param>
public EnumConverter(bool writeAsInt, bool warnOnMissingEntry)
internal class NullableEnumConverter : JsonConverter<T?>
{
_writeAsInt = writeAsInt;
_warnOnMissingEntry = warnOnMissingEntry;
}
private readonly EnumConverter<T> _enumConverter;
/// <inheritdoc />
public override bool CanConvert(Type typeToConvert)
{
return typeToConvert.IsEnum || Nullable.GetUnderlyingType(typeToConvert)?.IsEnum == true;
}
/// <inheritdoc />
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
JsonConverter converter = (JsonConverter)Activator.CreateInstance(
typeof(EnumConverterInner<>).MakeGenericType(
new Type[] { typeToConvert }),
BindingFlags.Instance | BindingFlags.Public,
binder: null,
args: new object[] { _writeAsInt, _warnOnMissingEntry },
culture: null)!;
return converter;
}
private static List<KeyValuePair<object, string>> AddMapping(Type objectType)
{
var mapping = new List<KeyValuePair<object, string>>();
var enumMembers = objectType.GetMembers();
foreach (var member in enumMembers)
public NullableEnumConverter(EnumConverter<T> enumConverter)
{
var maps = member.GetCustomAttributes(typeof(MapAttribute), false);
foreach (MapAttribute attribute in maps)
{
foreach (var value in attribute.Values)
mapping.Add(new KeyValuePair<object, string>(Enum.Parse(objectType, member.Name), value));
}
_enumConverter = enumConverter;
}
_mapping.TryAdd(objectType, mapping);
return mapping;
}
private class EnumConverterInner<T> : JsonConverter<T>
{
private bool _warnOnMissingEntry = true;
private bool _writeAsInt;
public EnumConverterInner(bool writeAsInt, bool warnOnMissingEntry)
{
_warnOnMissingEntry = warnOnMissingEntry;
_writeAsInt = writeAsInt;
}
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var enumType = Nullable.GetUnderlyingType(typeToConvert) ?? typeToConvert;
if (!_mapping.TryGetValue(enumType, out var mapping))
mapping = AddMapping(enumType);
var stringValue = reader.TokenType switch
{
JsonTokenType.String => reader.GetString(),
JsonTokenType.Number => reader.GetInt16().ToString(),
JsonTokenType.True => reader.GetBoolean().ToString(),
JsonTokenType.False => reader.GetBoolean().ToString(),
JsonTokenType.Null => null,
_ => throw new Exception("Invalid token type for enum deserialization: " + reader.TokenType)
};
if (string.IsNullOrEmpty(stringValue))
{
// Received null value
var emptyResult = GetDefaultValue(typeToConvert, enumType);
if (emptyResult != null)
// If the property we're parsing to isn't nullable there isn't a correct way to return this as null will either throw an exception (.net framework) or the default enum value (dotnet core).
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Received null enum value, but property type is not a nullable enum. EnumType: {enumType.Name}. If you think {enumType.Name} should be nullable please open an issue on the Github repo");
return (T?)emptyResult;
}
if (!GetValue(enumType, mapping, stringValue!, out var result))
{
var defaultValue = GetDefaultValue(typeToConvert, enumType);
if (string.IsNullOrWhiteSpace(stringValue))
{
if (defaultValue != null)
// We received an empty string and have no mapping for it, and the property isn't nullable
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Received empty string as enum value, but property type is not a nullable enum. EnumType: {enumType.Name}. If you think {enumType.Name} should be nullable please open an issue on the Github repo");
}
else
{
// We received an enum value but weren't able to parse it.
if (_warnOnMissingEntry)
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | 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");
}
return (T?)defaultValue;
}
return (T?)result;
return _enumConverter.ReadNullable(ref reader, typeToConvert, options, out var isEmptyString, out var warn);
}
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options)
{
if (value == null)
{
@ -139,99 +90,183 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
}
else
{
if (!_writeAsInt)
_enumConverter.Write(writer, value.Value, options);
}
}
}
/// <inheritdoc />
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var t = ReadNullable(ref reader, typeToConvert, options, out var isEmptyString, out var warn);
if (t == null)
{
if (warn)
{
if (isEmptyString)
{
var stringValue = GetString(value.GetType(), value);
writer.WriteStringValue(stringValue);
// We received an empty string and have no mapping for it, and the property isn't nullable
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Received empty string as 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");
}
else
{
writer.WriteNumberValue((int)Convert.ChangeType(value, typeof(int)));
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Received null 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");
}
}
}
private static object? GetDefaultValue(Type objectType, Type enumType)
return new T(); // return default value
}
else
{
if (Nullable.GetUnderlyingType(objectType) != null)
return null;
return t.Value;
}
}
return Activator.CreateInstance(enumType); // return default value
private T? ReadNullable(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, out bool isEmptyString, out bool warn)
{
isEmptyString = false;
warn = false;
var enumType = typeof(T);
if (_mapping == null)
_mapping = AddMapping();
var stringValue = reader.TokenType switch
{
JsonTokenType.String => reader.GetString(),
JsonTokenType.Number => reader.GetInt32().ToString(),
JsonTokenType.True => reader.GetBoolean().ToString(),
JsonTokenType.False => reader.GetBoolean().ToString(),
JsonTokenType.Null => null,
_ => throw new Exception("Invalid token type for enum deserialization: " + reader.TokenType)
};
if (string.IsNullOrEmpty(stringValue))
return null;
if (!GetValue(enumType, stringValue!, out var result))
{
if (string.IsNullOrWhiteSpace(stringValue))
{
isEmptyString = true;
}
else
{
// We received an enum value but weren't able to parse it.
if (!_unknownValuesWarned.Contains(stringValue))
{
warn = true;
_unknownValuesWarned.Add(stringValue!);
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | 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");
}
}
return null;
}
private static bool GetValue(Type objectType, List<KeyValuePair<object, string>> enumMapping, string value, out object? result)
return result;
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
var stringValue = GetString(value);
writer.WriteStringValue(stringValue);
}
private static bool GetValue(Type objectType, string value, out T? result)
{
if (_mapping != null)
{
// Check for exact match first, then if not found fallback to a case insensitive match
var mapping = enumMapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCulture));
if (mapping.Equals(default(KeyValuePair<object, string>)))
mapping = enumMapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase));
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<object, string>)))
if (!mapping.Equals(default(KeyValuePair<T, string>)))
{
result = mapping.Key;
return true;
}
}
try
if (objectType.IsDefined(typeof(FlagsAttribute)))
{
var intValue = int.Parse(value);
result = (T)Enum.ToObject(objectType, intValue);
return true;
}
if (_unknownValuesWarned.Contains(value))
{
// Check if it is an known unknown value
// Done here to prevent lookup overhead for normal conversions, but prevent expensive exception throwing
result = default;
return false;
}
try
{
// If no explicit mapping is found try to parse string
result = (T)Enum.Parse(objectType, value, true);
return true;
}
catch (Exception)
{
result = default;
return false;
}
}
private static List<KeyValuePair<T, string>> AddMapping()
{
var mapping = new List<KeyValuePair<T, string>>();
var enumType = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
var enumMembers = enumType.GetFields();
foreach (var member in enumMembers)
{
var maps = member.GetCustomAttributes(typeof(MapAttribute), false);
foreach (MapAttribute attribute in maps)
{
// If no explicit mapping is found try to parse string
result = Enum.Parse(objectType, value, true);
return true;
}
catch (Exception)
{
result = default;
return false;
foreach (var value in attribute.Values)
mapping.Add(new KeyValuePair<T, string>((T)Enum.Parse(enumType, member.Name), value));
}
}
_mapping = mapping;
return mapping;
}
/// <summary>
/// Get the string value for an enum value using the MapAttribute mapping. When multiple values are mapped for a enum entry the first value will be returned
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="enumValue"></param>
/// <returns></returns>
[return: NotNullIfNotNull("enumValue")]
public static string? GetString<T>(T enumValue) => GetString(typeof(T), enumValue);
/// <summary>
/// Get the string value for an enum value using the MapAttribute mapping. When multiple values are mapped for a enum entry the first value will be returned
/// </summary>
/// <param name="objectType"></param>
/// <param name="enumValue"></param>
/// <returns></returns>
[return: NotNullIfNotNull("enumValue")]
public static string? GetString(Type objectType, object? enumValue)
public static string? GetString(T? enumValue)
{
objectType = Nullable.GetUnderlyingType(objectType) ?? objectType;
if (_mapping == null)
_mapping = AddMapping();
if (!_mapping.TryGetValue(objectType, out var mapping))
mapping = AddMapping(objectType);
return enumValue == null ? null : (mapping.FirstOrDefault(v => v.Key.Equals(enumValue)).Value ?? enumValue.ToString());
return enumValue == null ? null : (_mapping.FirstOrDefault(v => v.Key.Equals(enumValue)).Value ?? enumValue.ToString());
}
/// <summary>
/// Get the enum value from a string
/// </summary>
/// <typeparam name="T">Enum type</typeparam>
/// <param name="value">String value</param>
/// <returns></returns>
public static T? ParseString<T>(string value) where T : Enum
public static T? ParseString(string value)
{
var type = typeof(T);
if (!_mapping.TryGetValue(type, out var enumMapping))
enumMapping = AddMapping(type);
var type = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
if (_mapping == null)
_mapping = AddMapping();
var mapping = enumMapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCulture));
if (mapping.Equals(default(KeyValuePair<object, string>)))
mapping = enumMapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase));
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<object, string>)))
{
return (T)mapping.Key;
}
if (!mapping.Equals(default(KeyValuePair<T, string>)))
return mapping.Key;
try
{
@ -243,5 +278,12 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
return default;
}
}
/// <inheritdoc />
public JsonConverter CreateNullableConverter()
{
_nullableEnumConverter ??= new NullableEnumConverter(this);
return _nullableEnumConverter;
}
}
}

View File

@ -0,0 +1,23 @@
using System;
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
/// Converter for serializing enum values as int
/// </summary>
public class EnumIntWriterConverter<T> : JsonConverter<T> where T: struct, Enum
{
/// <inheritdoc />
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
throw new NotImplementedException();
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
=> writer.WriteNumberValue((int)(object)value);
}
}

View File

@ -0,0 +1,9 @@
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
internal interface INullableConverterFactory
{
JsonConverter CreateNullableConverter();
}
}

View File

@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Text.Json.Serialization.Metadata;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
internal class NullableEnumConverterFactory : JsonConverterFactory
{
private readonly IJsonTypeInfoResolver _jsonTypeInfoResolver;
private static readonly JsonSerializerOptions _options = new JsonSerializerOptions();
public NullableEnumConverterFactory(IJsonTypeInfoResolver jsonTypeInfoResolver)
{
_jsonTypeInfoResolver = jsonTypeInfoResolver;
}
public override bool CanConvert(Type typeToConvert)
{
var b = Nullable.GetUnderlyingType(typeToConvert);
if (b == null)
return false;
var typeInfo = _jsonTypeInfoResolver.GetTypeInfo(b, _options);
if (typeInfo == null)
return false;
return typeInfo.Converter is INullableConverterFactory;
}
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
var b = Nullable.GetUnderlyingType(typeToConvert) ?? throw new ArgumentNullException($"Not nullable {typeToConvert.Name}");
var typeInfo = _jsonTypeInfoResolver.GetTypeInfo(b, _options) ?? throw new ArgumentNullException($"Can find type {typeToConvert.Name}");
if (typeInfo.Converter is not INullableConverterFactory nullConverterFactory)
throw new ArgumentNullException($"Can find type converter for {typeToConvert.Name}");
return nullConverterFactory.CreateNullableConverter();
}
}
}

View File

@ -1,5 +1,4 @@
using System;
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
@ -24,7 +23,14 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
return reader.GetDecimal().ToString();
}
return reader.GetString();
try
{
return reader.GetString();
}
catch (Exception)
{
return null;
}
}
/// <inheritdoc />

View File

@ -1,21 +1,20 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.Serialization;
using System.Text;
using System.Text.Json.Serialization;
using System.Text.Json;
using System.Globalization;
using System.Diagnostics.CodeAnalysis;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
///
/// Converter for values which contain a nested json value
/// </summary>
/// <typeparam name="T"></typeparam>
public class ObjectStringConverter<T> : JsonConverter<T>
{
/// <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 override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null)
@ -25,10 +24,14 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
if (string.IsNullOrEmpty(value))
return default;
return (T?)JsonDocument.Parse(value!).Deserialize(typeof(T));
return (T?)JsonDocument.Parse(value!).Deserialize(typeof(T), options);
}
/// <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 override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options)
{
if (value is null)

View File

@ -0,0 +1,41 @@
using System;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
/// Replace a value on a string property
/// </summary>
public abstract class ReplaceConverter : JsonConverter<string>
{
private readonly (string ValueToReplace, string ValueToReplaceWith)[] _replacementSets;
/// <summary>
/// ctor
/// </summary>
public ReplaceConverter(params string[] replaceSets)
{
_replacementSets = replaceSets.Select(x =>
{
var split = x.Split(new string[] { "->" }, StringSplitOptions.None);
if (split.Length != 2)
throw new ArgumentException("Invalid replacement config");
return (split[0], split[1]);
}).ToArray();
}
/// <inheritdoc />
public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var value = reader.GetString();
foreach (var set in _replacementSets)
value = value?.Replace(set.ValueToReplace, set.ValueToReplaceWith);
return value;
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) => writer.WriteStringValue(value);
}
}

View File

@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
/// Attribute to mark a model as json serializable. Used for AOT compilation.
/// </summary>
[AttributeUsage(System.AttributeTargets.Class | AttributeTargets.Enum | System.AttributeTargets.Interface)]
public class SerializationModelAttribute : Attribute
{
/// <summary>
/// ctor
/// </summary>
public SerializationModelAttribute() { }
/// <summary>
/// ctor
/// </summary>
/// <param name="type"></param>
public SerializationModelAttribute(Type type) { }
}
}

View File

@ -1,4 +1,5 @@
using System.Text.Json;
using System.Collections.Concurrent;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson
@ -8,22 +9,39 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
/// </summary>
public static class SerializerOptions
{
private static readonly ConcurrentDictionary<JsonSerializerContext, JsonSerializerOptions> _cache = new ConcurrentDictionary<JsonSerializerContext, JsonSerializerOptions>();
/// <summary>
/// Json serializer settings which includes the EnumConverter, DateTimeConverter, BoolConverter and DecimalConverter
/// Get Json serializer settings which includes standard converters for DateTime, bool, enum and number types
/// </summary>
public static JsonSerializerOptions WithConverters { get; } = new JsonSerializerOptions
public static JsonSerializerOptions WithConverters(JsonSerializerContext typeResolver, params JsonConverter[] additionalConverters)
{
NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals,
PropertyNameCaseInsensitive = false,
Converters =
if (!_cache.TryGetValue(typeResolver, out var options))
{
options = new JsonSerializerOptions
{
NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals,
PropertyNameCaseInsensitive = false,
Converters =
{
new DateTimeConverter(),
new EnumConverter(),
new BoolConverter(),
new DecimalConverter(),
new IntConverter(),
new LongConverter()
}
};
new LongConverter(),
new NullableEnumConverterFactory(typeResolver)
},
TypeInfoResolver = typeResolver,
};
foreach (var converter in additionalConverters)
options.Converters.Add(converter);
options.TypeInfoResolver = typeResolver;
_cache.TryAdd(typeResolver, options);
}
return options;
}
}
}

View File

@ -0,0 +1,60 @@
using CryptoExchange.Net.SharedApis;
using System;
using System.Collections.Generic;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
internal class SharedQuantityConverter : SharedQuantityReferenceConverter<SharedQuantity> { }
internal class SharedOrderQuantityConverter : SharedQuantityReferenceConverter<SharedOrderQuantity> { }
internal class SharedQuantityReferenceConverter<T> : JsonConverter<T> where T: SharedQuantityReference, new()
{
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartArray)
throw new Exception("");
reader.Read(); // Start array
var baseQuantity = reader.TokenType == JsonTokenType.Null ? (decimal?)null : reader.GetDecimal();
reader.Read();
var quoteQuantity = reader.TokenType == JsonTokenType.Null ? (decimal?)null : reader.GetDecimal();
reader.Read();
var contractQuantity = reader.TokenType == JsonTokenType.Null ? (decimal?)null : reader.GetDecimal();
reader.Read();
if (reader.TokenType != JsonTokenType.EndArray)
throw new Exception("");
reader.Read(); // End array
var result = new T();
result.QuantityInBaseAsset = baseQuantity;
result.QuantityInQuoteAsset = quoteQuantity;
result.QuantityInContracts = contractQuantity;
return result;
}
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
writer.WriteStartArray();
if (value.QuantityInBaseAsset == null)
writer.WriteNullValue();
else
writer.WriteNumberValue(value.QuantityInBaseAsset.Value);
if (value.QuantityInQuoteAsset == null)
writer.WriteNullValue();
else
writer.WriteNumberValue(value.QuantityInQuoteAsset.Value);
if (value.QuantityInContracts == null)
writer.WriteNullValue();
else
writer.WriteNumberValue(value.QuantityInContracts.Value);
writer.WriteEndArray();
}
}
}

View File

@ -0,0 +1,46 @@
using CryptoExchange.Net.SharedApis;
using System;
using System.Collections.Generic;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
internal class SharedSymbolConverter : JsonConverter<SharedSymbol>
{
public override SharedSymbol? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartArray)
throw new Exception("");
reader.Read(); // Start array
var tradingMode = (TradingMode)Enum.Parse(typeof(TradingMode), reader.GetString()!);
reader.Read();
var baseAsset = reader.GetString()!;
reader.Read();
var quoteAsset = reader.GetString()!;
reader.Read();
var timeStr = reader.GetString()!;
var deliverTime = string.IsNullOrEmpty(timeStr) ? (DateTime?)null : DateTime.Parse(timeStr);
reader.Read();
if (reader.TokenType != JsonTokenType.EndArray)
throw new Exception("");
reader.Read(); // End array
return new SharedSymbol(tradingMode, baseAsset, quoteAsset, deliverTime);
}
public override void Write(Utf8JsonWriter writer, SharedSymbol value, JsonSerializerOptions options)
{
writer.WriteStartArray();
writer.WriteStringValue(value.TradingMode.ToString());
writer.WriteStringValue(value.BaseAsset);
writer.WriteStringValue(value.QuoteAsset);
writer.WriteStringValue(value.DeliverTime?.ToString());
writer.WriteEndArray();
}
}
}

View File

@ -3,6 +3,7 @@ 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;
@ -20,7 +21,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
/// </summary>
protected JsonDocument? _document;
private static JsonSerializerOptions _serializerOptions = SerializerOptions.WithConverters;
private readonly JsonSerializerOptions? _customSerializerOptions;
/// <inheritdoc />
public bool IsJson { get; set; }
@ -31,7 +32,19 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
/// <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 (!IsJson)
@ -42,17 +55,26 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
try
{
var result = _document.Deserialize(type, _serializerOptions);
var result = _document.Deserialize(type, _customSerializerOptions);
return new CallResult<object>(result!);
}
catch (JsonException ex)
{
var info = $"Deserialize JsonException: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}";
return new CallResult<object>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
return new CallResult<object>(new DeserializeError(info, ex));
}
catch (Exception ex)
{
var info = $"Deserialize unknown Exception: {ex.Message}";
return new CallResult<object>(new DeserializeError(info, 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)
@ -60,18 +82,18 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
try
{
var result = _document.Deserialize<T>(_serializerOptions);
var result = _document.Deserialize<T>(_customSerializerOptions);
return new CallResult<T>(result!);
}
catch (JsonException ex)
{
var info = $"Deserialize JsonException: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}";
return new CallResult<T>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
return new CallResult<T>(new DeserializeError(info, ex));
}
catch (Exception ex)
{
var info = $"Unknown exception: {ex.Message}";
return new CallResult<T>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
return new CallResult<T>(new DeserializeError(info, ex));
}
}
@ -111,6 +133,10 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
}
/// <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 (!IsJson)
@ -121,7 +147,15 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
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))
{
@ -129,11 +163,28 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
return (T)(object)value.Value.GetInt64().ToString();
}
return value.Value.Deserialize<T>();
return value.Value.Deserialize<T>(_customSerializerOptions);
}
/// <inheritdoc />
public List<T?>? GetValues<T>(MessagePath path) => throw new NotImplementedException();
#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 (!IsJson)
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)
{
@ -198,6 +249,13 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
/// <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)
{
@ -222,13 +280,13 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
{
_document = await JsonDocument.ParseAsync(_stream ?? stream).ConfigureAwait(false);
IsJson = true;
return new CallResult(null);
return CallResult.SuccessResult;
}
catch (Exception ex)
{
// Not a json message
IsJson = false;
return new CallResult(new ServerError("JsonError: " + ex.Message));
return new CallResult(new DeserializeError("JsonError: " + ex.Message, ex));
}
}
@ -261,6 +319,13 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
{
private ReadOnlyMemory<byte> _bytes;
/// <summary>
/// ctor
/// </summary>
public SystemTextJsonByteMessageAccessor(JsonSerializerOptions options) : base(options)
{
}
/// <inheritdoc />
public CallResult Read(ReadOnlyMemory<byte> data)
{
@ -278,19 +343,19 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
_document = JsonDocument.Parse(data);
IsJson = true;
return new CallResult(null);
return CallResult.SuccessResult;
}
catch (Exception ex)
{
// Not a json message
IsJson = false;
return new CallResult(new ServerError("JsonError: " + ex.Message));
return new CallResult(new DeserializeError("JsonError: " + ex.Message, ex));
}
}
/// <inheritdoc />
public override string GetOriginalString() =>
// Netstandard 2.0 doesn't support GetString from a ReadonlySpan<byte>, so use ToArray there instead
// 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

View File

@ -1,12 +1,29 @@
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
{
/// <inheritdoc />
public class SystemTextJsonMessageSerializer : IMessageSerializer
{
private readonly JsonSerializerOptions _options;
/// <summary>
/// ctor
/// </summary>
public SystemTextJsonMessageSerializer(JsonSerializerOptions options)
{
_options = options;
}
/// <inheritdoc />
public string Serialize(object message) => JsonSerializer.Serialize(message, SerializerOptions.WithConverters);
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "Everything referenced in the loaded assembly is manually preserved, so it's safe")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "Everything referenced in the loaded assembly is manually preserved, so it's safe")]
#endif
public string Serialize<T>(T message) => JsonSerializer.Serialize(message, _options);
}
}

View File

@ -1,14 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks>
<TargetFrameworks>netstandard2.0;netstandard2.1;net8.0;net9.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>7.11.0</PackageVersion>
<AssemblyVersion>7.11.0</AssemblyVersion>
<FileVersion>7.11.0</FileVersion>
<PackageVersion>9.1.0</PackageVersion>
<AssemblyVersion>9.1.0</AssemblyVersion>
<FileVersion>9.1.0</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</PackageTags>
<RepositoryType>git</RepositoryType>
@ -20,13 +20,16 @@
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageReleaseNotes>https://github.com/JKorf/CryptoExchange.Net?tab=readme-ov-file#release-notes</PackageReleaseNotes>
<Nullable>enable</Nullable>
<LangVersion>10.0</LangVersion>
<LangVersion>12.0</LangVersion>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
</PropertyGroup>
<ItemGroup>
<None Include="Icon\icon.png" Pack="true" PackagePath="\" />
<None Include="..\README.md" Pack="true" PackagePath="\" />
</ItemGroup>
<PropertyGroup Label="AOT" Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net7.0'))">
<IsAotCompatible>true</IsAotCompatible>
</PropertyGroup>
<PropertyGroup Label="Deterministic Build" Condition="'$(Configuration)' == 'Release'">
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<IncludeSymbols>true</IncludeSymbols>
@ -34,12 +37,6 @@
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
</PropertyGroup>
<ItemGroup Label="Deterministic Build" Condition="'$(Configuration)' == 'Release'">
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<PropertyGroup>
<DocumentationFile>CryptoExchange.Net.xml</DocumentationFile>
</PropertyGroup>
@ -48,16 +45,17 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="8.0.0">
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1" />
<PackageReference Include="System.Text.Json" Version="8.0.4" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.5" />
<PackageReference Include="System.Text.Json" Version="9.0.5" />
</ItemGroup>
<ItemGroup Label="Transitive Client Packages">
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.5" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.5" />
</ItemGroup>
</Project>

View File

@ -1,7 +1,11 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.SharedApis;
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net
{
@ -11,6 +15,23 @@ namespace CryptoExchange.Net
public static class ExchangeHelpers
{
private const string _allowedRandomChars = "ABCDEFGHIJKLMONOPQRSTUVWXYZabcdefghijklmonopqrstuvwxyz0123456789";
private const string _allowedRandomHexChars = "0123456789ABCDEF";
private static readonly Dictionary<int, string> _monthSymbols = new Dictionary<int, string>()
{
{ 1, "F" },
{ 2, "G" },
{ 3, "H" },
{ 4, "J" },
{ 5, "K" },
{ 6, "M" },
{ 7, "N" },
{ 8, "Q" },
{ 9, "U" },
{ 10, "V" },
{ 11, "X" },
{ 12, "Z" },
};
/// <summary>
/// The last used id, use NextId() to get the next id and up this
@ -55,6 +76,11 @@ namespace CryptoExchange.Net
{
value -= offset;
}
else if(roundingType == RoundingType.Up)
{
if (offset != 0)
value += (step.Value - offset);
}
else
{
if (offset < step / 2)
@ -86,6 +112,34 @@ namespace CryptoExchange.Net
return RoundToSignificantDigits(value, precision.Value, roundingType);
}
/// <summary>
/// Apply the provided rules to the value
/// </summary>
/// <param name="value">Value to be adjusted</param>
/// <param name="decimals">Max decimal places</param>
/// <param name="valueStep">The value step for increase/decrease value</param>
/// <returns></returns>
public static decimal ApplyRules(
decimal value,
int? decimals = null,
decimal? valueStep = null)
{
if (valueStep.HasValue)
{
var offset = value % valueStep.Value;
if (offset != 0)
{
if (offset < valueStep.Value / 2)
value -= offset;
else value += (valueStep.Value - offset);
}
}
if (decimals.HasValue)
value = Math.Round(value, decimals.Value);
return value;
}
/// <summary>
/// Round a value to have the provided total number of digits. For example, value 253.12332 with 5 digits would be 253.12
/// </summary>
@ -107,17 +161,23 @@ namespace CryptoExchange.Net
}
/// <summary>
/// Rounds a value down to
/// Rounds a value down
/// </summary>
/// <param name="i"></param>
/// <param name="decimalPlaces"></param>
/// <returns></returns>
public static decimal RoundDown(decimal i, double decimalPlaces)
{
var power = Convert.ToDecimal(Math.Pow(10, decimalPlaces));
return Math.Floor(i * power) / power;
}
/// <summary>
/// Rounds a value up
/// </summary>
public static decimal RoundUp(decimal i, double decimalPlaces)
{
var power = Convert.ToDecimal(Math.Pow(10, decimalPlaces));
return Math.Ceiling(i * power) / power;
}
/// <summary>
/// Strips any trailing zero's of a decimal value, useful when converting the value to string.
/// </summary>
@ -129,7 +189,7 @@ namespace CryptoExchange.Net
}
/// <summary>
/// Generate a new unique id. The id is staticly stored so it is guarenteed to be unique
/// Generate a new unique id. The id is statically stored so it is guaranteed to be unique
/// </summary>
/// <returns></returns>
public static int NextId() => Interlocked.Increment(ref _lastId);
@ -149,7 +209,7 @@ namespace CryptoExchange.Net
{
var randomChars = new char[length];
#if NETSTANDARD2_1_OR_GREATER
#if NETSTANDARD2_1_OR_GREATER || NET9_0_OR_GREATER
for (int i = 0; i < length; i++)
randomChars[i] = _allowedRandomChars[RandomNumberGenerator.GetInt32(0, _allowedRandomChars.Length)];
#else
@ -161,6 +221,44 @@ namespace CryptoExchange.Net
return new string(randomChars);
}
/// <summary>
/// Generate a random string of specified length
/// </summary>
/// <param name="length">Length of the random string</param>
/// <returns></returns>
public static string RandomHexString(int length)
{
#if NET9_0_OR_GREATER
return "0x" + RandomNumberGenerator.GetHexString(length * 2);
#else
var randomChars = new char[length * 2];
var random = new Random();
for (int i = 0; i < length * 2; i++)
randomChars[i] = _allowedRandomHexChars[random.Next(0, _allowedRandomHexChars.Length)];
return "0x" + new string(randomChars);
#endif
}
/// <summary>
/// Generate a long value
/// </summary>
/// <param name="maxLength">Max character length</param>
/// <returns></returns>
public static long RandomLong(int maxLength)
{
#if NETSTANDARD2_1_OR_GREATER || NET9_0_OR_GREATER
var value = RandomNumberGenerator.GetInt32(0, int.MaxValue);
#else
var random = new Random();
var value = random.Next(0, int.MaxValue);
#endif
var val = value.ToString();
if (val.Length > maxLength)
return int.Parse(val.Substring(0, maxLength));
else
return value;
}
/// <summary>
/// Generate a random string of specified length
/// </summary>
@ -177,5 +275,71 @@ namespace CryptoExchange.Net
return source + RandomString(totalLength - source.Length);
}
/// <summary>
/// Get the month representation for futures symbol based on the delivery month
/// </summary>
/// <param name="time">Delivery time</param>
/// <returns></returns>
public static string GetDeliveryMonthSymbol(DateTime time) => _monthSymbols[time.Month];
/// <summary>
/// Execute multiple requests to retrieve multiple pages of the result set
/// </summary>
/// <typeparam name="T">Type of the client</typeparam>
/// <typeparam name="U">Type of the request</typeparam>
/// <param name="paginatedFunc">The func to execute with each request</param>
/// <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)
{
var result = new List<T>();
ExchangeWebResult<T[]> batch;
INextPageToken? nextPageToken = null;
while (true)
{
batch = await paginatedFunc(request, nextPageToken, ct).ConfigureAwait(false);
yield return batch;
if (!batch || ct.IsCancellationRequested)
break;
result.AddRange(batch.Data);
nextPageToken = batch.NextPageToken;
if (nextPageToken == null)
break;
}
}
/// <summary>
/// Apply the rules (price and quantity step size and decimals precision, min/max quantity) from the symbol to the quantity and price
/// </summary>
/// <param name="symbol">The symbol as retrieved from the exchange</param>
/// <param name="quantity">Quantity to trade</param>
/// <param name="price">Price to trade at</param>
/// <param name="adjustedQuantity">Quantity adjusted to match all trading rules</param>
/// <param name="adjustedPrice">Price adjusted to match all trading rules</param>
public static void ApplySymbolRules(SharedSpotSymbol symbol, decimal quantity, decimal? price, out decimal adjustedQuantity, out decimal? adjustedPrice)
{
adjustedPrice = price;
adjustedQuantity = quantity;
var minNotionalAdjust = false;
if (price != null)
{
adjustedPrice = AdjustValueStep(0, decimal.MaxValue, symbol.PriceStep, RoundingType.Down, price.Value);
adjustedPrice = symbol.PriceSignificantFigures.HasValue ? RoundToSignificantDigits(adjustedPrice.Value, symbol.PriceSignificantFigures.Value, RoundingType.Closest) : adjustedPrice;
adjustedPrice = symbol.PriceDecimals.HasValue ? RoundDown(price.Value, symbol.PriceDecimals.Value) : adjustedPrice;
if (adjustedPrice != 0 && adjustedPrice * quantity < symbol.MinNotionalValue)
{
adjustedQuantity = symbol.MinNotionalValue.Value / adjustedPrice.Value;
minNotionalAdjust = true;
}
}
adjustedQuantity = AdjustValueStep(symbol.MinTradeQuantity ?? 0, symbol.MaxTradeQuantity ?? decimal.MaxValue, symbol.QuantityStep, minNotionalAdjust ? RoundingType.Up : RoundingType.Down, adjustedQuantity);
adjustedQuantity = symbol.QuantityDecimals.HasValue ? (minNotionalAdjust ? RoundUp(adjustedQuantity, symbol.QuantityDecimals.Value) : RoundDown(adjustedQuantity, symbol.QuantityDecimals.Value)) : adjustedQuantity;
}
}
}

View File

@ -0,0 +1,70 @@
using CryptoExchange.Net.SharedApis;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace CryptoExchange.Net
{
/// <summary>
/// Cache for symbol parsing
/// </summary>
public static class ExchangeSymbolCache
{
private static ConcurrentDictionary<string, ExchangeInfo> _symbolInfos = new ConcurrentDictionary<string, ExchangeInfo>();
/// <summary>
/// Update the cached symbol data for an exchange
/// </summary>
/// <param name="topicId">Id for the provided data</param>
/// <param name="updateData">Symbol data</param>
public static void UpdateSymbolInfo(string topicId, SharedSpotSymbol[] updateData)
{
if(!_symbolInfos.TryGetValue(topicId, out var exchangeInfo))
{
exchangeInfo = new ExchangeInfo(DateTime.UtcNow, updateData.ToDictionary(x => x.Name, x => new SharedSymbol(x.TradingMode, x.BaseAsset.ToUpperInvariant(), x.QuoteAsset.ToUpperInvariant(), (x as SharedFuturesSymbol)?.DeliveryTime) { SymbolName = x.Name }));
_symbolInfos.TryAdd(topicId, exchangeInfo);
}
if (DateTime.UtcNow - exchangeInfo.UpdateTime < TimeSpan.FromMinutes(60))
return;
_symbolInfos[topicId] = new ExchangeInfo(DateTime.UtcNow, updateData.ToDictionary(x => x.Name, x => new SharedSymbol(x.TradingMode, x.BaseAsset.ToUpperInvariant(), x.QuoteAsset.ToUpperInvariant(), (x as SharedFuturesSymbol)?.DeliveryTime) { SymbolName = x.Name }));
}
/// <summary>
/// Parse a symbol name to a SharedSymbol
/// </summary>
/// <param name="topicId">Id for the provided data</param>
/// <param name="symbolName">Symbol name</param>
public static SharedSymbol? ParseSymbol(string topicId, string? symbolName)
{
if (symbolName == null)
return null;
if (!_symbolInfos.TryGetValue(topicId, out var exchangeInfo))
return null;
if (!exchangeInfo.Symbols.TryGetValue(symbolName, out var symbolInfo))
return null;
return new SharedSymbol(symbolInfo.TradingMode, symbolInfo.BaseAsset, symbolInfo.QuoteAsset, symbolName)
{
DeliverTime = symbolInfo.DeliverTime
};
}
class ExchangeInfo
{
public DateTime UpdateTime { get; set; }
public Dictionary<string, SharedSymbol> Symbols { get; set; }
public ExchangeInfo(DateTime updateTime, Dictionary<string, SharedSymbol> symbols)
{
UpdateTime = updateTime;
Symbols = symbols;
}
}
}
}

View File

@ -4,11 +4,15 @@ using System.IO.Compression;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Security;
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
{
@ -66,7 +70,7 @@ namespace CryptoExchange.Net
{
if (serializationType == ArrayParametersSerialization.Array)
{
uriString += $"{string.Join("&", ((object[])(urlEncodeValues ? Uri.EscapeDataString(arrayEntry.Value.ToString()) : arrayEntry.Value)).Select(v => $"{arrayEntry.Key}[]={string.Format(CultureInfo.InvariantCulture, "{0}", v)}"))}&";
uriString += $"{string.Join("&", ((object[])(urlEncodeValues ? Uri.EscapeDataString(arrayEntry.Value.ToString()!) : arrayEntry.Value)).Select(v => $"{arrayEntry.Key}[]={string.Format(CultureInfo.InvariantCulture, "{0}", v)}"))}&";
}
else if (serializationType == ArrayParametersSerialization.MultipleValues)
{
@ -110,7 +114,8 @@ namespace CryptoExchange.Net
formData.Add(kvp.Key, string.Format(CultureInfo.InvariantCulture, "{0}", kvp.Value));
}
}
return formData.ToString();
return formData.ToString()!;
}
/// <summary>
@ -188,6 +193,16 @@ namespace CryptoExchange.Net
throw new ArgumentException($"No values provided for parameter {argumentName}", argumentName);
}
/// <summary>
/// Format a string to RFC3339/ISO8601 string
/// </summary>
/// <param name="dateTime"></param>
/// <returns></returns>
public static string ToRfc3339String(this DateTime dateTime)
{
return dateTime.ToString("yyyy-MM-dd'T'HH:mm:ss.fffzzz", DateTimeFormatInfo.InvariantInfo);
}
/// <summary>
/// Format an exception and inner exception to a readable string
/// </summary>
@ -275,6 +290,7 @@ namespace CryptoExchange.Net
httpValueCollection.Add(parameter.Key, parameter.Value.ToString());
}
}
uriBuilder.Query = httpValueCollection.ToString();
return uriBuilder.Uri;
}
@ -322,6 +338,7 @@ namespace CryptoExchange.Net
httpValueCollection.Add(parameter.Key, parameter.Value.ToString());
}
}
uriBuilder.Query = httpValueCollection.ToString();
return uriBuilder.Uri;
}
@ -333,7 +350,7 @@ namespace CryptoExchange.Net
/// <param name="name"></param>
/// <param name="value"></param>
/// <returns></returns>
public static Uri AddQueryParmeter(this Uri uri, string name, string value)
public static Uri AddQueryParameter(this Uri uri, string name, string value)
{
var httpValueCollection = HttpUtility.ParseQueryString(uri.Query);
@ -355,7 +372,7 @@ namespace CryptoExchange.Net
{
using var decompressedStream = new MemoryStream();
using var dataStream = MemoryMarshal.TryGetArray(data, out var arraySegment)
? new MemoryStream(arraySegment.Array, arraySegment.Offset, arraySegment.Count)
? 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);
@ -378,6 +395,130 @@ namespace CryptoExchange.Net
output.Position = 0;
return new ReadOnlyMemory<byte>(output.GetBuffer(), 0, (int)output.Length);
}
/// <summary>
/// Whether the trading mode is linear
/// </summary>
public static bool IsLinear(this TradingMode type) => type == TradingMode.PerpetualLinear || type == TradingMode.DeliveryLinear;
/// <summary>
/// Whether the trading mode is inverse
/// </summary>
public static bool IsInverse(this TradingMode type) => type == TradingMode.PerpetualInverse || type == TradingMode.DeliveryInverse;
/// <summary>
/// Whether the trading mode is perpetual
/// </summary>
public static bool IsPerpetual(this TradingMode type) => type == TradingMode.PerpetualInverse || type == TradingMode.PerpetualLinear;
/// <summary>
/// Whether the trading mode is delivery
/// </summary>
public static bool IsDelivery(this TradingMode type) => type == TradingMode.DeliveryInverse || type == TradingMode.DeliveryLinear;
/// <summary>
/// Register rest client interfaces
/// </summary>
public static IServiceCollection RegisterSharedRestInterfaces<T>(this IServiceCollection services, Func<IServiceProvider, T> client)
{
if (typeof(IAssetsRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IAssetsRestClient)client(x)!);
if (typeof(IBalanceRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IBalanceRestClient)client(x)!);
if (typeof(IDepositRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IDepositRestClient)client(x)!);
if (typeof(IKlineRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IKlineRestClient)client(x)!);
if (typeof(IListenKeyRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IListenKeyRestClient)client(x)!);
if (typeof(IOrderBookRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IOrderBookRestClient)client(x)!);
if (typeof(IRecentTradeRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IRecentTradeRestClient)client(x)!);
if (typeof(ITradeHistoryRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (ITradeHistoryRestClient)client(x)!);
if (typeof(IWithdrawalRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IWithdrawalRestClient)client(x)!);
if (typeof(IWithdrawRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IWithdrawRestClient)client(x)!);
if (typeof(IFeeRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IFeeRestClient)client(x)!);
if (typeof(IBookTickerRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IBookTickerRestClient)client(x)!);
if (typeof(ISpotOrderRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (ISpotOrderRestClient)client(x)!);
if (typeof(ISpotSymbolRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (ISpotSymbolRestClient)client(x)!);
if (typeof(ISpotTickerRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (ISpotTickerRestClient)client(x)!);
if (typeof(ISpotTriggerOrderRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (ISpotTriggerOrderRestClient)client(x)!);
if (typeof(ISpotOrderClientIdRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (ISpotOrderClientIdRestClient)client(x)!);
if (typeof(IFundingRateRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IFundingRateRestClient)client(x)!);
if (typeof(IFuturesOrderRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IFuturesOrderRestClient)client(x)!);
if (typeof(IFuturesSymbolRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IFuturesSymbolRestClient)client(x)!);
if (typeof(IFuturesTickerRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IFuturesTickerRestClient)client(x)!);
if (typeof(IIndexPriceKlineRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IIndexPriceKlineRestClient)client(x)!);
if (typeof(ILeverageRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (ILeverageRestClient)client(x)!);
if (typeof(IMarkPriceKlineRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IMarkPriceKlineRestClient)client(x)!);
if (typeof(IOpenInterestRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IOpenInterestRestClient)client(x)!);
if (typeof(IPositionHistoryRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IPositionHistoryRestClient)client(x)!);
if (typeof(IPositionModeRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IPositionModeRestClient)client(x)!);
if (typeof(IFuturesTpSlRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IFuturesTpSlRestClient)client(x)!);
if (typeof(IFuturesTriggerOrderRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IFuturesTriggerOrderRestClient)client(x)!);
if (typeof(IFuturesOrderClientIdRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IFuturesOrderClientIdRestClient)client(x)!);
return services;
}
/// <summary>
/// Register socket client interfaces
/// </summary>
public static IServiceCollection RegisterSharedSocketInterfaces<T>(this IServiceCollection services, Func<IServiceProvider, T> client)
{
if (typeof(IBalanceSocketClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IBalanceSocketClient)client(x)!);
if (typeof(IBookTickerSocketClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IBookTickerSocketClient)client(x)!);
if (typeof(IKlineSocketClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IKlineSocketClient)client(x)!);
if (typeof(IOrderBookRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IOrderBookRestClient)client(x)!);
if (typeof(ITickerSocketClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (ITickerSocketClient)client(x)!);
if (typeof(ITickersSocketClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (ITickersSocketClient)client(x)!);
if (typeof(ITradeSocketClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (ITradeSocketClient)client(x)!);
if (typeof(IUserTradeSocketClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IUserTradeSocketClient)client(x)!);
if (typeof(ISpotOrderSocketClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (ISpotOrderSocketClient)client(x)!);
if (typeof(IFuturesOrderSocketClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IFuturesOrderSocketClient)client(x)!);
if (typeof(IPositionSocketClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IPositionSocketClient)client(x)!);
return services;
}
}
}

View File

@ -1,138 +0,0 @@
using CryptoExchange.Net.CommonObjects;
using CryptoExchange.Net.Objects;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Interfaces.CommonClients
{
/// <summary>
/// Common rest client endpoints
/// </summary>
public interface IBaseRestClient
{
/// <summary>
/// The name of the exchange
/// </summary>
string ExchangeName { get; }
/// <summary>
/// Should be triggered on order placing
/// </summary>
event Action<OrderId> OnOrderPlaced;
/// <summary>
/// Should be triggered on order cancelling
/// </summary>
event Action<OrderId> OnOrderCanceled;
/// <summary>
/// Get the symbol name based on a base and quote asset
/// </summary>
/// <param name="baseAsset">The base asset</param>
/// <param name="quoteAsset">The quote asset</param>
/// <returns></returns>
string GetSymbolName(string baseAsset, string quoteAsset);
/// <summary>
/// Get a list of symbols for the exchange
/// </summary>
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
/// <returns></returns>
Task<WebCallResult<IEnumerable<Symbol>>> GetSymbolsAsync(CancellationToken ct = default);
/// <summary>
/// Get a ticker for the exchange
/// </summary>
/// <param name="symbol">The symbol to get klines for</param>
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
/// <returns></returns>
Task<WebCallResult<Ticker>> GetTickerAsync(string symbol, CancellationToken ct = default);
/// <summary>
/// Get a list of tickers for the exchange
/// </summary>
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
/// <returns></returns>
Task<WebCallResult<IEnumerable<Ticker>>> GetTickersAsync(CancellationToken ct = default);
/// <summary>
/// Get a list of candles for a given symbol on the exchange
/// </summary>
/// <param name="symbol">The symbol to retrieve the candles for</param>
/// <param name="timespan">The timespan to retrieve the candles for. The supported value are dependent on the exchange</param>
/// <param name="startTime">[Optional] Start time to retrieve klines for</param>
/// <param name="endTime">[Optional] End time to retrieve klines for</param>
/// <param name="limit">[Optional] Max number of results</param>
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
/// <returns></returns>
Task<WebCallResult<IEnumerable<Kline>>> GetKlinesAsync(string symbol, TimeSpan timespan, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, CancellationToken ct = default);
/// <summary>
/// Get the order book for a symbol
/// </summary>
/// <param name="symbol">The symbol to get the book for</param>
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
/// <returns></returns>
Task<WebCallResult<CommonObjects.OrderBook>> GetOrderBookAsync(string symbol, CancellationToken ct = default);
/// <summary>
/// The recent trades for a symbol
/// </summary>
/// <param name="symbol">The symbol to get the trades for</param>
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
/// <returns></returns>
Task<WebCallResult<IEnumerable<Trade>>> GetRecentTradesAsync(string symbol, CancellationToken ct = default);
/// <summary>
/// Get balances
/// </summary>
/// <param name="accountId">[Optional] The account id to retrieve balances for, required for some exchanges, ignored otherwise</param>
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
/// <returns></returns>
Task<WebCallResult<IEnumerable<Balance>>> GetBalancesAsync(string? accountId = null, CancellationToken ct = default);
/// <summary>
/// Get an order by id
/// </summary>
/// <param name="orderId">The id</param>
/// <param name="symbol">[Optional] The symbol the order is on, required for some exchanges, ignored otherwise</param>
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
/// <returns></returns>
Task<WebCallResult<Order>> GetOrderAsync(string orderId, string? symbol = null, CancellationToken ct = default);
/// <summary>
/// Get trades for an order by id
/// </summary>
/// <param name="orderId">The id</param>
/// <param name="symbol">[Optional] The symbol the order is on, required for some exchanges, ignored otherwise</param>
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
/// <returns></returns>
Task<WebCallResult<IEnumerable<UserTrade>>> GetOrderTradesAsync(string orderId, string? symbol = null, CancellationToken ct = default);
/// <summary>
/// Get a list of open orders
/// </summary>
/// <param name="symbol">[Optional] The symbol to get open orders for, required for some exchanges, ignored otherwise</param>
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
/// <returns></returns>
Task<WebCallResult<IEnumerable<Order>>> GetOpenOrdersAsync(string? symbol = null, CancellationToken ct = default);
/// <summary>
/// Get a list of closed orders
/// </summary>
/// <param name="symbol">[Optional] The symbol to get closed orders for, required for some exchanges, ignored otherwise</param>
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
/// <returns></returns>
Task<WebCallResult<IEnumerable<Order>>> GetClosedOrdersAsync(string? symbol = null, CancellationToken ct = default);
/// <summary>
/// Cancel an order by id
/// </summary>
/// <param name="orderId">The id</param>
/// <param name="symbol">[Optional] The symbol the order is on, required for some exchanges, ignored otherwise</param>
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
/// <returns></returns>
Task<WebCallResult<OrderId>> CancelOrderAsync(string orderId, string? symbol = null, CancellationToken ct = default);
}
}

View File

@ -1,36 +0,0 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using CryptoExchange.Net.CommonObjects;
using CryptoExchange.Net.Objects;
namespace CryptoExchange.Net.Interfaces.CommonClients
{
/// <summary>
/// Common futures endpoints
/// </summary>
public interface IFuturesClient : IBaseRestClient
{
/// <summary>
/// Place an order
/// </summary>
/// <param name="symbol">The symbol the order is for</param>
/// <param name="side">The side of the order</param>
/// <param name="type">The type of the order</param>
/// <param name="quantity">The quantity of the order</param>
/// <param name="price">The price of the order, only for limit orders</param>
/// <param name="accountId">[Optional] The account id to place the order on, required for some exchanges, ignored otherwise</param>
/// <param name="leverage">[Optional] Leverage for this order. This is needed for some exchanges. For exchanges where this is not needed this parameter is ignored (and should be set before hand)</param>
/// <param name="clientOrderId">[Optional] Client specified id for this order</param>
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
/// <returns>The id of the resulting order</returns>
Task<WebCallResult<OrderId>> PlaceOrderAsync(string symbol, CommonOrderSide side, CommonOrderType type, decimal quantity, decimal? price = null, int? leverage = null, string? accountId = null, string? clientOrderId = null, CancellationToken ct = default);
/// <summary>
/// Get position
/// </summary>
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
/// <returns></returns>
Task<WebCallResult<IEnumerable<Position>>> GetPositionsAsync(CancellationToken ct = default);
}
}

View File

@ -1,27 +0,0 @@
using CryptoExchange.Net.CommonObjects;
using CryptoExchange.Net.Objects;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Interfaces.CommonClients
{
/// <summary>
/// Common spot endpoints
/// </summary>
public interface ISpotClient: IBaseRestClient
{
/// <summary>
/// Place an order
/// </summary>
/// <param name="symbol">The symbol the order is for</param>
/// <param name="side">The side of the order</param>
/// <param name="type">The type of the order</param>
/// <param name="quantity">The quantity of the order</param>
/// <param name="price">The price of the order, only for limit orders</param>
/// <param name="accountId">[Optional] The account id to place the order on, required for some exchanges, ignored otherwise</param>
/// <param name="clientOrderId">[Optional] Client specified id for this order</param>
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
/// <returns>The id of the resulting order</returns>
Task<WebCallResult<OrderId>> PlaceOrderAsync(string symbol, CommonOrderSide side, CommonOrderType type, decimal quantity, decimal? price = null, string? accountId = null, string? clientOrderId = null, CancellationToken ct = default);
}
}

View File

@ -1,4 +1,8 @@
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.SharedApis;
using System;
namespace CryptoExchange.Net.Interfaces
{
@ -12,13 +16,20 @@ namespace CryptoExchange.Net.Interfaces
/// </summary>
string BaseAddress { get; }
/// <summary>
/// Whether or not API credentials have been configured for this client. Does not check the credentials are actually valid.
/// </summary>
bool Authenticated { get; }
/// <summary>
/// Format a base and quote asset to an exchange accepted symbol
/// </summary>
/// <param name="baseAsset">The base asset</param>
/// <param name="quoteAsset">The quote asset</param>
/// <param name="tradingMode">The trading mode</param>
/// <param name="deliverDate">The deliver date for a delivery futures symbol</param>
/// <returns></returns>
string FormatSymbol(string baseAsset, string quoteAsset);
string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverDate = null);
/// <summary>
/// Set the API credentials for this API client
@ -26,5 +37,12 @@ namespace CryptoExchange.Net.Interfaces
/// <typeparam name="T"></typeparam>
/// <param name="credentials"></param>
void SetApiCredentials<T>(T credentials) where T : ApiCredentials;
/// <summary>
/// Set new options. Note that when using a proxy this should be provided in the options even when already set before or it will be reset.
/// </summary>
/// <typeparam name="T">Api credentials type</typeparam>
/// <param name="options">Options to set</param>
void SetOptions<T>(UpdateOptions<T> options) where T : ApiCredentials;
}
}

View File

@ -1,6 +1,4 @@
using CryptoExchange.Net.Interfaces.CommonClients;
using System;
using System.Collections.Generic;
using System;
namespace CryptoExchange.Net.Interfaces
{
@ -9,19 +7,6 @@ namespace CryptoExchange.Net.Interfaces
/// </summary>
public interface ICryptoRestClient
{
/// <summary>
/// Get a list of all registered common ISpotClient types
/// </summary>
/// <returns></returns>
IEnumerable<ISpotClient> GetSpotClients();
/// <summary>
/// Get an ISpotClient implementation by exchange name
/// </summary>
/// <param name="exchangeName"></param>
/// <returns></returns>
ISpotClient? SpotClient(string exchangeName);
/// <summary>
/// Try get
/// </summary>

View File

@ -52,7 +52,7 @@ namespace CryptoExchange.Net.Interfaces
/// <typeparam name="T"></typeparam>
/// <param name="path"></param>
/// <returns></returns>
List<T?>? GetValues<T>(MessagePath path);
T?[]? GetValues<T>(MessagePath path);
/// <summary>
/// Deserialize the message into this type
/// </summary>

View File

@ -10,6 +10,6 @@
/// </summary>
/// <param name="message"></param>
/// <returns></returns>
string Serialize(object message);
string Serialize<T>(T message);
}
}

View File

@ -1,4 +1,5 @@
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.SharedApis;
using System;
namespace CryptoExchange.Net.Interfaces
@ -23,5 +24,12 @@ namespace CryptoExchange.Net.Interfaces
/// <param name="options">Options for the order book</param>
/// <returns></returns>
public ISymbolOrderBook Create(string baseAsset, string quoteAsset, Action<TOptions>? options = null);
/// <summary>
/// Create a new order book by base and quote asset names
/// </summary>
/// <param name="symbol">Symbol</param>
/// <param name="options">Options for the order book</param>
/// <returns></returns>
public ISymbolOrderBook Create(SharedSymbol symbol, Action<TOptions>? options = null);
}
}

View File

@ -1,7 +1,6 @@
using CryptoExchange.Net.Objects;
using Microsoft.Extensions.Logging;
using System.Net.Http;
using System.Security;
using System.Threading;
using System.Threading.Tasks;

View File

@ -54,7 +54,7 @@ namespace CryptoExchange.Net.Interfaces
/// Get all headers
/// </summary>
/// <returns></returns>
Dictionary<string, IEnumerable<string>> GetHeaders();
KeyValuePair<string, string[]>[] GetHeaders();
/// <summary>
/// Get the response

View File

@ -24,6 +24,13 @@ namespace CryptoExchange.Net.Interfaces
/// <param name="requestTimeout">Request timeout to use</param>
/// <param name="httpClient">Optional shared http client instance</param>
/// <param name="proxy">Optional proxy to use when no http client is provided</param>
void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? httpClient=null);
void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? httpClient = null);
/// <summary>
/// Update settings
/// </summary>
/// <param name="proxy">Proxy to use</param>
/// <param name="requestTimeout">Request timeout to use</param>
void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout);
}
}

View File

@ -28,7 +28,7 @@ namespace CryptoExchange.Net.Interfaces
/// <summary>
/// The response headers
/// </summary>
IEnumerable<KeyValuePair<string, IEnumerable<string>>> ResponseHeaders { get; }
KeyValuePair<string, string[]>[] ResponseHeaders { get; }
/// <summary>
/// Get the response stream

View File

@ -19,7 +19,7 @@ namespace CryptoExchange.Net.Interfaces
/// </summary>
int CurrentSubscriptions { get; }
/// <summary>
/// Incoming data kpbs
/// Incoming data Kbps
/// </summary>
double IncomingKbps { get; }
/// <summary>
@ -62,7 +62,7 @@ namespace CryptoExchange.Net.Interfaces
Task UnsubscribeAsync(UpdateSubscription subscription);
/// <summary>
/// Prepare connections which can subsequently be used for sending websocket requests.
/// Prepare connections which can subsequently be used for sending websocket requests. Note that this is not required. If not prepared it will be initialized at the first websocket request.
/// </summary>
/// <returns></returns>
Task<CallResult> PrepareConnectionsAsync();

View File

@ -42,7 +42,7 @@ namespace CryptoExchange.Net.Interfaces
/// <summary>
/// Event when order book was updated. Be careful! It can generate a lot of events at high-liquidity markets
/// </summary>
event Action<(IEnumerable<ISymbolOrderBookEntry> Bids, IEnumerable<ISymbolOrderBookEntry> Asks)> OnOrderBookUpdate;
event Action<(ISymbolOrderBookEntry[] Bids, ISymbolOrderBookEntry[] Asks)> OnOrderBookUpdate;
/// <summary>
/// Event when the BestBid or BestAsk changes ie a Pricing Tick
/// </summary>
@ -64,17 +64,17 @@ namespace CryptoExchange.Net.Interfaces
/// <summary>
/// Get a snapshot of the book at this moment
/// </summary>
(IEnumerable<ISymbolOrderBookEntry> bids, IEnumerable<ISymbolOrderBookEntry> asks) Book { get; }
(ISymbolOrderBookEntry[] bids, ISymbolOrderBookEntry[] asks) Book { get; }
/// <summary>
/// The list of asks
/// </summary>
IEnumerable<ISymbolOrderBookEntry> Asks { get; }
ISymbolOrderBookEntry[] Asks { get; }
/// <summary>
/// The list of bids
/// </summary>
IEnumerable<ISymbolOrderBookEntry> Bids { get; }
ISymbolOrderBookEntry[] Bids { get; }
/// <summary>
/// The best bid currently in the order book
@ -105,7 +105,7 @@ namespace CryptoExchange.Net.Interfaces
Task StopAsync();
/// <summary>
/// Get the average price that a market order would fill at at the current order book state. This is no guarentee that an order of that quantity would actually be filled
/// Get the average price that a market order would fill at at the current order book state. This is no guarantee that an order of that quantity would actually be filled
/// at that price since between this calculation and the order placement the book might have changed.
/// </summary>
/// <param name="quantity">The quantity in base asset to fill</param>
@ -115,7 +115,7 @@ namespace CryptoExchange.Net.Interfaces
/// <summary>
/// Get the amount of base asset which can be traded with the quote quantity when placing a market order at at the current order book state.
/// This is no guarentee that an order of that quantity would actually be fill the quantity returned by this since between this calculation and the order placement the book might have changed.
/// This is no guarantee that an order of that quantity would actually be fill the quantity returned by this since between this calculation and the order placement the book might have changed.
/// </summary>
/// <param name="quoteQuantity">The quantity in quote asset looking to trade</param>
/// <param name="type">The type</param>

View File

@ -1,6 +1,7 @@
using CryptoExchange.Net.Objects;
using System;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Interfaces
@ -27,6 +28,10 @@ namespace CryptoExchange.Net.Interfaces
/// </summary>
event Func<int, Task>? OnRequestRateLimited;
/// <summary>
/// Connection was ratelimited and couldn't be established
/// </summary>
event Func<Task>? OnConnectRateLimited;
/// <summary>
/// Websocket error event
/// </summary>
event Func<Exception, Task> OnError;
@ -43,7 +48,7 @@ namespace CryptoExchange.Net.Interfaces
/// </summary>
event Func<Task> OnReconnected;
/// <summary>
/// Get reconntion url
/// Get reconnection url
/// </summary>
Func<Task<Uri?>>? GetReconnectionUrl { get; set; }
@ -71,7 +76,7 @@ namespace CryptoExchange.Net.Interfaces
/// Connect the socket
/// </summary>
/// <returns></returns>
Task<CallResult> ConnectAsync();
Task<CallResult> ConnectAsync(CancellationToken ct);
/// <summary>
/// Send data
/// </summary>
@ -89,5 +94,10 @@ namespace CryptoExchange.Net.Interfaces
/// </summary>
/// <returns></returns>
Task CloseAsync();
/// <summary>
/// Update proxy setting
/// </summary>
void UpdateProxy(ApiProxy? proxy);
}
}

View File

@ -0,0 +1,47 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net
{
/// <summary>
/// Helpers for client libraries
/// </summary>
public static class LibraryHelpers
{
/// <summary>
/// Client order id separator
/// </summary>
public const string ClientOrderIdSeparator = "JK";
/// <summary>
/// Apply broker id to a client order id
/// </summary>
/// <param name="clientOrderId"></param>
/// <param name="brokerId"></param>
/// <param name="maxLength"></param>
/// <param name="allowValueAdjustment"></param>
/// <returns></returns>
public static string ApplyBrokerId(string? clientOrderId, string brokerId, int maxLength, bool allowValueAdjustment)
{
var reservedLength = brokerId.Length + ClientOrderIdSeparator.Length;
if ((clientOrderId?.Length + reservedLength) > maxLength)
return clientOrderId!;
if (!string.IsNullOrEmpty(clientOrderId))
{
if (allowValueAdjustment)
clientOrderId = brokerId + ClientOrderIdSeparator + clientOrderId;
return clientOrderId!;
}
else
{
clientOrderId = ExchangeHelpers.AppendRandomString(brokerId + ClientOrderIdSeparator, maxLength);
}
return clientOrderId;
}
}
}

View File

@ -8,6 +8,7 @@ namespace CryptoExchange.Net.Logging.Extensions
{
private static readonly Action<ILogger, int, Exception?> _connecting;
private static readonly Action<ILogger, int, string, Exception?> _connectionFailed;
private static readonly Action<ILogger, int, Exception?> _connectingCanceled;
private static readonly Action<ILogger, int, Uri, Exception?> _connected;
private static readonly Action<ILogger, int, Exception?> _startingProcessing;
private static readonly Action<ILogger, int, Exception?> _finishedProcessing;
@ -25,6 +26,7 @@ namespace CryptoExchange.Net.Logging.Extensions
private static readonly Action<ILogger, int, string, Exception?> _sendLoopStoppedWithException;
private static readonly Action<ILogger, int, Exception?> _sendLoopFinished;
private static readonly Action<ILogger, int, string, string ,Exception?> _receivedCloseMessage;
private static readonly Action<ILogger, int, string, string ,Exception?> _receivedCloseConfirmation;
private static readonly Action<ILogger, int, int, Exception?> _receivedPartialMessage;
private static readonly Action<ILogger, int, int, Exception?> _receivedSingleMessage;
private static readonly Action<ILogger, int, long, Exception?> _reassembledMessage;
@ -32,7 +34,9 @@ namespace CryptoExchange.Net.Logging.Extensions
private static readonly Action<ILogger, int, Exception?> _receiveLoopStoppedWithException;
private static readonly Action<ILogger, int, Exception?> _receiveLoopFinished;
private static readonly Action<ILogger, int, TimeSpan?, Exception?> _startingTaskForNoDataReceivedCheck;
private static readonly Action<ILogger, int, TimeSpan?, Exception?> _noDataReceiveTimoutReconnect;
private static readonly Action<ILogger, int, TimeSpan?, Exception?> _noDataReceiveTimeoutReconnect;
private static readonly Action<ILogger, int, string, string, Exception?> _socketProcessingStateChanged;
private static readonly Action<ILogger, int, Exception?> _socketPingTimeout;
static CryptoExchangeWebSocketClientLoggingExtension()
{
@ -166,10 +170,32 @@ namespace CryptoExchange.Net.Logging.Extensions
new EventId(1026, "StartingTaskForNoDataReceivedCheck"),
"[Sckt {SocketId}] starting task checking for no data received for {Timeout}");
_noDataReceiveTimoutReconnect = LoggerMessage.Define<int, TimeSpan?>(
LogLevel.Debug,
_noDataReceiveTimeoutReconnect = LoggerMessage.Define<int, TimeSpan?>(
LogLevel.Warning,
new EventId(1027, "NoDataReceiveTimeoutReconnect"),
"[Sckt {SocketId}] no data received for {Timeout}, reconnecting socket");
_receivedCloseConfirmation = LoggerMessage.Define<int, string, string>(
LogLevel.Debug,
new EventId(1028, "ReceivedCloseMessage"),
"[Sckt {SocketId}] received `Close` message confirming our close request, CloseStatus: {CloseStatus}, CloseStatusDescription: {CloseStatusDescription}");
_socketProcessingStateChanged = LoggerMessage.Define<int, string, string>(
LogLevel.Trace,
new EventId(1029, "SocketProcessingStateChanged"),
"[Sckt {Id}] processing state change: {PreviousState} -> {NewState}");
_socketPingTimeout = LoggerMessage.Define<int>(
LogLevel.Warning,
new EventId(1030, "SocketPingTimeout"),
"[Sckt {Id}] ping frame timeout; reconnecting socket");
_connectingCanceled = LoggerMessage.Define<int>(
LogLevel.Debug,
new EventId(1031, "ConnectingCanceled"),
"[Sckt {SocketId}] connecting canceled");
}
public static void SocketConnecting(
@ -286,6 +312,12 @@ namespace CryptoExchange.Net.Logging.Extensions
_receivedCloseMessage(logger, socketId, webSocketCloseStatus, closeStatusDescription, null);
}
public static void SocketReceivedCloseConfirmation(
this ILogger logger, int socketId, string webSocketCloseStatus, string closeStatusDescription)
{
_receivedCloseConfirmation(logger, socketId, webSocketCloseStatus, closeStatusDescription, null);
}
public static void SocketReceivedPartialMessage(
this ILogger logger, int socketId, int countBytes)
{
@ -331,7 +363,25 @@ namespace CryptoExchange.Net.Logging.Extensions
public static void SocketNoDataReceiveTimoutReconnect(
this ILogger logger, int socketId, TimeSpan? timeSpan)
{
_noDataReceiveTimoutReconnect(logger, socketId, timeSpan, null);
_noDataReceiveTimeoutReconnect(logger, socketId, timeSpan, null);
}
public static void SocketProcessingStateChanged(
this ILogger logger, int socketId, string prevState, string newState)
{
_socketProcessingStateChanged(logger, socketId, prevState, newState, null);
}
public static void SocketPingTimeout(
this ILogger logger, int socketId)
{
_socketPingTimeout(logger, socketId, null);
}
public static void SocketConnectingCanceled(
this ILogger logger, int socketId)
{
_connectingCanceled(logger, socketId, null);
}
}
}

View File

@ -9,7 +9,7 @@ namespace CryptoExchange.Net.Logging.Extensions
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
public static class RestApiClientLoggingExtensions
{
private static readonly Action<ILogger, int?, int?, long, string?, Exception?> _restApiErrorReceived;
private static readonly Action<ILogger, int?, int?, long, string?, string?, Exception?> _restApiErrorReceived;
private static readonly Action<ILogger, int?, int?, long, string?, Exception?> _restApiResponseReceived;
private static readonly Action<ILogger, int, string, Exception?> _restApiFailedToSyncTime;
private static readonly Action<ILogger, int, string, Exception?> _restApiNoApiCredentials;
@ -21,14 +21,14 @@ namespace CryptoExchange.Net.Logging.Extensions
private static readonly Action<ILogger, string, Exception?> _restApiCheckingCache;
private static readonly Action<ILogger, string, Exception?> _restApiCacheHit;
private static readonly Action<ILogger, string, Exception?> _restApiCacheNotHit;
private static readonly Action<ILogger, int?, Exception?> _restApiCancellationRequested;
static RestApiClientLoggingExtensions()
{
_restApiErrorReceived = LoggerMessage.Define<int?, int?, long, string?>(
_restApiErrorReceived = LoggerMessage.Define<int?, int?, long, string?, string?>(
LogLevel.Warning,
new EventId(4000, "RestApiErrorReceived"),
"[Req {RequestId}] {ResponseStatusCode} - Error received in {ResponseTime}ms: {ErrorMessage}");
"[Req {RequestId}] {ResponseStatusCode} - Error received in {ResponseTime}ms: {ErrorMessage}, Data: {OriginalData}");
_restApiResponseReceived = LoggerMessage.Define<int?, int?, long, string?>(
LogLevel.Debug,
@ -37,7 +37,7 @@ namespace CryptoExchange.Net.Logging.Extensions
_restApiFailedToSyncTime = LoggerMessage.Define<int, string>(
LogLevel.Debug,
new EventId(4002, "RestApifailedToSyncTime"),
new EventId(4002, "RestApiFailedToSyncTime"),
"[Req {RequestId}] Failed to sync time, aborting request: {ErrorMessage}");
_restApiNoApiCredentials = LoggerMessage.Define<int, string>(
@ -84,11 +84,17 @@ namespace CryptoExchange.Net.Logging.Extensions
LogLevel.Trace,
new EventId(4011, "RestApiCacheNotHit"),
"Cache not hit for key {Key}");
_restApiCancellationRequested = LoggerMessage.Define<int?>(
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)
public static void RestApiErrorReceived(this ILogger logger, int? requestId, HttpStatusCode? responseStatusCode, long responseTime, string? error, string? originalData, Exception? exception)
{
_restApiErrorReceived(logger, requestId, (int?)responseStatusCode, responseTime, error, null);
_restApiErrorReceived(logger, requestId, (int?)responseStatusCode, responseTime, error, originalData, exception);
}
public static void RestApiResponseReceived(this ILogger logger, int? requestId, HttpStatusCode? responseStatusCode, long responseTime, string? originalData)
@ -145,5 +151,9 @@ namespace CryptoExchange.Net.Logging.Extensions
{
_restApiCacheNotHit(logger, key, null);
}
public static void RestApiCancellationRequested(this ILogger logger, int? requestId)
{
_restApiCancellationRequested(logger, requestId, null);
}
}
}

View File

@ -22,6 +22,7 @@ namespace CryptoExchange.Net.Logging.Extensions
private static readonly Action<ILogger, Exception?> _disposingSocketClient;
private static readonly Action<ILogger, int, int, Exception?> _unsubscribingSubscription;
private static readonly Action<ILogger, int, Exception?> _reconnectingAllConnections;
private static readonly Action<ILogger, DateTime, Exception?> _addingRetryAfterGuard;
static SocketApiClientLoggingExtension()
{
@ -104,6 +105,11 @@ namespace CryptoExchange.Net.Logging.Extensions
LogLevel.Information,
new EventId(3017, "ReconnectingAll"),
"Reconnecting all {ConnectionCount} connections");
_addingRetryAfterGuard = LoggerMessage.Define<DateTime>(
LogLevel.Warning,
new EventId(3018, "AddRetryAfterGuard"),
"Adding RetryAfterGuard ({RetryAfter}) because the connection attempt was rate limited");
}
public static void FailedToAddSubscriptionRetryOnDifferentConnection(this ILogger logger, int socketId)
@ -185,5 +191,10 @@ namespace CryptoExchange.Net.Logging.Extensions
{
_reconnectingAllConnections(logger, connectionCount, null);
}
public static void AddingRetryAfterGuard(this ILogger logger, DateTime retryAfter)
{
_addingRetryAfterGuard(logger, retryAfter, null);
}
}
}

View File

@ -10,7 +10,7 @@ namespace CryptoExchange.Net.Logging.Extensions
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, string?, Exception?> _failedReconnectProcessing;
private static readonly Action<ILogger, int, Exception?> _unkownExceptionWhileProcessingReconnection;
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;
@ -28,7 +28,7 @@ namespace CryptoExchange.Net.Logging.Extensions
private static readonly Action<ILogger, int, Exception?> _closingNoMoreSubscriptions;
private static readonly Action<ILogger, int, int, int, Exception?> _addingNewSubscription;
private static readonly Action<ILogger, int, Exception?> _nothingToResubscribeCloseConnection;
private static readonly Action<ILogger, int, Exception?> _failedAuthenticationDisconnectAndRecoonect;
private static readonly Action<ILogger, int, Exception?> _failedAuthenticationDisconnectAndReconnect;
private static readonly Action<ILogger, int, Exception?> _authenticationSucceeded;
private static readonly Action<ILogger, int, string?, Exception?> _failedRequestRevitalization;
private static readonly Action<ILogger, int, Exception?> _allSubscriptionResubscribed;
@ -55,15 +55,15 @@ namespace CryptoExchange.Net.Logging.Extensions
new EventId(2002, "FailedReconnectProcessing"),
"[Sckt {SocketId}] failed reconnect processing: {ErrorMessage}, reconnecting again");
_unkownExceptionWhileProcessingReconnection = LoggerMessage.Define<int>(
_unknownExceptionWhileProcessingReconnection = LoggerMessage.Define<int>(
LogLevel.Warning,
new EventId(2003, "UnkownExceptionWhileProcessingReconnection"),
new EventId(2003, "UnknownExceptionWhileProcessingReconnection"),
"[Sckt {SocketId}] Unknown exception while processing reconnection, reconnecting again");
_webSocketErrorCodeAndDetails = LoggerMessage.Define<int, WebSocketError, string?>(
LogLevel.Warning,
new EventId(2004, "WebSocketErrorCode"),
"[Sckt {SocketId}] error: Websocket error code {WebSocketErrorCdoe}, details: {Details}");
"[Sckt {SocketId}] error: Websocket error code {WebSocketErrorCode}, details: {Details}");
_webSocketError = LoggerMessage.Define<int, string?>(
LogLevel.Warning,
@ -145,7 +145,7 @@ namespace CryptoExchange.Net.Logging.Extensions
new EventId(2020, "NothingToResubscribe"),
"[Sckt {SocketId}] nothing to resubscribe, closing connection");
_failedAuthenticationDisconnectAndRecoonect = LoggerMessage.Define<int>(
_failedAuthenticationDisconnectAndReconnect = LoggerMessage.Define<int>(
LogLevel.Warning,
new EventId(2021, "FailedAuthentication"),
"[Sckt {SocketId}] authentication failed on reconnected socket. Disconnecting and reconnecting");
@ -183,7 +183,7 @@ namespace CryptoExchange.Net.Logging.Extensions
_sendingData = LoggerMessage.Define<int, int, string>(
LogLevel.Trace,
new EventId(2028, "SendingData"),
"[Sckt {SocketId}] [Req {RequestId}] sending messsage: {Data}");
"[Sckt {SocketId}] [Req {RequestId}] sending message: {Data}");
_receivedMessageNotMatchedToAnyListener = LoggerMessage.Define<int, string, string>(
LogLevel.Warning,
@ -206,9 +206,9 @@ namespace CryptoExchange.Net.Logging.Extensions
_failedReconnectProcessing(logger, socketId, error, null);
}
public static void UnkownExceptionWhileProcessingReconnection(this ILogger logger, int socketId, Exception e)
public static void UnknownExceptionWhileProcessingReconnection(this ILogger logger, int socketId, Exception e)
{
_unkownExceptionWhileProcessingReconnection(logger, socketId, e);
_unknownExceptionWhileProcessingReconnection(logger, socketId, e);
}
public static void WebSocketErrorCodeAndDetails(this ILogger logger, int socketId, WebSocketError error, string? details, Exception e)
@ -246,9 +246,9 @@ namespace CryptoExchange.Net.Logging.Extensions
{
_receivedMessageNotRecognized(logger, socketId, id, null);
}
public static void FailedToDeserializeMessage(this ILogger logger, int socketId, string? errorMessage)
public static void FailedToDeserializeMessage(this ILogger logger, int socketId, string? errorMessage, Exception? ex)
{
_failedToDeserializeMessage(logger, socketId, errorMessage, null);
_failedToDeserializeMessage(logger, socketId, errorMessage, ex);
}
public static void UserMessageProcessingFailed(this ILogger logger, int socketId, string errorMessage, Exception e)
{
@ -285,7 +285,7 @@ namespace CryptoExchange.Net.Logging.Extensions
}
public static void FailedAuthenticationDisconnectAndRecoonect(this ILogger logger, int socketId)
{
_failedAuthenticationDisconnectAndRecoonect(logger, socketId, null);
_failedAuthenticationDisconnectAndReconnect(logger, socketId, null);
}
public static void AuthenticationSucceeded(this ILogger logger, int socketId)
{

View File

@ -53,7 +53,7 @@ namespace CryptoExchange.Net.Logging.Extensions
"{Api} order book {Symbol} connection lost");
_orderBookDisconnected = LoggerMessage.Define<string, string>(
LogLevel.Warning,
LogLevel.Debug,
new EventId(5004, "OrderBookDisconnected"),
"{Api} order book {Symbol} disconnected");
@ -62,7 +62,6 @@ namespace CryptoExchange.Net.Logging.Extensions
new EventId(5005, "OrderBookStopping"),
"{Api} order book {Symbol} stopping");
_orderBookStopped = LoggerMessage.Define<string, string>(
LogLevel.Trace,
new EventId(5006, "OrderBookStopped"),

View File

@ -0,0 +1,291 @@
using System;
using CryptoExchange.Net.Objects;
using Microsoft.Extensions.Logging;
namespace CryptoExchange.Net.Logging.Extensions
{
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
public static class TrackerLoggingExtensions
{
private static readonly Action<ILogger, string, SyncStatus, SyncStatus, Exception?> _klineTrackerStatusChanged;
private static readonly Action<ILogger, string, Exception?> _klineTrackerStarting;
private static readonly Action<ILogger, string, string, Exception?> _klineTrackerStartFailed;
private static readonly Action<ILogger, string, Exception?> _klineTrackerStarted;
private static readonly Action<ILogger, string, Exception?> _klineTrackerStopping;
private static readonly Action<ILogger, string, Exception?> _klineTrackerStopped;
private static readonly Action<ILogger, string, DateTime, Exception?> _klineTrackerInitialDataSet;
private static readonly Action<ILogger, string, DateTime, Exception?> _klineTrackerKlineUpdated;
private static readonly Action<ILogger, string, DateTime, Exception?> _klineTrackerKlineAdded;
private static readonly Action<ILogger, string, Exception?> _klineTrackerConnectionLost;
private static readonly Action<ILogger, string, Exception?> _klineTrackerConnectionClosed;
private static readonly Action<ILogger, string, Exception?> _klineTrackerConnectionRestored;
private static readonly Action<ILogger, string, SyncStatus, SyncStatus, Exception?> _tradeTrackerStatusChanged;
private static readonly Action<ILogger, string, Exception?> _tradeTrackerStarting;
private static readonly Action<ILogger, string, string, Exception?> _tradeTrackerStartFailed;
private static readonly Action<ILogger, string, Exception?> _tradeTrackerStarted;
private static readonly Action<ILogger, string, Exception?> _tradeTrackerStopping;
private static readonly Action<ILogger, string, Exception?> _tradeTrackerStopped;
private static readonly Action<ILogger, string, int, long, Exception?> _tradeTrackerInitialDataSet;
private static readonly Action<ILogger, string, long, Exception?> _tradeTrackerPreSnapshotSkip;
private static readonly Action<ILogger, string, long, Exception?> _tradeTrackerPreSnapshotApplied;
private static readonly Action<ILogger, string, long, Exception?> _tradeTrackerTradeAdded;
private static readonly Action<ILogger, string, Exception?> _tradeTrackerConnectionLost;
private static readonly Action<ILogger, string, Exception?> _tradeTrackerConnectionClosed;
private static readonly Action<ILogger, string, Exception?> _tradeTrackerConnectionRestored;
static TrackerLoggingExtensions()
{
_klineTrackerStatusChanged = LoggerMessage.Define<string, SyncStatus, SyncStatus>(
LogLevel.Debug,
new EventId(6001, "KlineTrackerStatusChanged"),
"Kline tracker for {Symbol} status changed: {OldStatus} => {NewStatus}");
_klineTrackerStarting = LoggerMessage.Define<string>(
LogLevel.Debug,
new EventId(6002, "KlineTrackerStarting"),
"Kline tracker for {Symbol} starting");
_klineTrackerStartFailed = LoggerMessage.Define<string, string>(
LogLevel.Warning,
new EventId(6003, "KlineTrackerStartFailed"),
"Kline tracker for {Symbol} failed to start: {Error}");
_klineTrackerStarted = LoggerMessage.Define<string>(
LogLevel.Information,
new EventId(6004, "KlineTrackerStarted"),
"Kline tracker for {Symbol} started");
_klineTrackerStopping = LoggerMessage.Define<string>(
LogLevel.Debug,
new EventId(6005, "KlineTrackerStopping"),
"Kline tracker for {Symbol} stopping");
_klineTrackerStopped = LoggerMessage.Define<string>(
LogLevel.Information,
new EventId(6006, "KlineTrackerStopped"),
"Kline tracker for {Symbol} stopped");
_klineTrackerInitialDataSet = LoggerMessage.Define<string, DateTime>(
LogLevel.Debug,
new EventId(6007, "KlineTrackerInitialDataSet"),
"Kline tracker for {Symbol} initial data set, last timestamp: {LastTime}");
_klineTrackerKlineUpdated = LoggerMessage.Define<string, DateTime>(
LogLevel.Trace,
new EventId(6008, "KlineTrackerKlineUpdated"),
"Kline tracker for {Symbol} kline updated for open time: {LastTime}");
_klineTrackerKlineAdded = LoggerMessage.Define<string, DateTime>(
LogLevel.Trace,
new EventId(6009, "KlineTrackerKlineAdded"),
"Kline tracker for {Symbol} new kline for open time: {LastTime}");
_klineTrackerConnectionLost = LoggerMessage.Define<string>(
LogLevel.Warning,
new EventId(6010, "KlineTrackerConnectionLost"),
"Kline tracker for {Symbol} connection lost");
_klineTrackerConnectionClosed = LoggerMessage.Define<string>(
LogLevel.Warning,
new EventId(6011, "KlineTrackerConnectionClosed"),
"Kline tracker for {Symbol} disconnected");
_klineTrackerConnectionRestored = LoggerMessage.Define<string>(
LogLevel.Information,
new EventId(6012, "KlineTrackerConnectionRestored"),
"Kline tracker for {Symbol} successfully resynchronized");
_tradeTrackerStatusChanged = LoggerMessage.Define<string, SyncStatus, SyncStatus>(
LogLevel.Debug,
new EventId(6013, "KlineTrackerStatusChanged"),
"Trade tracker for {Symbol} status changed: {OldStatus} => {NewStatus}");
_tradeTrackerStarting = LoggerMessage.Define<string>(
LogLevel.Debug,
new EventId(6014, "KlineTrackerStarting"),
"Trade tracker for {Symbol} starting");
_tradeTrackerStartFailed = LoggerMessage.Define<string, string>(
LogLevel.Warning,
new EventId(6015, "KlineTrackerStartFailed"),
"Trade tracker for {Symbol} failed to start: {Error}");
_tradeTrackerStarted = LoggerMessage.Define<string>(
LogLevel.Information,
new EventId(6016, "KlineTrackerStarted"),
"Trade tracker for {Symbol} started");
_tradeTrackerStopping = LoggerMessage.Define<string>(
LogLevel.Debug,
new EventId(6017, "KlineTrackerStopping"),
"Trade tracker for {Symbol} stopping");
_tradeTrackerStopped = LoggerMessage.Define<string>(
LogLevel.Information,
new EventId(6018, "KlineTrackerStopped"),
"Trade tracker for {Symbol} stopped");
_tradeTrackerInitialDataSet = LoggerMessage.Define<string, int, long>(
LogLevel.Debug,
new EventId(6019, "TradeTrackerInitialDataSet"),
"Trade tracker for {Symbol} snapshot set, Count: {Count}, Last id: {LastId}");
_tradeTrackerPreSnapshotSkip = LoggerMessage.Define<string, long>(
LogLevel.Trace,
new EventId(6020, "TradeTrackerPreSnapshotSkip"),
"Trade tracker for {Symbol} skipping {Id}, already in snapshot");
_tradeTrackerPreSnapshotApplied = LoggerMessage.Define<string, long>(
LogLevel.Trace,
new EventId(6021, "TradeTrackerPreSnapshotApplied"),
"Trade tracker for {Symbol} adding {Id} from pre-snapshot");
_tradeTrackerTradeAdded = LoggerMessage.Define<string, long>(
LogLevel.Trace,
new EventId(6022, "TradeTrackerTradeAdded"),
"Trade tracker for {Symbol} adding trade {Id}");
_tradeTrackerConnectionLost = LoggerMessage.Define<string>(
LogLevel.Warning,
new EventId(6023, "TradeTrackerConnectionLost"),
"Trade tracker for {Symbol} connection lost");
_tradeTrackerConnectionClosed = LoggerMessage.Define<string>(
LogLevel.Warning,
new EventId(6024, "TradeTrackerConnectionClosed"),
"Trade tracker for {Symbol} disconnected");
_tradeTrackerConnectionRestored = LoggerMessage.Define<string>(
LogLevel.Information,
new EventId(6025, "TradeTrackerConnectionRestored"),
"Trade tracker for {Symbol} successfully resynchronized");
}
public static void KlineTrackerStatusChanged(this ILogger logger, string symbol, SyncStatus oldStatus, SyncStatus newStatus)
{
_klineTrackerStatusChanged(logger, symbol, oldStatus, newStatus, null);
}
public static void KlineTrackerStarting(this ILogger logger, string symbol)
{
_klineTrackerStarting(logger, symbol, null);
}
public static void KlineTrackerStartFailed(this ILogger logger, string symbol, string error, Exception? exception)
{
_klineTrackerStartFailed(logger, symbol, error, exception);
}
public static void KlineTrackerStarted(this ILogger logger, string symbol)
{
_klineTrackerStarted(logger, symbol, null);
}
public static void KlineTrackerStopping(this ILogger logger, string symbol)
{
_klineTrackerStopping(logger, symbol, null);
}
public static void KlineTrackerStopped(this ILogger logger, string symbol)
{
_klineTrackerStopped(logger, symbol, null);
}
public static void KlineTrackerInitialDataSet(this ILogger logger, string symbol, DateTime lastTime)
{
_klineTrackerInitialDataSet(logger, symbol, lastTime, null);
}
public static void KlineTrackerKlineUpdated(this ILogger logger, string symbol, DateTime lastTime)
{
_klineTrackerKlineUpdated(logger, symbol, lastTime, null);
}
public static void KlineTrackerKlineAdded(this ILogger logger, string symbol, DateTime lastTime)
{
_klineTrackerKlineAdded(logger, symbol, lastTime, null);
}
public static void KlineTrackerConnectionLost(this ILogger logger, string symbol)
{
_klineTrackerConnectionLost(logger, symbol, null);
}
public static void KlineTrackerConnectionClosed(this ILogger logger, string symbol)
{
_klineTrackerConnectionClosed(logger, symbol, null);
}
public static void KlineTrackerConnectionRestored(this ILogger logger, string symbol)
{
_klineTrackerConnectionRestored(logger, symbol, null);
}
public static void TradeTrackerStatusChanged(this ILogger logger, string symbol, SyncStatus oldStatus, SyncStatus newStatus)
{
_tradeTrackerStatusChanged(logger, symbol, oldStatus, newStatus, null);
}
public static void TradeTrackerStarting(this ILogger logger, string symbol)
{
_tradeTrackerStarting(logger, symbol, null);
}
public static void TradeTrackerStartFailed(this ILogger logger, string symbol, string error, Exception? ex)
{
_tradeTrackerStartFailed(logger, symbol, error, ex);
}
public static void TradeTrackerStarted(this ILogger logger, string symbol)
{
_tradeTrackerStarted(logger, symbol, null);
}
public static void TradeTrackerStopping(this ILogger logger, string symbol)
{
_tradeTrackerStopping(logger, symbol, null);
}
public static void TradeTrackerStopped(this ILogger logger, string symbol)
{
_tradeTrackerStopped(logger, symbol, null);
}
public static void TradeTrackerInitialDataSet(this ILogger logger, string symbol, int count, long lastId)
{
_tradeTrackerInitialDataSet(logger, symbol, count, lastId, null);
}
public static void TradeTrackerPreSnapshotSkip(this ILogger logger, string symbol, long lastId)
{
_tradeTrackerPreSnapshotSkip(logger, symbol, lastId, null);
}
public static void TradeTrackerPreSnapshotApplied(this ILogger logger, string symbol, long lastId)
{
_tradeTrackerPreSnapshotApplied(logger, symbol, lastId, null);
}
public static void TradeTrackerTradeAdded(this ILogger logger, string symbol, long lastId)
{
_tradeTrackerTradeAdded(logger, symbol, lastId, null);
}
public static void TradeTrackerConnectionLost(this ILogger logger, string symbol)
{
_tradeTrackerConnectionLost(logger, symbol, null);
}
public static void TradeTrackerConnectionClosed(this ILogger logger, string symbol)
{
_tradeTrackerConnectionClosed(logger, symbol, null);
}
public static void TradeTrackerConnectionRestored(this ILogger logger, string symbol)
{
_tradeTrackerConnectionRestored(logger, symbol, null);
}
}
}

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