From 4c953e2c871b113aa4bba4ce2a3910f4bdb783ed Mon Sep 17 00:00:00 2001 From: JKorf Date: Sun, 24 Aug 2025 21:58:52 +0200 Subject: [PATCH] wip --- .../CryptoExchange.Net.UnitTests.csproj | 4 + CryptoExchange.Net/.editorconfig | 182 ++ CryptoExchange.Net/AssemblyInfo.cs | 9 +- .../Attributes/JsonConversionAttribute.cs | 17 +- CryptoExchange.Net/Attributes/MapAttribute.cs | 35 +- .../Authentication/ApiCredentials.cs | 98 +- .../Authentication/ApiCredentialsType.cs | 33 +- .../Authentication/AuthenticationProvider.cs | 908 +++---- .../Authentication/SignOutputType.cs | 25 +- CryptoExchange.Net/Caching/MemoryCache.cs | 85 +- CryptoExchange.Net/Clients/BaseApiClient.cs | 217 +- CryptoExchange.Net/Clients/BaseClient.cs | 198 +- CryptoExchange.Net/Clients/BaseRestClient.cs | 35 +- .../Clients/BaseSocketClient.cs | 224 +- .../Clients/CryptoBaseClient.cs | 103 +- .../Clients/CryptoRestClient.cs | 36 +- .../Clients/CryptoSocketClient.cs | 37 +- CryptoExchange.Net/Clients/RestApiClient.cs | 1277 +++++----- CryptoExchange.Net/Clients/SocketApiClient.cs | 1575 ++++++------ .../Converters/ArrayPropertyAttribute.cs | 35 +- .../Converters/JsonSerializerContextCache.cs | 39 +- .../Converters/MessageParsing/MessageNode.cs | 85 +- .../Converters/MessageParsing/MessagePath.cs | 79 +- .../MessageParsing/MessagePathExtension.cs | 69 +- .../Converters/MessageParsing/NodeType.cs | 33 +- .../SystemTextJson/ArrayConverter.cs | 348 ++- .../SystemTextJson/BigDecimalConverter.cs | 57 +- .../SystemTextJson/BoolConverter.cs | 149 +- .../SystemTextJson/CommaSplitEnumConverter.cs | 45 +- .../SystemTextJson/DateTimeConverter.cs | 463 ++-- .../SystemTextJson/DecimalConverter.cs | 64 +- .../DecimalStringWriterConverter.cs | 31 +- .../SystemTextJson/EnumConverter.cs | 437 ++-- .../SystemTextJson/EnumIntWriterConverter.cs | 32 +- .../INullableConverterFactory.cs | 11 +- .../Converters/SystemTextJson/IntConverter.cs | 53 +- .../SystemTextJson/LongConverter.cs | 53 +- .../NullableEnumConverterFactory.cs | 59 +- .../SystemTextJson/NumberStringConverter.cs | 57 +- .../SystemTextJson/ObjectStringConverter.cs | 65 +- .../SystemTextJson/ReplaceConverter.cs | 59 +- .../SystemTextJson/SerializationModel.cs | 33 +- .../SystemTextJson/SerializerOptions.cs | 63 +- .../SystemTextJson/SharedQuantityConverter.cs | 87 +- .../SystemTextJson/SharedSymbolConverter.cs | 63 +- .../SystemTextJsonMessageAccessor.cs | 650 ++--- .../SystemTextJsonMessageSerializer.cs | 49 +- CryptoExchange.Net/CryptoExchange.Net.csproj | 10 + CryptoExchange.Net/ExchangeHelpers.cs | 679 ++--- CryptoExchange.Net/ExchangeSymbolCache.cs | 100 +- CryptoExchange.Net/ExtensionMethods.cs | 916 ++++--- .../Interfaces/IAuthTimeProvider.cs | 21 +- .../Interfaces/IBaseApiClient.cs | 70 +- .../Interfaces/ICryptoRestClient.cs | 23 +- .../Interfaces/ICryptoSocketClient.cs | 23 +- .../Interfaces/IMessageAccessor.cs | 186 +- .../Interfaces/IMessageProcessor.cs | 50 +- .../Interfaces/IMessageSerializer.cs | 61 +- .../Interfaces/INonceProvider.cs | 19 +- .../Interfaces/IOrderBookFactory.cs | 55 +- CryptoExchange.Net/Interfaces/IRateLimiter.cs | 35 +- CryptoExchange.Net/Interfaces/IRequest.cs | 107 +- .../Interfaces/IRequestFactory.cs | 53 +- CryptoExchange.Net/Interfaces/IResponse.cs | 61 +- .../Interfaces/IRestApiClient.cs | 25 +- CryptoExchange.Net/Interfaces/IRestClient.cs | 35 +- .../Interfaces/ISocketApiClient.cs | 121 +- .../Interfaces/ISocketClient.cs | 87 +- .../Interfaces/ISymbolOrderBook.cs | 212 +- .../Interfaces/ISymbolOrderBookEntry.cs | 43 +- CryptoExchange.Net/Interfaces/IWebsocket.cs | 197 +- .../Interfaces/IWebsocketFactory.cs | 25 +- CryptoExchange.Net/LibraryHelpers.cs | 67 +- ...ExchangeWebSocketClientLoggingExtension.cs | 635 +++-- .../RateLimitGateLoggingExtensions.cs | 123 +- .../RestApiClientLoggingExtensions.cs | 249 +- .../SocketApiClientLoggingExtension.cs | 321 ++- .../SocketConnectionLoggingExtension.cs | 591 +++-- .../SymbolOrderBookLoggingExtensions.cs | 393 ++- .../Extensions/TrackerLoggingExtensions.cs | 467 ++-- CryptoExchange.Net/Objects/ApiProxy.cs | 71 +- CryptoExchange.Net/Objects/AssetAlias.cs | 41 +- .../Objects/AssetAliasConfiguration.cs | 44 +- .../Objects/AsyncAutoResetEvent.cs | 197 +- .../Objects/AuthTimeProvider.cs | 11 +- .../Objects/ByteOrderComparer.cs | 89 +- CryptoExchange.Net/Objects/CallResult.cs | 1133 +++++---- CryptoExchange.Net/Objects/Constants.cs | 33 +- CryptoExchange.Net/Objects/Enums.cs | 522 ++-- CryptoExchange.Net/Objects/Error.cs | 622 +++-- .../Objects/Errors/ErrorEvaluator.cs | 61 +- .../Objects/Errors/ErrorInfo.cs | 97 +- .../Objects/Errors/ErrorMapping.cs | 77 +- .../Objects/Errors/ErrorType.cs | 301 ++- .../Objects/Options/ApiOptions.cs | 27 +- .../Objects/Options/ExchangeOptions.cs | 75 +- .../Objects/Options/LibraryOptions.cs | 97 +- .../Objects/Options/OrderBookOptions.cs | 37 +- .../Objects/Options/RestApiOptions.cs | 75 +- .../Objects/Options/RestExchangeOptions.cs | 153 +- .../Objects/Options/SocketApiOptions.cs | 77 +- .../Objects/Options/SocketExchangeOptions.cs | 231 +- .../Objects/Options/UpdateOptions.cs | 43 +- .../Objects/OrderedStringComparer.cs | 39 +- .../Objects/ParameterCollection.cs | 403 +-- .../Objects/RequestDefinition.cs | 159 +- .../Objects/RequestDefinitionCache.cs | 195 +- .../Objects/RestRequestConfiguration.cs | 219 +- .../Objects/Sockets/DataEvent.cs | 343 ++- .../Objects/Sockets/UpdateSubscription.cs | 229 +- .../Objects/Sockets/WebSocketParameters.cs | 155 +- CryptoExchange.Net/Objects/TimeSyncState.cs | 153 +- CryptoExchange.Net/Objects/TraceLogger.cs | 91 +- .../Objects/TradeEnvironment.cs | 55 +- .../OrderBook/OrderBookFactory.cs | 51 +- .../OrderBook/ProcessBufferEntry.cs | 44 +- .../OrderBook/ProcessQueueItem.cs | 44 +- .../OrderBook/SymbolOrderBook.cs | 1505 ++++++------ .../Filters/AuthenticatedEndpointFilter.cs | 35 +- .../RateLimiting/Filters/ExactPathFilter.cs | 35 +- .../RateLimiting/Filters/ExactPathsFilter.cs | 35 +- .../RateLimiting/Filters/HostFilter.cs | 37 +- .../Filters/LimitItemTypeFilter.cs | 35 +- .../RateLimiting/Filters/PathStartFilter.cs | 35 +- .../RateLimiting/Guards/RateLimitGuard.cs | 249 +- .../RateLimiting/Guards/RetryAfterGuard.cs | 119 +- .../RateLimiting/Guards/SingleLimitGuard.cs | 147 +- .../RateLimiting/Interfaces/IGuardFilter.cs | 29 +- .../RateLimiting/Interfaces/IRateLimitGate.cs | 125 +- .../Interfaces/IRateLimitGuard.cs | 71 +- .../RateLimiting/Interfaces/IWindowTracker.cs | 57 +- CryptoExchange.Net/RateLimiting/LimitCheck.cs | 105 +- CryptoExchange.Net/RateLimiting/LimitEntry.cs | 45 +- .../RateLimiting/RateLimitEvent.cs | 133 +- .../RateLimiting/RateLimitGate.cs | 303 +-- .../RateLimiting/RateLimitItemType.cs | 29 +- .../RateLimiting/RateLimitState.cs | 95 +- .../RateLimiting/RateLimitUpdateEvent.cs | 82 +- .../Trackers/DecayWindowTracker.cs | 141 +- .../Trackers/FixedAfterStartWindowTracker.cs | 167 +- .../Trackers/FixedWindowTracker.cs | 163 +- .../Trackers/SlidingWindowTracker.cs | 171 +- CryptoExchange.Net/Requests/Request.cs | 125 +- CryptoExchange.Net/Requests/RequestFactory.cs | 99 +- CryptoExchange.Net/Requests/Response.cs | 71 +- .../SharedApis/Enums/SharedFeeAssetType.cs | 49 +- .../Enums/SharedFeeDeductionType.cs | 25 +- .../SharedApis/Enums/SharedKlineInterval.cs | 121 +- .../Enums/SharedLeverageSettingMode.cs | 33 +- .../SharedApis/Enums/SharedMarginMode.cs | 25 +- .../SharedApis/Enums/SharedOrderSide.cs | 25 +- .../SharedApis/Enums/SharedOrderStatus.cs | 33 +- .../SharedApis/Enums/SharedOrderType.cs | 41 +- .../Enums/SharedPaginationSupport.cs | 33 +- .../SharedApis/Enums/SharedPositionMode.cs | 25 +- .../Enums/SharedPositionModeSelection.cs | 25 +- .../SharedApis/Enums/SharedPositionSide.cs | 25 +- .../SharedApis/Enums/SharedQuantityType.cs | 49 +- .../SharedApis/Enums/SharedRole.cs | 25 +- .../SharedApis/Enums/SharedSymbolType.cs | 41 +- .../SharedApis/Enums/SharedTimeInForce.cs | 33 +- .../SharedApis/Enums/SharedTpSlSide.cs | 27 +- .../Enums/SharedTriggerOrderDirection.cs | 27 +- .../Enums/SharedTriggerOrderStatus.cs | 43 +- .../Enums/SharedTriggerPriceDirection.cs | 27 +- .../Enums/SharedTriggerPriceType.cs | 35 +- .../SharedApis/Enums/TradingMode.cs | 49 +- .../SharedApis/Interfaces/INextPageToken.cs | 201 +- .../SharedApis/Interfaces/ISharedClient.cs | 72 +- .../Rest/Futures/IFundingRateRestClient.cs | 32 +- .../IFuturesOrderClientIdRestClient.cs | 52 +- .../Rest/Futures/IFuturesOrderRestClient.cs | 246 +- .../Rest/Futures/IFuturesSymbolRestClient.cs | 30 +- .../Rest/Futures/IFuturesTickerRestClient.cs | 50 +- .../Rest/Futures/IFuturesTpSlRestClient.cs | 56 +- .../Futures/IFuturesTriggerOrderRestClient.cs | 74 +- .../Futures/IIndexPriceKlineRestClient.cs | 32 +- .../Rest/Futures/ILeverageRestClient.cs | 59 +- .../Rest/Futures/IMarkPriceKlineRestClient.cs | 32 +- .../Rest/Futures/IOpenInterestRestClient.cs | 31 +- .../Futures/IPositionHistoryRestClient.cs | 32 +- .../Rest/Futures/IPositionModeRestClient.cs | 59 +- .../Interfaces/Rest/IAssetsRestClient.cs | 50 +- .../Interfaces/Rest/IBalanceRestClient.cs | 32 +- .../Interfaces/Rest/IBookTickerRestClient.cs | 33 +- .../Interfaces/Rest/IDepositRestClient.cs | 56 +- .../Interfaces/Rest/IFeeRestClient.cs | 32 +- .../Interfaces/Rest/IKlineRestClient.cs | 34 +- .../Interfaces/Rest/IListenKeyRestClient.cs | 77 +- .../Interfaces/Rest/IOrderBookRestClient.cs | 33 +- .../Interfaces/Rest/IRecentTradeRestClient.cs | 32 +- .../Rest/ITradeHistoryRestClient.cs | 34 +- .../Interfaces/Rest/IWithdrawRestClient.cs | 33 +- .../Interfaces/Rest/IWithdrawalRestClient .cs | 34 +- .../Rest/Spot/ISpotOrderClientIdRestClient.cs | 52 +- .../Rest/Spot/ISpotOrderRestClient.cs | 204 +- .../Rest/Spot/ISpotSymbolRestClient.cs | 30 +- .../Rest/Spot/ISpotTickerRestClient.cs | 50 +- .../Rest/Spot/ISpotTriggerOrderRestClient.cs | 74 +- .../Futures/IFuturesOrderSocketClient.cs | 36 +- .../Socket/Futures/IPositionSocketClient.cs | 36 +- .../Interfaces/Socket/IBalanceSocketClient.cs | 36 +- .../Socket/IBookTickerSocketClient.cs | 35 +- .../Interfaces/Socket/IKlineSocketClient.cs | 35 +- .../Socket/IOrderBookSocketClient.cs | 35 +- .../Interfaces/Socket/ITickerSocketClient.cs | 35 +- .../Interfaces/Socket/ITickersSocketClient.cs | 36 +- .../Interfaces/Socket/ITradeSocketClient.cs | 36 +- .../Socket/IUserTradeSocketClient.cs | 36 +- .../Socket/Spot/ISpotOrderSocketClient.cs | 36 +- .../SharedApis/Models/ExchangeEvent.cs | 53 +- .../SharedApis/Models/ExchangeParameter.cs | 55 +- .../SharedApis/Models/ExchangeParameters.cs | 277 ++- .../SharedApis/Models/ExchangeResult.cs | 73 +- .../SharedApis/Models/ExchangeWebResult.cs | 273 +- .../Options/Endpoints/EndpointOptions.cs | 327 ++- .../Endpoints/GetClosedOrdersOptions.cs | 59 +- .../Options/Endpoints/GetDepositsOptions.cs | 59 +- .../Endpoints/GetFundingRateHistoryOptions.cs | 17 +- .../Options/Endpoints/GetKlinesOptions.cs | 178 +- .../Options/Endpoints/GetOrderBookOptions.cs | 106 +- .../Endpoints/GetPositionHistoryOptions.cs | 17 +- .../Endpoints/GetPositionModeOptions.cs | 17 +- .../Endpoints/GetRecentTradesOptions.cs | 59 +- .../Endpoints/GetTradeHistoryOptions.cs | 59 +- .../Endpoints/GetWithdrawalsOptions.cs | 59 +- .../Endpoints/PaginatedEndpointOptions.cs | 86 +- .../Endpoints/PlaceFuturesOrderOptions.cs | 98 +- .../PlaceFuturesTriggerOrderOptions.cs | 66 +- .../Endpoints/PlaceSpotOrderOptions.cs | 70 +- .../Endpoints/PlaceSpotTriggerOrderOptions.cs | 64 +- .../Options/Endpoints/SetLeverageOptions.cs | 17 +- .../Endpoints/SetPositionModeOptions.cs | 17 +- .../Options/Endpoints/WithdrawOptions.cs | 17 +- .../Models/Options/ParameterDescription.cs | 101 +- .../Subscriptions/SubscribeKlineOptions.cs | 106 +- .../SubscribeOrderBookOptions.cs | 50 +- .../Models/Rest/CancelOrderRequest.cs | 35 +- .../Models/Rest/CancelTpSlRequest.cs | 97 +- .../Models/Rest/ClosePositionRequest.cs | 83 +- .../SharedApis/Models/Rest/GetAssetRequest.cs | 33 +- .../Models/Rest/GetAssetsRequest.cs | 19 +- .../Models/Rest/GetBalancesRequest.cs | 33 +- .../Models/Rest/GetBookTickerRequest.cs | 21 +- .../Models/Rest/GetClosedOrdersRequest.cs | 61 +- .../Models/Rest/GetDepositAddressesRequest.cs | 45 +- .../Models/Rest/GetDepositsRequest.cs | 71 +- .../SharedApis/Models/Rest/GetFeeRequest.cs | 23 +- .../Rest/GetFundingRateHistoryRequest.cs | 61 +- .../Models/Rest/GetKlinesRequest.cs | 73 +- .../Models/Rest/GetLeverageRequest.cs | 47 +- .../Models/Rest/GetOpenInterestRequest.cs | 21 +- .../Models/Rest/GetOpenOrdersRequest.cs | 59 +- .../Models/Rest/GetOrderBookRequest.cs | 35 +- .../SharedApis/Models/Rest/GetOrderRequest.cs | 35 +- .../Models/Rest/GetOrderTradesRequest.cs | 35 +- .../Models/Rest/GetPositionHistoryRequest.cs | 108 +- .../Models/Rest/GetPositionModeRequest.cs | 59 +- .../Models/Rest/GetPositionsRequest.cs | 59 +- .../Models/Rest/GetRecentTradesRequest.cs | 35 +- .../Models/Rest/GetSymbolsRequest.cs | 33 +- .../Models/Rest/GetTickerRequest.cs | 21 +- .../Models/Rest/GetTickersRequest.cs | 34 +- .../Models/Rest/GetTradeHistoryRequest.cs | 61 +- .../Models/Rest/GetUserTradesRequest.cs | 61 +- .../Models/Rest/GetWithdrawalsRequest.cs | 71 +- .../Models/Rest/KeepAliveListenKeyRequest.cs | 45 +- .../Models/Rest/PlaceFuturesOrderRequest.cs | 187 +- .../Rest/PlaceFuturesTriggerOrderRequest.cs | 157 +- .../Models/Rest/PlaceSpotOrderRequest.cs | 111 +- .../Rest/PlaceSpotTriggerOrderRequest.cs | 111 +- .../Models/Rest/SetLeverageRequest.cs | 59 +- .../Models/Rest/SetPositionModeRequest.cs | 75 +- .../SharedApis/Models/Rest/SetTpSlRequest.cs | 87 +- .../Models/Rest/StartListenKeyRequest.cs | 33 +- .../Models/Rest/StopListenKeyRequest.cs | 45 +- .../SharedApis/Models/Rest/WithdrawRequest.cs | 81 +- .../Models/SharedQuantitySupport.cs | 179 +- .../SharedApis/Models/SharedRequest.cs | 29 +- .../SharedApis/Models/SharedSymbolRequest.cs | 74 +- .../Socket/SubscribeAllTickersRequest.cs | 33 +- .../Models/Socket/SubscribeBalancesRequest.cs | 45 +- .../Socket/SubscribeBookTickerRequest.cs | 53 +- .../Socket/SubscribeFuturesOrderRequest.cs | 45 +- .../Models/Socket/SubscribeKlineRequest.cs | 75 +- .../Socket/SubscribeOrderBookRequest.cs | 67 +- .../Models/Socket/SubscribePositionRequest.cs | 45 +- .../Socket/SubscribeSpotOrderRequest.cs | 33 +- .../Models/Socket/SubscribeTickerRequest.cs | 52 +- .../Models/Socket/SubscribeTradeRequest.cs | 53 +- .../Socket/SubscribeUserTradeRequest.cs | 45 +- .../SharedApis/ResponseModels/SharedAsset.cs | 136 +- .../ResponseModels/SharedBalance.cs | 57 +- .../ResponseModels/SharedBookTicker.cs | 61 +- .../ResponseModels/SharedDeposit.cs | 102 +- .../ResponseModels/SharedDepositAddress.cs | 56 +- .../SharedApis/ResponseModels/SharedFee.cs | 41 +- .../ResponseModels/SharedFundingRate.cs | 41 +- .../ResponseModels/SharedFuturesKline.cs | 71 +- .../ResponseModels/SharedFuturesOrder.cs | 241 +- .../ResponseModels/SharedFuturesSymbol.cs | 59 +- .../ResponseModels/SharedFuturesTicker.cs | 105 +- .../SharedFuturesTriggerOrder.cs | 195 +- .../SharedApis/ResponseModels/SharedId.cs | 27 +- .../SharedApis/ResponseModels/SharedKline.cs | 81 +- .../ResponseModels/SharedLeverage.cs | 45 +- .../ResponseModels/SharedOpenInterest.cs | 29 +- .../ResponseModels/SharedOrderBook.cs | 43 +- .../ResponseModels/SharedPosition.cs | 107 +- .../ResponseModels/SharedPositionHistory.cs | 123 +- .../SharedPositionModeResult.cs | 29 +- .../ResponseModels/SharedSpotOrder.cs | 185 +- .../ResponseModels/SharedSpotSymbol.cs | 145 +- .../ResponseModels/SharedSpotTicker.cs | 79 +- .../ResponseModels/SharedSpotTriggerOrder.cs | 183 +- .../ResponseModels/SharedSymbolModel.cs | 41 +- .../SharedApis/ResponseModels/SharedTrade.cs | 59 +- .../ResponseModels/SharedUserTrade.cs | 109 +- .../ResponseModels/SharedWithdrawal.cs | 120 +- .../SharedApis/SharedQuantity.cs | 224 +- CryptoExchange.Net/SharedApis/SharedSymbol.cs | 111 +- .../Sockets/CryptoExchangeWebSocketClient.cs | 1535 ++++++------ .../Sockets/DedicatedConnectionConfig.cs | 51 +- CryptoExchange.Net/Sockets/MessageMatcher.cs | 293 ++- .../Sockets/PeriodicTaskRegistration.cs | 43 +- CryptoExchange.Net/Sockets/Query.cs | 461 ++-- .../Sockets/SocketConnection.cs | 2187 ++++++++--------- CryptoExchange.Net/Sockets/Subscription.cs | 413 ++-- .../Sockets/SystemSubscription.cs | 39 +- .../Sockets/WebsocketFactory.cs | 21 +- .../Comparers/SystemTextJsonComparer.cs | 726 +++--- .../Testing/EnumValueTraceListener.cs | 41 +- .../Implementations/TestAuthTimeProvider.cs | 21 +- .../Implementations/TestNonceProvider.cs | 31 +- .../Testing/Implementations/TestRequest.cs | 57 +- .../Implementations/TestRequestFactory.cs | 45 +- .../Testing/Implementations/TestResponse.cs | 49 +- .../Testing/Implementations/TestSocket.cs | 179 +- .../Implementations/TestWebsocketFactory.cs | 21 +- .../Testing/RestIntegrationTest.cs | 207 +- .../Testing/RestRequestValidator.cs | 317 ++- .../Testing/SocketIntegrationTest.cs | 180 +- .../Testing/SocketRequestValidator.cs | 296 ++- .../Testing/SocketSubscriptionValidator.cs | 325 ++- CryptoExchange.Net/Testing/TestHelpers.cs | 307 ++- CryptoExchange.Net/Trackers/CompareValue.cs | 46 +- .../Trackers/Klines/IKlineTracker.cs | 164 +- .../Trackers/Klines/KlineTracker.cs | 920 ++++--- .../Trackers/Klines/KlinesCompare.cs | 43 +- .../Trackers/Klines/KlinesStats.cs | 99 +- .../Trackers/Trades/ITradeTracker.cs | 156 +- .../Trackers/Trades/TradeTracker.cs | 942 ++++--- .../Trackers/Trades/TradesCompare.cs | 59 +- .../Trackers/Trades/TradesStats.cs | 111 +- 354 files changed, 23357 insertions(+), 23680 deletions(-) create mode 100644 CryptoExchange.Net/.editorconfig diff --git a/CryptoExchange.Net.UnitTests/CryptoExchange.Net.UnitTests.csproj b/CryptoExchange.Net.UnitTests/CryptoExchange.Net.UnitTests.csproj index 3072a08..59f0095 100644 --- a/CryptoExchange.Net.UnitTests/CryptoExchange.Net.UnitTests.csproj +++ b/CryptoExchange.Net.UnitTests/CryptoExchange.Net.UnitTests.csproj @@ -5,6 +5,10 @@ false + + + + diff --git a/CryptoExchange.Net/.editorconfig b/CryptoExchange.Net/.editorconfig new file mode 100644 index 0000000..3f12941 --- /dev/null +++ b/CryptoExchange.Net/.editorconfig @@ -0,0 +1,182 @@ +root = true + +[*] + +# Indentation and spacing +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true +charset = utf-8 +insert_final_newline = true + +# ReSharper code style properties +resharper_csharp_keep_existing_embedded_arrangement = false +resharper_csharp_place_accessorholder_attribute_on_same_line = false +resharper_csharp_wrap_after_declaration_lpar = true +resharper_csharp_wrap_parameters_style = chop_if_long +resharper_csharp_blank_lines_around_single_line_auto_property = 1 +resharper_csharp_keep_blank_lines_in_declarations = 1 +resharper_trailing_comma_in_multiline_lists = true + +[*.cs] + +indent_size = 4 + +# Code style conventions +dotnet_style_predefined_type_for_member_access = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_object_initializer = true:suggestion +csharp_style_var_when_type_is_apparent = true:suggestion +csharp_style_expression_bodied_methods = true:suggestion +csharp_style_namespace_declarations = file_scoped:warning +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion +csharp_prefer_braces = when_multiline:warning + +# Analyzer preferences +dotnet_diagnostic.CA2007.severity = warning # Call ConfigureAwait on the awaited Task. +dotnet_code_quality.CA2007.exclude_async_void_methods = true +dotnet_code_quality.CA2007.output_kind = DynamicallyLinkedLibrary + +dotnet_diagnostic.CA1000.severity = none # Do not declare static members on generic types +dotnet_diagnostic.CA1051.severity = none # Do not declare visible instance fields +dotnet_diagnostic.CA1510.severity = none # Use ArgumentNullException throw helper +dotnet_diagnostic.CA1720.severity = none # Identifiers should not contain type names +dotnet_diagnostic.CA1716.severity = none # Identifiers should not match keywords +dotnet_diagnostic.CA1835.severity = none # Use ArgumentNullException throw helper +dotnet_diagnostic.CA1846.severity = none # Prefer AsSpan over Substring +dotnet_diagnostic.CA1848.severity = none # Use the LoggerMessage delegates +dotnet_diagnostic.CA1850.severity = none # Prefer static HashData method over ComputeHash +dotnet_diagnostic.CA1866.severity = none # Use 'string.Method(char)' instead of 'string.Method(string)' for string with single char +dotnet_diagnostic.CA2201.severity = none # Do not raise reserved exception types +dotnet_diagnostic.CA2208.severity = none # Do not raise reserved exception types +dotnet_diagnostic.IDE0005.severity = warning # Using directive is unnecessary + +[*.xml] +ij_xml_space_inside_empty_tag = true +[*.cs] +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = warning +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = warning +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.private_or_internal_field_should_be_fields_start_with__.severity = warning +dotnet_naming_rule.private_or_internal_field_should_be_fields_start_with__.symbols = private_or_internal_field +dotnet_naming_rule.private_or_internal_field_should_be_fields_start_with__.style = fields_start_with__ + +# Symbol specifications + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +dotnet_naming_symbols.private_or_internal_field.applicable_kinds = field +dotnet_naming_symbols.private_or_internal_field.applicable_accessibilities = internal, private, private_protected +dotnet_naming_symbols.private_or_internal_field.required_modifiers = + +# Naming styles + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.fields_start_with__.required_prefix = _ +dotnet_naming_style.fields_start_with__.required_suffix = +dotnet_naming_style.fields_start_with__.word_separator = +dotnet_naming_style.fields_start_with__.capitalization = camel_case +csharp_indent_labels = one_less_than_current +csharp_using_directive_placement = outside_namespace:suggestion +csharp_prefer_simple_using_statement = true:suggestion +csharp_style_prefer_method_group_conversion = true:silent +csharp_style_prefer_top_level_statements = true:silent +csharp_style_prefer_primary_constructors = true:suggestion +csharp_prefer_system_threading_lock = true:suggestion +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:suggestion +csharp_style_expression_bodied_indexers = true:suggestion +csharp_style_expression_bodied_accessors = true:suggestion +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = true:silent + +[*.vb] +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# Symbol specifications + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, friend, private, protected, protected_friend, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, friend, private, protected, protected_friend, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# Naming styles + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +[*.{cs,vb}] +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +# Symbol specifications + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +# Naming styles + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case +dotnet_style_operator_placement_when_wrapping = beginning_of_line +tab_width = 4 +end_of_line = crlf +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion diff --git a/CryptoExchange.Net/AssemblyInfo.cs b/CryptoExchange.Net/AssemblyInfo.cs index 6a54bd0..0227759 100644 --- a/CryptoExchange.Net/AssemblyInfo.cs +++ b/CryptoExchange.Net/AssemblyInfo.cs @@ -1,6 +1,5 @@ -[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("CryptoExchange.Net.UnitTests")] +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("CryptoExchange.Net.UnitTests")] -namespace System.Runtime.CompilerServices -{ - internal static class IsExternalInit { } -} \ No newline at end of file +namespace System.Runtime.CompilerServices; + +internal static class IsExternalInit { } \ No newline at end of file diff --git a/CryptoExchange.Net/Attributes/JsonConversionAttribute.cs b/CryptoExchange.Net/Attributes/JsonConversionAttribute.cs index e3fdb29..173cdd4 100644 --- a/CryptoExchange.Net/Attributes/JsonConversionAttribute.cs +++ b/CryptoExchange.Net/Attributes/JsonConversionAttribute.cs @@ -1,12 +1,11 @@ -using System; +using System; -namespace CryptoExchange.Net.Attributes +namespace CryptoExchange.Net.Attributes; + +/// +/// Used for conversion in ArrayConverter +/// +[AttributeUsage(AttributeTargets.Property)] +public class JsonConversionAttribute: Attribute { - /// - /// Used for conversion in ArrayConverter - /// - [AttributeUsage(AttributeTargets.Property)] - public class JsonConversionAttribute: Attribute - { - } } diff --git a/CryptoExchange.Net/Attributes/MapAttribute.cs b/CryptoExchange.Net/Attributes/MapAttribute.cs index f824ea4..d63c84f 100644 --- a/CryptoExchange.Net/Attributes/MapAttribute.cs +++ b/CryptoExchange.Net/Attributes/MapAttribute.cs @@ -1,25 +1,24 @@ -using System; +using System; -namespace CryptoExchange.Net.Attributes +namespace CryptoExchange.Net.Attributes; + +/// +/// Map a enum entry to string values +/// +[AttributeUsage(AttributeTargets.Field)] +public class MapAttribute : Attribute { /// - /// Map a enum entry to string values + /// Values mapping to the enum entry /// - [AttributeUsage(AttributeTargets.Field)] - public class MapAttribute : Attribute - { - /// - /// Values mapping to the enum entry - /// - public string[] Values { get; set; } + public string[] Values { get; set; } - /// - /// ctor - /// - /// - public MapAttribute(params string[] maps) - { - Values = maps; - } + /// + /// ctor + /// + /// + public MapAttribute(params string[] maps) + { + Values = maps; } } diff --git a/CryptoExchange.Net/Authentication/ApiCredentials.cs b/CryptoExchange.Net/Authentication/ApiCredentials.cs index 3542b3b..fae466d 100644 --- a/CryptoExchange.Net/Authentication/ApiCredentials.cs +++ b/CryptoExchange.Net/Authentication/ApiCredentials.cs @@ -1,60 +1,56 @@ -using System; -using System.IO; -using CryptoExchange.Net.Converters.SystemTextJson; -using CryptoExchange.Net.Converters.MessageParsing; +using System; -namespace CryptoExchange.Net.Authentication +namespace CryptoExchange.Net.Authentication; + +/// +/// Api credentials, used to sign requests accessing private endpoints +/// +public class ApiCredentials { /// - /// Api credentials, used to sign requests accessing private endpoints + /// The api key / label to authenticate requests /// - public class ApiCredentials + public string Key { get; set; } + + /// + /// The api secret or private key to authenticate requests + /// + public string Secret { get; set; } + + /// + /// The api passphrase. Not needed on all exchanges + /// + public string? Pass { get; set; } + + /// + /// Type of the credentials + /// + public ApiCredentialsType CredentialType { get; set; } + + /// + /// Create Api credentials providing an api key and secret for authentication + /// + /// The api key / label used for identification + /// The api secret or private key used for signing + /// The api pass for the key. Not always needed + /// The type of credentials + public ApiCredentials(string key, string secret, string? pass = null, ApiCredentialsType credentialType = ApiCredentialsType.Hmac) { - /// - /// The api key / label to authenticate requests - /// - public string Key { get; set; } + if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(secret)) + throw new ArgumentException("Key and secret can't be null/empty"); - /// - /// The api secret or private key to authenticate requests - /// - public string Secret { get; set; } + CredentialType = credentialType; + Key = key; + Secret = secret; + Pass = pass; + } - /// - /// The api passphrase. Not needed on all exchanges - /// - public string? Pass { get; set; } - - /// - /// Type of the credentials - /// - public ApiCredentialsType CredentialType { get; set; } - - /// - /// Create Api credentials providing an api key and secret for authentication - /// - /// The api key / label used for identification - /// The api secret or private key used for signing - /// The api pass for the key. Not always needed - /// The type of credentials - 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 = credentialType; - Key = key; - Secret = secret; - Pass = pass; - } - - /// - /// Copy the credentials - /// - /// - public virtual ApiCredentials Copy() - { - return new ApiCredentials(Key, Secret, Pass, CredentialType); - } + /// + /// Copy the credentials + /// + /// + public virtual ApiCredentials Copy() + { + return new ApiCredentials(Key, Secret, Pass, CredentialType); } } diff --git a/CryptoExchange.Net/Authentication/ApiCredentialsType.cs b/CryptoExchange.Net/Authentication/ApiCredentialsType.cs index 2da474f..ae4b57c 100644 --- a/CryptoExchange.Net/Authentication/ApiCredentialsType.cs +++ b/CryptoExchange.Net/Authentication/ApiCredentialsType.cs @@ -1,21 +1,20 @@ -namespace CryptoExchange.Net.Authentication +namespace CryptoExchange.Net.Authentication; + +/// +/// Credentials type +/// +public enum ApiCredentialsType { /// - /// Credentials type + /// Hmac keys credentials /// - public enum ApiCredentialsType - { - /// - /// Hmac keys credentials - /// - Hmac, - /// - /// Rsa keys credentials in xml format - /// - RsaXml, - /// - /// Rsa keys credentials in pem/base64 format. Only available for .NetStandard 2.1 and up, use xml format for lower. - /// - RsaPem - } + Hmac, + /// + /// Rsa keys credentials in xml format + /// + RsaXml, + /// + /// Rsa keys credentials in pem/base64 format. Only available for .NetStandard 2.1 and up, use xml format for lower. + /// + RsaPem } diff --git a/CryptoExchange.Net/Authentication/AuthenticationProvider.cs b/CryptoExchange.Net/Authentication/AuthenticationProvider.cs index 1c49f1d..56c8ec0 100644 --- a/CryptoExchange.Net/Authentication/AuthenticationProvider.cs +++ b/CryptoExchange.Net/Authentication/AuthenticationProvider.cs @@ -1,473 +1,477 @@ -using CryptoExchange.Net.Clients; +using CryptoExchange.Net.Clients; using CryptoExchange.Net.Converters.SystemTextJson; using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Objects; using System; using System.Collections.Generic; using System.Globalization; -using System.Net.Http; using System.Security.Cryptography; using System.Text; -namespace CryptoExchange.Net.Authentication +namespace CryptoExchange.Net.Authentication; + +/// +/// Base class for authentication providers +/// +public abstract class AuthenticationProvider { + internal IAuthTimeProvider TimeProvider { get; set; } = new AuthTimeProvider(); + /// - /// Base class for authentication providers + /// Provided credentials /// - public abstract class AuthenticationProvider + protected internal readonly ApiCredentials _credentials; + + /// + /// Byte representation of the secret + /// + protected byte[] _sBytes; + + /// + /// Get the API key of the current credentials + /// + public string ApiKey => _credentials.Key!; + /// + /// Get the Passphrase of the current credentials + /// + public string? Pass => _credentials.Pass; + + /// + /// ctor + /// + /// + protected AuthenticationProvider(ApiCredentials credentials) { - internal IAuthTimeProvider TimeProvider { get; set; } = new AuthTimeProvider(); + if (credentials.Key == null || credentials.Secret == null) + throw new ArgumentException("ApiKey/Secret needed"); - /// - /// Provided credentials - /// - protected internal readonly ApiCredentials _credentials; - - /// - /// Byte representation of the secret - /// - protected byte[] _sBytes; - - /// - /// Get the API key of the current credentials - /// - public string ApiKey => _credentials.Key!; - /// - /// Get the Passphrase of the current credentials - /// - public string? Pass => _credentials.Pass; - - /// - /// ctor - /// - /// - protected AuthenticationProvider(ApiCredentials credentials) - { - if (credentials.Key == null || credentials.Secret == null) - throw new ArgumentException("ApiKey/Secret needed"); - - _credentials = credentials; - _sBytes = Encoding.UTF8.GetBytes(credentials.Secret); - } - - /// - /// Authenticate a request - /// - /// The Api client sending the request - /// The request configuration - public abstract void ProcessRequest(RestApiClient apiClient, RestRequestConfiguration requestConfig); - - /// - /// SHA256 sign the data and return the bytes - /// - /// - /// - protected static byte[] SignSHA256Bytes(string data) - { - using var encryptor = SHA256.Create(); - return encryptor.ComputeHash(Encoding.UTF8.GetBytes(data)); - } - - /// - /// SHA256 sign the data and return the bytes - /// - /// - /// - protected static byte[] SignSHA256Bytes(byte[] data) - { - using var encryptor = SHA256.Create(); - return encryptor.ComputeHash(data); - } - - /// - /// SHA256 sign the data and return the hash - /// - /// Data to sign - /// String type - /// - protected static string SignSHA256(string data, SignOutputType? outputType = null) - { - using var encryptor = SHA256.Create(); - var resultBytes = encryptor.ComputeHash(Encoding.UTF8.GetBytes(data)); - return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes); - } - - /// - /// SHA256 sign the data and return the hash - /// - /// Data to sign - /// String type - /// - protected static string SignSHA256(byte[] data, SignOutputType? outputType = null) - { - using var encryptor = SHA256.Create(); - var resultBytes = encryptor.ComputeHash(data); - return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes); - } - - /// - /// SHA384 sign the data and return the hash - /// - /// Data to sign - /// String type - /// - protected static string SignSHA384(string data, SignOutputType? outputType = null) - { - using var encryptor = SHA384.Create(); - var resultBytes = encryptor.ComputeHash(Encoding.UTF8.GetBytes(data)); - return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes); - } - - /// - /// SHA384 sign the data and return the hash - /// - /// Data to sign - /// String type - /// - protected static string SignSHA384(byte[] data, SignOutputType? outputType = null) - { - using var encryptor = SHA384.Create(); - var resultBytes = encryptor.ComputeHash(data); - return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes); - } - - /// - /// SHA384 sign the data and return the hash - /// - /// Data to sign - /// - protected static byte[] SignSHA384Bytes(string data) - { - using var encryptor = SHA384.Create(); - return encryptor.ComputeHash(Encoding.UTF8.GetBytes(data)); - } - - /// - /// SHA384 sign the data and return the hash - /// - /// Data to sign - /// - protected static byte[] SignSHA384Bytes(byte[] data) - { - using var encryptor = SHA384.Create(); - return encryptor.ComputeHash(data); - } - - /// - /// SHA512 sign the data and return the hash - /// - /// Data to sign - /// String type - /// - protected static string SignSHA512(string data, SignOutputType? outputType = null) - { - using var encryptor = SHA512.Create(); - var resultBytes = encryptor.ComputeHash(Encoding.UTF8.GetBytes(data)); - return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes); - } - - /// - /// SHA512 sign the data and return the hash - /// - /// Data to sign - /// String type - /// - protected static string SignSHA512(byte[] data, SignOutputType? outputType = null) - { - using var encryptor = SHA512.Create(); - var resultBytes = encryptor.ComputeHash(data); - return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes); - } - - /// - /// SHA512 sign the data and return the hash - /// - /// Data to sign - /// - protected static byte[] SignSHA512Bytes(string data) - { - using var encryptor = SHA512.Create(); - return encryptor.ComputeHash(Encoding.UTF8.GetBytes(data)); - } - - /// - /// SHA512 sign the data and return the hash - /// - /// Data to sign - /// - protected static byte[] SignSHA512Bytes(byte[] data) - { - using var encryptor = SHA512.Create(); - return encryptor.ComputeHash(data); - } - - /// - /// MD5 sign the data and return the hash - /// - /// Data to sign - /// String type - /// - protected static string SignMD5(string data, SignOutputType? outputType = null) - { - using var encryptor = MD5.Create(); - var resultBytes = encryptor.ComputeHash(Encoding.UTF8.GetBytes(data)); - return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes); - } - - /// - /// MD5 sign the data and return the hash - /// - /// Data to sign - /// String type - /// - protected static string SignMD5(byte[] data, SignOutputType? outputType = null) - { - using var encryptor = MD5.Create(); - var resultBytes = encryptor.ComputeHash(data); - return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes); - } - - /// - /// MD5 sign the data and return the hash - /// - /// Data to sign - /// - protected static byte[] SignMD5Bytes(string data) - { - using var encryptor = MD5.Create(); - return encryptor.ComputeHash(Encoding.UTF8.GetBytes(data)); - } - - /// - /// HMACSHA256 sign the data and return the hash - /// - /// Data to sign - /// String type - /// - protected string SignHMACSHA256(string data, SignOutputType? outputType = null) - => SignHMACSHA256(Encoding.UTF8.GetBytes(data), outputType); - - /// - /// HMACSHA256 sign the data and return the hash - /// - /// Data to sign - /// String type - /// - protected string SignHMACSHA256(byte[] data, SignOutputType? outputType = null) - { - using var encryptor = new HMACSHA256(_sBytes); - var resultBytes = encryptor.ComputeHash(data); - return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes); - } - - /// - /// HMACSHA384 sign the data and return the hash - /// - /// Data to sign - /// String type - /// - protected string SignHMACSHA384(string data, SignOutputType? outputType = null) - => SignHMACSHA384(Encoding.UTF8.GetBytes(data), outputType); - - /// - /// HMACSHA384 sign the data and return the hash - /// - /// Data to sign - /// String type - /// - protected string SignHMACSHA384(byte[] data, SignOutputType? outputType = null) - { - using var encryptor = new HMACSHA384(_sBytes); - var resultBytes = encryptor.ComputeHash(data); - return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes); - } - - /// - /// HMACSHA512 sign the data and return the hash - /// - /// Data to sign - /// String type - /// - protected string SignHMACSHA512(string data, SignOutputType? outputType = null) - => SignHMACSHA512(Encoding.UTF8.GetBytes(data), outputType); - - /// - /// HMACSHA512 sign the data and return the hash - /// - /// Data to sign - /// String type - /// - protected string SignHMACSHA512(byte[] data, SignOutputType? outputType = null) - { - using var encryptor = new HMACSHA512(_sBytes); - var resultBytes = encryptor.ComputeHash(data); - return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes); - } - - /// - /// SHA256 sign the data - /// - /// - /// - /// - protected string SignRSASHA256(byte[] data, SignOutputType? outputType = null) - { - using var rsa = CreateRSA(); - using var sha256 = SHA256.Create(); - var hash = sha256.ComputeHash(data); - var resultBytes = rsa.SignHash(hash, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); - return outputType == SignOutputType.Base64? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes); - } - - /// - /// SHA384 sign the data - /// - /// - /// - /// - protected string SignRSASHA384(byte[] data, SignOutputType? outputType = null) - { - using var rsa = CreateRSA(); - using var sha384 = SHA384.Create(); - var hash = sha384.ComputeHash(data); - var resultBytes = rsa.SignHash(hash, HashAlgorithmName.SHA384, RSASignaturePadding.Pkcs1); - return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes); - } - - /// - /// SHA512 sign the data - /// - /// - /// - /// - protected string SignRSASHA512(byte[] data, SignOutputType? outputType = null) - { - using var rsa = CreateRSA(); - using var sha512 = SHA512.Create(); - var hash = sha512.ComputeHash(data); - var resultBytes = rsa.SignHash(hash, HashAlgorithmName.SHA512, RSASignaturePadding.Pkcs1); - return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes); - } - - private RSA CreateRSA() - { - var rsa = RSA.Create(); - if (_credentials.CredentialType == ApiCredentialsType.RsaPem) - { -#if NETSTANDARD2_1_OR_GREATER || NET9_0_OR_GREATER - // Read from pem private key - var key = _credentials.Secret! - .Replace("\n", "") - .Replace("-----BEGIN PRIVATE KEY-----", "") - .Replace("-----END PRIVATE KEY-----", "") - .Trim(); - rsa.ImportPkcs8PrivateKey(Convert.FromBase64String( - key) - , out _); -#else - throw new Exception("Pem format not supported when running from .NetStandard2.0. Convert the private key to xml format."); -#endif - } - else if (_credentials.CredentialType == ApiCredentialsType.RsaXml) - { - // Read from xml private key format - rsa.FromXmlString(_credentials.Secret!); - } - else - { - throw new Exception("Invalid credentials type"); - } - - return rsa; - } - - /// - /// Convert byte array to hex string - /// - /// - /// - 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 - } - - /// - /// Convert byte array to base64 string - /// - /// - /// - protected static string BytesToBase64String(byte[] buff) - { - return Convert.ToBase64String(buff); - } - - /// - /// Get current timestamp including the time sync offset from the api client - /// - /// - /// - protected DateTime GetTimestamp(RestApiClient apiClient) - { - return TimeProvider.GetTime().Add(apiClient.GetTimeOffset() ?? TimeSpan.Zero)!; - } - - /// - /// Get millisecond timestamp as a string including the time sync offset from the api client - /// - /// - /// - protected string GetMillisecondTimestamp(RestApiClient apiClient) - { - return DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient)).Value.ToString(CultureInfo.InvariantCulture); - } - - /// - /// Get millisecond timestamp as a long including the time sync offset from the api client - /// - /// - /// - protected long GetMillisecondTimestampLong(RestApiClient apiClient) - { - return DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient)).Value; - } - - /// - /// Return the serialized request body - /// - /// - /// - /// - protected static string GetSerializedBody(IMessageSerializer serializer, IDictionary parameters) - { - if (serializer is not IStringMessageSerializer stringSerializer) - throw new InvalidOperationException("Non-string message serializer can't get serialized request body"); - - if (parameters.Count == 1 && parameters.TryGetValue(Constants.BodyPlaceHolderKey, out object? value)) - return stringSerializer.Serialize(value); - else - return stringSerializer.Serialize(parameters); - } + _credentials = credentials; + _sBytes = Encoding.UTF8.GetBytes(credentials.Secret); } - /// - public abstract class AuthenticationProvider : AuthenticationProvider where TApiCredentials : ApiCredentials - { - /// - protected new TApiCredentials _credentials => (TApiCredentials)base._credentials; + /// + /// Authenticate a request + /// + /// The Api client sending the request + /// The request configuration + public abstract void ProcessRequest(RestApiClient apiClient, RestRequestConfiguration requestConfig); - /// - /// ctor - /// - /// - protected AuthenticationProvider(TApiCredentials credentials) : base(credentials) + /// + /// SHA256 sign the data and return the bytes + /// + /// + /// + protected static byte[] SignSHA256Bytes(string data) + { + using var encryptor = SHA256.Create(); + return encryptor.ComputeHash(Encoding.UTF8.GetBytes(data)); + } + + /// + /// SHA256 sign the data and return the bytes + /// + /// + /// + protected static byte[] SignSHA256Bytes(byte[] data) + { + using var encryptor = SHA256.Create(); + return encryptor.ComputeHash(data); + } + + /// + /// SHA256 sign the data and return the hash + /// + /// Data to sign + /// String type + /// + protected static string SignSHA256(string data, SignOutputType? outputType = null) + { + using var encryptor = SHA256.Create(); + var resultBytes = encryptor.ComputeHash(Encoding.UTF8.GetBytes(data)); + return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes); + } + + /// + /// SHA256 sign the data and return the hash + /// + /// Data to sign + /// String type + /// + protected static string SignSHA256(byte[] data, SignOutputType? outputType = null) + { + using var encryptor = SHA256.Create(); + var resultBytes = encryptor.ComputeHash(data); + return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes); + } + + /// + /// SHA384 sign the data and return the hash + /// + /// Data to sign + /// String type + /// + protected static string SignSHA384(string data, SignOutputType? outputType = null) + { + using var encryptor = SHA384.Create(); + var resultBytes = encryptor.ComputeHash(Encoding.UTF8.GetBytes(data)); + return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes); + } + + /// + /// SHA384 sign the data and return the hash + /// + /// Data to sign + /// String type + /// + protected static string SignSHA384(byte[] data, SignOutputType? outputType = null) + { + using var encryptor = SHA384.Create(); + var resultBytes = encryptor.ComputeHash(data); + return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes); + } + + /// + /// SHA384 sign the data and return the hash + /// + /// Data to sign + /// + protected static byte[] SignSHA384Bytes(string data) + { + using var encryptor = SHA384.Create(); + return encryptor.ComputeHash(Encoding.UTF8.GetBytes(data)); + } + + /// + /// SHA384 sign the data and return the hash + /// + /// Data to sign + /// + protected static byte[] SignSHA384Bytes(byte[] data) + { + using var encryptor = SHA384.Create(); + return encryptor.ComputeHash(data); + } + + /// + /// SHA512 sign the data and return the hash + /// + /// Data to sign + /// String type + /// + protected static string SignSHA512(string data, SignOutputType? outputType = null) + { + using var encryptor = SHA512.Create(); + var resultBytes = encryptor.ComputeHash(Encoding.UTF8.GetBytes(data)); + return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes); + } + + /// + /// SHA512 sign the data and return the hash + /// + /// Data to sign + /// String type + /// + protected static string SignSHA512(byte[] data, SignOutputType? outputType = null) + { + using var encryptor = SHA512.Create(); + var resultBytes = encryptor.ComputeHash(data); + return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes); + } + + /// + /// SHA512 sign the data and return the hash + /// + /// Data to sign + /// + protected static byte[] SignSHA512Bytes(string data) + { + using var encryptor = SHA512.Create(); + return encryptor.ComputeHash(Encoding.UTF8.GetBytes(data)); + } + + /// + /// SHA512 sign the data and return the hash + /// + /// Data to sign + /// + protected static byte[] SignSHA512Bytes(byte[] data) + { + using var encryptor = SHA512.Create(); + return encryptor.ComputeHash(data); + } + + /// + /// MD5 sign the data and return the hash + /// + /// Data to sign + /// String type + /// + protected static string SignMD5(string data, SignOutputType? outputType = null) + { +#pragma warning disable CA5351 + using var encryptor = MD5.Create(); +#pragma warning restore CA5351 + var resultBytes = encryptor.ComputeHash(Encoding.UTF8.GetBytes(data)); + return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes); + } + + /// + /// MD5 sign the data and return the hash + /// + /// Data to sign + /// String type + /// + protected static string SignMD5(byte[] data, SignOutputType? outputType = null) + { +#pragma warning disable CA5351 + using var encryptor = MD5.Create(); +#pragma warning restore CA5351 + var resultBytes = encryptor.ComputeHash(data); + return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes); + } + + /// + /// MD5 sign the data and return the hash + /// + /// Data to sign + /// + protected static byte[] SignMD5Bytes(string data) + { +#pragma warning disable CA5351 + using var encryptor = MD5.Create(); +#pragma warning restore CA5351 + return encryptor.ComputeHash(Encoding.UTF8.GetBytes(data)); + } + + /// + /// HMACSHA256 sign the data and return the hash + /// + /// Data to sign + /// String type + /// + protected string SignHMACSHA256(string data, SignOutputType? outputType = null) + => SignHMACSHA256(Encoding.UTF8.GetBytes(data), outputType); + + /// + /// HMACSHA256 sign the data and return the hash + /// + /// Data to sign + /// String type + /// + protected string SignHMACSHA256(byte[] data, SignOutputType? outputType = null) + { + using var encryptor = new HMACSHA256(_sBytes); + var resultBytes = encryptor.ComputeHash(data); + return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes); + } + + /// + /// HMACSHA384 sign the data and return the hash + /// + /// Data to sign + /// String type + /// + protected string SignHMACSHA384(string data, SignOutputType? outputType = null) + => SignHMACSHA384(Encoding.UTF8.GetBytes(data), outputType); + + /// + /// HMACSHA384 sign the data and return the hash + /// + /// Data to sign + /// String type + /// + protected string SignHMACSHA384(byte[] data, SignOutputType? outputType = null) + { + using var encryptor = new HMACSHA384(_sBytes); + var resultBytes = encryptor.ComputeHash(data); + return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes); + } + + /// + /// HMACSHA512 sign the data and return the hash + /// + /// Data to sign + /// String type + /// + protected string SignHMACSHA512(string data, SignOutputType? outputType = null) + => SignHMACSHA512(Encoding.UTF8.GetBytes(data), outputType); + + /// + /// HMACSHA512 sign the data and return the hash + /// + /// Data to sign + /// String type + /// + protected string SignHMACSHA512(byte[] data, SignOutputType? outputType = null) + { + using var encryptor = new HMACSHA512(_sBytes); + var resultBytes = encryptor.ComputeHash(data); + return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes); + } + + /// + /// SHA256 sign the data + /// + /// + /// + /// + protected string SignRSASHA256(byte[] data, SignOutputType? outputType = null) + { + using var rsa = CreateRSA(); + using var sha256 = SHA256.Create(); + var hash = sha256.ComputeHash(data); + var resultBytes = rsa.SignHash(hash, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + return outputType == SignOutputType.Base64? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes); + } + + /// + /// SHA384 sign the data + /// + /// + /// + /// + protected string SignRSASHA384(byte[] data, SignOutputType? outputType = null) + { + using var rsa = CreateRSA(); + using var sha384 = SHA384.Create(); + var hash = sha384.ComputeHash(data); + var resultBytes = rsa.SignHash(hash, HashAlgorithmName.SHA384, RSASignaturePadding.Pkcs1); + return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes); + } + + /// + /// SHA512 sign the data + /// + /// + /// + /// + protected string SignRSASHA512(byte[] data, SignOutputType? outputType = null) + { + using var rsa = CreateRSA(); + using var sha512 = SHA512.Create(); + var hash = sha512.ComputeHash(data); + var resultBytes = rsa.SignHash(hash, HashAlgorithmName.SHA512, RSASignaturePadding.Pkcs1); + return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes); + } + + private RSA CreateRSA() + { + var rsa = RSA.Create(); + if (_credentials.CredentialType == ApiCredentialsType.RsaPem) { +#if NETSTANDARD2_1_OR_GREATER || NET9_0_OR_GREATER + // Read from pem private key + var key = _credentials.Secret! + .Replace("\n", "") + .Replace("-----BEGIN PRIVATE KEY-----", "") + .Replace("-----END PRIVATE KEY-----", "") + .Trim(); + rsa.ImportPkcs8PrivateKey(Convert.FromBase64String( + key) + , out _); +#else + throw new Exception("Pem format not supported when running from .NetStandard2.0. Convert the private key to xml format."); +#endif } + else if (_credentials.CredentialType == ApiCredentialsType.RsaXml) + { + // Read from xml private key format + rsa.FromXmlString(_credentials.Secret!); + } + else + { + throw new Exception("Invalid credentials type"); + } + + return rsa; + } + + /// + /// Convert byte array to hex string + /// + /// + /// + 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 + } + + /// + /// Convert byte array to base64 string + /// + /// + /// + protected static string BytesToBase64String(byte[] buff) + { + return Convert.ToBase64String(buff); + } + + /// + /// Get current timestamp including the time sync offset from the api client + /// + /// + /// + protected DateTime GetTimestamp(RestApiClient apiClient) + { + return TimeProvider.GetTime().Add(apiClient.GetTimeOffset() ?? TimeSpan.Zero)!; + } + + /// + /// Get millisecond timestamp as a string including the time sync offset from the api client + /// + /// + /// + protected string GetMillisecondTimestamp(RestApiClient apiClient) + { + return DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient)).Value.ToString(CultureInfo.InvariantCulture); + } + + /// + /// Get millisecond timestamp as a long including the time sync offset from the api client + /// + /// + /// + protected long GetMillisecondTimestampLong(RestApiClient apiClient) + { + return DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient)).Value; + } + + /// + /// Return the serialized request body + /// + /// + /// + /// + protected static string GetSerializedBody(IMessageSerializer serializer, IDictionary parameters) + { + if (serializer is not IStringMessageSerializer stringSerializer) + throw new InvalidOperationException("Non-string message serializer can't get serialized request body"); + + if (parameters.Count == 1 && parameters.TryGetValue(Constants.BodyPlaceHolderKey, out object? value)) + return stringSerializer.Serialize(value); + else + return stringSerializer.Serialize(parameters); + } +} + +/// +public abstract class AuthenticationProvider : AuthenticationProvider where TApiCredentials : ApiCredentials +{ + /// + protected new TApiCredentials _credentials => (TApiCredentials)base._credentials; + + /// + /// ctor + /// + /// + protected AuthenticationProvider(TApiCredentials credentials) : base(credentials) + { } } diff --git a/CryptoExchange.Net/Authentication/SignOutputType.cs b/CryptoExchange.Net/Authentication/SignOutputType.cs index 2c8ae5a..8dd46f4 100644 --- a/CryptoExchange.Net/Authentication/SignOutputType.cs +++ b/CryptoExchange.Net/Authentication/SignOutputType.cs @@ -1,17 +1,16 @@ -namespace CryptoExchange.Net.Authentication +namespace CryptoExchange.Net.Authentication; + +/// +/// Output string type +/// +public enum SignOutputType { /// - /// Output string type + /// Hex string /// - public enum SignOutputType - { - /// - /// Hex string - /// - Hex, - /// - /// Base64 string - /// - Base64 - } + Hex, + /// + /// Base64 string + /// + Base64 } diff --git a/CryptoExchange.Net/Caching/MemoryCache.cs b/CryptoExchange.Net/Caching/MemoryCache.cs index ca2c3c4..6cd642e 100644 --- a/CryptoExchange.Net/Caching/MemoryCache.cs +++ b/CryptoExchange.Net/Caching/MemoryCache.cs @@ -1,53 +1,52 @@ -using System; +using System; using System.Collections.Concurrent; using System.Linq; -namespace CryptoExchange.Net.Caching +namespace CryptoExchange.Net.Caching; + +internal class MemoryCache { - internal class MemoryCache + private readonly ConcurrentDictionary _cache = new ConcurrentDictionary(); + private readonly object _lock = new object(); + + /// + /// Add a new cache entry. Will override an existing entry if it already exists + /// + /// The key identifier + /// Cache value + public void Add(string key, object value) { - private readonly ConcurrentDictionary _cache = new ConcurrentDictionary(); - private readonly object _lock = new object(); + var cacheItem = new CacheItem(DateTime.UtcNow, value); + _cache.AddOrUpdate(key, cacheItem, (key, val1) => cacheItem); + } - /// - /// Add a new cache entry. Will override an existing entry if it already exists - /// - /// The key identifier - /// Cache value - public void Add(string key, object value) + /// + /// Get a cached value + /// + /// The key identifier + /// The max age of the cached entry + /// Cached value if it was in cache + public object? Get(string key, TimeSpan maxAge) + { + 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; + + return value.Value; + } + + private class CacheItem + { + public DateTime CacheTime { get; } + public object Value { get; } + + public CacheItem(DateTime cacheTime, object value) { - var cacheItem = new CacheItem(DateTime.UtcNow, value); - _cache.AddOrUpdate(key, cacheItem, (key, val1) => cacheItem); - } - - /// - /// Get a cached value - /// - /// The key identifier - /// The max age of the cached entry - /// Cached value if it was in cache - public object? Get(string key, TimeSpan maxAge) - { - 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; - - return value.Value; - } - - private class CacheItem - { - public DateTime CacheTime { get; } - public object Value { get; } - - public CacheItem(DateTime cacheTime, object value) - { - CacheTime = cacheTime; - Value = value; - } + CacheTime = cacheTime; + Value = value; } } } diff --git a/CryptoExchange.Net/Clients/BaseApiClient.cs b/CryptoExchange.Net/Clients/BaseApiClient.cs index 7806e1f..4c5739a 100644 --- a/CryptoExchange.Net/Clients/BaseApiClient.cs +++ b/CryptoExchange.Net/Clients/BaseApiClient.cs @@ -1,134 +1,131 @@ using System; -using System.Collections.Generic; using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Interfaces; -using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects.Errors; using CryptoExchange.Net.Objects.Options; using CryptoExchange.Net.SharedApis; using Microsoft.Extensions.Logging; -namespace CryptoExchange.Net.Clients +namespace CryptoExchange.Net.Clients; + +/// +/// Base API for all API clients +/// +public abstract class BaseApiClient : IDisposable, IBaseApiClient { /// - /// Base API for all API clients + /// Logger /// - public abstract class BaseApiClient : IDisposable, IBaseApiClient + protected ILogger _logger; + + /// + /// If we are disposing + /// + protected bool _disposing; + + /// + /// The authentication provider for this API client. (null if no credentials are set) + /// + public AuthenticationProvider? AuthenticationProvider { get; private set; } + + /// + /// The environment this client communicates to + /// + public string BaseAddress { get; } + + /// + /// Output the original string data along with the deserialized object + /// + public bool OutputOriginalData { get; } + + /// + public bool Authenticated => ApiCredentials != null; + + /// + public ApiCredentials? ApiCredentials { get; set; } + + /// + /// Api options + /// + public ApiOptions ApiOptions { get; } + + /// + /// Client Options + /// + public ExchangeOptions ClientOptions { get; } + + /// + /// Mapping of a response code to known error types + /// + protected internal virtual ErrorMapping ErrorMapping { get; } = new ErrorMapping([]); + + /// + /// ctor + /// + /// Logger + /// Should data from this client include the original data in the call result + /// Base address for this API client + /// Api credentials + /// Client options + /// Api options + protected BaseApiClient(ILogger logger, bool outputOriginalData, ApiCredentials? apiCredentials, string baseAddress, ExchangeOptions clientOptions, ApiOptions apiOptions) { - /// - /// Logger - /// - protected ILogger _logger; + _logger = logger; - /// - /// If we are disposing - /// - protected bool _disposing; + ClientOptions = clientOptions; + ApiOptions = apiOptions; + OutputOriginalData = outputOriginalData; + BaseAddress = baseAddress; + ApiCredentials = apiCredentials?.Copy(); - /// - /// The authentication provider for this API client. (null if no credentials are set) - /// - public AuthenticationProvider? AuthenticationProvider { get; private set; } + if (ApiCredentials != null) + AuthenticationProvider = CreateAuthenticationProvider(ApiCredentials); + } - /// - /// The environment this client communicates to - /// - public string BaseAddress { get; } + /// + /// Create an AuthenticationProvider implementation instance based on the provided credentials + /// + /// + /// + protected abstract AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials); - /// - /// Output the original string data along with the deserialized object - /// - public bool OutputOriginalData { get; } + /// + public abstract string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverDate = null); - /// - public bool Authenticated => ApiCredentials != null; + /// + /// Get error info for a response code + /// + public ErrorInfo GetErrorInfo(int code, string? message = null) => GetErrorInfo(code.ToString(), message); - /// - public ApiCredentials? ApiCredentials { get; set; } + /// + /// Get error info for a response code + /// + public ErrorInfo GetErrorInfo(string code, string? message = null) => ErrorMapping.GetErrorInfo(code.ToString(), message); - /// - /// Api options - /// - public ApiOptions ApiOptions { get; } + /// + public void SetApiCredentials(T credentials) where T : ApiCredentials + { + ApiCredentials = credentials?.Copy(); + if (ApiCredentials != null) + AuthenticationProvider = CreateAuthenticationProvider(ApiCredentials); + } - /// - /// Client Options - /// - public ExchangeOptions ClientOptions { get; } + /// + public virtual void SetOptions(UpdateOptions options) where T : ApiCredentials + { + ClientOptions.Proxy = options.Proxy; + ClientOptions.RequestTimeout = options.RequestTimeout ?? ClientOptions.RequestTimeout; - /// - /// Mapping of a response code to known error types - /// - protected internal virtual ErrorMapping ErrorMapping { get; } = new ErrorMapping([]); + ApiCredentials = options.ApiCredentials?.Copy() ?? ApiCredentials; + if (ApiCredentials != null) + AuthenticationProvider = CreateAuthenticationProvider(ApiCredentials); + } - /// - /// ctor - /// - /// Logger - /// Should data from this client include the original data in the call result - /// Base address for this API client - /// Api credentials - /// Client options - /// Api options - protected BaseApiClient(ILogger logger, bool outputOriginalData, ApiCredentials? apiCredentials, string baseAddress, ExchangeOptions clientOptions, ApiOptions apiOptions) - { - _logger = logger; - - ClientOptions = clientOptions; - ApiOptions = apiOptions; - OutputOriginalData = outputOriginalData; - BaseAddress = baseAddress; - ApiCredentials = apiCredentials?.Copy(); - - if (ApiCredentials != null) - AuthenticationProvider = CreateAuthenticationProvider(ApiCredentials); - } - - /// - /// Create an AuthenticationProvider implementation instance based on the provided credentials - /// - /// - /// - protected abstract AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials); - - /// - public abstract string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverDate = null); - - /// - /// Get error info for a response code - /// - public ErrorInfo GetErrorInfo(int code, string? message = null) => GetErrorInfo(code.ToString(), message); - - /// - /// Get error info for a response code - /// - public ErrorInfo GetErrorInfo(string code, string? message = null) => ErrorMapping.GetErrorInfo(code.ToString(), message); - - /// - public void SetApiCredentials(T credentials) where T : ApiCredentials - { - ApiCredentials = credentials?.Copy(); - if (ApiCredentials != null) - AuthenticationProvider = CreateAuthenticationProvider(ApiCredentials); - } - - /// - public virtual void SetOptions(UpdateOptions 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); - } - - /// - /// Dispose - /// - public virtual void Dispose() - { - _disposing = true; - } + /// + /// Dispose + /// + public virtual void Dispose() + { + _disposing = true; } } diff --git a/CryptoExchange.Net/Clients/BaseClient.cs b/CryptoExchange.Net/Clients/BaseClient.cs index 7523423..6a148c9 100644 --- a/CryptoExchange.Net/Clients/BaseClient.cs +++ b/CryptoExchange.Net/Clients/BaseClient.cs @@ -1,130 +1,128 @@ -using CryptoExchange.Net.Authentication; +using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Objects.Options; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; using System; using System.Collections.Generic; -namespace CryptoExchange.Net.Clients +namespace CryptoExchange.Net.Clients; + +/// +/// The base for all clients, websocket client and rest client +/// +public abstract class BaseClient : IDisposable { /// - /// The base for all clients, websocket client and rest client + /// Version of the CryptoExchange.Net base library /// - public abstract class BaseClient : IDisposable - { - /// - /// Version of the CryptoExchange.Net base library - /// - public Version CryptoExchangeLibVersion { get; } = typeof(BaseClient).Assembly.GetName().Version!; + public Version CryptoExchangeLibVersion { get; } = typeof(BaseClient).Assembly.GetName().Version!; - /// - /// Version of the client implementation - /// - public Version ExchangeLibVersion - { - get + /// + /// Version of the client implementation + /// + public Version ExchangeLibVersion + { + get + { + lock(_versionLock) { - lock(_versionLock) - { - if (_exchangeVersion == null) - _exchangeVersion = GetType().Assembly.GetName().Version!; + if (_exchangeVersion == null) + _exchangeVersion = GetType().Assembly.GetName().Version!; - return _exchangeVersion; - } + return _exchangeVersion; } } + } - /// - /// The name of the API the client is for - /// - public string Exchange { get; } + /// + /// The name of the API the client is for + /// + public string Exchange { get; } - /// - /// Api clients in this client - /// - internal List ApiClients { get; } = new List(); + /// + /// Api clients in this client + /// + internal List ApiClients { get; } = new List(); - /// - /// The log object - /// - protected internal ILogger _logger; + /// + /// The log object + /// + protected internal ILogger _logger; - private readonly object _versionLock = new object(); - private Version _exchangeVersion; + private readonly object _versionLock = new object(); + private Version _exchangeVersion; - /// - /// Provided client options - /// - public ExchangeOptions ClientOptions { get; private set; } + /// + /// Provided client options + /// + public ExchangeOptions ClientOptions { get; private set; } - /// - /// ctor - /// - /// Logger - /// The name of the exchange this client is for + /// + /// ctor + /// + /// Logger + /// The name of the exchange this client is for #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. - protected BaseClient(ILoggerFactory? logger, string exchange) + 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. - { - Exchange = exchange; - } + { + Exchange = exchange; + } - /// - /// Initialize the client with the specified options - /// - /// - /// - protected virtual void Initialize(ExchangeOptions options) - { - if (options == null) - throw new ArgumentNullException(nameof(options)); + /// + /// Initialize the client with the specified options + /// + /// + /// + protected virtual void Initialize(ExchangeOptions options) + { + if (options == null) + throw new ArgumentNullException(nameof(options)); - ClientOptions = options; - _logger.Log(LogLevel.Trace, $"Client configuration: {options}, CryptoExchange.Net: v{CryptoExchangeLibVersion}, {Exchange}.Net: v{ExchangeLibVersion}"); - } + ClientOptions = options; + _logger.Log(LogLevel.Trace, "Client configuration: {Options}, CryptoExchange.Net: v{CryptoExchangeVersion}, {Exchange}.Net: v{ExchangeVersion}", options, CryptoExchangeLibVersion, Exchange, ExchangeLibVersion); + } - /// - /// Set the API credentials for this client. All Api clients in this client will use the new credentials, regardless of earlier set options. - /// - /// The credentials to set - protected virtual void SetApiCredentials(T credentials) where T : ApiCredentials - { - foreach (var apiClient in ApiClients) - apiClient.SetApiCredentials(credentials); - } + /// + /// Set the API credentials for this client. All Api clients in this client will use the new credentials, regardless of earlier set options. + /// + /// The credentials to set + protected virtual void SetApiCredentials(T credentials) where T : ApiCredentials + { + foreach (var apiClient in ApiClients) + apiClient.SetApiCredentials(credentials); + } - /// - /// Register an API client - /// - /// The client - protected T AddApiClient(T apiClient) where T : BaseApiClient - { - if (ClientOptions == null) - throw new InvalidOperationException("Client should have called Initialize before adding API clients"); + /// + /// Register an API client + /// + /// The client + protected T AddApiClient(T apiClient) where T : BaseApiClient + { + if (ClientOptions == null) + throw new InvalidOperationException("Client should have called Initialize before adding API clients"); - _logger.Log(LogLevel.Trace, $" {apiClient.GetType().Name}, base address: {apiClient.BaseAddress}"); - ApiClients.Add(apiClient); - return apiClient; - } + _logger.Log(LogLevel.Trace, " {ApiClient}, base address: {BaseAddress}", apiClient.GetType().Name, apiClient.BaseAddress); + ApiClients.Add(apiClient); + return apiClient; + } - /// - /// Apply the options delegate to a new options instance - /// - protected static T ApplyOptionsDelegate(Action? del) where T: new() - { - var opts = new T(); - del?.Invoke(opts); - return opts; - } + /// + /// Apply the options delegate to a new options instance + /// + protected static T ApplyOptionsDelegate(Action? del) where T: new() + { + var opts = new T(); + del?.Invoke(opts); + return opts; + } - /// - /// Dispose - /// - public virtual void Dispose() - { - _logger.Log(LogLevel.Debug, "Disposing client"); - foreach (var client in ApiClients) - client.Dispose(); - } + /// + /// Dispose + /// + public virtual void Dispose() + { + _logger.Log(LogLevel.Debug, "Disposing client"); + foreach (var client in ApiClients) + client.Dispose(); } } diff --git a/CryptoExchange.Net/Clients/BaseRestClient.cs b/CryptoExchange.Net/Clients/BaseRestClient.cs index bc5464b..fba0261 100644 --- a/CryptoExchange.Net/Clients/BaseRestClient.cs +++ b/CryptoExchange.Net/Clients/BaseRestClient.cs @@ -3,24 +3,23 @@ using CryptoExchange.Net.Interfaces; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -namespace CryptoExchange.Net.Clients -{ - /// - /// Base rest client - /// - public abstract class BaseRestClient : BaseClient, IRestClient - { - /// - public int TotalRequestsMade => ApiClients.OfType().Sum(s => s.TotalRequestsMade); +namespace CryptoExchange.Net.Clients; - /// - /// ctor - /// - /// Logger factory - /// The name of the API this client is for - protected BaseRestClient(ILoggerFactory? loggerFactory, string name) : base(loggerFactory, name) - { - _logger = loggerFactory?.CreateLogger(name + ".RestClient") ?? NullLoggerFactory.Instance.CreateLogger(name); - } +/// +/// Base rest client +/// +public abstract class BaseRestClient : BaseClient, IRestClient +{ + /// + public int TotalRequestsMade => ApiClients.OfType().Sum(s => s.TotalRequestsMade); + + /// + /// ctor + /// + /// Logger factory + /// The name of the API this client is for + protected BaseRestClient(ILoggerFactory? loggerFactory, string name) : base(loggerFactory, name) + { + _logger = loggerFactory?.CreateLogger(name + ".RestClient") ?? NullLoggerFactory.Instance.CreateLogger(name); } } diff --git a/CryptoExchange.Net/Clients/BaseSocketClient.cs b/CryptoExchange.Net/Clients/BaseSocketClient.cs index 38f34e7..ef80ed0 100644 --- a/CryptoExchange.Net/Clients/BaseSocketClient.cs +++ b/CryptoExchange.Net/Clients/BaseSocketClient.cs @@ -1,132 +1,130 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; -using System.Xml.Linq; using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Logging.Extensions; using CryptoExchange.Net.Objects.Sockets; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -namespace CryptoExchange.Net.Clients +namespace CryptoExchange.Net.Clients; + +/// +/// Base for socket client implementations +/// +public abstract class BaseSocketClient : BaseClient, ISocketClient { + #region fields + /// - /// Base for socket client implementations + /// If client is disposing /// - public abstract class BaseSocketClient : BaseClient, ISocketClient + protected bool _disposing; + + /// + public int CurrentConnections => ApiClients.OfType().Sum(c => c.CurrentConnections); + /// + public int CurrentSubscriptions => ApiClients.OfType().Sum(s => s.CurrentSubscriptions); + /// + public double IncomingKbps => ApiClients.OfType().Sum(s => s.IncomingKbps); + #endregion + + /// + /// ctor + /// + /// Logger factory + /// The name of the exchange this client is for + protected BaseSocketClient(ILoggerFactory? loggerFactory, string name) : base(loggerFactory, name) { - #region fields + _logger = loggerFactory?.CreateLogger(name + ".SocketClient") ?? NullLoggerFactory.Instance.CreateLogger(name); + } - /// - /// If client is disposing - /// - protected bool _disposing; - - /// - public int CurrentConnections => ApiClients.OfType().Sum(c => c.CurrentConnections); - /// - public int CurrentSubscriptions => ApiClients.OfType().Sum(s => s.CurrentSubscriptions); - /// - public double IncomingKbps => ApiClients.OfType().Sum(s => s.IncomingKbps); - #endregion - - /// - /// ctor - /// - /// Logger factory - /// The name of the exchange this client is for - protected BaseSocketClient(ILoggerFactory? loggerFactory, string name) : base(loggerFactory, name) + /// + /// Unsubscribe an update subscription + /// + /// The id of the subscription to unsubscribe + /// + public virtual async Task UnsubscribeAsync(int subscriptionId) + { + foreach (var socket in ApiClients.OfType()) { - _logger = loggerFactory?.CreateLogger(name + ".SocketClient") ?? NullLoggerFactory.Instance.CreateLogger(name); - } - - /// - /// Unsubscribe an update subscription - /// - /// The id of the subscription to unsubscribe - /// - public virtual async Task UnsubscribeAsync(int subscriptionId) - { - foreach (var socket in ApiClients.OfType()) - { - var result = await socket.UnsubscribeAsync(subscriptionId).ConfigureAwait(false); - if (result) - break; - } - } - - /// - /// Unsubscribe an update subscription - /// - /// The subscription to unsubscribe - /// - public virtual async Task UnsubscribeAsync(UpdateSubscription subscription) - { - if (subscription == null) - throw new ArgumentNullException(nameof(subscription)); - - _logger.UnsubscribingSubscription(subscription.SocketId, subscription.Id); - await subscription.CloseAsync().ConfigureAwait(false); - } - - /// - /// Unsubscribe all subscriptions - /// - /// - public virtual async Task UnsubscribeAllAsync() - { - var tasks = new List(); - foreach (var client in ApiClients.OfType()) - tasks.Add(client.UnsubscribeAllAsync()); - - await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false); - } - - /// - /// Reconnect all connections - /// - /// - public virtual async Task ReconnectAsync() - { - _logger.ReconnectingAllConnections(CurrentConnections); - var tasks = new List(); - foreach (var client in ApiClients.OfType()) - { - tasks.Add(client.ReconnectAsync()); - } - - await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false); - } - - /// - /// Log the current state of connections and subscriptions - /// - public string GetSubscriptionsState() - { - var result = new StringBuilder(); - foreach (var client in ApiClients.OfType().Where(c => c.CurrentSubscriptions > 0)) - { - result.AppendLine(client.GetSubscriptionsState()); - } - - return result.ToString(); - } - - /// - /// Returns the state of all socket api clients - /// - /// - public List GetSocketApiClientStates() - { - var result = new List(); - foreach (var client in ApiClients.OfType()) - { - result.Add(client.GetState()); - } - - return result; + var result = await socket.UnsubscribeAsync(subscriptionId).ConfigureAwait(false); + if (result) + break; } } + + /// + /// Unsubscribe an update subscription + /// + /// The subscription to unsubscribe + /// + public virtual async Task UnsubscribeAsync(UpdateSubscription subscription) + { + if (subscription == null) + throw new ArgumentNullException(nameof(subscription)); + + _logger.UnsubscribingSubscription(subscription.SocketId, subscription.Id); + await subscription.CloseAsync().ConfigureAwait(false); + } + + /// + /// Unsubscribe all subscriptions + /// + /// + public virtual async Task UnsubscribeAllAsync() + { + var tasks = new List(); + foreach (var client in ApiClients.OfType()) + tasks.Add(client.UnsubscribeAllAsync()); + + await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false); + } + + /// + /// Reconnect all connections + /// + /// + public virtual async Task ReconnectAsync() + { + _logger.ReconnectingAllConnections(CurrentConnections); + var tasks = new List(); + foreach (var client in ApiClients.OfType()) + { + tasks.Add(client.ReconnectAsync()); + } + + await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false); + } + + /// + /// Log the current state of connections and subscriptions + /// + public string GetSubscriptionsState() + { + var result = new StringBuilder(); + foreach (var client in ApiClients.OfType().Where(c => c.CurrentSubscriptions > 0)) + { + result.AppendLine(client.GetSubscriptionsState()); + } + + return result.ToString(); + } + + /// + /// Returns the state of all socket api clients + /// + /// + public List GetSocketApiClientStates() + { + var result = new List(); + foreach (var client in ApiClients.OfType()) + { + result.Add(client.GetState()); + } + + return result; + } } diff --git a/CryptoExchange.Net/Clients/CryptoBaseClient.cs b/CryptoExchange.Net/Clients/CryptoBaseClient.cs index c781a66..4252dcc 100644 --- a/CryptoExchange.Net/Clients/CryptoBaseClient.cs +++ b/CryptoExchange.Net/Clients/CryptoBaseClient.cs @@ -1,67 +1,66 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; using System; using System.Collections.Generic; -namespace CryptoExchange.Net.Clients +namespace CryptoExchange.Net.Clients; + +/// +/// Base crypto client +/// +public class CryptoBaseClient : IDisposable { + private readonly Dictionary _serviceCache = new Dictionary(); + /// - /// Base crypto client + /// Service provider /// - public class CryptoBaseClient : IDisposable + protected readonly IServiceProvider? _serviceProvider; + + /// + /// ctor + /// + public CryptoBaseClient() { } + + /// + /// ctor + /// + /// + public CryptoBaseClient(IServiceProvider serviceProvider) { - private readonly Dictionary _serviceCache = new Dictionary(); + _serviceProvider = serviceProvider; + _serviceCache = new Dictionary(); + } - /// - /// Service provider - /// - protected readonly IServiceProvider? _serviceProvider; + /// + /// Try get a client by type for the service collection + /// + /// + /// + public T TryGet(Func createFunc) + { + var type = typeof(T); + if (_serviceCache.TryGetValue(type, out var value)) + return (T)value; - /// - /// ctor - /// - public CryptoBaseClient() { } - - /// - /// ctor - /// - /// - public CryptoBaseClient(IServiceProvider serviceProvider) + if (_serviceProvider == null) { - _serviceProvider = serviceProvider; - _serviceCache = new Dictionary(); + // Create with default options + var createResult = createFunc(); + _serviceCache.Add(typeof(T), createResult!); + return createResult; } - /// - /// Try get a client by type for the service collection - /// - /// - /// - public T TryGet(Func createFunc) - { - var type = typeof(T); - if (_serviceCache.TryGetValue(type, out var value)) - return (T)value; + var result = _serviceProvider.GetService() + ?? throw new InvalidOperationException($"No service was found for {typeof(T).Name}, make sure the exchange is registered in dependency injection with the `services.Add[Exchange]()` method"); + _serviceCache.Add(type, result!); + return result; + } - if (_serviceProvider == null) - { - // Create with default options - var createResult = createFunc(); - _serviceCache.Add(typeof(T), createResult!); - return createResult; - } - - var result = _serviceProvider.GetService() - ?? throw new InvalidOperationException($"No service was found for {typeof(T).Name}, make sure the exchange is registered in dependency injection with the `services.Add[Exchange]()` method"); - _serviceCache.Add(type, result!); - return result; - } - - /// - /// Dispose - /// - public void Dispose() - { - _serviceCache.Clear(); - } + /// + /// Dispose + /// + public void Dispose() + { + _serviceCache.Clear(); } } diff --git a/CryptoExchange.Net/Clients/CryptoRestClient.cs b/CryptoExchange.Net/Clients/CryptoRestClient.cs index d4ee0bb..c582bc6 100644 --- a/CryptoExchange.Net/Clients/CryptoRestClient.cs +++ b/CryptoExchange.Net/Clients/CryptoRestClient.cs @@ -1,27 +1,23 @@ -using CryptoExchange.Net.Interfaces; -using Microsoft.Extensions.DependencyInjection; +using CryptoExchange.Net.Interfaces; using System; -using System.Collections.Generic; -using System.Linq; -namespace CryptoExchange.Net.Clients +namespace CryptoExchange.Net.Clients; + +/// +public class CryptoRestClient : CryptoBaseClient, ICryptoRestClient { - /// - public class CryptoRestClient : CryptoBaseClient, ICryptoRestClient + /// + /// ctor + /// + public CryptoRestClient() { - /// - /// ctor - /// - public CryptoRestClient() - { - } + } - /// - /// ctor - /// - /// - public CryptoRestClient(IServiceProvider serviceProvider) : base(serviceProvider) - { - } + /// + /// ctor + /// + /// + public CryptoRestClient(IServiceProvider serviceProvider) : base(serviceProvider) + { } } \ No newline at end of file diff --git a/CryptoExchange.Net/Clients/CryptoSocketClient.cs b/CryptoExchange.Net/Clients/CryptoSocketClient.cs index 5c33ef9..8ed16b7 100644 --- a/CryptoExchange.Net/Clients/CryptoSocketClient.cs +++ b/CryptoExchange.Net/Clients/CryptoSocketClient.cs @@ -1,24 +1,23 @@ -using CryptoExchange.Net.Interfaces; +using CryptoExchange.Net.Interfaces; using System; -namespace CryptoExchange.Net.Clients -{ - /// - public class CryptoSocketClient : CryptoBaseClient, ICryptoSocketClient - { - /// - /// ctor - /// - public CryptoSocketClient() - { - } +namespace CryptoExchange.Net.Clients; - /// - /// ctor - /// - /// - public CryptoSocketClient(IServiceProvider serviceProvider) : base(serviceProvider) - { - } +/// +public class CryptoSocketClient : CryptoBaseClient, ICryptoSocketClient +{ + /// + /// ctor + /// + public CryptoSocketClient() + { + } + + /// + /// ctor + /// + /// + public CryptoSocketClient(IServiceProvider serviceProvider) : base(serviceProvider) + { } } diff --git a/CryptoExchange.Net/Clients/RestApiClient.cs b/CryptoExchange.Net/Clients/RestApiClient.cs index 4f85a04..88f74e9 100644 --- a/CryptoExchange.Net/Clients/RestApiClient.cs +++ b/CryptoExchange.Net/Clients/RestApiClient.cs @@ -18,708 +18,707 @@ using CryptoExchange.Net.RateLimiting.Interfaces; using CryptoExchange.Net.Requests; using Microsoft.Extensions.Logging; -namespace CryptoExchange.Net.Clients +namespace CryptoExchange.Net.Clients; + +/// +/// Base rest API client for interacting with a REST API +/// +public abstract class RestApiClient : BaseApiClient, IRestApiClient { + /// + public IRequestFactory RequestFactory { get; set; } = new RequestFactory(); + + /// + public abstract TimeSyncInfo? GetTimeSyncInfo(); + + /// + public abstract TimeSpan? GetTimeOffset(); + + /// + public int TotalRequestsMade { get; set; } + /// - /// Base rest API client for interacting with a REST API + /// Request body content type /// - public abstract class RestApiClient : BaseApiClient, IRestApiClient + protected internal RequestBodyFormat RequestBodyFormat = RequestBodyFormat.Json; + + /// + /// How to serialize array parameters when making requests + /// + protected internal ArrayParametersSerialization ArraySerialization = ArrayParametersSerialization.Array; + + /// + /// What request body should be set when no data is send (only used in combination with postParametersPosition.InBody) + /// + protected internal string RequestBodyEmptyContent = "{}"; + + /// + /// Request headers to be sent with each request + /// + protected Dictionary StandardRequestHeaders { get; set; } = []; + + /// + /// Whether parameters need to be ordered + /// + protected internal bool OrderParameters { get; set; } = true; + + /// + /// Parameter order comparer + /// + protected IComparer ParameterOrderComparer { get; } = new OrderedStringComparer(); + + /// + /// Where to put the parameters for requests with different Http methods + /// + public Dictionary ParameterPositions { get; set; } = new Dictionary { - /// - public IRequestFactory RequestFactory { get; set; } = new RequestFactory(); + { HttpMethod.Get, HttpMethodParameterPosition.InUri }, + { HttpMethod.Post, HttpMethodParameterPosition.InBody }, + { HttpMethod.Delete, HttpMethodParameterPosition.InBody }, + { HttpMethod.Put, HttpMethodParameterPosition.InBody }, + { new HttpMethod("Patch"), HttpMethodParameterPosition.InBody }, + }; - /// - public abstract TimeSyncInfo? GetTimeSyncInfo(); + /// + public new RestExchangeOptions ClientOptions => (RestExchangeOptions)base.ClientOptions; - /// - public abstract TimeSpan? GetTimeOffset(); + /// + public new RestApiOptions ApiOptions => (RestApiOptions)base.ApiOptions; - /// - public int TotalRequestsMade { get; set; } + /// + /// Memory cache + /// + private readonly static MemoryCache _cache = new MemoryCache(); - /// - /// Request body content type - /// - protected internal RequestBodyFormat RequestBodyFormat = RequestBodyFormat.Json; + /// + /// ctor + /// + /// Logger + /// HttpClient to use + /// Base address for this API client + /// The base client options + /// The Api client options + public RestApiClient(ILogger logger, HttpClient? httpClient, string baseAddress, RestExchangeOptions options, RestApiOptions apiOptions) + : base(logger, + apiOptions.OutputOriginalData ?? options.OutputOriginalData, + apiOptions.ApiCredentials ?? options.ApiCredentials, + baseAddress, + options, + apiOptions) + { + RequestFactory.Configure(options.Proxy, options.RequestTimeout, httpClient); + } - /// - /// How to serialize array parameters when making requests - /// - protected internal ArrayParametersSerialization ArraySerialization = ArrayParametersSerialization.Array; + /// + /// Create a message accessor instance + /// + /// + protected abstract IStreamMessageAccessor CreateAccessor(); - /// - /// What request body should be set when no data is send (only used in combination with postParametersPosition.InBody) - /// - protected internal string RequestBodyEmptyContent = "{}"; + /// + /// Create a serializer instance + /// + /// + protected abstract IMessageSerializer CreateSerializer(); - /// - /// Request headers to be sent with each request - /// - protected Dictionary StandardRequestHeaders { get; set; } = []; + /// + /// Send a request to the base address based on the request definition + /// + /// Host and schema + /// Request definition + /// Request parameters + /// Cancellation token + /// Additional headers for this request + /// Override the request weight for this request definition, for example when the weight depends on the parameters + /// + protected virtual async Task SendAsync( + string baseAddress, + RequestDefinition definition, + ParameterCollection? parameters, + CancellationToken cancellationToken, + Dictionary? additionalHeaders = null, + int? weight = null) + { + var result = await SendAsync(baseAddress, definition, parameters, cancellationToken, additionalHeaders, weight).ConfigureAwait(false); + return result.AsDataless(); + } - /// - /// Whether parameters need to be ordered - /// - protected internal bool OrderParameters { get; set; } = true; + /// + /// Send a request to the base address based on the request definition + /// + /// Response type + /// Host and schema + /// Request definition + /// Request parameters + /// Cancellation token + /// Additional headers for this request + /// Override the request weight for this request definition, for example when the weight depends on the parameters + /// Specify the weight to apply to the individual rate limit guard for this request + /// An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters. + /// + protected virtual Task> SendAsync( + string baseAddress, + RequestDefinition definition, + ParameterCollection? parameters, + CancellationToken cancellationToken, + Dictionary? additionalHeaders = null, + int? weight = null, + int? weightSingleLimiter = null, + string? rateLimitKeySuffix = null) + { + var parameterPosition = definition.ParameterPosition ?? ParameterPositions[definition.Method]; + return SendAsync( + baseAddress, + definition, + parameterPosition == HttpMethodParameterPosition.InUri ? parameters : null, + parameterPosition == HttpMethodParameterPosition.InBody ? parameters : null, + cancellationToken, + additionalHeaders, + weight, + weightSingleLimiter, + rateLimitKeySuffix); + } - /// - /// Parameter order comparer - /// - protected IComparer ParameterOrderComparer { get; } = new OrderedStringComparer(); - - /// - /// Where to put the parameters for requests with different Http methods - /// - public Dictionary ParameterPositions { get; set; } = new Dictionary + /// + /// Send a request to the base address based on the request definition + /// + /// Response type + /// Host and schema + /// Request definition + /// Request query parameters + /// Request body parameters + /// Cancellation token + /// Additional headers for this request + /// Override the request weight for this request definition, for example when the weight depends on the parameters + /// Specify the weight to apply to the individual rate limit guard for this request + /// An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters. + /// + protected virtual async Task> SendAsync( + string baseAddress, + RequestDefinition definition, + ParameterCollection? uriParameters, + ParameterCollection? bodyParameters, + CancellationToken cancellationToken, + Dictionary? additionalHeaders = null, + int? weight = null, + int? weightSingleLimiter = null, + string? rateLimitKeySuffix = null) + { + string? cacheKey = null; + if (ShouldCache(definition)) { - { HttpMethod.Get, HttpMethodParameterPosition.InUri }, - { HttpMethod.Post, HttpMethodParameterPosition.InBody }, - { HttpMethod.Delete, HttpMethodParameterPosition.InBody }, - { HttpMethod.Put, HttpMethodParameterPosition.InBody }, - { new HttpMethod("Patch"), HttpMethodParameterPosition.InBody }, - }; - - /// - public new RestExchangeOptions ClientOptions => (RestExchangeOptions)base.ClientOptions; - - /// - public new RestApiOptions ApiOptions => (RestApiOptions)base.ApiOptions; - - /// - /// Memory cache - /// - private readonly static MemoryCache _cache = new MemoryCache(); - - /// - /// ctor - /// - /// Logger - /// HttpClient to use - /// Base address for this API client - /// The base client options - /// The Api client options - public RestApiClient(ILogger logger, HttpClient? httpClient, string baseAddress, RestExchangeOptions options, RestApiOptions apiOptions) - : base(logger, - apiOptions.OutputOriginalData ?? options.OutputOriginalData, - apiOptions.ApiCredentials ?? options.ApiCredentials, - baseAddress, - options, - apiOptions) - { - RequestFactory.Configure(options.Proxy, options.RequestTimeout, httpClient); - } - - /// - /// Create a message accessor instance - /// - /// - protected abstract IStreamMessageAccessor CreateAccessor(); - - /// - /// Create a serializer instance - /// - /// - protected abstract IMessageSerializer CreateSerializer(); - - /// - /// Send a request to the base address based on the request definition - /// - /// Host and schema - /// Request definition - /// Request parameters - /// Cancellation token - /// Additional headers for this request - /// Override the request weight for this request definition, for example when the weight depends on the parameters - /// - protected virtual async Task SendAsync( - string baseAddress, - RequestDefinition definition, - ParameterCollection? parameters, - CancellationToken cancellationToken, - Dictionary? additionalHeaders = null, - int? weight = null) - { - var result = await SendAsync(baseAddress, definition, parameters, cancellationToken, additionalHeaders, weight).ConfigureAwait(false); - return result.AsDataless(); - } - - /// - /// Send a request to the base address based on the request definition - /// - /// Response type - /// Host and schema - /// Request definition - /// Request parameters - /// Cancellation token - /// Additional headers for this request - /// Override the request weight for this request definition, for example when the weight depends on the parameters - /// Specify the weight to apply to the individual rate limit guard for this request - /// An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters. - /// - protected virtual Task> SendAsync( - string baseAddress, - RequestDefinition definition, - ParameterCollection? parameters, - CancellationToken cancellationToken, - Dictionary? additionalHeaders = null, - int? weight = null, - int? weightSingleLimiter = null, - string? rateLimitKeySuffix = null) - { - var parameterPosition = definition.ParameterPosition ?? ParameterPositions[definition.Method]; - return SendAsync( - baseAddress, - definition, - parameterPosition == HttpMethodParameterPosition.InUri ? parameters : null, - parameterPosition == HttpMethodParameterPosition.InBody ? parameters : null, - cancellationToken, - additionalHeaders, - weight, - weightSingleLimiter, - rateLimitKeySuffix); - } - - /// - /// Send a request to the base address based on the request definition - /// - /// Response type - /// Host and schema - /// Request definition - /// Request query parameters - /// Request body parameters - /// Cancellation token - /// Additional headers for this request - /// Override the request weight for this request definition, for example when the weight depends on the parameters - /// Specify the weight to apply to the individual rate limit guard for this request - /// An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters. - /// - protected virtual async Task> SendAsync( - string baseAddress, - RequestDefinition definition, - ParameterCollection? uriParameters, - ParameterCollection? bodyParameters, - CancellationToken cancellationToken, - Dictionary? additionalHeaders = null, - int? weight = null, - int? weightSingleLimiter = null, - string? rateLimitKeySuffix = null) - { - string? cacheKey = null; - if (ShouldCache(definition)) + cacheKey = baseAddress + definition + uriParameters?.ToFormData(); + _logger.CheckingCache(cacheKey); + var cachedValue = _cache.Get(cacheKey, ClientOptions.CachingMaxAge); + if (cachedValue != null) { - cacheKey = baseAddress + definition + uriParameters?.ToFormData(); - _logger.CheckingCache(cacheKey); - var cachedValue = _cache.Get(cacheKey, ClientOptions.CachingMaxAge); - if (cachedValue != null) - { - _logger.CacheHit(cacheKey); - var original = (WebCallResult)cachedValue; - return original.Cached(); - } - - _logger.CacheNotHit(cacheKey); + _logger.CacheHit(cacheKey); + var original = (WebCallResult)cachedValue; + return original.Cached(); } - int currentTry = 0; - while (true) + _logger.CacheNotHit(cacheKey); + } + + int currentTry = 0; + while (true) + { + currentTry++; + var requestId = ExchangeHelpers.NextId(); + + var prepareResult = await PrepareAsync(requestId, baseAddress, definition, cancellationToken, additionalHeaders, weight, weightSingleLimiter, rateLimitKeySuffix).ConfigureAwait(false); + if (!prepareResult) + return new WebCallResult(prepareResult.Error!); + + var request = CreateRequest( + requestId, + baseAddress, + definition, + uriParameters, + bodyParameters, + additionalHeaders); + _logger.RestApiSendRequest(request.RequestId, definition, request.Content, string.IsNullOrEmpty(request.Uri.Query) ? "-" : request.Uri.Query, string.Join(", ", request.GetHeaders().Select(h => h.Key + $"=[{string.Join(",", h.Value)}]"))); + TotalRequestsMade++; + var result = await GetResponseAsync(request, definition.RateLimitGate, cancellationToken).ConfigureAwait(false); + if (result.Error is not CancellationRequestedError) { - currentTry++; - var requestId = ExchangeHelpers.NextId(); + 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.RestApiCancellationRequested(result.RequestId); + } - var prepareResult = await PrepareAsync(requestId, baseAddress, definition, cancellationToken, additionalHeaders, weight, weightSingleLimiter, rateLimitKeySuffix).ConfigureAwait(false); - if (!prepareResult) - return new WebCallResult(prepareResult.Error!); + if (await ShouldRetryRequestAsync(definition.RateLimitGate, result, currentTry).ConfigureAwait(false)) + continue; - var request = CreateRequest( - requestId, - baseAddress, - definition, - uriParameters, - bodyParameters, - additionalHeaders); - _logger.RestApiSendRequest(request.RequestId, definition, request.Content, string.IsNullOrEmpty(request.Uri.Query) ? "-" : request.Uri.Query, string.Join(", ", request.GetHeaders().Select(h => h.Key + $"=[{string.Join(",", h.Value)}]"))); - TotalRequestsMade++; - var result = await GetResponseAsync(request, definition.RateLimitGate, cancellationToken).ConfigureAwait(false); - if (result.Error is not CancellationRequestedError) + if (result.Success && + ShouldCache(definition)) + { + _cache.Add(cacheKey!, result); + } + + return result; + } + } + + /// + /// Prepare before sending a request. Sync time between client and server and check rate limits + /// + /// Request id + /// Host and schema + /// Request definition + /// Cancellation token + /// Additional headers for this request + /// Override the request weight for this request + /// Specify the weight to apply to the individual rate limit guard for this request + /// An additional optional suffix for the key selector + /// + /// + protected virtual async Task PrepareAsync( + int requestId, + string baseAddress, + RequestDefinition definition, + CancellationToken cancellationToken, + Dictionary? additionalHeaders = null, + int? weight = null, + int? weightSingleLimiter = null, + string? rateLimitKeySuffix = null) + { + // Time sync + if (definition.Authenticated) + { + if (AuthenticationProvider == null) + { + _logger.RestApiNoApiCredentials(requestId, definition.Path); + return new CallResult(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) { - 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); + _logger.RestApiFailedToSyncTime(requestId, syncTimeResult.Error!.ToString()); + return syncTimeResult.AsDataless(); + } + } + } + + // Rate limiting + var requestWeight = weight ?? definition.Weight; + if (requestWeight != 0) + { + if (definition.RateLimitGate == null) + throw new Exception("Ratelimit gate not set when request weight is not 0"); + + if (ClientOptions.RateLimiterEnabled) + { + var limitResult = await definition.RateLimitGate.ProcessAsync(_logger, requestId, RateLimitItemType.Request, definition, baseAddress, AuthenticationProvider?._credentials.Key, requestWeight, ClientOptions.RateLimitingBehaviour, rateLimitKeySuffix, cancellationToken).ConfigureAwait(false); + if (!limitResult) + return new CallResult(limitResult.Error!); + } + } + + // Endpoint specific rate limiting + if (definition.LimitGuard != null && ClientOptions.RateLimiterEnabled) + { + if (definition.RateLimitGate == null) + throw new Exception("Ratelimit gate not set when endpoint limit is specified"); + + if (ClientOptions.RateLimiterEnabled) + { + var singleRequestWeight = weightSingleLimiter ?? 1; + var limitResult = await definition.RateLimitGate.ProcessSingleAsync(_logger, requestId, definition.LimitGuard, RateLimitItemType.Request, definition, baseAddress, AuthenticationProvider?._credentials.Key, singleRequestWeight, ClientOptions.RateLimitingBehaviour, rateLimitKeySuffix, cancellationToken).ConfigureAwait(false); + if (!limitResult) + return new CallResult(limitResult.Error!); + } + } + + return CallResult.SuccessResult; + } + + /// + /// Creates a request object + /// + /// Id of the request + /// Host and schema + /// Request definition + /// The query parameters of the request + /// The body parameters of the request + /// Additional headers to send with the request + /// + protected virtual IRequest CreateRequest( + int requestId, + string baseAddress, + RequestDefinition definition, + ParameterCollection? uriParameters, + ParameterCollection? bodyParameters, + Dictionary? additionalHeaders) + { + var requestConfiguration = new RestRequestConfiguration( + definition, + baseAddress, + uriParameters == null ? new Dictionary() : CreateParameterDictionary(uriParameters), + bodyParameters == null ? new Dictionary() : CreateParameterDictionary(bodyParameters), + new Dictionary(additionalHeaders ?? []), + definition.ArraySerialization ?? ArraySerialization, + definition.ParameterPosition ?? ParameterPositions[definition.Method], + definition.RequestBodyFormat ?? RequestBodyFormat); + + try + { + AuthenticationProvider?.ProcessRequest(this, requestConfiguration); + } + catch (Exception ex) + { + throw new Exception("Failed to authenticate request, make sure your API credentials are correct", ex); + } + + var queryString = requestConfiguration.GetQueryString(true); + if (!string.IsNullOrEmpty(queryString) && !queryString.StartsWith("?")) + queryString = $"?{queryString}"; + + var uri = new Uri(baseAddress.AppendPath(definition.Path) + queryString); + var request = RequestFactory.Create(definition.Method, uri, requestId); + request.Accept = Constants.JsonContentHeader; + + foreach (var header in requestConfiguration.Headers) + request.AddHeader(header.Key, header.Value); + + foreach (var header in StandardRequestHeaders) + { + // Only add it if it isn't overwritten + if (!requestConfiguration.Headers.ContainsKey(header.Key)) + request.AddHeader(header.Key, header.Value); + } + + if (requestConfiguration.ParameterPosition == HttpMethodParameterPosition.InBody) + { + var contentType = requestConfiguration.BodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader; + var bodyContent = requestConfiguration.GetBodyContent(); + if (bodyContent != null) + { + request.SetContent(bodyContent, contentType); + } + else + { + if (requestConfiguration.BodyParameters != null && requestConfiguration.BodyParameters.Count != 0) + WriteParamBody(request, requestConfiguration.BodyParameters, contentType); + else + request.SetContent(RequestBodyEmptyContent, contentType); + } + } + + return request; + } + + /// + /// Executes the request and returns the result deserialized into the type parameter class + /// + /// The request object to execute + /// The ratelimit gate used + /// Cancellation token + /// + protected virtual async Task> GetResponseAsync( + IRequest request, + IRateLimitGate? gate, + CancellationToken cancellationToken) + { + var sw = Stopwatch.StartNew(); + Stream? responseStream = null; + IResponse? response = null; + IStreamMessageAccessor? accessor = null; + try + { + response = await request.GetResponseAsync(cancellationToken).ConfigureAwait(false); + sw.Stop(); + var statusCode = response.StatusCode; + var headers = response.ResponseHeaders; + var responseLength = response.ContentLength; + responseStream = await response.GetResponseStreamAsync().ConfigureAwait(false); + var outputOriginalData = ApiOptions.OutputOriginalData ?? ClientOptions.OutputOriginalData; + + accessor = CreateAccessor(); + if (!response.IsSuccessStatusCode) + { + // Error response + var readResult = await accessor.Read(responseStream, true).ConfigureAwait(false); + + Error error; + if (response.StatusCode == (HttpStatusCode)418 || response.StatusCode == (HttpStatusCode)429) + { + var rateError = ParseRateLimitResponse((int)response.StatusCode, response.ResponseHeaders, accessor); + if (rateError.RetryAfter != null && gate != null && ClientOptions.RateLimiterEnabled) + { + _logger.RestApiRateLimitPauseUntil(request.RequestId, rateError.RetryAfter.Value); + await gate.SetRetryAfterGuardAsync(rateError.RetryAfter.Value).ConfigureAwait(false); + } + + error = rateError; } else { - _logger.RestApiCancellationRequested(result.RequestId); + error = ParseErrorResponse((int)response.StatusCode, response.ResponseHeaders, accessor, readResult.Error?.Exception); } - if (await ShouldRetryRequestAsync(definition.RateLimitGate, result, currentTry).ConfigureAwait(false)) - continue; + if (error.Code == null || error.Code == 0) + error.Code = (int)response.StatusCode; - if (result.Success && - ShouldCache(definition)) - { - _cache.Add(cacheKey!, result); - } - - return result; + return new WebCallResult(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(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); + + if (!valid) + { + // Invalid json + return new WebCallResult(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, valid.Error); + } + + // Json response received + 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(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(); + return new WebCallResult(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); } - - /// - /// Prepare before sending a request. Sync time between client and server and check rate limits - /// - /// Request id - /// Host and schema - /// Request definition - /// Cancellation token - /// Additional headers for this request - /// Override the request weight for this request - /// Specify the weight to apply to the individual rate limit guard for this request - /// An additional optional suffix for the key selector - /// - /// - protected virtual async Task PrepareAsync( - int requestId, - string baseAddress, - RequestDefinition definition, - CancellationToken cancellationToken, - Dictionary? additionalHeaders = null, - int? weight = null, - int? weightSingleLimiter = null, - string? rateLimitKeySuffix = null) + catch (HttpRequestException requestException) { - // Time sync - if (definition.Authenticated) - { - if (AuthenticationProvider == null) - { - _logger.RestApiNoApiCredentials(requestId, definition.Path); - return new CallResult(new NoApiCredentialsError()); - } - - var syncTask = SyncTimeAsync(); - var timeSyncInfo = GetTimeSyncInfo(); - - if (timeSyncInfo != null && timeSyncInfo.TimeSyncState.LastSyncTime == default) - { - // Initially with first request we'll need to wait for the time syncing, if it's not the first request we can just continue - var syncTimeResult = await syncTask.ConfigureAwait(false); - if (!syncTimeResult) - { - _logger.RestApiFailedToSyncTime(requestId, syncTimeResult.Error!.ToString()); - return syncTimeResult.AsDataless(); - } - } - } - - // Rate limiting - var requestWeight = weight ?? definition.Weight; - if (requestWeight != 0) - { - if (definition.RateLimitGate == null) - throw new Exception("Ratelimit gate not set when request weight is not 0"); - - if (ClientOptions.RateLimiterEnabled) - { - var limitResult = await definition.RateLimitGate.ProcessAsync(_logger, requestId, RateLimitItemType.Request, definition, baseAddress, AuthenticationProvider?._credentials.Key, requestWeight, ClientOptions.RateLimitingBehaviour, rateLimitKeySuffix, cancellationToken).ConfigureAwait(false); - if (!limitResult) - return new CallResult(limitResult.Error!); - } - } - - // Endpoint specific rate limiting - if (definition.LimitGuard != null && ClientOptions.RateLimiterEnabled) - { - if (definition.RateLimitGate == null) - throw new Exception("Ratelimit gate not set when endpoint limit is specified"); - - if (ClientOptions.RateLimiterEnabled) - { - var singleRequestWeight = weightSingleLimiter ?? 1; - var limitResult = await definition.RateLimitGate.ProcessSingleAsync(_logger, requestId, definition.LimitGuard, RateLimitItemType.Request, definition, baseAddress, AuthenticationProvider?._credentials.Key, singleRequestWeight, ClientOptions.RateLimitingBehaviour, rateLimitKeySuffix, cancellationToken).ConfigureAwait(false); - if (!limitResult) - return new CallResult(limitResult.Error!); - } - } - - return CallResult.SuccessResult; + // Request exception, can't reach server for instance + var error = new WebError(requestException.Message, requestException); + return new WebCallResult(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error); } - - /// - /// Creates a request object - /// - /// Id of the request - /// Host and schema - /// Request definition - /// The query parameters of the request - /// The body parameters of the request - /// Additional headers to send with the request - /// - protected virtual IRequest CreateRequest( - int requestId, - string baseAddress, - RequestDefinition definition, - ParameterCollection? uriParameters, - ParameterCollection? bodyParameters, - Dictionary? additionalHeaders) + catch (OperationCanceledException canceledException) { - var requestConfiguration = new RestRequestConfiguration( - definition, - baseAddress, - uriParameters == null ? new Dictionary() : CreateParameterDictionary(uriParameters), - bodyParameters == null ? new Dictionary() : CreateParameterDictionary(bodyParameters), - new Dictionary(additionalHeaders ?? []), - definition.ArraySerialization ?? ArraySerialization, - definition.ParameterPosition ?? ParameterPositions[definition.Method], - definition.RequestBodyFormat ?? RequestBodyFormat); - - try + if (cancellationToken != default && canceledException.CancellationToken == cancellationToken) { - AuthenticationProvider?.ProcessRequest(this, requestConfiguration); + // Cancellation token canceled by caller + return new WebCallResult(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new CancellationRequestedError(canceledException)); } - catch (Exception ex) + else { - throw new Exception("Failed to authenticate request, make sure your API credentials are correct", ex); - } - - var queryString = requestConfiguration.GetQueryString(true); - if (!string.IsNullOrEmpty(queryString) && !queryString.StartsWith("?")) - queryString = $"?{queryString}"; - - var uri = new Uri(baseAddress.AppendPath(definition.Path) + queryString); - var request = RequestFactory.Create(definition.Method, uri, requestId); - request.Accept = Constants.JsonContentHeader; - - foreach (var header in requestConfiguration.Headers) - request.AddHeader(header.Key, header.Value); - - foreach (var header in StandardRequestHeaders) - { - // Only add it if it isn't overwritten - if (!requestConfiguration.Headers.ContainsKey(header.Key)) - request.AddHeader(header.Key, header.Value); - } - - if (requestConfiguration.ParameterPosition == HttpMethodParameterPosition.InBody) - { - var contentType = requestConfiguration.BodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader; - var bodyContent = requestConfiguration.GetBodyContent(); - if (bodyContent != null) - { - request.SetContent(bodyContent, contentType); - } - else - { - if (requestConfiguration.BodyParameters != null && requestConfiguration.BodyParameters.Count != 0) - WriteParamBody(request, requestConfiguration.BodyParameters, contentType); - else - request.SetContent(RequestBodyEmptyContent, contentType); - } - } - - return request; - } - - /// - /// Executes the request and returns the result deserialized into the type parameter class - /// - /// The request object to execute - /// The ratelimit gate used - /// Cancellation token - /// - protected virtual async Task> GetResponseAsync( - IRequest request, - IRateLimitGate? gate, - CancellationToken cancellationToken) - { - var sw = Stopwatch.StartNew(); - Stream? responseStream = null; - IResponse? response = null; - IStreamMessageAccessor? accessor = null; - try - { - response = await request.GetResponseAsync(cancellationToken).ConfigureAwait(false); - sw.Stop(); - var statusCode = response.StatusCode; - var headers = response.ResponseHeaders; - var responseLength = response.ContentLength; - responseStream = await response.GetResponseStreamAsync().ConfigureAwait(false); - var outputOriginalData = ApiOptions.OutputOriginalData ?? ClientOptions.OutputOriginalData; - - accessor = CreateAccessor(); - if (!response.IsSuccessStatusCode) - { - // Error response - var readResult = await accessor.Read(responseStream, true).ConfigureAwait(false); - - Error error; - if (response.StatusCode == (HttpStatusCode)418 || response.StatusCode == (HttpStatusCode)429) - { - var rateError = ParseRateLimitResponse((int)response.StatusCode, response.ResponseHeaders, accessor); - if (rateError.RetryAfter != null && gate != null && ClientOptions.RateLimiterEnabled) - { - _logger.RestApiRateLimitPauseUntil(request.RequestId, rateError.RetryAfter.Value); - await gate.SetRetryAfterGuardAsync(rateError.RetryAfter.Value).ConfigureAwait(false); - } - - error = rateError; - } - else - { - error = ParseErrorResponse((int)response.StatusCode, response.ResponseHeaders, accessor, readResult.Error?.Exception); - } - - if (error.Code == null || error.Code == 0) - error.Code = (int)response.StatusCode; - - return new WebCallResult(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(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); - - if (!valid) - { - // Invalid json - return new WebCallResult(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, valid.Error); - } - - // Json response received - 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(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(); - return new WebCallResult(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); - } - catch (HttpRequestException requestException) - { - // Request exception, can't reach server for instance - var error = new WebError(requestException.Message, requestException); + // Request timed out + var error = new WebError($"Request timed out", exception: canceledException); + error.ErrorType = ErrorType.Timeout; return new WebCallResult(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error); } - catch (OperationCanceledException canceledException) - { - if (cancellationToken != default && canceledException.CancellationToken == cancellationToken) - { - // Cancellation token canceled by caller - return new WebCallResult(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 - var error = new WebError($"Request timed out", exception: canceledException); - error.ErrorType = ErrorType.Timeout; - return new WebCallResult(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error); - } - } - finally - { - accessor?.Clear(); - responseStream?.Close(); - response?.Close(); - } } - - /// - /// Can be used to parse an error even though response status indicates success. Some apis always return 200 OK, even though there is an error. - /// This method will be called for each response to be able to check if the response is an error or not. - /// If the response is an error this method should return the parsed error, else it should return null - /// - /// Data accessor - /// The response headers - /// Null if not an error, Error otherwise - protected virtual Error? TryParseError(KeyValuePair[] responseHeaders, IMessageAccessor accessor) => null; - - /// - /// Can be used to indicate that a request should be retried. Defaults to false. Make sure to retry a max number of times (based on the the tries parameter) or the request will retry forever. - /// Note that this is always called; even when the request might be successful - /// - /// WebCallResult type parameter - /// The rate limit gate the call used - /// The result of the call - /// The current try number - /// True if call should retry, false if the call should return - protected virtual async Task ShouldRetryRequestAsync(IRateLimitGate? gate, WebCallResult callResult, int tries) + finally { - if (tries >= 2) - // Only retry once + accessor?.Clear(); + responseStream?.Close(); + response?.Close(); + } + } + + /// + /// Can be used to parse an error even though response status indicates success. Some apis always return 200 OK, even though there is an error. + /// This method will be called for each response to be able to check if the response is an error or not. + /// If the response is an error this method should return the parsed error, else it should return null + /// + /// Data accessor + /// The response headers + /// Null if not an error, Error otherwise + protected virtual Error? TryParseError(KeyValuePair[] responseHeaders, IMessageAccessor accessor) => null; + + /// + /// Can be used to indicate that a request should be retried. Defaults to false. Make sure to retry a max number of times (based on the the tries parameter) or the request will retry forever. + /// Note that this is always called; even when the request might be successful + /// + /// WebCallResult type parameter + /// The rate limit gate the call used + /// The result of the call + /// The current try number + /// True if call should retry, false if the call should return + protected virtual async Task ShouldRetryRequestAsync(IRateLimitGate? gate, WebCallResult callResult, int tries) + { + if (tries >= 2) + // Only retry once + return false; + + if (callResult.Error is ServerRateLimitError + && ClientOptions.RateLimiterEnabled + && ClientOptions.RateLimitingBehaviour != RateLimitingBehaviour.Fail + && gate != null) + { + var retryTime = await gate.GetRetryAfterTime().ConfigureAwait(false); + if (retryTime == null) return false; - if (callResult.Error is ServerRateLimitError - && ClientOptions.RateLimiterEnabled - && ClientOptions.RateLimitingBehaviour != RateLimitingBehaviour.Fail - && gate != null) + if (retryTime.Value - DateTime.UtcNow < TimeSpan.FromSeconds(60)) { - var retryTime = await gate.GetRetryAfterTime().ConfigureAwait(false); - if (retryTime == null) - return false; - - if (retryTime.Value - DateTime.UtcNow < TimeSpan.FromSeconds(60)) - { - _logger.RestApiRateLimitRetry(callResult.RequestId!.Value, retryTime.Value); - return true; - } - } - - return false; - } - - /// - /// Writes the parameters of the request to the request object body - /// - /// The request to set the parameters on - /// The parameters to set - /// The content type of the data - protected virtual void WriteParamBody(IRequest request, IDictionary parameters, string contentType) - { - if (contentType == Constants.JsonContentHeader) - { - var serializer = CreateSerializer(); - if (serializer is not IStringMessageSerializer stringSerializer) - throw new InvalidOperationException("Non-string message serializer can't get serialized request body"); - - // Write the parameters as json in the body - string stringData; - if (parameters.Count == 1 && parameters.TryGetValue(Constants.BodyPlaceHolderKey, out object? value)) - stringData = stringSerializer.Serialize(value); - else - stringData = stringSerializer.Serialize(parameters); - request.SetContent(stringData, contentType); - } - else if (contentType == Constants.FormContentHeader) - { - // Write the parameters as form data in the body - var stringData = parameters.ToFormData(); - request.SetContent(stringData, contentType); + _logger.RestApiRateLimitRetry(callResult.RequestId!.Value, retryTime.Value); + return true; } } - /// - /// Parse an error response from the server. Only used when server returns a status other than Success(200) or ratelimit error (429 or 418) - /// - /// The response status code - /// The response headers - /// Data accessor - /// Exception - /// - protected virtual Error ParseErrorResponse(int httpStatusCode, KeyValuePair[] responseHeaders, IMessageAccessor accessor, Exception? exception) + return false; + } + + /// + /// Writes the parameters of the request to the request object body + /// + /// The request to set the parameters on + /// The parameters to set + /// The content type of the data + protected virtual void WriteParamBody(IRequest request, IDictionary parameters, string contentType) + { + if (contentType == Constants.JsonContentHeader) { - return new ServerError(ErrorInfo.Unknown, exception); + var serializer = CreateSerializer(); + if (serializer is not IStringMessageSerializer stringSerializer) + throw new InvalidOperationException("Non-string message serializer can't get serialized request body"); + + // Write the parameters as json in the body + string stringData; + if (parameters.Count == 1 && parameters.TryGetValue(Constants.BodyPlaceHolderKey, out object? value)) + stringData = stringSerializer.Serialize(value); + else + stringData = stringSerializer.Serialize(parameters); + request.SetContent(stringData, contentType); } - - /// - /// Parse a rate limit error response from the server. Only used when server returns http status 429 or 418 - /// - /// The response status code - /// The response headers - /// Data accessor - /// - protected virtual ServerRateLimitError ParseRateLimitResponse(int httpStatusCode, KeyValuePair[] responseHeaders, IMessageAccessor accessor) + else if (contentType == Constants.FormContentHeader) { - // Handle retry after header - var retryAfterHeader = responseHeaders.SingleOrDefault(r => r.Key.Equals("Retry-After", StringComparison.InvariantCultureIgnoreCase)); - if (retryAfterHeader.Value?.Any() != true) - return new ServerRateLimitError(); + // Write the parameters as form data in the body + var stringData = parameters.ToFormData(); + request.SetContent(stringData, contentType); + } + } - var value = retryAfterHeader.Value.First(); - if (int.TryParse(value, out var seconds)) - return new ServerRateLimitError() { RetryAfter = DateTime.UtcNow.AddSeconds(seconds) }; - - if (DateTime.TryParse(value, out var datetime)) - return new ServerRateLimitError() { RetryAfter = datetime }; + /// + /// Parse an error response from the server. Only used when server returns a status other than Success(200) or ratelimit error (429 or 418) + /// + /// The response status code + /// The response headers + /// Data accessor + /// Exception + /// + protected virtual Error ParseErrorResponse(int httpStatusCode, KeyValuePair[] responseHeaders, IMessageAccessor accessor, Exception? exception) + { + return new ServerError(ErrorInfo.Unknown, exception); + } + /// + /// Parse a rate limit error response from the server. Only used when server returns http status 429 or 418 + /// + /// The response status code + /// The response headers + /// Data accessor + /// + protected virtual ServerRateLimitError ParseRateLimitResponse(int httpStatusCode, KeyValuePair[] responseHeaders, IMessageAccessor accessor) + { + // Handle retry after header + var retryAfterHeader = responseHeaders.SingleOrDefault(r => r.Key.Equals("Retry-After", StringComparison.InvariantCultureIgnoreCase)); + if (retryAfterHeader.Value?.Any() != true) return new ServerRateLimitError(); - } - /// - /// Create the parameter IDictionary - /// - /// - /// - protected internal IDictionary CreateParameterDictionary(IDictionary parameters) + var value = retryAfterHeader.Value.First(); + if (int.TryParse(value, out var seconds)) + return new ServerRateLimitError() { RetryAfter = DateTime.UtcNow.AddSeconds(seconds) }; + + if (DateTime.TryParse(value, out var datetime)) + return new ServerRateLimitError() { RetryAfter = datetime }; + + return new ServerRateLimitError(); + } + + /// + /// Create the parameter IDictionary + /// + /// + /// + protected internal IDictionary CreateParameterDictionary(IDictionary parameters) + { + if (!OrderParameters) + return parameters; + + return new SortedDictionary(parameters, ParameterOrderComparer); + } + + /// + /// Retrieve the server time for the purpose of syncing time between client and server to prevent authentication issues + /// + /// Server time + protected virtual Task> GetServerTimestampAsync() => throw new NotImplementedException(); + + /// + public override void SetOptions(UpdateOptions options) + { + base.SetOptions(options); + + RequestFactory.UpdateSettings(options.Proxy, options.RequestTimeout ?? ClientOptions.RequestTimeout); + } + + internal async Task> SyncTimeAsync() + { + var timeSyncParams = GetTimeSyncInfo(); + if (timeSyncParams == null) + return new WebCallResult(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null); + + if (await timeSyncParams.TimeSyncState.Semaphore.WaitAsync(0).ConfigureAwait(false)) { - if (!OrderParameters) - return parameters; - - return new SortedDictionary(parameters, ParameterOrderComparer); - } - - /// - /// Retrieve the server time for the purpose of syncing time between client and server to prevent authentication issues - /// - /// Server time - protected virtual Task> GetServerTimestampAsync() => throw new NotImplementedException(); - - /// - public override void SetOptions(UpdateOptions options) - { - base.SetOptions(options); - - RequestFactory.UpdateSettings(options.Proxy, options.RequestTimeout ?? ClientOptions.RequestTimeout); - } - - internal async Task> SyncTimeAsync() - { - var timeSyncParams = GetTimeSyncInfo(); - if (timeSyncParams == null) - return new WebCallResult(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null); - - if (await timeSyncParams.TimeSyncState.Semaphore.WaitAsync(0).ConfigureAwait(false)) + if (!timeSyncParams.SyncTime || DateTime.UtcNow - timeSyncParams.TimeSyncState.LastSyncTime < timeSyncParams.RecalculationInterval) { - if (!timeSyncParams.SyncTime || DateTime.UtcNow - timeSyncParams.TimeSyncState.LastSyncTime < timeSyncParams.RecalculationInterval) - { - timeSyncParams.TimeSyncState.Semaphore.Release(); - return new WebCallResult(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null); - } + timeSyncParams.TimeSyncState.Semaphore.Release(); + return new WebCallResult(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null); + } - var localTime = DateTime.UtcNow; - var result = await GetServerTimestampAsync().ConfigureAwait(false); + var localTime = DateTime.UtcNow; + var result = await GetServerTimestampAsync().ConfigureAwait(false); + if (!result) + { + timeSyncParams.TimeSyncState.Semaphore.Release(); + return result.As(false); + } + + if (TotalRequestsMade == 1) + { + // If this was the first request make another one to calculate the offset since the first one can be slower + localTime = DateTime.UtcNow; + result = await GetServerTimestampAsync().ConfigureAwait(false); if (!result) { timeSyncParams.TimeSyncState.Semaphore.Release(); return result.As(false); } - - if (TotalRequestsMade == 1) - { - // If this was the first request make another one to calculate the offset since the first one can be slower - localTime = DateTime.UtcNow; - result = await GetServerTimestampAsync().ConfigureAwait(false); - if (!result) - { - timeSyncParams.TimeSyncState.Semaphore.Release(); - return result.As(false); - } - } - - // Calculate time offset between local and server - var offset = result.Data - localTime.AddMilliseconds(result.ResponseTime!.Value.TotalMilliseconds / 2); - timeSyncParams.UpdateTimeOffset(offset); - timeSyncParams.TimeSyncState.Semaphore.Release(); } - return new WebCallResult(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null); + // Calculate time offset between local and server + var offset = result.Data - localTime.AddMilliseconds(result.ResponseTime!.Value.TotalMilliseconds / 2); + timeSyncParams.UpdateTimeOffset(offset); + timeSyncParams.TimeSyncState.Semaphore.Release(); } - private bool ShouldCache(RequestDefinition definition) - => ClientOptions.CachingEnabled - && definition.Method == HttpMethod.Get - && !definition.PreventCaching; + return new WebCallResult(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null); } + + private bool ShouldCache(RequestDefinition definition) + => ClientOptions.CachingEnabled + && definition.Method == HttpMethod.Get + && !definition.PreventCaching; } diff --git a/CryptoExchange.Net/Clients/SocketApiClient.cs b/CryptoExchange.Net/Clients/SocketApiClient.cs index 0626864..fcbfbe9 100644 --- a/CryptoExchange.Net/Clients/SocketApiClient.cs +++ b/CryptoExchange.Net/Clients/SocketApiClient.cs @@ -17,849 +17,860 @@ using System.Text; using System.Threading; using System.Threading.Tasks; -namespace CryptoExchange.Net.Clients +namespace CryptoExchange.Net.Clients; + +/// +/// Base socket API client for interaction with a websocket API +/// +public abstract class SocketApiClient : BaseApiClient, ISocketApiClient { + #region Fields + /// + public IWebsocketFactory SocketFactory { get; set; } = new WebsocketFactory(); + /// - /// Base socket API client for interaction with a websocket API + /// List of socket connections currently connecting/connected /// - public abstract class SocketApiClient : BaseApiClient, ISocketApiClient + protected internal ConcurrentDictionary socketConnections = new(); + + /// + /// Semaphore used while creating sockets + /// + protected internal readonly SemaphoreSlim semaphoreSlim = new(1); + + /// + /// Keep alive interval for websocket connection + /// + protected TimeSpan KeepAliveInterval { get; set; } = TimeSpan.FromSeconds(10); + + /// + /// Keep alive timeout for websocket connection + /// + protected TimeSpan KeepAliveTimeout { get; set; } = TimeSpan.FromSeconds(10); + + /// + /// Handlers for data from the socket which doesn't need to be forwarded to the caller. Ping or welcome messages for example. + /// + protected List systemSubscriptions = new(); + + /// + /// If a message is received on the socket which is not handled by a handler this boolean determines whether this logs an error message + /// + protected internal bool UnhandledMessageExpected { get; set; } + + /// + /// The rate limiters + /// + protected internal IRateLimitGate? RateLimiter { get; set; } + + /// + /// The max size a websocket message size can be + /// + protected internal int? MessageSendSizeLimit { get; set; } + + /// + /// Periodic task registrations + /// + protected List PeriodicTaskRegistrations { get; set; } = new List(); + + /// + /// List of address to keep an alive connection to + /// + protected List DedicatedConnectionConfigs { get; set; } = new List(); + + /// + /// Whether to allow multiple subscriptions with the same topic on the same connection + /// + protected bool AllowTopicsOnTheSameConnection { get; set; } = true; + + /// + /// Whether to continue processing and forward unparsable messages to handlers + /// + protected internal bool ProcessUnparsableMessages { get; set; } + + /// + public double IncomingKbps { - #region Fields - /// - public IWebsocketFactory SocketFactory { get; set; } = new WebsocketFactory(); - - /// - /// List of socket connections currently connecting/connected - /// - protected internal ConcurrentDictionary socketConnections = new(); - - /// - /// Semaphore used while creating sockets - /// - protected internal readonly SemaphoreSlim semaphoreSlim = new(1); - - /// - /// Keep alive interval for websocket connection - /// - protected TimeSpan KeepAliveInterval { get; set; } = TimeSpan.FromSeconds(10); - - /// - /// Keep alive timeout for websocket connection - /// - protected TimeSpan KeepAliveTimeout { get; set; } = TimeSpan.FromSeconds(10); - - /// - /// Handlers for data from the socket which doesn't need to be forwarded to the caller. Ping or welcome messages for example. - /// - protected List systemSubscriptions = new(); - - /// - /// If a message is received on the socket which is not handled by a handler this boolean determines whether this logs an error message - /// - protected internal bool UnhandledMessageExpected { get; set; } - - /// - /// The rate limiters - /// - protected internal IRateLimitGate? RateLimiter { get; set; } - - /// - /// The max size a websocket message size can be - /// - protected internal int? MessageSendSizeLimit { get; set; } - - /// - /// Periodic task registrations - /// - protected List PeriodicTaskRegistrations { get; set; } = new List(); - - /// - /// List of address to keep an alive connection to - /// - protected List DedicatedConnectionConfigs { get; set; } = new List(); - - /// - /// Whether to allow multiple subscriptions with the same topic on the same connection - /// - protected bool AllowTopicsOnTheSameConnection { get; set; } = true; - - /// - /// Whether to continue processing and forward unparsable messages to handlers - /// - protected internal bool ProcessUnparsableMessages { get; set; } = false; - - /// - public double IncomingKbps + get { - get - { - if (socketConnections.IsEmpty) - return 0; + if (socketConnections.IsEmpty) + return 0; - return socketConnections.Sum(s => s.Value.IncomingKbps); - } + return socketConnections.Sum(s => s.Value.IncomingKbps); + } + } + + /// + public int CurrentConnections => socketConnections.Count; + + /// + public int CurrentSubscriptions + { + get + { + if (socketConnections.IsEmpty) + return 0; + + return socketConnections.Sum(s => s.Value.UserSubscriptionCount); + } + } + + /// + public new SocketExchangeOptions ClientOptions => (SocketExchangeOptions)base.ClientOptions; + + /// + public new SocketApiOptions ApiOptions => (SocketApiOptions)base.ApiOptions; + + #endregion + + /// + /// ctor + /// + /// log + /// Client options + /// Base address for this API client + /// The Api client options + public SocketApiClient(ILogger logger, string baseAddress, SocketExchangeOptions options, SocketApiOptions apiOptions) + : base(logger, + apiOptions.OutputOriginalData ?? options.OutputOriginalData, + apiOptions.ApiCredentials ?? options.ApiCredentials, + baseAddress, + options, + apiOptions) + { + } + + /// + /// Create a message accessor instance + /// + /// + protected internal abstract IByteMessageAccessor CreateAccessor(WebSocketMessageType messageType); + + /// + /// Create a serializer instance + /// + /// + protected internal abstract IMessageSerializer CreateSerializer(); + + /// + /// Keep an open connection to this url + /// + /// + /// + protected virtual void SetDedicatedConnection(string url, bool auth) + { + DedicatedConnectionConfigs.Add(new DedicatedConnectionConfig() { SocketAddress = url, Authenticated = auth }); + } + + /// + /// Add a query to periodically send on each connection + /// + /// + /// + /// + /// + protected virtual void RegisterPeriodicQuery(string identifier, TimeSpan interval, Func queryDelegate, Action? callback) + { + PeriodicTaskRegistrations.Add(new PeriodicTaskRegistration + { + Identifier = identifier, + Callback = callback, + Interval = interval, + QueryDelegate = queryDelegate + }); + } + + /// + /// Connect to an url and listen for data on the BaseAddress + /// + /// The subscription + /// Cancellation token for closing this subscription + /// + protected virtual Task> SubscribeAsync(Subscription subscription, CancellationToken ct) + { + return SubscribeAsync(BaseAddress, subscription, ct); + } + + /// + /// Connect to an url and listen for data + /// + /// The URL to connect to + /// The subscription + /// Cancellation token for closing this subscription + /// + protected virtual async Task> SubscribeAsync(string url, Subscription subscription, CancellationToken ct) + { + if (_disposing) + return new CallResult(new InvalidOperationError("Client disposed, can't subscribe")); + + if (subscription.Authenticated && AuthenticationProvider == null) + { + _logger.LogWarning("Failed to subscribe, private subscription but no API credentials set"); + return new CallResult(new NoApiCredentialsError()); } - /// - public int CurrentConnections => socketConnections.Count; - - /// - public int CurrentSubscriptions + SocketConnection socketConnection; + var released = false; + // Wait for a semaphore here, so we only connect 1 socket at a time. + // This is necessary for being able to see if connections can be combined + try { - get - { - if (socketConnections.IsEmpty) - return 0; - - return socketConnections.Sum(s => s.Value.UserSubscriptionCount); - } + await semaphoreSlim.WaitAsync(ct).ConfigureAwait(false); + } + catch (OperationCanceledException tce) + { + return new CallResult(new CancellationRequestedError(tce)); } - /// - public new SocketExchangeOptions ClientOptions => (SocketExchangeOptions)base.ClientOptions; - - /// - public new SocketApiOptions ApiOptions => (SocketApiOptions)base.ApiOptions; - - #endregion - - /// - /// ctor - /// - /// log - /// Client options - /// Base address for this API client - /// The Api client options - public SocketApiClient(ILogger logger, string baseAddress, SocketExchangeOptions options, SocketApiOptions apiOptions) - : base(logger, - apiOptions.OutputOriginalData ?? options.OutputOriginalData, - apiOptions.ApiCredentials ?? options.ApiCredentials, - baseAddress, - options, - apiOptions) + try { - } - - /// - /// Create a message accessor instance - /// - /// - protected internal abstract IByteMessageAccessor CreateAccessor(WebSocketMessageType messageType); - - /// - /// Create a serializer instance - /// - /// - protected internal abstract IMessageSerializer CreateSerializer(); - - /// - /// Keep an open connection to this url - /// - /// - /// - protected virtual void SetDedicatedConnection(string url, bool auth) - { - DedicatedConnectionConfigs.Add(new DedicatedConnectionConfig() { SocketAddress = url, Authenticated = auth }); - } - - /// - /// Add a query to periodically send on each connection - /// - /// - /// - /// - /// - protected virtual void RegisterPeriodicQuery(string identifier, TimeSpan interval, Func queryDelegate, Action? callback) - { - PeriodicTaskRegistrations.Add(new PeriodicTaskRegistration + while (true) { - Identifier = identifier, - Callback = callback, - Interval = interval, - QueryDelegate = queryDelegate - }); - } - - /// - /// Connect to an url and listen for data on the BaseAddress - /// - /// The subscription - /// Cancellation token for closing this subscription - /// - protected virtual Task> SubscribeAsync(Subscription subscription, CancellationToken ct) - { - return SubscribeAsync(BaseAddress, subscription, ct); - } - - /// - /// Connect to an url and listen for data - /// - /// The URL to connect to - /// The subscription - /// Cancellation token for closing this subscription - /// - protected virtual async Task> SubscribeAsync(string url, Subscription subscription, CancellationToken ct) - { - if (_disposing) - return new CallResult(new InvalidOperationError("Client disposed, can't subscribe")); - - if (subscription.Authenticated && AuthenticationProvider == null) - { - _logger.LogWarning("Failed to subscribe, private subscription but no API credentials set"); - return new CallResult(new NoApiCredentialsError()); - } - - SocketConnection socketConnection; - var released = false; - // Wait for a semaphore here, so we only connect 1 socket at a time. - // This is necessary for being able to see if connections can be combined - try - { - await semaphoreSlim.WaitAsync(ct).ConfigureAwait(false); - } - catch (OperationCanceledException tce) - { - return new CallResult(new CancellationRequestedError(tce)); - } - - try - { - while (true) - { - // Get a new or existing socket connection - var socketResult = await GetSocketConnection(url, subscription.Authenticated, false, subscription.Topic).ConfigureAwait(false); - if (!socketResult) - return socketResult.As(null); - - socketConnection = socketResult.Data; - - // Add a subscription on the socket connection - var success = socketConnection.AddSubscription(subscription); - if (!success) - { - _logger.FailedToAddSubscriptionRetryOnDifferentConnection(socketConnection.SocketId); - continue; - } - - if (ClientOptions.SocketSubscriptionsCombineTarget == 1) - { - // Only 1 subscription per connection, so no need to wait for connection since a new subscription will create a new connection anyway - semaphoreSlim.Release(); - released = true; - } - - var needsConnecting = !socketConnection.Connected; - - var connectResult = await ConnectIfNeededAsync(socketConnection, subscription.Authenticated, ct).ConfigureAwait(false); - if (!connectResult) - return new CallResult(connectResult.Error!); - - break; - } - } - finally - { - if (!released) - semaphoreSlim.Release(); - } - - if (socketConnection.PausedActivity) - { - _logger.HasBeenPausedCantSubscribeAtThisMoment(socketConnection.SocketId); - return new CallResult(new ServerError(new ErrorInfo(ErrorType.WebsocketPaused, "Socket is paused"))); - } - - var waitEvent = new AsyncResetEvent(false); - var subQuery = subscription.CreateSubscriptionQuery(socketConnection); - if (subQuery != null) - { - // Send the request and wait for answer - var subResult = await socketConnection.SendAndWaitQueryAsync(subQuery, waitEvent, ct).ConfigureAwait(false); - if (!subResult) - { - waitEvent?.Set(); - var isTimeout = subResult.Error is CancellationRequestedError; - if (isTimeout && subscription.Confirmed) - { - // No response received, but the subscription did receive updates. We'll assume success - } - else - { - _logger.FailedToSubscribe(socketConnection.SocketId, subResult.Error?.ToString()); - // If this was a timeout we still need to send an unsubscribe to prevent messages coming in later - await socketConnection.CloseAsync(subscription).ConfigureAwait(false); - return new CallResult(subResult.Error!); - } - } - - subscription.HandleSubQueryResponse(subQuery.Response!); - } - - subscription.Confirmed = true; - if (ct != default) - { - subscription.CancellationTokenRegistration = ct.Register(async () => - { - _logger.CancellationTokenSetClosingSubscription(socketConnection.SocketId, subscription.Id); - await socketConnection.CloseAsync(subscription).ConfigureAwait(false); - }, false); - } - - waitEvent?.Set(); - _logger.SubscriptionCompletedSuccessfully(socketConnection.SocketId, subscription.Id); - return new CallResult(new UpdateSubscription(socketConnection, subscription)); - } - - /// - /// Send a query on a socket connection to the BaseAddress and wait for the response - /// - /// Expected result type - /// The query - /// Cancellation token - /// - protected virtual Task> QueryAsync(Query query, CancellationToken ct = default) - { - return QueryAsync(BaseAddress, query, ct); - } - - /// - /// Send a query on a socket connection and wait for the response - /// - /// Expected result type - /// The url for the request - /// The query - /// Cancellation token - /// - protected virtual async Task> QueryAsync(string url, Query query, CancellationToken ct = default) - { - if (_disposing) - return new CallResult(new InvalidOperationError("Client disposed, can't query")); - - if (ct.IsCancellationRequested) - return new CallResult(new CancellationRequestedError()); - - SocketConnection socketConnection; - var released = false; - await semaphoreSlim.WaitAsync().ConfigureAwait(false); - try - { - var socketResult = await GetSocketConnection(url, query.Authenticated, true).ConfigureAwait(false); + // Get a new or existing socket connection + var socketResult = await GetSocketConnection(url, subscription.Authenticated, false, subscription.Topic).ConfigureAwait(false); if (!socketResult) - return socketResult.As(default); + return socketResult.As(null); socketConnection = socketResult.Data; + // Add a subscription on the socket connection + var success = socketConnection.AddSubscription(subscription); + if (!success) + { + _logger.FailedToAddSubscriptionRetryOnDifferentConnection(socketConnection.SocketId); + continue; + } + if (ClientOptions.SocketSubscriptionsCombineTarget == 1) { - // Can release early when only a single sub per connection + // Only 1 subscription per connection, so no need to wait for connection since a new subscription will create a new connection anyway semaphoreSlim.Release(); released = true; } - var connectResult = await ConnectIfNeededAsync(socketConnection, query.Authenticated, ct).ConfigureAwait(false); + var needsConnecting = !socketConnection.Connected; + + var connectResult = await ConnectIfNeededAsync(socketConnection, subscription.Authenticated, ct).ConfigureAwait(false); if (!connectResult) - return new CallResult(connectResult.Error!); - } - finally - { - if (!released) - semaphoreSlim.Release(); - } + return new CallResult(connectResult.Error!); - if (socketConnection.PausedActivity) - { - _logger.HasBeenPausedCantSendQueryAtThisMoment(socketConnection.SocketId); - return new CallResult(new ServerError(new ErrorInfo(ErrorType.WebsocketPaused, "Socket is paused"))); + break; } - - if (ct.IsCancellationRequested) - return new CallResult(new CancellationRequestedError()); - - return await socketConnection.SendAndWaitQueryAsync(query, null, ct).ConfigureAwait(false); + } + finally + { + if (!released) + semaphoreSlim.Release(); } - /// - /// Checks if a socket needs to be connected and does so if needed. Also authenticates on the socket if needed - /// - /// The connection to check - /// Whether the socket should authenticated - /// Cancellation token - /// - protected virtual async Task ConnectIfNeededAsync(SocketConnection socket, bool authenticated, CancellationToken ct) + if (socketConnection.PausedActivity) { - if (socket.Connected) - return CallResult.SuccessResult; - - var connectResult = await ConnectSocketAsync(socket, ct).ConfigureAwait(false); - if (!connectResult) - return connectResult; - - if (ClientOptions.DelayAfterConnect != TimeSpan.Zero) - await Task.Delay(ClientOptions.DelayAfterConnect).ConfigureAwait(false); - - if (!authenticated || socket.Authenticated) - return CallResult.SuccessResult; - - var result = await AuthenticateSocketAsync(socket).ConfigureAwait(false); - if (!result) - await socket.CloseAsync().ConfigureAwait(false); - - return result; + _logger.HasBeenPausedCantSubscribeAtThisMoment(socketConnection.SocketId); + return new CallResult(new ServerError(new ErrorInfo(ErrorType.WebsocketPaused, "Socket is paused"))); } - /// - /// Authenticate a socket connection - /// - /// Socket to authenticate - /// - public virtual async Task AuthenticateSocketAsync(SocketConnection socket) + var waitEvent = new AsyncResetEvent(false); + var subQuery = subscription.CreateSubscriptionQuery(socketConnection); + if (subQuery != null) { - if (AuthenticationProvider == null) - return new CallResult(new NoApiCredentialsError()); - - _logger.AttemptingToAuthenticate(socket.SocketId); - var authRequest = await GetAuthenticationRequestAsync(socket).ConfigureAwait(false); - if (authRequest != null) + // Send the request and wait for answer + var subResult = await socketConnection.SendAndWaitQueryAsync(subQuery, waitEvent, ct).ConfigureAwait(false); + if (!subResult) { - var result = await socket.SendAndWaitQueryAsync(authRequest).ConfigureAwait(false); - - if (!result) + waitEvent?.Set(); + var isTimeout = subResult.Error is CancellationRequestedError; + if (isTimeout && subscription.Confirmed) { - _logger.AuthenticationFailed(socket.SocketId); - if (socket.Connected) - await socket.CloseAsync().ConfigureAwait(false); - - result.Error!.Message = "Authentication failed: " + result.Error.Message; - return new CallResult(result.Error)!; + // No response received, but the subscription did receive updates. We'll assume success } - - _logger.Authenticated(socket.SocketId); - } - - socket.Authenticated = true; - return CallResult.SuccessResult; - } - - /// - /// Should return the request which can be used to authenticate a socket connection - /// - /// - protected internal virtual Task GetAuthenticationRequestAsync(SocketConnection connection) => throw new NotImplementedException(); - - /// - /// Adds a system subscription. Used for example to reply to ping requests - /// - /// The subscription - protected void AddSystemSubscription(SystemSubscription systemSubscription) - { - systemSubscriptions.Add(systemSubscription); - foreach (var connection in socketConnections.Values) - connection.AddSubscription(systemSubscription); - } - - /// - /// Get the url to connect to (defaults to BaseAddress form the client options) - /// - /// - /// - /// - protected virtual Task> GetConnectionUrlAsync(string address, bool authentication) - { - return Task.FromResult(new CallResult(address)); - } - - /// - /// Get the url to reconnect to after losing a connection - /// - /// - /// - protected internal virtual Task GetReconnectUriAsync(SocketConnection connection) - { - return Task.FromResult(connection.ConnectionUri); - } - - /// - /// Update the subscription when the connection is restored after disconnecting. Can be used to update an authentication token for example. - /// - /// The subscription - /// - protected internal virtual Task RevitalizeRequestAsync(Subscription subscription) - { - return Task.FromResult(CallResult.SuccessResult); - } - - /// - /// Gets a connection for a new subscription or query. Can be an existing if there are open position or a new one. - /// - /// The address the socket is for - /// Whether the socket should be authenticated - /// Whether a dedicated request connection should be returned - /// The subscription topic, can be provided when multiple of the same topics are not allowed on a connection - /// - protected virtual async Task> 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.IsDedicatedRequestConnection).OrderBy(s => s.Value.UserSubscriptionCount).FirstOrDefault().Value; - } - else - { - 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))) - // 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(connection); - } - - var connectionAddress = await GetConnectionUrlAsync(address, authenticated).ConfigureAwait(false); - if (!connectionAddress) - { - _logger.FailedToDetermineConnectionUrl(connectionAddress.Error?.ToString()); - return connectionAddress.As(null); - } - - if (connectionAddress.Data != address) - _logger.ConnectionAddressSetTo(connectionAddress.Data!); - - // Create new socket - var socket = CreateSocket(connectionAddress.Data!); - var socketConnection = new SocketConnection(_logger, this, socket, address); - socketConnection.UnhandledMessage += HandleUnhandledMessage; - socketConnection.ConnectRateLimitedAsync += HandleConnectRateLimitedAsync; - if (dedicatedRequestConnection) - { - socketConnection.DedicatedRequestConnection = new DedicatedConnectionState + else { - IsDedicatedRequestConnection = dedicatedRequestConnection, - Authenticated = authenticated - }; + _logger.FailedToSubscribe(socketConnection.SocketId, subResult.Error?.ToString()); + // If this was a timeout we still need to send an unsubscribe to prevent messages coming in later + await socketConnection.CloseAsync(subscription).ConfigureAwait(false); + return new CallResult(subResult.Error!); + } } - foreach (var ptg in PeriodicTaskRegistrations) - socketConnection.QueryPeriodic(ptg.Identifier, ptg.Interval, ptg.QueryDelegate, ptg.Callback); - - foreach (var systemSubscription in systemSubscriptions) - socketConnection.AddSubscription(systemSubscription); - - return new CallResult(socketConnection); + subscription.HandleSubQueryResponse(subQuery.Response!); } - /// - /// Process an unhandled message - /// - /// The message that wasn't processed - protected virtual void HandleUnhandledMessage(IMessageAccessor message) + subscription.Confirmed = true; + if (ct != default) { - } - - /// - /// Process connect rate limited - /// - protected async virtual Task HandleConnectRateLimitedAsync() - { - if (ClientOptions.RateLimiterEnabled && ClientOptions.ConnectDelayAfterRateLimited.HasValue) + subscription.CancellationTokenRegistration = ct.Register(async () => { - var retryAfter = DateTime.UtcNow.Add(ClientOptions.ConnectDelayAfterRateLimited.Value); - _logger.AddingRetryAfterGuard(retryAfter); - RateLimiter ??= new RateLimitGate("Connection"); - await RateLimiter.SetRetryAfterGuardAsync(retryAfter, RateLimitItemType.Connection).ConfigureAwait(false); - } + _logger.CancellationTokenSetClosingSubscription(socketConnection.SocketId, subscription.Id); + await socketConnection.CloseAsync(subscription).ConfigureAwait(false); + }, false); } - /// - /// Connect a socket - /// - /// The socket to connect - /// Cancellation token - /// - protected virtual async Task ConnectSocketAsync(SocketConnection socketConnection, CancellationToken ct) + waitEvent?.Set(); + _logger.SubscriptionCompletedSuccessfully(socketConnection.SocketId, subscription.Id); + return new CallResult(new UpdateSubscription(socketConnection, subscription)); + } + + /// + /// Send a query on a socket connection to the BaseAddress and wait for the response + /// + /// Expected result type + /// The query + /// Cancellation token + /// + protected virtual Task> QueryAsync(Query query, CancellationToken ct = default) + { + return QueryAsync(BaseAddress, query, ct); + } + + /// + /// Send a query on a socket connection and wait for the response + /// + /// Expected result type + /// The url for the request + /// The query + /// Cancellation token + /// + protected virtual async Task> QueryAsync(string url, Query query, CancellationToken ct = default) + { + if (_disposing) + return new CallResult(new InvalidOperationError("Client disposed, can't query")); + + if (ct.IsCancellationRequested) + return new CallResult(new CancellationRequestedError()); + + SocketConnection socketConnection; + var released = false; + + try { - var connectResult = await socketConnection.ConnectAsync(ct).ConfigureAwait(false); - if (connectResult) + await semaphoreSlim.WaitAsync(ct).ConfigureAwait(false); + } + catch (OperationCanceledException) { } + + try + { + var socketResult = await GetSocketConnection(url, query.Authenticated, true).ConfigureAwait(false); + if (!socketResult) + return socketResult.As(default); + + socketConnection = socketResult.Data; + + if (ClientOptions.SocketSubscriptionsCombineTarget == 1) { - socketConnections.TryAdd(socketConnection.SocketId, socketConnection); - return connectResult; + // Can release early when only a single sub per connection + semaphoreSlim.Release(); + released = true; } - socketConnection.Dispose(); + var connectResult = await ConnectIfNeededAsync(socketConnection, query.Authenticated, ct).ConfigureAwait(false); + if (!connectResult) + return new CallResult(connectResult.Error!); + } + finally + { + if (!released) + semaphoreSlim.Release(); + } + + if (socketConnection.PausedActivity) + { + _logger.HasBeenPausedCantSendQueryAtThisMoment(socketConnection.SocketId); + return new CallResult(new ServerError(new ErrorInfo(ErrorType.WebsocketPaused, "Socket is paused"))); + } + + if (ct.IsCancellationRequested) + return new CallResult(new CancellationRequestedError()); + + return await socketConnection.SendAndWaitQueryAsync(query, null, ct).ConfigureAwait(false); + } + + /// + /// Checks if a socket needs to be connected and does so if needed. Also authenticates on the socket if needed + /// + /// The connection to check + /// Whether the socket should authenticated + /// Cancellation token + /// + protected virtual async Task ConnectIfNeededAsync(SocketConnection socket, bool authenticated, CancellationToken ct) + { + if (socket.Connected) + return CallResult.SuccessResult; + + var connectResult = await ConnectSocketAsync(socket, ct).ConfigureAwait(false); + if (!connectResult) + return connectResult; + + if (ClientOptions.DelayAfterConnect != TimeSpan.Zero) + { + try + { + await Task.Delay(ClientOptions.DelayAfterConnect, ct).ConfigureAwait(false); + } + catch (OperationCanceledException) { } + } + + if (!authenticated || socket.Authenticated) + return CallResult.SuccessResult; + + var result = await AuthenticateSocketAsync(socket).ConfigureAwait(false); + if (!result) + await socket.CloseAsync().ConfigureAwait(false); + + return result; + } + + /// + /// Authenticate a socket connection + /// + /// Socket to authenticate + /// + public virtual async Task AuthenticateSocketAsync(SocketConnection socket) + { + if (AuthenticationProvider == null) + return new CallResult(new NoApiCredentialsError()); + + _logger.AttemptingToAuthenticate(socket.SocketId); + var authRequest = await GetAuthenticationRequestAsync(socket).ConfigureAwait(false); + if (authRequest != null) + { + var result = await socket.SendAndWaitQueryAsync(authRequest).ConfigureAwait(false); + + if (!result) + { + _logger.AuthenticationFailed(socket.SocketId); + if (socket.Connected) + await socket.CloseAsync().ConfigureAwait(false); + + result.Error!.Message = "Authentication failed: " + result.Error.Message; + return new CallResult(result.Error)!; + } + + _logger.Authenticated(socket.SocketId); + } + + socket.Authenticated = true; + return CallResult.SuccessResult; + } + + /// + /// Should return the request which can be used to authenticate a socket connection + /// + /// + protected internal virtual Task GetAuthenticationRequestAsync(SocketConnection connection) => throw new NotImplementedException(); + + /// + /// Adds a system subscription. Used for example to reply to ping requests + /// + /// The subscription + protected void AddSystemSubscription(SystemSubscription systemSubscription) + { + systemSubscriptions.Add(systemSubscription); + foreach (var connection in socketConnections.Values) + connection.AddSubscription(systemSubscription); + } + + /// + /// Get the url to connect to (defaults to BaseAddress form the client options) + /// + /// + /// + /// + protected virtual Task> GetConnectionUrlAsync(string address, bool authentication) + { + return Task.FromResult(new CallResult(address)); + } + + /// + /// Get the url to reconnect to after losing a connection + /// + /// + /// + protected internal virtual Task GetReconnectUriAsync(SocketConnection connection) + { + return Task.FromResult(connection.ConnectionUri); + } + + /// + /// Update the subscription when the connection is restored after disconnecting. Can be used to update an authentication token for example. + /// + /// The subscription + /// + protected internal virtual Task RevitalizeRequestAsync(Subscription subscription) + { + return Task.FromResult(CallResult.SuccessResult); + } + + /// + /// Gets a connection for a new subscription or query. Can be an existing if there are open position or a new one. + /// + /// The address the socket is for + /// Whether the socket should be authenticated + /// Whether a dedicated request connection should be returned + /// The subscription topic, can be provided when multiple of the same topics are not allowed on a connection + /// + protected virtual async Task> 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.IsDedicatedRequestConnection).OrderBy(s => s.Value.UserSubscriptionCount).FirstOrDefault().Value; + } + else + { + 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))) + // 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(connection); + } + + var connectionAddress = await GetConnectionUrlAsync(address, authenticated).ConfigureAwait(false); + if (!connectionAddress) + { + _logger.FailedToDetermineConnectionUrl(connectionAddress.Error?.ToString()); + return connectionAddress.As(null); + } + + if (connectionAddress.Data != address) + _logger.ConnectionAddressSetTo(connectionAddress.Data!); + + // Create new socket + var socket = CreateSocket(connectionAddress.Data!); + var socketConnection = new SocketConnection(_logger, this, socket, address); + socketConnection.UnhandledMessage += HandleUnhandledMessage; + 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); + + foreach (var systemSubscription in systemSubscriptions) + socketConnection.AddSubscription(systemSubscription); + + return new CallResult(socketConnection); + } + + /// + /// Process an unhandled message + /// + /// The message that wasn't processed + protected virtual void HandleUnhandledMessage(IMessageAccessor message) + { + } + + /// + /// Process connect rate limited + /// + 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); + } + } + + /// + /// Connect a socket + /// + /// The socket to connect + /// Cancellation token + /// + protected virtual async Task ConnectSocketAsync(SocketConnection socketConnection, CancellationToken ct) + { + var connectResult = await socketConnection.ConnectAsync(ct).ConfigureAwait(false); + if (connectResult) + { + socketConnections.TryAdd(socketConnection.SocketId, socketConnection); return connectResult; } - /// - /// Get parameters for the websocket connection - /// - /// The address to connect to - /// - protected virtual WebSocketParameters GetWebSocketParameters(string address) - => new(new Uri(address), ClientOptions.ReconnectPolicy) - { - KeepAliveInterval = KeepAliveInterval, - KeepAliveTimeout = KeepAliveTimeout, - ReconnectInterval = ClientOptions.ReconnectInterval, - RateLimiter = ClientOptions.RateLimiterEnabled ? RateLimiter : null, - RateLimitingBehavior = ClientOptions.RateLimitingBehaviour, - Proxy = ClientOptions.Proxy, - Timeout = ApiOptions.SocketNoDataTimeout ?? ClientOptions.SocketNoDataTimeout, - ReceiveBufferSize = ClientOptions.ReceiveBufferSize, - }; + socketConnection.Dispose(); + return connectResult; + } - /// - /// Create a socket for an address - /// - /// The address the socket should connect to - /// - protected virtual IWebsocket CreateSocket(string address) + /// + /// Get parameters for the websocket connection + /// + /// The address to connect to + /// + protected virtual WebSocketParameters GetWebSocketParameters(string address) + => new(new Uri(address), ClientOptions.ReconnectPolicy) { - var socket = SocketFactory.CreateWebsocket(_logger, GetWebSocketParameters(address)); - _logger.SocketCreatedForAddress(socket.Id, address); - return socket; + KeepAliveInterval = KeepAliveInterval, + KeepAliveTimeout = KeepAliveTimeout, + ReconnectInterval = ClientOptions.ReconnectInterval, + RateLimiter = ClientOptions.RateLimiterEnabled ? RateLimiter : null, + RateLimitingBehavior = ClientOptions.RateLimitingBehaviour, + Proxy = ClientOptions.Proxy, + Timeout = ApiOptions.SocketNoDataTimeout ?? ClientOptions.SocketNoDataTimeout, + ReceiveBufferSize = ClientOptions.ReceiveBufferSize, + }; + + /// + /// Create a socket for an address + /// + /// The address the socket should connect to + /// + protected virtual IWebsocket CreateSocket(string address) + { + var socket = SocketFactory.CreateWebsocket(_logger, GetWebSocketParameters(address)); + _logger.SocketCreatedForAddress(socket.Id, address); + return socket; + } + + /// + /// Unsubscribe an update subscription + /// + /// The id of the subscription to unsubscribe + /// + public virtual async Task UnsubscribeAsync(int subscriptionId) + { + Subscription? subscription = null; + SocketConnection? connection = null; + foreach (var socket in socketConnections.Values.ToList()) + { + subscription = socket.GetSubscription(subscriptionId); + if (subscription != null) + { + connection = socket; + break; + } } - /// - /// Unsubscribe an update subscription - /// - /// The id of the subscription to unsubscribe - /// - public virtual async Task UnsubscribeAsync(int subscriptionId) + if (subscription == null || connection == null) + return false; + + _logger.UnsubscribingSubscription(connection.SocketId, subscriptionId); + await connection.CloseAsync(subscription).ConfigureAwait(false); + return true; + } + + /// + /// Unsubscribe an update subscription + /// + /// The subscription to unsubscribe + /// + public virtual async Task UnsubscribeAsync(UpdateSubscription subscription) + { + if (subscription == null) + throw new ArgumentNullException(nameof(subscription)); + + _logger.UnsubscribingSubscription(subscription.SocketId, subscription.Id); + await subscription.CloseAsync().ConfigureAwait(false); + } + + /// + /// Unsubscribe all subscriptions + /// + /// + public virtual async Task UnsubscribeAllAsync() + { + var sum = socketConnections.Sum(s => s.Value.UserSubscriptionCount); + if (sum == 0) + return; + + _logger.UnsubscribingAll(socketConnections.Sum(s => s.Value.UserSubscriptionCount)); + var tasks = new List(); { - Subscription? subscription = null; - SocketConnection? connection = null; - foreach (var socket in socketConnections.Values.ToList()) + var socketList = socketConnections.Values; + foreach (var connection in socketList) { - subscription = socket.GetSubscription(subscriptionId); - if (subscription != null) - { - connection = socket; - break; - } + foreach(var subscription in connection.Subscriptions.Where(x => x.UserSubscription)) + tasks.Add(connection.CloseAsync(subscription)); } + } - if (subscription == null || connection == null) - return false; + await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false); + } + + /// + /// Reconnect all connections + /// + /// + public virtual async Task ReconnectAsync() + { + _logger.ReconnectingAllConnections(socketConnections.Count); + var tasks = new List(); + { + var socketList = socketConnections.Values; + foreach (var sub in socketList) + tasks.Add(sub.TriggerReconnectAsync()); + } + + await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false); + } + + /// + public virtual async Task PrepareConnectionsAsync() + { + foreach (var item in DedicatedConnectionConfigs) + { + var socketResult = await GetSocketConnection(item.SocketAddress, item.Authenticated, true).ConfigureAwait(false); + if (!socketResult) + return socketResult.AsDataless(); + + var connectResult = await ConnectIfNeededAsync(socketResult.Data, item.Authenticated, default).ConfigureAwait(false); + if (!connectResult) + return new CallResult(connectResult.Error!); + } + + return CallResult.SuccessResult; + } + + /// + public override void SetOptions(UpdateOptions 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); + } + + /// + /// Log the current state of connections and subscriptions + /// + public string GetSubscriptionsState(bool includeSubDetails = true) + { + return GetState(includeSubDetails).ToString(); + } + + /// + /// Gets the state of the client + /// + /// True to get details for each subscription + /// + public SocketApiClientState GetState(bool includeSubDetails = true) + { + var connectionStates = new List(); + foreach (var socketIdAndConnection in socketConnections) + { + SocketConnection connection = socketIdAndConnection.Value; + SocketConnection.SocketConnectionState connectionState = connection.GetState(includeSubDetails); + connectionStates.Add(connectionState); + } + + return new SocketApiClientState(socketConnections.Count, CurrentSubscriptions, IncomingKbps, connectionStates); + } + + /// + /// Get the current state of the client + /// + /// Number of sockets for this client + /// Total number of subscriptions + /// Total download speed + /// State of each socket connection + public record SocketApiClientState( + int Connections, + int Subscriptions, + double DownloadSpeed, + List ConnectionStates) + { + /// + /// Print the state of the client + /// + /// + /// + protected virtual bool PrintMembers(StringBuilder sb) + { + sb.AppendLine(); + sb.AppendLine($"\tTotal connections: {Connections}"); + sb.AppendLine($"\tTotal subscriptions: {Subscriptions}"); + sb.AppendLine($"\tDownload speed: {DownloadSpeed} kbps"); + sb.AppendLine($"\tConnections:"); + ConnectionStates.ForEach(cs => + { + sb.AppendLine($"\t\tId: {cs.Id}"); + sb.AppendLine($"\t\tAddress: {cs.Address}"); + sb.AppendLine($"\t\tTotal subscriptions: {cs.Subscriptions}"); + sb.AppendLine($"\t\tStatus: {cs.Status}"); + sb.AppendLine($"\t\tAuthenticated: {cs.Authenticated}"); + sb.AppendLine($"\t\tDownload speed: {cs.DownloadSpeed} kbps"); + sb.AppendLine($"\t\tPending queries: {cs.PendingQueries}"); + if (cs.SubscriptionStates?.Count > 0) + { + sb.AppendLine($"\t\tSubscriptions:"); + cs.SubscriptionStates.ForEach(subState => + { + sb.AppendLine($"\t\t\tId: {subState.Id}"); + sb.AppendLine($"\t\t\tConfirmed: {subState.Confirmed}"); + sb.AppendLine($"\t\t\tInvocations: {subState.Invocations}"); + sb.AppendLine($"\t\t\tIdentifiers: [{subState.ListenMatcher.ToString()}]"); + }); + } + }); - _logger.UnsubscribingSubscription(connection.SocketId, subscriptionId); - await connection.CloseAsync(subscription).ConfigureAwait(false); return true; } - - /// - /// Unsubscribe an update subscription - /// - /// The subscription to unsubscribe - /// - public virtual async Task UnsubscribeAsync(UpdateSubscription subscription) - { - if (subscription == null) - throw new ArgumentNullException(nameof(subscription)); - - _logger.UnsubscribingSubscription(subscription.SocketId, subscription.Id); - await subscription.CloseAsync().ConfigureAwait(false); - } - - /// - /// Unsubscribe all subscriptions - /// - /// - public virtual async Task UnsubscribeAllAsync() - { - var sum = socketConnections.Sum(s => s.Value.UserSubscriptionCount); - if (sum == 0) - return; - - _logger.UnsubscribingAll(socketConnections.Sum(s => s.Value.UserSubscriptionCount)); - var tasks = new List(); - { - var socketList = socketConnections.Values; - 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); - } - - /// - /// Reconnect all connections - /// - /// - public virtual async Task ReconnectAsync() - { - _logger.ReconnectingAllConnections(socketConnections.Count); - var tasks = new List(); - { - var socketList = socketConnections.Values; - foreach (var sub in socketList) - tasks.Add(sub.TriggerReconnectAsync()); - } - - await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false); - } - - /// - public virtual async Task PrepareConnectionsAsync() - { - foreach (var item in DedicatedConnectionConfigs) - { - var socketResult = await GetSocketConnection(item.SocketAddress, item.Authenticated, true).ConfigureAwait(false); - if (!socketResult) - return socketResult.AsDataless(); - - var connectResult = await ConnectIfNeededAsync(socketResult.Data, item.Authenticated, default).ConfigureAwait(false); - if (!connectResult) - return new CallResult(connectResult.Error!); - } - - return CallResult.SuccessResult; - } - - /// - public override void SetOptions(UpdateOptions 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); - } - - /// - /// Log the current state of connections and subscriptions - /// - public string GetSubscriptionsState(bool includeSubDetails = true) - { - return GetState(includeSubDetails).ToString(); - } - - /// - /// Gets the state of the client - /// - /// True to get details for each subscription - /// - public SocketApiClientState GetState(bool includeSubDetails = true) - { - var connectionStates = new List(); - foreach (var socketIdAndConnection in socketConnections) - { - SocketConnection connection = socketIdAndConnection.Value; - SocketConnection.SocketConnectionState connectionState = connection.GetState(includeSubDetails); - connectionStates.Add(connectionState); - } - - return new SocketApiClientState(socketConnections.Count, CurrentSubscriptions, IncomingKbps, connectionStates); - } - - /// - /// Get the current state of the client - /// - /// Number of sockets for this client - /// Total number of subscriptions - /// Total download speed - /// State of each socket connection - public record SocketApiClientState( - int Connections, - int Subscriptions, - double DownloadSpeed, - List ConnectionStates) - { - /// - /// Print the state of the client - /// - /// - /// - protected virtual bool PrintMembers(StringBuilder sb) - { - sb.AppendLine(); - sb.AppendLine($"\tTotal connections: {Connections}"); - sb.AppendLine($"\tTotal subscriptions: {Subscriptions}"); - sb.AppendLine($"\tDownload speed: {DownloadSpeed} kbps"); - sb.AppendLine($"\tConnections:"); - ConnectionStates.ForEach(cs => - { - sb.AppendLine($"\t\tId: {cs.Id}"); - sb.AppendLine($"\t\tAddress: {cs.Address}"); - sb.AppendLine($"\t\tTotal subscriptions: {cs.Subscriptions}"); - sb.AppendLine($"\t\tStatus: {cs.Status}"); - sb.AppendLine($"\t\tAuthenticated: {cs.Authenticated}"); - sb.AppendLine($"\t\tDownload speed: {cs.DownloadSpeed} kbps"); - sb.AppendLine($"\t\tPending queries: {cs.PendingQueries}"); - if (cs.SubscriptionStates?.Count > 0) - { - sb.AppendLine($"\t\tSubscriptions:"); - cs.SubscriptionStates.ForEach(subState => - { - sb.AppendLine($"\t\t\tId: {subState.Id}"); - sb.AppendLine($"\t\t\tConfirmed: {subState.Confirmed}"); - sb.AppendLine($"\t\t\tInvocations: {subState.Invocations}"); - sb.AppendLine($"\t\t\tIdentifiers: [{subState.ListenMatcher.ToString()}]"); - }); - } - }); - - return true; - } - } - - /// - /// Dispose the client - /// - public override void Dispose() - { - _disposing = true; - var tasks = new List(); - { - var socketList = socketConnections.Values.Where(x => x.UserSubscriptionCount > 0 || x.Connected); - if (socketList.Any()) - _logger.DisposingSocketClient(); - - foreach (var connection in socketList) - { - tasks.Add(connection.CloseAsync()); - } - } - - semaphoreSlim?.Dispose(); - base.Dispose(); - } - - /// - /// Get the listener identifier for the message - /// - /// - /// - public abstract string? GetListenerIdentifier(IMessageAccessor messageAccessor); - - /// - /// Preprocess a stream message - /// - /// - /// - /// - /// - public virtual ReadOnlyMemory PreprocessStreamMessage(SocketConnection connection, WebSocketMessageType type, ReadOnlyMemory data) => data; } + + /// + /// Dispose the client + /// + public override void Dispose() + { + _disposing = true; + var tasks = new List(); + { + var socketList = socketConnections.Values.Where(x => x.UserSubscriptionCount > 0 || x.Connected); + if (socketList.Any()) + _logger.DisposingSocketClient(); + + foreach (var connection in socketList) + { + tasks.Add(connection.CloseAsync()); + } + } + + semaphoreSlim?.Dispose(); + base.Dispose(); + } + + /// + /// Get the listener identifier for the message + /// + /// + /// + public abstract string? GetListenerIdentifier(IMessageAccessor messageAccessor); + + /// + /// Preprocess a stream message + /// + /// + /// + /// + /// + public virtual ReadOnlyMemory PreprocessStreamMessage(SocketConnection connection, WebSocketMessageType type, ReadOnlyMemory data) => data; } diff --git a/CryptoExchange.Net/Converters/ArrayPropertyAttribute.cs b/CryptoExchange.Net/Converters/ArrayPropertyAttribute.cs index d688f8e..445b83c 100644 --- a/CryptoExchange.Net/Converters/ArrayPropertyAttribute.cs +++ b/CryptoExchange.Net/Converters/ArrayPropertyAttribute.cs @@ -1,25 +1,24 @@ -using System; +using System; -namespace CryptoExchange.Net.Converters +namespace CryptoExchange.Net.Converters; + +/// +/// Mark property as an index in the array +/// +[AttributeUsage(AttributeTargets.Property)] +public class ArrayPropertyAttribute : Attribute { /// - /// Mark property as an index in the array + /// The index in the array /// - [AttributeUsage(AttributeTargets.Property)] - public class ArrayPropertyAttribute : Attribute - { - /// - /// The index in the array - /// - public int Index { get; } + public int Index { get; } - /// - /// ctor - /// - /// - public ArrayPropertyAttribute(int index) - { - Index = index; - } + /// + /// ctor + /// + /// + public ArrayPropertyAttribute(int index) + { + Index = index; } } diff --git a/CryptoExchange.Net/Converters/JsonSerializerContextCache.cs b/CryptoExchange.Net/Converters/JsonSerializerContextCache.cs index 55cb48f..acf53d3 100644 --- a/CryptoExchange.Net/Converters/JsonSerializerContextCache.cs +++ b/CryptoExchange.Net/Converters/JsonSerializerContextCache.cs @@ -1,31 +1,28 @@ -using System; +using System; using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Text; using System.Text.Json.Serialization; -namespace CryptoExchange.Net.Converters +namespace CryptoExchange.Net.Converters; + +/// +/// Caching for JsonSerializerContext instances +/// +public static class JsonSerializerContextCache { + private static ConcurrentDictionary _cache = new ConcurrentDictionary(); + /// - /// Caching for JsonSerializerContext instances + /// Get the instance of the provided type T. It will be created if it doesn't exist yet. /// - public static class JsonSerializerContextCache + /// Implementation type of the JsonSerializerContext + public static JsonSerializerContext GetOrCreate() where T: JsonSerializerContext, new() { - private static ConcurrentDictionary _cache = new ConcurrentDictionary(); + var contextType = typeof(T); + if (_cache.TryGetValue(contextType, out var context)) + return context; - /// - /// Get the instance of the provided type T. It will be created if it doesn't exist yet. - /// - /// Implementation type of the JsonSerializerContext - public static JsonSerializerContext GetOrCreate() 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; - } + var instance = new T(); + _cache[contextType] = instance; + return instance; } } diff --git a/CryptoExchange.Net/Converters/MessageParsing/MessageNode.cs b/CryptoExchange.Net/Converters/MessageParsing/MessageNode.cs index b20eb1b..00886b9 100644 --- a/CryptoExchange.Net/Converters/MessageParsing/MessageNode.cs +++ b/CryptoExchange.Net/Converters/MessageParsing/MessageNode.cs @@ -1,49 +1,48 @@ -namespace CryptoExchange.Net.Converters.MessageParsing +namespace CryptoExchange.Net.Converters.MessageParsing; + +/// +/// Node accessor +/// +public readonly struct NodeAccessor { /// - /// Node accessor + /// Index /// - public readonly struct NodeAccessor + public int? Index { get; } + /// + /// Property name + /// + public string? Property { get; } + + /// + /// Type (0 = int, 1 = string, 2 = prop name) + /// + public int Type { get; } + + private NodeAccessor(int? index, string? property, int type) { - /// - /// Index - /// - public int? Index { get; } - /// - /// Property name - /// - public string? Property { get; } - - /// - /// Type (0 = int, 1 = string, 2 = prop name) - /// - public int Type { get; } - - private NodeAccessor(int? index, string? property, int type) - { - Index = index; - Property = property; - Type = type; - } - - /// - /// Create an int node accessor - /// - /// - /// - public static NodeAccessor Int(int value) { return new NodeAccessor(value, null, 0); } - - /// - /// Create a string node accessor - /// - /// - /// - public static NodeAccessor String(string value) { return new NodeAccessor(null, value, 1); } - - /// - /// Create a property name node accessor - /// - /// - public static NodeAccessor PropertyName() { return new NodeAccessor(null, null, 2); } + Index = index; + Property = property; + Type = type; } + + /// + /// Create an int node accessor + /// + /// + /// + public static NodeAccessor Int(int value) { return new NodeAccessor(value, null, 0); } + + /// + /// Create a string node accessor + /// + /// + /// + public static NodeAccessor String(string value) { return new NodeAccessor(null, value, 1); } + + /// + /// Create a property name node accessor + /// + /// + public static NodeAccessor PropertyName() { return new NodeAccessor(null, null, 2); } } diff --git a/CryptoExchange.Net/Converters/MessageParsing/MessagePath.cs b/CryptoExchange.Net/Converters/MessageParsing/MessagePath.cs index 54588ed..434cd55 100644 --- a/CryptoExchange.Net/Converters/MessageParsing/MessagePath.cs +++ b/CryptoExchange.Net/Converters/MessageParsing/MessagePath.cs @@ -1,50 +1,49 @@ -using System.Collections; +using System.Collections; using System.Collections.Generic; -namespace CryptoExchange.Net.Converters.MessageParsing +namespace CryptoExchange.Net.Converters.MessageParsing; + +/// +/// Message access definition +/// +public readonly struct MessagePath : IEnumerable { - /// - /// Message access definition - /// - public readonly struct MessagePath : IEnumerable + private readonly List _path; + + internal void Add(NodeAccessor node) { - private readonly List _path; + _path.Add(node); + } - internal void Add(NodeAccessor node) - { - _path.Add(node); - } + /// + /// ctor + /// + public MessagePath() + { + _path = new List(); + } - /// - /// ctor - /// - public MessagePath() - { - _path = new List(); - } + /// + /// Create a new message path + /// + /// + public static MessagePath Get() + { + return new MessagePath(); + } - /// - /// Create a new message path - /// - /// - public static MessagePath Get() - { - return new MessagePath(); - } + /// + /// IEnumerable implementation + /// + /// + public IEnumerator GetEnumerator() + { + for (var i = 0; i < _path.Count; i++) + yield return _path[i]; + } - /// - /// IEnumerable implementation - /// - /// - public IEnumerator GetEnumerator() - { - for (var i = 0; i < _path.Count; i++) - yield return _path[i]; - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); } } diff --git a/CryptoExchange.Net/Converters/MessageParsing/MessagePathExtension.cs b/CryptoExchange.Net/Converters/MessageParsing/MessagePathExtension.cs index 2f5cde9..f3039d6 100644 --- a/CryptoExchange.Net/Converters/MessageParsing/MessagePathExtension.cs +++ b/CryptoExchange.Net/Converters/MessageParsing/MessagePathExtension.cs @@ -1,43 +1,42 @@ -namespace CryptoExchange.Net.Converters.MessageParsing +namespace CryptoExchange.Net.Converters.MessageParsing; + +/// +/// Message path extension methods +/// +public static class MessagePathExtension { /// - /// Message path extension methods + /// Add a string node accessor /// - public static class MessagePathExtension + /// + /// + /// + public static MessagePath Property(this MessagePath path, string propName) { - /// - /// Add a string node accessor - /// - /// - /// - /// - public static MessagePath Property(this MessagePath path, string propName) - { - path.Add(NodeAccessor.String(propName)); - return path; - } + path.Add(NodeAccessor.String(propName)); + return path; + } - /// - /// Add a property name node accessor - /// - /// - /// - public static MessagePath PropertyName(this MessagePath path) - { - path.Add(NodeAccessor.PropertyName()); - return path; - } + /// + /// Add a property name node accessor + /// + /// + /// + public static MessagePath PropertyName(this MessagePath path) + { + path.Add(NodeAccessor.PropertyName()); + return path; + } - /// - /// Add a int node accessor - /// - /// - /// - /// - public static MessagePath Index(this MessagePath path, int index) - { - path.Add(NodeAccessor.Int(index)); - return path; - } + /// + /// Add a int node accessor + /// + /// + /// + /// + public static MessagePath Index(this MessagePath path, int index) + { + path.Add(NodeAccessor.Int(index)); + return path; } } diff --git a/CryptoExchange.Net/Converters/MessageParsing/NodeType.cs b/CryptoExchange.Net/Converters/MessageParsing/NodeType.cs index 21bccb8..154c64a 100644 --- a/CryptoExchange.Net/Converters/MessageParsing/NodeType.cs +++ b/CryptoExchange.Net/Converters/MessageParsing/NodeType.cs @@ -1,21 +1,20 @@ -namespace CryptoExchange.Net.Converters.MessageParsing +namespace CryptoExchange.Net.Converters.MessageParsing; + +/// +/// Message node type +/// +public enum NodeType { /// - /// Message node type + /// Array node /// - public enum NodeType - { - /// - /// Array node - /// - Array, - /// - /// Object node - /// - Object, - /// - /// Value node - /// - Value - } + Array, + /// + /// Object node + /// + Object, + /// + /// Value node + /// + Value } diff --git a/CryptoExchange.Net/Converters/SystemTextJson/ArrayConverter.cs b/CryptoExchange.Net/Converters/SystemTextJson/ArrayConverter.cs index c378ebc..be3673c 100644 --- a/CryptoExchange.Net/Converters/SystemTextJson/ArrayConverter.cs +++ b/CryptoExchange.Net/Converters/SystemTextJson/ArrayConverter.cs @@ -1,234 +1,232 @@ -using System; -using System.Collections.Concurrent; +using System; using System.Globalization; using System.Linq; using System.Reflection; using System.Text.Json.Serialization; using System.Text.Json; -using CryptoExchange.Net.Attributes; using System.Collections.Generic; +#if NET5_0_OR_GREATER using System.Diagnostics.CodeAnalysis; +#endif using System.Threading; -using System.Diagnostics; -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 - /// +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 +/// #if NET5_0_OR_GREATER - public class ArrayConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T> : JsonConverter where T : new() +public class ArrayConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T> : JsonConverter where T : new() #else - public class ArrayConverter : JsonConverter where T : new() +public class ArrayConverter : JsonConverter where T : new() #endif - { - private static readonly Lazy> _typePropertyInfo = new Lazy>(CacheTypeAttributes, LazyThreadSafetyMode.PublicationOnly); - - /// +{ + private static readonly Lazy> _typePropertyInfo = new Lazy>(CacheTypeAttributes, LazyThreadSafetyMode.PublicationOnly); + + /// #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")] + [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) + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + if (value == null) { - if (value == null) + writer.WriteNullValue(); + return; + } + + writer.WriteStartArray(); + + var ordered = _typePropertyInfo.Value.Where(x => x.ArrayProperty != null).OrderBy(p => p.ArrayProperty.Index); + var last = -1; + foreach (var prop in ordered) + { + if (prop.ArrayProperty.Index == last) + continue; + + while (prop.ArrayProperty.Index != last + 1) { writer.WriteNullValue(); - return; + last += 1; } - writer.WriteStartArray(); + last = prop.ArrayProperty.Index; - var ordered = _typePropertyInfo.Value.Where(x => x.ArrayProperty != null).OrderBy(p => p.ArrayProperty.Index); - var last = -1; - foreach (var prop in ordered) + var objValue = prop.PropertyInfo.GetValue(value); + if (objValue == null) { - if (prop.ArrayProperty.Index == last) - continue; - - while (prop.ArrayProperty.Index != last + 1) - { - writer.WriteNullValue(); - last += 1; - } - - 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); - } + writer.WriteNullValue(); + continue; } - writer.WriteEndArray(); + 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); + } } - /// - public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - if (reader.TokenType == JsonTokenType.Null) - return default; + writer.WriteEndArray(); + } - var result = new T(); - return ParseObject(ref reader, result, options); - } + /// + public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + return default; + + var result = new T(); + return ParseObject(ref reader, result, 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 T ParseObject(ref Utf8JsonReader reader, T result, JsonSerializerOptions options) + [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")] + [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")] + private static T ParseObject(ref Utf8JsonReader reader, T result, JsonSerializerOptions options) #else - private static T ParseObject(ref Utf8JsonReader reader, T result, JsonSerializerOptions options) + private static T ParseObject(ref Utf8JsonReader reader, T result, 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; - 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; + index++; + continue; + } - var indexAttributes = _typePropertyInfo.Value.Where(a => a.ArrayProperty.Index == index); - if (!indexAttributes.Any()) + foreach (var attribute in indexAttributes) + { + var targetType = attribute.TargetType; + object? value = null; + if (attribute.JsonConverter != null) { - index++; - continue; - } - - foreach (var attribute in indexAttributes) - { - var targetType = attribute.TargetType; - object? value = null; - if (attribute.JsonConverter != null) + if (attribute.JsonSerializerOptions == null) { - if (attribute.JsonSerializerOptions == null) + attribute.JsonSerializerOptions = new JsonSerializerOptions { - attribute.JsonSerializerOptions = new JsonSerializerOptions - { - NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals, - PropertyNameCaseInsensitive = false, - Converters = { attribute.JsonConverter }, - TypeInfoResolver = options.TypeInfoResolver, - }; - } - - var doc = JsonDocument.ParseValue(ref reader); - value = doc.Deserialize(attribute.PropertyInfo.PropertyType, attribute.JsonSerializerOptions); - } - else if (attribute.DefaultDeserialization) - { - value = JsonDocument.ParseValue(ref reader).Deserialize(options.GetTypeInfo(attribute.PropertyInfo.PropertyType)); - } - else - { - value = reader.TokenType switch - { - JsonTokenType.Null => null, - JsonTokenType.False => false, - 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"), + NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals, + PropertyNameCaseInsensitive = false, + Converters = { attribute.JsonConverter }, + TypeInfoResolver = options.TypeInfoResolver, }; } - if (targetType.IsAssignableFrom(value?.GetType())) - attribute.PropertyInfo.SetValue(result, value); - else - attribute.PropertyInfo.SetValue(result, value == null ? null : Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture)); + var doc = JsonDocument.ParseValue(ref reader); + value = doc.Deserialize(attribute.PropertyInfo.PropertyType, attribute.JsonSerializerOptions); + } + else if (attribute.DefaultDeserialization) + { + value = JsonDocument.ParseValue(ref reader).Deserialize(options.GetTypeInfo(attribute.PropertyInfo.PropertyType)); + } + else + { + value = reader.TokenType switch + { + JsonTokenType.Null => null, + JsonTokenType.False => false, + 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"), + }; } - 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++; } - private static bool IsSimple(Type type) + return result; + } + + private static bool IsSimple(Type type) + { + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) { - 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); + // 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 CacheTypeAttributes() + [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 CacheTypeAttributes() #else - private static List CacheTypeAttributes() + private static List CacheTypeAttributes() #endif + { + var attributes = new List(); + var properties = typeof(T).GetProperties(); + foreach (var property in properties) { - var attributes = new List(); - var properties = typeof(T).GetProperties(); - foreach (var property in properties) + var att = property.GetCustomAttribute(); + if (att == null) + continue; + + var targetType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType; + var converterType = property.GetCustomAttribute()?.ConverterType ?? targetType.GetCustomAttribute()?.ConverterType; + attributes.Add(new ArrayPropertyInfo { - var att = property.GetCustomAttribute(); - if (att == null) - continue; - - var targetType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType; - var converterType = property.GetCustomAttribute()?.ConverterType ?? targetType.GetCustomAttribute()?.ConverterType; - attributes.Add(new ArrayPropertyInfo - { - ArrayProperty = att, - PropertyInfo = property, - DefaultDeserialization = property.GetCustomAttribute() != null, - JsonConverter = converterType == null ? null : (JsonConverter)Activator.CreateInstance(converterType)!, - TargetType = targetType - }); - } - - return attributes; + ArrayProperty = att, + PropertyInfo = property, + DefaultDeserialization = property.GetCustomAttribute() != null, + JsonConverter = converterType == null ? null : (JsonConverter)Activator.CreateInstance(converterType)!, + TargetType = targetType + }); } - 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!; - public JsonSerializerOptions? JsonSerializerOptions { get; set; } = null; - } + 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!; + public JsonSerializerOptions? JsonSerializerOptions { get; set; } } } diff --git a/CryptoExchange.Net/Converters/SystemTextJson/BigDecimalConverter.cs b/CryptoExchange.Net/Converters/SystemTextJson/BigDecimalConverter.cs index 4598f16..7d7aeac 100644 --- a/CryptoExchange.Net/Converters/SystemTextJson/BigDecimalConverter.cs +++ b/CryptoExchange.Net/Converters/SystemTextJson/BigDecimalConverter.cs @@ -1,46 +1,45 @@ -using System; +using System; using System.Globalization; using System.Text.Json; using System.Text.Json.Serialization; -namespace CryptoExchange.Net.Converters.SystemTextJson -{ - /// - /// Decimal converter that handles overflowing decimal values (by setting it to decimal.MaxValue) - /// - public class BigDecimalConverter : JsonConverter - { - /// - public override decimal Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - if (reader.TokenType == JsonTokenType.String) - { - try - { - return decimal.Parse(reader.GetString()!, NumberStyles.Float, CultureInfo.InvariantCulture); - } - catch(OverflowException) - { - // Value doesn't fit decimal, default to max value - return decimal.MaxValue; - } - } +namespace CryptoExchange.Net.Converters.SystemTextJson; +/// +/// Decimal converter that handles overflowing decimal values (by setting it to decimal.MaxValue) +/// +public class BigDecimalConverter : JsonConverter +{ + /// + public override decimal Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.String) + { try { - return reader.GetDecimal(); + return decimal.Parse(reader.GetString()!, NumberStyles.Float, CultureInfo.InvariantCulture); } - catch(FormatException) + catch(OverflowException) { - // Format issue, assume value is too large + // Value doesn't fit decimal, default to max value return decimal.MaxValue; } } - /// - public override void Write(Utf8JsonWriter writer, decimal value, JsonSerializerOptions options) + try { - writer.WriteNumberValue(value); + return reader.GetDecimal(); + } + catch(FormatException) + { + // Format issue, assume value is too large + return decimal.MaxValue; } } + + /// + public override void Write(Utf8JsonWriter writer, decimal value, JsonSerializerOptions options) + { + writer.WriteNumberValue(value); + } } diff --git a/CryptoExchange.Net/Converters/SystemTextJson/BoolConverter.cs b/CryptoExchange.Net/Converters/SystemTextJson/BoolConverter.cs index 9ada30b..06efb91 100644 --- a/CryptoExchange.Net/Converters/SystemTextJson/BoolConverter.cs +++ b/CryptoExchange.Net/Converters/SystemTextJson/BoolConverter.cs @@ -1,84 +1,83 @@ -using System; +using System; using System.Diagnostics; using System.Runtime.Serialization; using System.Text.Json; using System.Text.Json.Serialization; -namespace CryptoExchange.Net.Converters.SystemTextJson +namespace CryptoExchange.Net.Converters.SystemTextJson; + +/// +/// Bool converter +/// +public class BoolConverter : JsonConverterFactory { - /// - /// Bool converter - /// - public class BoolConverter : JsonConverterFactory + /// + public override bool CanConvert(Type typeToConvert) { - /// - public override bool CanConvert(Type typeToConvert) - { - return typeToConvert == typeof(bool) || typeToConvert == typeof(bool?); - } - - /// - public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) - { - return typeToConvert == typeof(bool) ? new BoolConverterInner() : new BoolConverterInner(); - } - - private class BoolConverterInner : JsonConverter - { - public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - => (T)((object?)ReadBool(ref reader, typeToConvert, options) ?? default(T))!; - - public bool? ReadBool(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - if (reader.TokenType == JsonTokenType.True) - return true; - - if (reader.TokenType == JsonTokenType.False) - return false; - - var value = reader.TokenType switch - { - JsonTokenType.String => reader.GetString(), - JsonTokenType.Number => reader.GetInt16().ToString(), - _ => null - }; - - value = value?.ToLowerInvariant().Trim(); - if (string.IsNullOrEmpty(value)) - { - if (typeToConvert == typeof(bool)) - Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Received null bool value, but property type is not a nullable bool"); - return default; - } - - switch (value) - { - case "true": - case "yes": - case "y": - case "1": - case "on": - return true; - case "false": - case "no": - case "n": - case "0": - case "off": - case "-1": - return false; - } - - throw new SerializationException($"Can't convert bool value {value}"); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - if (value is bool boolVal) - writer.WriteBooleanValue(boolVal); - else - writer.WriteNullValue(); - } - } - + return typeToConvert == typeof(bool) || typeToConvert == typeof(bool?); } + + /// + public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + return typeToConvert == typeof(bool) ? new BoolConverterInner() : new BoolConverterInner(); + } + + private class BoolConverterInner : JsonConverter + { + public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + => (T)((object?)ReadBool(ref reader, typeToConvert, options) ?? default(T))!; + + public static bool? ReadBool(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.True) + return true; + + if (reader.TokenType == JsonTokenType.False) + return false; + + var value = reader.TokenType switch + { + JsonTokenType.String => reader.GetString(), + JsonTokenType.Number => reader.GetInt16().ToString(), + _ => null + }; + + value = value?.ToLowerInvariant().Trim(); + if (string.IsNullOrEmpty(value)) + { + if (typeToConvert == typeof(bool)) + Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Received null bool value, but property type is not a nullable bool"); + return default; + } + + switch (value) + { + case "true": + case "yes": + case "y": + case "1": + case "on": + return true; + case "false": + case "no": + case "n": + case "0": + case "off": + case "-1": + return false; + } + + throw new SerializationException($"Can't convert bool value {value}"); + } + + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + if (value is bool boolVal) + writer.WriteBooleanValue(boolVal); + else + writer.WriteNullValue(); + } + } + } diff --git a/CryptoExchange.Net/Converters/SystemTextJson/CommaSplitEnumConverter.cs b/CryptoExchange.Net/Converters/SystemTextJson/CommaSplitEnumConverter.cs index c327079..eda9146 100644 --- a/CryptoExchange.Net/Converters/SystemTextJson/CommaSplitEnumConverter.cs +++ b/CryptoExchange.Net/Converters/SystemTextJson/CommaSplitEnumConverter.cs @@ -1,37 +1,36 @@ -using System; -using System.Collections.Generic; +using System; +#if NET5_0_OR_GREATER using System.Diagnostics.CodeAnalysis; +#endif using System.Linq; -using System.Text; using System.Text.Json; using System.Text.Json.Serialization; -namespace CryptoExchange.Net.Converters.SystemTextJson -{ - /// - /// Converter for comma separated enum values - /// +namespace CryptoExchange.Net.Converters.SystemTextJson; + +/// +/// Converter for comma separated enum values +/// #if NET5_0_OR_GREATER - public class CommaSplitEnumConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T> : JsonConverter where T : struct, Enum +public class CommaSplitEnumConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T> : JsonConverter where T : struct, Enum #else - public class CommaSplitEnumConverter : JsonConverter where T : struct, Enum +public class CommaSplitEnumConverter : JsonConverter where T : struct, Enum #endif +{ + /// + public override T[]? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - /// - public override T[]? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - var str = reader.GetString(); - if (string.IsNullOrEmpty(str)) - return []; + var str = reader.GetString(); + if (string.IsNullOrEmpty(str)) + return []; - return str!.Split(',').Select(x => (T)EnumConverter.ParseString(x)!).ToArray() ?? []; - } + return str!.Split(',').Select(x => (T)EnumConverter.ParseString(x)!).ToArray() ?? []; + } - /// - public override void Write(Utf8JsonWriter writer, T[] value, JsonSerializerOptions options) - { - writer.WriteStringValue(string.Join(",", value.Select(x => EnumConverter.GetString(x)))); - } + /// + public override void Write(Utf8JsonWriter writer, T[] value, JsonSerializerOptions options) + { + writer.WriteStringValue(string.Join(",", value.Select(x => EnumConverter.GetString(x)))); } } diff --git a/CryptoExchange.Net/Converters/SystemTextJson/DateTimeConverter.cs b/CryptoExchange.Net/Converters/SystemTextJson/DateTimeConverter.cs index e491add..bc4cd01 100644 --- a/CryptoExchange.Net/Converters/SystemTextJson/DateTimeConverter.cs +++ b/CryptoExchange.Net/Converters/SystemTextJson/DateTimeConverter.cs @@ -1,242 +1,241 @@ -using System; +using System; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Text.Json; using System.Text.Json.Serialization; -namespace CryptoExchange.Net.Converters.SystemTextJson +namespace CryptoExchange.Net.Converters.SystemTextJson; + +/// +/// Date time converter +/// +public class DateTimeConverter : JsonConverterFactory { - /// - /// Date time converter - /// - public class DateTimeConverter : JsonConverterFactory + private static readonly DateTime _epoch = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + private const long _ticksPerSecond = TimeSpan.TicksPerMillisecond * 1000; + private const double _ticksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000d; + private const double _ticksPerNanosecond = TimeSpan.TicksPerMillisecond / 1000d / 1000; + + /// + public override bool CanConvert(Type typeToConvert) { - private static readonly DateTime _epoch = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); - private const long _ticksPerSecond = TimeSpan.TicksPerMillisecond * 1000; - private const double _ticksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000d; - private const double _ticksPerNanosecond = TimeSpan.TicksPerMillisecond / 1000d / 1000; - - /// - public override bool CanConvert(Type typeToConvert) - { - return typeToConvert == typeof(DateTime) || typeToConvert == typeof(DateTime?); - } - - /// - public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) - { - return typeToConvert == typeof(DateTime) ? new DateTimeConverterInner() : new DateTimeConverterInner(); - } - - private class DateTimeConverterInner : JsonConverter - { - public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - => (T)((object?)ReadDateTime(ref reader, typeToConvert, options) ?? default(T))!; - - private DateTime? ReadDateTime(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - if (reader.TokenType == JsonTokenType.Null) - { - if (typeToConvert == typeof(DateTime)) - Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | DateTime value of null, but property is not nullable"); - return default; - } - - if (reader.TokenType is JsonTokenType.Number) - { - var longValue = reader.GetDouble(); - if (longValue == 0 || longValue < 0) - return default; - - return ParseFromDouble(longValue); - } - else if (reader.TokenType is JsonTokenType.String) - { - 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; - } - - return ParseFromString(stringValue!); - } - else - { - return reader.GetDateTime(); - } - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - if (value == null) - { - writer.WriteNullValue(); - } - else - { - var dtValue = (DateTime)(object)value; - if (dtValue == default) - writer.WriteStringValue(default(DateTime)); - else - writer.WriteNumberValue((long)Math.Round((dtValue - new DateTime(1970, 1, 1)).TotalMilliseconds)); - } - } - } - - /// - /// Parse a long value to datetime - /// - /// - /// - public static DateTime ParseFromDouble(double longValue) - { - if (longValue < 19999999999) - return ConvertFromSeconds(longValue); - if (longValue < 19999999999999) - return ConvertFromMilliseconds(longValue); - if (longValue < 19999999999999999) - return ConvertFromMicroseconds(longValue); - - return ConvertFromNanoseconds(longValue); - } - - /// - /// Parse a string value to datetime - /// - /// - /// - 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 <= 0) - return default; - 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); - } - - /// - /// Convert a seconds since epoch (01-01-1970) value to DateTime - /// - /// - /// - public static DateTime ConvertFromSeconds(double seconds) => _epoch.AddTicks((long)Math.Round(seconds * _ticksPerSecond)); - /// - /// Convert a milliseconds since epoch (01-01-1970) value to DateTime - /// - /// - /// - public static DateTime ConvertFromMilliseconds(double milliseconds) => _epoch.AddTicks((long)Math.Round(milliseconds * TimeSpan.TicksPerMillisecond)); - /// - /// Convert a microseconds since epoch (01-01-1970) value to DateTime - /// - /// - /// - public static DateTime ConvertFromMicroseconds(double microseconds) => _epoch.AddTicks((long)Math.Round(microseconds * _ticksPerMicrosecond)); - /// - /// Convert a nanoseconds since epoch (01-01-1970) value to DateTime - /// - /// - /// - public static DateTime ConvertFromNanoseconds(double nanoseconds) => _epoch.AddTicks((long)Math.Round(nanoseconds * _ticksPerNanosecond)); - - /// - /// Convert a DateTime value to seconds since epoch (01-01-1970) value - /// - /// - /// - [return: NotNullIfNotNull("time")] - public static long? ConvertToSeconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).TotalSeconds); - /// - /// Convert a DateTime value to milliseconds since epoch (01-01-1970) value - /// - /// - /// - [return: NotNullIfNotNull("time")] - public static long? ConvertToMilliseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).TotalMilliseconds); - /// - /// Convert a DateTime value to microseconds since epoch (01-01-1970) value - /// - /// - /// - [return: NotNullIfNotNull("time")] - public static long? ConvertToMicroseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / _ticksPerMicrosecond); - /// - /// Convert a DateTime value to nanoseconds since epoch (01-01-1970) value - /// - /// - /// - [return: NotNullIfNotNull("time")] - public static long? ConvertToNanoseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / _ticksPerNanosecond); + return typeToConvert == typeof(DateTime) || typeToConvert == typeof(DateTime?); } + + /// + public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + return typeToConvert == typeof(DateTime) ? new DateTimeConverterInner() : new DateTimeConverterInner(); + } + + private class DateTimeConverterInner : JsonConverter + { + public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + => (T)((object?)ReadDateTime(ref reader, typeToConvert, options) ?? default(T))!; + + private static DateTime? ReadDateTime(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + { + if (typeToConvert == typeof(DateTime)) + Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | DateTime value of null, but property is not nullable"); + return default; + } + + if (reader.TokenType is JsonTokenType.Number) + { + var longValue = reader.GetDouble(); + if (longValue == 0 || longValue < 0) + return default; + + return ParseFromDouble(longValue); + } + else if (reader.TokenType is JsonTokenType.String) + { + 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; + } + + return ParseFromString(stringValue!); + } + else + { + return reader.GetDateTime(); + } + } + + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + if (value == null) + { + writer.WriteNullValue(); + } + else + { + var dtValue = (DateTime)(object)value; + if (dtValue == default) + writer.WriteStringValue(default(DateTime)); + else + writer.WriteNumberValue((long)Math.Round((dtValue - new DateTime(1970, 1, 1)).TotalMilliseconds)); + } + } + } + + /// + /// Parse a long value to datetime + /// + /// + /// + public static DateTime ParseFromDouble(double longValue) + { + if (longValue < 19999999999) + return ConvertFromSeconds(longValue); + if (longValue < 19999999999999) + return ConvertFromMilliseconds(longValue); + if (longValue < 19999999999999999) + return ConvertFromMicroseconds(longValue); + + return ConvertFromNanoseconds(longValue); + } + + /// + /// Parse a string value to datetime + /// + /// + /// + 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 <= 0) + return default; + 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); + } + + /// + /// Convert a seconds since epoch (01-01-1970) value to DateTime + /// + /// + /// + public static DateTime ConvertFromSeconds(double seconds) => _epoch.AddTicks((long)Math.Round(seconds * _ticksPerSecond)); + /// + /// Convert a milliseconds since epoch (01-01-1970) value to DateTime + /// + /// + /// + public static DateTime ConvertFromMilliseconds(double milliseconds) => _epoch.AddTicks((long)Math.Round(milliseconds * TimeSpan.TicksPerMillisecond)); + /// + /// Convert a microseconds since epoch (01-01-1970) value to DateTime + /// + /// + /// + public static DateTime ConvertFromMicroseconds(double microseconds) => _epoch.AddTicks((long)Math.Round(microseconds * _ticksPerMicrosecond)); + /// + /// Convert a nanoseconds since epoch (01-01-1970) value to DateTime + /// + /// + /// + public static DateTime ConvertFromNanoseconds(double nanoseconds) => _epoch.AddTicks((long)Math.Round(nanoseconds * _ticksPerNanosecond)); + + /// + /// Convert a DateTime value to seconds since epoch (01-01-1970) value + /// + /// + /// + [return: NotNullIfNotNull("time")] + public static long? ConvertToSeconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).TotalSeconds); + /// + /// Convert a DateTime value to milliseconds since epoch (01-01-1970) value + /// + /// + /// + [return: NotNullIfNotNull("time")] + public static long? ConvertToMilliseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).TotalMilliseconds); + /// + /// Convert a DateTime value to microseconds since epoch (01-01-1970) value + /// + /// + /// + [return: NotNullIfNotNull("time")] + public static long? ConvertToMicroseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / _ticksPerMicrosecond); + /// + /// Convert a DateTime value to nanoseconds since epoch (01-01-1970) value + /// + /// + /// + [return: NotNullIfNotNull("time")] + public static long? ConvertToNanoseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / _ticksPerNanosecond); } diff --git a/CryptoExchange.Net/Converters/SystemTextJson/DecimalConverter.cs b/CryptoExchange.Net/Converters/SystemTextJson/DecimalConverter.cs index 5d7b694..b4baf8c 100644 --- a/CryptoExchange.Net/Converters/SystemTextJson/DecimalConverter.cs +++ b/CryptoExchange.Net/Converters/SystemTextJson/DecimalConverter.cs @@ -1,45 +1,43 @@ -using System; -using System.Globalization; +using System; using System.Text.Json; using System.Text.Json.Serialization; -namespace CryptoExchange.Net.Converters.SystemTextJson +namespace CryptoExchange.Net.Converters.SystemTextJson; + +/// +/// Decimal converter +/// +public class DecimalConverter : JsonConverter { - /// - /// Decimal converter - /// - public class DecimalConverter : JsonConverter + /// + public override decimal? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - /// - public override decimal? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + if (reader.TokenType == JsonTokenType.Null) + return null; + + if (reader.TokenType == JsonTokenType.String) { - if (reader.TokenType == JsonTokenType.Null) - return null; - - if (reader.TokenType == JsonTokenType.String) - { - var value = reader.GetString(); - return ExchangeHelpers.ParseDecimal(value); - } - - try - { - return reader.GetDecimal(); - } - catch(FormatException) - { - // Format issue, assume value is too large - return decimal.MaxValue; - } + var value = reader.GetString(); + return ExchangeHelpers.ParseDecimal(value); } - /// - public override void Write(Utf8JsonWriter writer, decimal? value, JsonSerializerOptions options) + try { - if (value == null) - writer.WriteNullValue(); - else - writer.WriteNumberValue(value.Value); + return reader.GetDecimal(); + } + catch(FormatException) + { + // Format issue, assume value is too large + return decimal.MaxValue; } } + + /// + public override void Write(Utf8JsonWriter writer, decimal? value, JsonSerializerOptions options) + { + if (value == null) + writer.WriteNullValue(); + else + writer.WriteNumberValue(value.Value); + } } diff --git a/CryptoExchange.Net/Converters/SystemTextJson/DecimalStringWriterConverter.cs b/CryptoExchange.Net/Converters/SystemTextJson/DecimalStringWriterConverter.cs index 551030c..4d039c4 100644 --- a/CryptoExchange.Net/Converters/SystemTextJson/DecimalStringWriterConverter.cs +++ b/CryptoExchange.Net/Converters/SystemTextJson/DecimalStringWriterConverter.cs @@ -1,23 +1,22 @@ -using System; +using System; using System.Globalization; using System.Text.Json; using System.Text.Json.Serialization; -namespace CryptoExchange.Net.Converters.SystemTextJson -{ - /// - /// Converter for serializing decimal values as string - /// - public class DecimalStringWriterConverter : JsonConverter - { - /// - public override decimal Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - throw new NotImplementedException(); - } +namespace CryptoExchange.Net.Converters.SystemTextJson; - /// - public override void Write(Utf8JsonWriter writer, decimal value, JsonSerializerOptions options) - => writer.WriteStringValue(value.ToString(CultureInfo.InvariantCulture) ?? null); +/// +/// Converter for serializing decimal values as string +/// +public class DecimalStringWriterConverter : JsonConverter +{ + /// + public override decimal Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + throw new NotImplementedException(); } + + /// + public override void Write(Utf8JsonWriter writer, decimal value, JsonSerializerOptions options) + => writer.WriteStringValue(value.ToString(CultureInfo.InvariantCulture) ?? null); } diff --git a/CryptoExchange.Net/Converters/SystemTextJson/EnumConverter.cs b/CryptoExchange.Net/Converters/SystemTextJson/EnumConverter.cs index ff5dfb8..340052e 100644 --- a/CryptoExchange.Net/Converters/SystemTextJson/EnumConverter.cs +++ b/CryptoExchange.Net/Converters/SystemTextJson/EnumConverter.cs @@ -1,4 +1,4 @@ -using CryptoExchange.Net.Attributes; +using CryptoExchange.Net.Attributes; using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -9,281 +9,280 @@ using System.Reflection; using System.Text.Json; using System.Text.Json.Serialization; -namespace CryptoExchange.Net.Converters.SystemTextJson +namespace CryptoExchange.Net.Converters.SystemTextJson; + +/// +/// Static EnumConverter methods +/// +public static class EnumConverter { /// - /// Static EnumConverter methods + /// Get the enum value from a string /// - public static class EnumConverter - { - /// - /// Get the enum value from a string - /// - /// String value - /// + /// String value + /// #if NET5_0_OR_GREATER - public static T? ParseString<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string value) where T : struct, Enum + public static T? ParseString<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string value) where T : struct, Enum #else - public static T? ParseString(string value) where T : struct, Enum + public static T? ParseString(string value) where T : struct, Enum #endif - => EnumConverter.ParseString(value); - - /// - /// 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 - /// - /// - /// -#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 enumValue) where T : struct, Enum -#endif - => EnumConverter.GetString(enumValue); - - /// - /// 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 - /// - /// - /// - [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? enumValue) where T : struct, Enum -#endif - => EnumConverter.GetString(enumValue); - } + => EnumConverter.ParseString(value); /// - /// Converter for enum values. Enums entries should be noted with a MapAttribute to map the enum value to a string value + /// 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 /// + /// + /// #if NET5_0_OR_GREATER - public class EnumConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T> + public static string GetString<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(T enumValue) where T : struct, Enum #else - public class EnumConverter + public static string GetString(T enumValue) where T : struct, Enum #endif - : JsonConverter, INullableConverterFactory where T : struct, Enum + => EnumConverter.GetString(enumValue); + + /// + /// 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 + /// + /// + /// + [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? enumValue) where T : struct, Enum +#endif + => EnumConverter.GetString(enumValue); +} + +/// +/// Converter for enum values. Enums entries should be noted with a MapAttribute to map the enum value to a string value +/// +#if NET5_0_OR_GREATER +public class EnumConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T> +#else +public class EnumConverter +#endif + : JsonConverter, INullableConverterFactory where T : struct, Enum +{ + private static List>? _mapping; + private NullableEnumConverter? _nullableEnumConverter; + + private static ConcurrentBag _unknownValuesWarned = new ConcurrentBag(); + + internal class NullableEnumConverter : JsonConverter { - private static List>? _mapping = null; - private NullableEnumConverter? _nullableEnumConverter = null; + private readonly EnumConverter _enumConverter; - private static ConcurrentBag _unknownValuesWarned = new ConcurrentBag(); - - internal class NullableEnumConverter : JsonConverter + public NullableEnumConverter(EnumConverter enumConverter) { - private readonly EnumConverter _enumConverter; - - public NullableEnumConverter(EnumConverter enumConverter) - { - _enumConverter = enumConverter; - } - public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - return _enumConverter.ReadNullable(ref reader, typeToConvert, options, out var isEmptyString, out var warn); - } - - public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options) - { - if (value == null) - { - writer.WriteNullValue(); - } - else - { - _enumConverter.Write(writer, value.Value, options); - } - } + _enumConverter = enumConverter; + } + public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return EnumConverter.ReadNullable(ref reader, typeToConvert, options, out var isEmptyString, out var warn); } - /// - public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options) { - var t = ReadNullable(ref reader, typeToConvert, options, out var isEmptyString, out var warn); - if (t == null) + if (value == null) { - if (warn) - { - if (isEmptyString) - { - // 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 - { - 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"); - } - } - - return new T(); // return default value + writer.WriteNullValue(); } else { - return t.Value; + _enumConverter.Write(writer, value.Value, options); } } + } - private T? ReadNullable(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, out bool isEmptyString, out bool warn) + /// + 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) { - isEmptyString = false; - warn = false; - var enumType = typeof(T); - if (_mapping == null) - _mapping = AddMapping(); - - var stringValue = reader.TokenType switch + if (warn) { - 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)) + if (isEmptyString) { - isEmptyString = true; + // 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 { - // 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"); - } + 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"); } - - return null; } - return result; + return new T(); // return default value } - - /// - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + else { - var stringValue = GetString(value); - writer.WriteStringValue(stringValue); + return t.Value; } + } - private static bool GetValue(Type objectType, string value, out T? result) + private static 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 { - if (_mapping != null) + 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)) { - // 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))) - mapping = _mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase)); - - if (!mapping.Equals(default(KeyValuePair))) + isEmptyString = true; + } + else + { + // We received an enum value but weren't able to parse it. + if (!_unknownValuesWarned.Contains(stringValue)) { - result = mapping.Key; - return true; + 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"); } } - 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; - } + return null; } - private static List> AddMapping() + return result; + } + + /// + 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) { - var mapping = new List>(); - 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) - { - foreach (var value in attribute.Values) - mapping.Add(new KeyValuePair((T)Enum.Parse(enumType, member.Name), value)); - } - } - - _mapping = mapping; - return mapping; - } - - /// - /// 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 - /// - /// - /// - [return: NotNullIfNotNull("enumValue")] - public static string? GetString(T? enumValue) - { - if (_mapping == null) - _mapping = AddMapping(); - - return enumValue == null ? null : (_mapping.FirstOrDefault(v => v.Key.Equals(enumValue)).Value ?? enumValue.ToString()); - } - - /// - /// Get the enum value from a string - /// - /// String value - /// - public static T? ParseString(string value) - { - var type = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T); - if (_mapping == null) - _mapping = AddMapping(); - + // 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))) mapping = _mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase)); if (!mapping.Equals(default(KeyValuePair))) - return mapping.Key; - - try { - // If no explicit mapping is found try to parse string - return (T)Enum.Parse(type, value, true); - } - catch (Exception) - { - return default; + result = mapping.Key; + return true; } } - /// - public JsonConverter CreateNullableConverter() + if (objectType.IsDefined(typeof(FlagsAttribute))) { - _nullableEnumConverter ??= new NullableEnumConverter(this); - return _nullableEnumConverter; + 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> AddMapping() + { + var mapping = new List>(); + 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) + { + foreach (var value in attribute.Values) + mapping.Add(new KeyValuePair((T)Enum.Parse(enumType, member.Name), value)); + } + } + + _mapping = mapping; + return mapping; + } + + /// + /// 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 + /// + /// + /// + [return: NotNullIfNotNull("enumValue")] + public static string? GetString(T? enumValue) + { + if (_mapping == null) + _mapping = AddMapping(); + + return enumValue == null ? null : (_mapping.FirstOrDefault(v => v.Key.Equals(enumValue)).Value ?? enumValue.ToString()); + } + + /// + /// Get the enum value from a string + /// + /// String value + /// + public static T? ParseString(string value) + { + var type = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T); + if (_mapping == null) + _mapping = AddMapping(); + + var mapping = _mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCulture)); + if (mapping.Equals(default(KeyValuePair))) + mapping = _mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase)); + + if (!mapping.Equals(default(KeyValuePair))) + return mapping.Key; + + try + { + // If no explicit mapping is found try to parse string + return (T)Enum.Parse(type, value, true); + } + catch (Exception) + { + return default; + } + } + + /// + public JsonConverter CreateNullableConverter() + { + _nullableEnumConverter ??= new NullableEnumConverter(this); + return _nullableEnumConverter; + } } diff --git a/CryptoExchange.Net/Converters/SystemTextJson/EnumIntWriterConverter.cs b/CryptoExchange.Net/Converters/SystemTextJson/EnumIntWriterConverter.cs index 9a0c9a7..f7bec54 100644 --- a/CryptoExchange.Net/Converters/SystemTextJson/EnumIntWriterConverter.cs +++ b/CryptoExchange.Net/Converters/SystemTextJson/EnumIntWriterConverter.cs @@ -1,23 +1,21 @@ -using System; -using System.Globalization; +using System; using System.Text.Json; using System.Text.Json.Serialization; -namespace CryptoExchange.Net.Converters.SystemTextJson -{ - /// - /// Converter for serializing enum values as int - /// - public class EnumIntWriterConverter : JsonConverter where T: struct, Enum - { - /// - public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - throw new NotImplementedException(); - } +namespace CryptoExchange.Net.Converters.SystemTextJson; - /// - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - => writer.WriteNumberValue((int)(object)value); +/// +/// Converter for serializing enum values as int +/// +public class EnumIntWriterConverter : JsonConverter where T: struct, Enum +{ + /// + public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + throw new NotImplementedException(); } + + /// + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + => writer.WriteNumberValue((int)(object)value); } diff --git a/CryptoExchange.Net/Converters/SystemTextJson/INullableConverterFactory.cs b/CryptoExchange.Net/Converters/SystemTextJson/INullableConverterFactory.cs index ea01b5a..412ea53 100644 --- a/CryptoExchange.Net/Converters/SystemTextJson/INullableConverterFactory.cs +++ b/CryptoExchange.Net/Converters/SystemTextJson/INullableConverterFactory.cs @@ -1,9 +1,8 @@ -using System.Text.Json.Serialization; +using System.Text.Json.Serialization; -namespace CryptoExchange.Net.Converters.SystemTextJson +namespace CryptoExchange.Net.Converters.SystemTextJson; + +internal interface INullableConverterFactory { - internal interface INullableConverterFactory - { - JsonConverter CreateNullableConverter(); - } + JsonConverter CreateNullableConverter(); } diff --git a/CryptoExchange.Net/Converters/SystemTextJson/IntConverter.cs b/CryptoExchange.Net/Converters/SystemTextJson/IntConverter.cs index 486367f..a956e62 100644 --- a/CryptoExchange.Net/Converters/SystemTextJson/IntConverter.cs +++ b/CryptoExchange.Net/Converters/SystemTextJson/IntConverter.cs @@ -1,40 +1,39 @@ -using System; +using System; using System.Globalization; using System.Text.Json; using System.Text.Json.Serialization; -namespace CryptoExchange.Net.Converters.SystemTextJson +namespace CryptoExchange.Net.Converters.SystemTextJson; + +/// +/// Int converter +/// +public class IntConverter : JsonConverter { - /// - /// Int converter - /// - public class IntConverter : JsonConverter + /// + public override int? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - /// - public override int? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + if (reader.TokenType == JsonTokenType.Null) + return null; + + if (reader.TokenType == JsonTokenType.String) { - if (reader.TokenType == JsonTokenType.Null) + var value = reader.GetString(); + if (string.IsNullOrEmpty(value)) return null; - if (reader.TokenType == JsonTokenType.String) - { - var value = reader.GetString(); - if (string.IsNullOrEmpty(value)) - return null; - - return int.Parse(value, NumberStyles.Integer, CultureInfo.InvariantCulture); - } - - return reader.GetInt32(); + return int.Parse(value, NumberStyles.Integer, CultureInfo.InvariantCulture); } - /// - public override void Write(Utf8JsonWriter writer, int? value, JsonSerializerOptions options) - { - if (value == null) - writer.WriteNullValue(); - else - writer.WriteNumberValue(value.Value); - } + return reader.GetInt32(); + } + + /// + public override void Write(Utf8JsonWriter writer, int? value, JsonSerializerOptions options) + { + if (value == null) + writer.WriteNullValue(); + else + writer.WriteNumberValue(value.Value); } } diff --git a/CryptoExchange.Net/Converters/SystemTextJson/LongConverter.cs b/CryptoExchange.Net/Converters/SystemTextJson/LongConverter.cs index 96698e9..ada1ce6 100644 --- a/CryptoExchange.Net/Converters/SystemTextJson/LongConverter.cs +++ b/CryptoExchange.Net/Converters/SystemTextJson/LongConverter.cs @@ -1,40 +1,39 @@ -using System; +using System; using System.Globalization; using System.Text.Json; using System.Text.Json.Serialization; -namespace CryptoExchange.Net.Converters.SystemTextJson +namespace CryptoExchange.Net.Converters.SystemTextJson; + +/// +/// Int converter +/// +public class LongConverter : JsonConverter { - /// - /// Int converter - /// - public class LongConverter : JsonConverter + /// + public override long? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - /// - public override long? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + if (reader.TokenType == JsonTokenType.Null) + return null; + + if (reader.TokenType == JsonTokenType.String) { - if (reader.TokenType == JsonTokenType.Null) + var value = reader.GetString(); + if (string.IsNullOrEmpty(value)) return null; - if (reader.TokenType == JsonTokenType.String) - { - var value = reader.GetString(); - if (string.IsNullOrEmpty(value)) - return null; - - return long.Parse(value, NumberStyles.Integer, CultureInfo.InvariantCulture); - } - - return reader.GetInt64(); + return long.Parse(value, NumberStyles.Integer, CultureInfo.InvariantCulture); } - /// - public override void Write(Utf8JsonWriter writer, long? value, JsonSerializerOptions options) - { - if (value == null) - writer.WriteNullValue(); - else - writer.WriteNumberValue(value.Value); - } + return reader.GetInt64(); + } + + /// + public override void Write(Utf8JsonWriter writer, long? value, JsonSerializerOptions options) + { + if (value == null) + writer.WriteNullValue(); + else + writer.WriteNumberValue(value.Value); } } diff --git a/CryptoExchange.Net/Converters/SystemTextJson/NullableEnumConverterFactory.cs b/CryptoExchange.Net/Converters/SystemTextJson/NullableEnumConverterFactory.cs index 4cd7e68..cbfa6c8 100644 --- a/CryptoExchange.Net/Converters/SystemTextJson/NullableEnumConverterFactory.cs +++ b/CryptoExchange.Net/Converters/SystemTextJson/NullableEnumConverterFactory.cs @@ -1,43 +1,40 @@ -using System; -using System.Collections.Generic; -using System.Text; +using System; using System.Text.Json.Serialization.Metadata; using System.Text.Json; using System.Text.Json.Serialization; -namespace CryptoExchange.Net.Converters.SystemTextJson +namespace CryptoExchange.Net.Converters.SystemTextJson; + +internal class NullableEnumConverterFactory : JsonConverterFactory { - internal class NullableEnumConverterFactory : JsonConverterFactory + private readonly IJsonTypeInfoResolver _jsonTypeInfoResolver; + private static readonly JsonSerializerOptions _options = new JsonSerializerOptions(); + + public NullableEnumConverterFactory(IJsonTypeInfoResolver jsonTypeInfoResolver) { - private readonly IJsonTypeInfoResolver _jsonTypeInfoResolver; - private static readonly JsonSerializerOptions _options = new JsonSerializerOptions(); + _jsonTypeInfoResolver = jsonTypeInfoResolver; + } - public NullableEnumConverterFactory(IJsonTypeInfoResolver jsonTypeInfoResolver) - { - _jsonTypeInfoResolver = jsonTypeInfoResolver; - } + public override bool CanConvert(Type typeToConvert) + { + var b = Nullable.GetUnderlyingType(typeToConvert); + if (b == null) + return false; - 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; - var typeInfo = _jsonTypeInfoResolver.GetTypeInfo(b, _options); - if (typeInfo == null) - return false; + return typeInfo.Converter is INullableConverterFactory; + } - 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(); - } + 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(); } } diff --git a/CryptoExchange.Net/Converters/SystemTextJson/NumberStringConverter.cs b/CryptoExchange.Net/Converters/SystemTextJson/NumberStringConverter.cs index 123b6f3..1671aaa 100644 --- a/CryptoExchange.Net/Converters/SystemTextJson/NumberStringConverter.cs +++ b/CryptoExchange.Net/Converters/SystemTextJson/NumberStringConverter.cs @@ -1,42 +1,41 @@ -using System; +using System; using System.Text.Json; using System.Text.Json.Serialization; -namespace CryptoExchange.Net.Converters.SystemTextJson +namespace CryptoExchange.Net.Converters.SystemTextJson; + +/// +/// Read string or number as string +/// +public class NumberStringConverter : JsonConverter { - /// - /// Read string or number as string - /// - public class NumberStringConverter : JsonConverter + /// + public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - /// - public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + if (reader.TokenType == JsonTokenType.Null) + return null; + + if (reader.TokenType == JsonTokenType.Number) { - if (reader.TokenType == JsonTokenType.Null) - return null; + if (reader.TryGetInt64(out var value)) + return value.ToString(); - if (reader.TokenType == JsonTokenType.Number) - { - if (reader.TryGetInt64(out var value)) - return value.ToString(); - - return reader.GetDecimal().ToString(); - } - - try - { - return reader.GetString(); - } - catch (Exception) - { - return null; - } + return reader.GetDecimal().ToString(); } - /// - public override void Write(Utf8JsonWriter writer, string? value, JsonSerializerOptions options) + try { - writer.WriteStringValue(value); + return reader.GetString(); + } + catch (Exception) + { + return null; } } + + /// + public override void Write(Utf8JsonWriter writer, string? value, JsonSerializerOptions options) + { + writer.WriteStringValue(value); + } } diff --git a/CryptoExchange.Net/Converters/SystemTextJson/ObjectStringConverter.cs b/CryptoExchange.Net/Converters/SystemTextJson/ObjectStringConverter.cs index 53a77ee..94f5753 100644 --- a/CryptoExchange.Net/Converters/SystemTextJson/ObjectStringConverter.cs +++ b/CryptoExchange.Net/Converters/SystemTextJson/ObjectStringConverter.cs @@ -1,43 +1,44 @@ -using System; +using System; using System.Text.Json.Serialization; using System.Text.Json; +#if NET5_0_OR_GREATER using System.Diagnostics.CodeAnalysis; +#endif -namespace CryptoExchange.Net.Converters.SystemTextJson +namespace CryptoExchange.Net.Converters.SystemTextJson; + +/// +/// Converter for values which contain a nested json value +/// +public class ObjectStringConverter : JsonConverter { - /// - /// Converter for values which contain a nested json value - /// - public class ObjectStringConverter : JsonConverter + /// +#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) + return default; + + var value = reader.GetString(); + if (string.IsNullOrEmpty(value)) + return default; + + return JsonDocument.Parse(value!).Deserialize(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")] + [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) - return default; + public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options) + { + if (value is null) + writer.WriteStringValue(""); - var value = reader.GetString(); - if (string.IsNullOrEmpty(value)) - return default; - - return (T?)JsonDocument.Parse(value!).Deserialize(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")] -#endif - public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options) - { - if (value is null) - writer.WriteStringValue(""); - - writer.WriteStringValue(JsonSerializer.Serialize(value, options)); - } + writer.WriteStringValue(JsonSerializer.Serialize(value, options)); } } diff --git a/CryptoExchange.Net/Converters/SystemTextJson/ReplaceConverter.cs b/CryptoExchange.Net/Converters/SystemTextJson/ReplaceConverter.cs index 6006c80..80b85aa 100644 --- a/CryptoExchange.Net/Converters/SystemTextJson/ReplaceConverter.cs +++ b/CryptoExchange.Net/Converters/SystemTextJson/ReplaceConverter.cs @@ -1,41 +1,40 @@ -using System; +using System; using System.Linq; using System.Text.Json; using System.Text.Json.Serialization; -namespace CryptoExchange.Net.Converters.SystemTextJson +namespace CryptoExchange.Net.Converters.SystemTextJson; + +/// +/// Replace a value on a string property +/// +public abstract class ReplaceConverter : JsonConverter { + private readonly (string ValueToReplace, string ValueToReplaceWith)[] _replacementSets; + /// - /// Replace a value on a string property + /// ctor /// - public abstract class ReplaceConverter : JsonConverter + public ReplaceConverter(params string[] replaceSets) { - private readonly (string ValueToReplace, string ValueToReplaceWith)[] _replacementSets; - - /// - /// ctor - /// - public ReplaceConverter(params string[] replaceSets) + _replacementSets = replaceSets.Select(x => { - _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(); - } - - /// - 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; - } - - /// - public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) => writer.WriteStringValue(value); + var split = x.Split(["->"], StringSplitOptions.None); + if (split.Length != 2) + throw new ArgumentException("Invalid replacement config"); + return (split[0], split[1]); + }).ToArray(); } + + /// + 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; + } + + /// + public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) => writer.WriteStringValue(value); } diff --git a/CryptoExchange.Net/Converters/SystemTextJson/SerializationModel.cs b/CryptoExchange.Net/Converters/SystemTextJson/SerializationModel.cs index 3c958ec..e91c8d0 100644 --- a/CryptoExchange.Net/Converters/SystemTextJson/SerializationModel.cs +++ b/CryptoExchange.Net/Converters/SystemTextJson/SerializationModel.cs @@ -1,23 +1,20 @@ -using System; -using System.Collections.Generic; -using System.Text; +using System; -namespace CryptoExchange.Net.Converters.SystemTextJson +namespace CryptoExchange.Net.Converters.SystemTextJson; + +/// +/// Attribute to mark a model as json serializable. Used for AOT compilation. +/// +[AttributeUsage(System.AttributeTargets.Class | AttributeTargets.Enum | System.AttributeTargets.Interface)] +public class SerializationModelAttribute : Attribute { /// - /// Attribute to mark a model as json serializable. Used for AOT compilation. + /// ctor /// - [AttributeUsage(System.AttributeTargets.Class | AttributeTargets.Enum | System.AttributeTargets.Interface)] - public class SerializationModelAttribute : Attribute - { - /// - /// ctor - /// - public SerializationModelAttribute() { } - /// - /// ctor - /// - /// - public SerializationModelAttribute(Type type) { } - } + public SerializationModelAttribute() { } + /// + /// ctor + /// + /// + public SerializationModelAttribute(Type type) { } } diff --git a/CryptoExchange.Net/Converters/SystemTextJson/SerializerOptions.cs b/CryptoExchange.Net/Converters/SystemTextJson/SerializerOptions.cs index 39ef682..fb4e9a6 100644 --- a/CryptoExchange.Net/Converters/SystemTextJson/SerializerOptions.cs +++ b/CryptoExchange.Net/Converters/SystemTextJson/SerializerOptions.cs @@ -1,47 +1,46 @@ -using System.Collections.Concurrent; +using System.Collections.Concurrent; using System.Text.Json; using System.Text.Json.Serialization; -namespace CryptoExchange.Net.Converters.SystemTextJson +namespace CryptoExchange.Net.Converters.SystemTextJson; + +/// +/// Serializer options +/// +public static class SerializerOptions { + private static readonly ConcurrentDictionary _cache = new ConcurrentDictionary(); + /// - /// Serializer options + /// Get Json serializer settings which includes standard converters for DateTime, bool, enum and number types /// - public static class SerializerOptions + public static JsonSerializerOptions WithConverters(JsonSerializerContext typeResolver, params JsonConverter[] additionalConverters) { - private static readonly ConcurrentDictionary _cache = new ConcurrentDictionary(); - - /// - /// Get Json serializer settings which includes standard converters for DateTime, bool, enum and number types - /// - public static JsonSerializerOptions WithConverters(JsonSerializerContext typeResolver, params JsonConverter[] additionalConverters) + if (!_cache.TryGetValue(typeResolver, out var options)) { - if (!_cache.TryGetValue(typeResolver, out var options)) + options = new JsonSerializerOptions { - options = new JsonSerializerOptions + NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals, + PropertyNameCaseInsensitive = false, + Converters = { - NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals, - PropertyNameCaseInsensitive = false, - Converters = - { - new DateTimeConverter(), - new BoolConverter(), - new DecimalConverter(), - new IntConverter(), - new LongConverter(), - new NullableEnumConverterFactory(typeResolver) - }, - TypeInfoResolver = typeResolver, - }; + new DateTimeConverter(), + new BoolConverter(), + new DecimalConverter(), + new IntConverter(), + new LongConverter(), + new NullableEnumConverterFactory(typeResolver) + }, + TypeInfoResolver = typeResolver, + }; - foreach (var converter in additionalConverters) - options.Converters.Add(converter); + foreach (var converter in additionalConverters) + options.Converters.Add(converter); - options.TypeInfoResolver = typeResolver; - _cache.TryAdd(typeResolver, options); - } - - return options; + options.TypeInfoResolver = typeResolver; + _cache.TryAdd(typeResolver, options); } + + return options; } } diff --git a/CryptoExchange.Net/Converters/SystemTextJson/SharedQuantityConverter.cs b/CryptoExchange.Net/Converters/SystemTextJson/SharedQuantityConverter.cs index bedfebe..968eae8 100644 --- a/CryptoExchange.Net/Converters/SystemTextJson/SharedQuantityConverter.cs +++ b/CryptoExchange.Net/Converters/SystemTextJson/SharedQuantityConverter.cs @@ -1,60 +1,57 @@ -using CryptoExchange.Net.SharedApis; +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 +namespace CryptoExchange.Net.Converters.SystemTextJson; + +internal class SharedQuantityConverter : SharedQuantityReferenceConverter { } +internal class SharedOrderQuantityConverter : SharedQuantityReferenceConverter { } + +internal class SharedQuantityReferenceConverter : JsonConverter where T: SharedQuantityReference, new() { - internal class SharedQuantityConverter : SharedQuantityReferenceConverter { } - internal class SharedOrderQuantityConverter : SharedQuantityReferenceConverter { } - - internal class SharedQuantityReferenceConverter : JsonConverter where T: SharedQuantityReference, new() + public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - if (reader.TokenType != JsonTokenType.StartArray) - throw new Exception(""); + 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(); + 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(""); + if (reader.TokenType != JsonTokenType.EndArray) + throw new Exception(""); - reader.Read(); // End array + reader.Read(); // End array - var result = new T(); - result.QuantityInBaseAsset = baseQuantity; - result.QuantityInQuoteAsset = quoteQuantity; - result.QuantityInContracts = contractQuantity; - return result; - } + 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); + 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.QuantityInQuoteAsset == null) + writer.WriteNullValue(); + else + writer.WriteNumberValue(value.QuantityInQuoteAsset.Value); - if (value.QuantityInContracts == null) - writer.WriteNullValue(); - else - writer.WriteNumberValue(value.QuantityInContracts.Value); - writer.WriteEndArray(); - } + if (value.QuantityInContracts == null) + writer.WriteNullValue(); + else + writer.WriteNumberValue(value.QuantityInContracts.Value); + writer.WriteEndArray(); } } diff --git a/CryptoExchange.Net/Converters/SystemTextJson/SharedSymbolConverter.cs b/CryptoExchange.Net/Converters/SystemTextJson/SharedSymbolConverter.cs index 6622305..7266907 100644 --- a/CryptoExchange.Net/Converters/SystemTextJson/SharedSymbolConverter.cs +++ b/CryptoExchange.Net/Converters/SystemTextJson/SharedSymbolConverter.cs @@ -1,46 +1,43 @@ -using CryptoExchange.Net.SharedApis; +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 +namespace CryptoExchange.Net.Converters.SystemTextJson; + +internal class SharedSymbolConverter : JsonConverter { - internal class SharedSymbolConverter : JsonConverter + public override SharedSymbol? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - public override SharedSymbol? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - if (reader.TokenType != JsonTokenType.StartArray) - throw new Exception(""); + 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(); + 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(""); + if (reader.TokenType != JsonTokenType.EndArray) + throw new Exception(""); - reader.Read(); // End array + reader.Read(); // End array - return new SharedSymbol(tradingMode, baseAsset, quoteAsset, deliverTime); - } + 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(); - } + 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(); } } diff --git a/CryptoExchange.Net/Converters/SystemTextJson/SystemTextJsonMessageAccessor.cs b/CryptoExchange.Net/Converters/SystemTextJson/SystemTextJsonMessageAccessor.cs index c464f23..7abdab2 100644 --- a/CryptoExchange.Net/Converters/SystemTextJson/SystemTextJsonMessageAccessor.cs +++ b/CryptoExchange.Net/Converters/SystemTextJson/SystemTextJsonMessageAccessor.cs @@ -1,374 +1,376 @@ -using CryptoExchange.Net.Converters.MessageParsing; +using CryptoExchange.Net.Converters.MessageParsing; using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Objects; using System; -using System.Collections.Generic; +#if NET5_0_OR_GREATER using System.Diagnostics.CodeAnalysis; +#endif using System.IO; using System.Text; using System.Text.Json; using System.Threading.Tasks; -namespace CryptoExchange.Net.Converters.SystemTextJson +namespace CryptoExchange.Net.Converters.SystemTextJson; + +/// +/// System.Text.Json message accessor +/// +public abstract class SystemTextJsonMessageAccessor : IMessageAccessor { /// - /// System.Text.Json message accessor + /// The JsonDocument loaded /// - public abstract class SystemTextJsonMessageAccessor : IMessageAccessor - { - /// - /// The JsonDocument loaded - /// - protected JsonDocument? _document; + protected JsonDocument? _document; - private readonly JsonSerializerOptions? _customSerializerOptions; + private readonly JsonSerializerOptions? _customSerializerOptions; - /// - public bool IsValid { get; set; } + /// + public bool IsValid { get; set; } - /// - public abstract bool OriginalDataAvailable { get; } + /// + public abstract bool OriginalDataAvailable { get; } - /// - public object? Underlying => throw new NotImplementedException(); - - /// - /// ctor - /// - public SystemTextJsonMessageAccessor(JsonSerializerOptions options) - { - _customSerializerOptions = options; - } - - /// -#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 Deserialize(Type type, MessagePath? path = null) - { - if (!IsValid) - return new CallResult(GetOriginalString()); - - if (_document == null) - throw new InvalidOperationException("No json document loaded"); - - try - { - var result = _document.Deserialize(type, _customSerializerOptions); - return new CallResult(result!); - } - catch (JsonException ex) - { - var info = $"Json deserialization failed: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}"; - return new CallResult(new DeserializeError(info, ex)); - } - catch (Exception ex) - { - return new CallResult(new DeserializeError($"Json deserialization failed: {ex.Message}", ex)); - } - } - - /// -#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 Deserialize(MessagePath? path = null) - { - if (_document == null) - throw new InvalidOperationException("No json document loaded"); - - try - { - var result = _document.Deserialize(_customSerializerOptions); - return new CallResult(result!); - } - catch (JsonException ex) - { - var info = $"Json deserialization failed: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}"; - return new CallResult(new DeserializeError(info, ex)); - } - catch (Exception ex) - { - return new CallResult(new DeserializeError($"Json deserialization failed: {ex.Message}", ex)); - } - } - - /// - public NodeType? GetNodeType() - { - if (!IsValid) - throw new InvalidOperationException("Can't access json data on non-json message"); - - if (_document == null) - throw new InvalidOperationException("No json document loaded"); - - return _document.RootElement.ValueKind switch - { - JsonValueKind.Object => NodeType.Object, - JsonValueKind.Array => NodeType.Array, - _ => NodeType.Value - }; - } - - /// - public NodeType? GetNodeType(MessagePath path) - { - if (!IsValid) - throw new InvalidOperationException("Can't access json data on non-json message"); - - var node = GetPathNode(path); - if (!node.HasValue) - return null; - - return node.Value.ValueKind switch - { - JsonValueKind.Object => NodeType.Object, - JsonValueKind.Array => NodeType.Array, - _ => NodeType.Value - }; - } - - /// -#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(MessagePath path) - { - if (!IsValid) - throw new InvalidOperationException("Can't access json data on non-json message"); - - var value = GetPathNode(path); - if (value == null) - return default; - - if (value.Value.ValueKind == JsonValueKind.Object || value.Value.ValueKind == JsonValueKind.Array) - { - try - { - return value.Value.Deserialize(_customSerializerOptions); - } - catch { } - - return default; - } - - if (typeof(T) == typeof(string)) - { - if (value.Value.ValueKind == JsonValueKind.Number) - return (T)(object)value.Value.GetInt64().ToString(); - } - - return value.Value.Deserialize(_customSerializerOptions); - } - - /// -#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(MessagePath path) - { - if (!IsValid) - throw new InvalidOperationException("Can't access json data on non-json message"); - - var value = GetPathNode(path); - if (value == null) - return default; - - if (value.Value.ValueKind != JsonValueKind.Array) - return default; - - return value.Value.Deserialize(_customSerializerOptions)!; - } - - private JsonElement? GetPathNode(MessagePath path) - { - if (!IsValid) - throw new InvalidOperationException("Can't access json data on non-json message"); - - if (_document == null) - throw new InvalidOperationException("No json document loaded"); - - JsonElement? currentToken = _document.RootElement; - foreach (var node in path) - { - if (node.Type == 0) - { - // Int value - var val = node.Index!.Value; - if (currentToken!.Value.ValueKind != JsonValueKind.Array || currentToken.Value.GetArrayLength() <= val) - return null; - - currentToken = currentToken.Value[val]; - } - else if (node.Type == 1) - { - // String value - if (currentToken!.Value.ValueKind != JsonValueKind.Object) - return null; - - if (!currentToken.Value.TryGetProperty(node.Property!, out var token)) - return null; - currentToken = token; - } - else - { - // Property name - if (currentToken!.Value.ValueKind != JsonValueKind.Object) - return null; - - throw new NotImplementedException(); - } - - if (currentToken == null) - return null; - } - - return currentToken; - } - - /// - public abstract string GetOriginalString(); - - /// - public abstract void Clear(); - } + /// + public object? Underlying => throw new NotImplementedException(); /// - /// System.Text.Json stream message accessor + /// ctor /// - public class SystemTextJsonStreamMessageAccessor : SystemTextJsonMessageAccessor, IStreamMessageAccessor + public SystemTextJsonMessageAccessor(JsonSerializerOptions options) { - private Stream? _stream; + _customSerializerOptions = options; + } - /// - public override bool OriginalDataAvailable => _stream?.CanSeek == true; + /// +#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 Deserialize(Type type, MessagePath? path = null) + { + if (!IsValid) + return new CallResult(GetOriginalString()); - /// - /// ctor - /// - public SystemTextJsonStreamMessageAccessor(JsonSerializerOptions options): base(options) + if (_document == null) + throw new InvalidOperationException("No json document loaded"); + + try { + var result = _document.Deserialize(type, _customSerializerOptions); + return new CallResult(result!); + } + catch (JsonException ex) + { + var info = $"Json deserialization failed: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}"; + return new CallResult(new DeserializeError(info, ex)); + } + catch (Exception ex) + { + return new CallResult(new DeserializeError($"Json deserialization failed: {ex.Message}", ex)); + } + } + + /// +#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 Deserialize(MessagePath? path = null) + { + if (_document == null) + throw new InvalidOperationException("No json document loaded"); + + try + { + var result = _document.Deserialize(_customSerializerOptions); + return new CallResult(result!); + } + catch (JsonException ex) + { + var info = $"Json deserialization failed: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}"; + return new CallResult(new DeserializeError(info, ex)); + } + catch (Exception ex) + { + return new CallResult(new DeserializeError($"Json deserialization failed: {ex.Message}", ex)); + } + } + + /// + public NodeType? GetNodeType() + { + if (!IsValid) + throw new InvalidOperationException("Can't access json data on non-json message"); + + if (_document == null) + throw new InvalidOperationException("No json document loaded"); + + return _document.RootElement.ValueKind switch + { + JsonValueKind.Object => NodeType.Object, + JsonValueKind.Array => NodeType.Array, + _ => NodeType.Value + }; + } + + /// + public NodeType? GetNodeType(MessagePath path) + { + if (!IsValid) + throw new InvalidOperationException("Can't access json data on non-json message"); + + var node = GetPathNode(path); + if (!node.HasValue) + return null; + + return node.Value.ValueKind switch + { + JsonValueKind.Object => NodeType.Object, + JsonValueKind.Array => NodeType.Array, + _ => NodeType.Value + }; + } + + /// +#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(MessagePath path) + { + if (!IsValid) + throw new InvalidOperationException("Can't access json data on non-json message"); + + var value = GetPathNode(path); + if (value == null) + return default; + + if (value.Value.ValueKind == JsonValueKind.Object || value.Value.ValueKind == JsonValueKind.Array) + { + try + { + return value.Value.Deserialize(_customSerializerOptions); + } + catch { } + + return default; } - /// - public async Task Read(Stream stream, bool bufferStream) + if (typeof(T) == typeof(string)) { - if (bufferStream && stream is not MemoryStream) + if (value.Value.ValueKind == JsonValueKind.Number) + return (T)(object)value.Value.GetInt64().ToString(); + } + + return value.Value.Deserialize(_customSerializerOptions); + } + + /// +#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(MessagePath path) + { + if (!IsValid) + throw new InvalidOperationException("Can't access json data on non-json message"); + + var value = GetPathNode(path); + if (value == null) + return default; + + if (value.Value.ValueKind != JsonValueKind.Array) + return default; + + return value.Value.Deserialize(_customSerializerOptions)!; + } + + private JsonElement? GetPathNode(MessagePath path) + { + if (!IsValid) + throw new InvalidOperationException("Can't access json data on non-json message"); + + if (_document == null) + throw new InvalidOperationException("No json document loaded"); + + JsonElement? currentToken = _document.RootElement; + foreach (var node in path) + { + if (node.Type == 0) { - // 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; + // Int value + var val = node.Index!.Value; + if (currentToken!.Value.ValueKind != JsonValueKind.Array || currentToken.Value.GetArrayLength() <= val) + return null; + + currentToken = currentToken.Value[val]; } - else if (bufferStream) + else if (node.Type == 1) { - // We need to buffer the stream, and the current stream is seekable, store as is - _stream = stream; + // String value + if (currentToken!.Value.ValueKind != JsonValueKind.Object) + return null; + + if (!currentToken.Value.TryGetProperty(node.Property!, out var token)) + return null; + currentToken = token; } else { - // We don't need to buffer the stream, so don't bother keeping the reference + // Property name + if (currentToken!.Value.ValueKind != JsonValueKind.Object) + return null; + + throw new NotImplementedException(); } - try - { - _document = await JsonDocument.ParseAsync(_stream ?? stream).ConfigureAwait(false); - IsValid = true; - return CallResult.SuccessResult; - } - catch (Exception ex) - { - // Not a json message - IsValid = false; - return new CallResult(new DeserializeError($"Json deserialization failed: {ex.Message}", ex)); - } - } - - /// - 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(); - } - - /// - public override void Clear() - { - _stream?.Dispose(); - _stream = null; - _document?.Dispose(); - _document = null; + if (currentToken == null) + return null; } + return currentToken; } + /// + public abstract string GetOriginalString(); + + /// + public abstract void Clear(); +} + +/// +/// System.Text.Json stream message accessor +/// +#pragma warning disable CA1001 // Types that own disposable fields should be disposable +public class SystemTextJsonStreamMessageAccessor : SystemTextJsonMessageAccessor, IStreamMessageAccessor +#pragma warning restore CA1001 // Types that own disposable fields should be disposable +{ + private Stream? _stream; + + /// + public override bool OriginalDataAvailable => _stream?.CanSeek == true; + /// - /// System.Text.Json byte message accessor + /// ctor /// - public class SystemTextJsonByteMessageAccessor : SystemTextJsonMessageAccessor, IByteMessageAccessor + public SystemTextJsonStreamMessageAccessor(JsonSerializerOptions options): base(options) { - private ReadOnlyMemory _bytes; + } - /// - /// ctor - /// - public SystemTextJsonByteMessageAccessor(JsonSerializerOptions options) : base(options) + /// + public async Task 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 } - /// - public CallResult Read(ReadOnlyMemory data) + try { - _bytes = data; - - try - { - var firstByte = data.Span[0]; - if (firstByte != 0x7b && firstByte != 0x5b) - { - // Value doesn't start with `{` or `[`, prevent deserialization attempt as it's slow - IsValid = false; - return new CallResult(new DeserializeError("Not a json value")); - } - - _document = JsonDocument.Parse(data); - IsValid = true; - return CallResult.SuccessResult; - } - catch (Exception ex) - { - // Not a json message - IsValid = false; - return new CallResult(new DeserializeError($"Json deserialization failed: {ex.Message}", ex)); - } + _document = await JsonDocument.ParseAsync(_stream ?? stream).ConfigureAwait(false); + IsValid = true; + return CallResult.SuccessResult; } - - /// - public override string GetOriginalString() => - // NetStandard 2.0 doesn't support GetString from a ReadonlySpan, so use ToArray there instead -#if NETSTANDARD2_0 - Encoding.UTF8.GetString(_bytes.ToArray()); -#else - Encoding.UTF8.GetString(_bytes.Span); -#endif - - /// - public override bool OriginalDataAvailable => true; - - /// - public override void Clear() + catch (Exception ex) { - _bytes = null; - _document?.Dispose(); - _document = null; + // Not a json message + IsValid = false; + return new CallResult(new DeserializeError($"Json deserialization failed: {ex.Message}", ex)); } } -} \ No newline at end of file + + /// + 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(); + } + + /// + public override void Clear() + { + _stream?.Dispose(); + _stream = null; + _document?.Dispose(); + _document = null; + } + +} + +/// +/// System.Text.Json byte message accessor +/// +public class SystemTextJsonByteMessageAccessor : SystemTextJsonMessageAccessor, IByteMessageAccessor +{ + private ReadOnlyMemory _bytes; + + /// + /// ctor + /// + public SystemTextJsonByteMessageAccessor(JsonSerializerOptions options) : base(options) + { + } + + /// + public CallResult Read(ReadOnlyMemory data) + { + _bytes = data; + + try + { + var firstByte = data.Span[0]; + if (firstByte != 0x7b && firstByte != 0x5b) + { + // Value doesn't start with `{` or `[`, prevent deserialization attempt as it's slow + IsValid = false; + return new CallResult(new DeserializeError("Not a json value")); + } + + _document = JsonDocument.Parse(data); + IsValid = true; + return CallResult.SuccessResult; + } + catch (Exception ex) + { + // Not a json message + IsValid = false; + return new CallResult(new DeserializeError($"Json deserialization failed: {ex.Message}", ex)); + } + } + + /// + public override string GetOriginalString() => + // NetStandard 2.0 doesn't support GetString from a ReadonlySpan, so use ToArray there instead +#if NETSTANDARD2_0 + Encoding.UTF8.GetString(_bytes.ToArray()); +#else + Encoding.UTF8.GetString(_bytes.Span); +#endif + + /// + public override bool OriginalDataAvailable => true; + + /// + public override void Clear() + { + _bytes = null; + _document?.Dispose(); + _document = null; + } +} diff --git a/CryptoExchange.Net/Converters/SystemTextJson/SystemTextJsonMessageSerializer.cs b/CryptoExchange.Net/Converters/SystemTextJson/SystemTextJsonMessageSerializer.cs index f3cf340..7ed1195 100644 --- a/CryptoExchange.Net/Converters/SystemTextJson/SystemTextJsonMessageSerializer.cs +++ b/CryptoExchange.Net/Converters/SystemTextJson/SystemTextJsonMessageSerializer.cs @@ -1,29 +1,28 @@ -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 -{ - /// - public class SystemTextJsonMessageSerializer : IStringMessageSerializer - { - private readonly JsonSerializerOptions _options; - - /// - /// ctor - /// - public SystemTextJsonMessageSerializer(JsonSerializerOptions options) - { - _options = options; - } - - /// +using CryptoExchange.Net.Interfaces; #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")] +using System.Diagnostics.CodeAnalysis; #endif - public string Serialize(T message) => JsonSerializer.Serialize(message, _options); +using System.Text.Json; + +namespace CryptoExchange.Net.Converters.SystemTextJson; + +/// +public class SystemTextJsonMessageSerializer : IStringMessageSerializer +{ + private readonly JsonSerializerOptions _options; + + /// + /// ctor + /// + public SystemTextJsonMessageSerializer(JsonSerializerOptions options) + { + _options = options; } + + /// +#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 message) => JsonSerializer.Serialize(message, _options); } diff --git a/CryptoExchange.Net/CryptoExchange.Net.csproj b/CryptoExchange.Net/CryptoExchange.Net.csproj index d1404e4..50a370f 100644 --- a/CryptoExchange.Net/CryptoExchange.Net.csproj +++ b/CryptoExchange.Net/CryptoExchange.Net.csproj @@ -24,6 +24,7 @@ MIT + @@ -40,6 +41,12 @@ CryptoExchange.Net.xml + + true + Recommended + None + true + all @@ -58,4 +65,7 @@ + + + \ No newline at end of file diff --git a/CryptoExchange.Net/ExchangeHelpers.cs b/CryptoExchange.Net/ExchangeHelpers.cs index 668821a..d4c24c4 100644 --- a/CryptoExchange.Net/ExchangeHelpers.cs +++ b/CryptoExchange.Net/ExchangeHelpers.cs @@ -1,389 +1,390 @@ -using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects; using CryptoExchange.Net.SharedApis; using System; using System.Collections.Generic; using System.Globalization; using System.Runtime.CompilerServices; +#if NETSTANDARD2_1_OR_GREATER || NET9_0_OR_GREATER using System.Security.Cryptography; +#endif using System.Threading; using System.Threading.Tasks; -namespace CryptoExchange.Net +namespace CryptoExchange.Net; + +/// +/// General helpers functions +/// +public static class ExchangeHelpers { - /// - /// General helpers functions - /// - public static class ExchangeHelpers + private const string _allowedRandomChars = "ABCDEFGHIJKLMONOPQRSTUVWXYZabcdefghijklmonopqrstuvwxyz0123456789"; + private const string _allowedRandomHexChars = "0123456789ABCDEF"; + + private static readonly Dictionary _monthSymbols = new Dictionary() { - private const string _allowedRandomChars = "ABCDEFGHIJKLMONOPQRSTUVWXYZabcdefghijklmonopqrstuvwxyz0123456789"; - private const string _allowedRandomHexChars = "0123456789ABCDEF"; + { 1, "F" }, + { 2, "G" }, + { 3, "H" }, + { 4, "J" }, + { 5, "K" }, + { 6, "M" }, + { 7, "N" }, + { 8, "Q" }, + { 9, "U" }, + { 10, "V" }, + { 11, "X" }, + { 12, "Z" }, + }; - private static readonly Dictionary _monthSymbols = new Dictionary() - { - { 1, "F" }, - { 2, "G" }, - { 3, "H" }, - { 4, "J" }, - { 5, "K" }, - { 6, "M" }, - { 7, "N" }, - { 8, "Q" }, - { 9, "U" }, - { 10, "V" }, - { 11, "X" }, - { 12, "Z" }, - }; + /// + /// The last used id, use NextId() to get the next id and up this + /// + private static int _lastId; - /// - /// The last used id, use NextId() to get the next id and up this - /// - private static int _lastId; + /// + /// Clamp a value between a min and max + /// + /// + /// + /// + /// + public static decimal ClampValue(decimal min, decimal max, decimal value) + { + value = Math.Min(max, value); + value = Math.Max(min, value); + return value; + } - /// - /// Clamp a value between a min and max - /// - /// - /// - /// - /// - public static decimal ClampValue(decimal min, decimal max, decimal value) - { - value = Math.Min(max, value); - value = Math.Max(min, value); + /// + /// Adjust a value to be between the min and max parameters and rounded to the closest step. + /// + /// The min value + /// The max value + /// The step size the value should be floored to. For example, value 2.548 with a step size of 0.01 will output 2.54 + /// How to round + /// The input value + /// + public static decimal AdjustValueStep(decimal min, decimal max, decimal? step, RoundingType roundingType, decimal value) + { + if(step == 0) + throw new ArgumentException($"0 not allowed for parameter {nameof(step)}, pass in null to ignore the step size", nameof(step)); + + value = Math.Min(max, value); + value = Math.Max(min, value); + if (step == null) return value; - } - /// - /// Adjust a value to be between the min and max parameters and rounded to the closest step. - /// - /// The min value - /// The max value - /// The step size the value should be floored to. For example, value 2.548 with a step size of 0.01 will output 2.54 - /// How to round - /// The input value - /// - public static decimal AdjustValueStep(decimal min, decimal max, decimal? step, RoundingType roundingType, decimal value) + var offset = value % step.Value; + if(roundingType == RoundingType.Down) { - if(step == 0) - throw new ArgumentException($"0 not allowed for parameter {nameof(step)}, pass in null to ignore the step size", nameof(step)); - - value = Math.Min(max, value); - value = Math.Max(min, value); - if (step == null) - return value; - - var offset = value % step.Value; - if(roundingType == RoundingType.Down) - { + value -= offset; + } + else if(roundingType == RoundingType.Up) + { + if (offset != 0) + value += (step.Value - offset); + } + else + { + if (offset < step / 2) value -= offset; - } - else if(roundingType == RoundingType.Up) - { - if (offset != 0) - value += (step.Value - offset); - } - else - { - if (offset < step / 2) - value -= offset; - else value += (step.Value - offset); - } - - value = RoundDown(value, 8); - - return value.Normalize(); + else value += (step.Value - offset); } - /// - /// Adjust a value to be between the min and max parameters and rounded to the closest precision. - /// - /// The min value - /// The max value - /// The precision the value should be rounded to. For example, value 2.554215 with a precision of 5 will output 2.5542 - /// How to round - /// The input value - /// - public static decimal AdjustValuePrecision(decimal min, decimal max, int? precision, RoundingType roundingType, decimal value) - { - value = Math.Min(max, value); - value = Math.Max(min, value); - if (precision == null) - return value; - - return RoundToSignificantDigits(value, precision.Value, roundingType); - } - - /// - /// Apply the provided rules to the value - /// - /// Value to be adjusted - /// Max decimal places - /// The value step for increase/decrease value - /// - 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); + value = RoundDown(value, 8); + + return value.Normalize(); + } + /// + /// Adjust a value to be between the min and max parameters and rounded to the closest precision. + /// + /// The min value + /// The max value + /// The precision the value should be rounded to. For example, value 2.554215 with a precision of 5 will output 2.5542 + /// How to round + /// The input value + /// + public static decimal AdjustValuePrecision(decimal min, decimal max, int? precision, RoundingType roundingType, decimal value) + { + value = Math.Min(max, value); + value = Math.Max(min, value); + if (precision == null) return value; - } - /// - /// Round a value to have the provided total number of digits. For example, value 253.12332 with 5 digits would be 253.12 - /// - /// The value to round - /// The total amount of digits (NOT decimal places) to round to - /// How to round - /// - public static decimal RoundToSignificantDigits(decimal value, int digits, RoundingType roundingType) + return RoundToSignificantDigits(value, precision.Value, roundingType); + } + + /// + /// Apply the provided rules to the value + /// + /// Value to be adjusted + /// Max decimal places + /// The value step for increase/decrease value + /// + public static decimal ApplyRules( + decimal value, + int? decimals = null, + decimal? valueStep = null) + { + if (valueStep.HasValue) { - var val = (double)value; - if (value == 0) - return 0; - - double scale = Math.Pow(10, Math.Floor(Math.Log10(Math.Abs(val))) + 1); - if(roundingType == RoundingType.Closest) - return (decimal)(scale * Math.Round(val / scale, digits)); - else - return (decimal)(scale * (double)RoundDown((decimal)(val / scale), digits)); + 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); - /// - /// Rounds a value down - /// - public static decimal RoundDown(decimal i, double decimalPlaces) - { - var power = Convert.ToDecimal(Math.Pow(10, decimalPlaces)); - return Math.Floor(i * power) / power; - } + return value; + } - /// - /// Rounds a value up - /// - public static decimal RoundUp(decimal i, double decimalPlaces) - { - var power = Convert.ToDecimal(Math.Pow(10, decimalPlaces)); - return Math.Ceiling(i * power) / power; - } + /// + /// Round a value to have the provided total number of digits. For example, value 253.12332 with 5 digits would be 253.12 + /// + /// The value to round + /// The total amount of digits (NOT decimal places) to round to + /// How to round + /// + public static decimal RoundToSignificantDigits(decimal value, int digits, RoundingType roundingType) + { + var val = (double)value; + if (value == 0) + return 0; - /// - /// Strips any trailing zero's of a decimal value, useful when converting the value to string. - /// - /// - /// - public static decimal Normalize(this decimal value) - { - return value / 1.000000000000000000000000000000000m; - } + double scale = Math.Pow(10, Math.Floor(Math.Log10(Math.Abs(val))) + 1); + if(roundingType == RoundingType.Closest) + return (decimal)(scale * Math.Round(val / scale, digits)); + else + return (decimal)(scale * (double)RoundDown((decimal)(val / scale), digits)); + } - /// - /// Generate a new unique id. The id is statically stored so it is guaranteed to be unique - /// - /// - public static int NextId() => Interlocked.Increment(ref _lastId); + /// + /// Rounds a value down + /// + public static decimal RoundDown(decimal i, double decimalPlaces) + { + var power = Convert.ToDecimal(Math.Pow(10, decimalPlaces)); + return Math.Floor(i * power) / power; + } - /// - /// Return the last unique id that was generated - /// - /// - public static int LastId() => _lastId; + /// + /// Rounds a value up + /// + public static decimal RoundUp(decimal i, double decimalPlaces) + { + var power = Convert.ToDecimal(Math.Pow(10, decimalPlaces)); + return Math.Ceiling(i * power) / power; + } - /// - /// Generate a random string of specified length - /// - /// Length of the random string - /// - public static string RandomString(int length) - { - var randomChars = new char[length]; + /// + /// Strips any trailing zero's of a decimal value, useful when converting the value to string. + /// + /// + /// + public static decimal Normalize(this decimal value) + { + return value / 1.000000000000000000000000000000000m; + } + + /// + /// Generate a new unique id. The id is statically stored so it is guaranteed to be unique + /// + /// + public static int NextId() => Interlocked.Increment(ref _lastId); + + /// + /// Return the last unique id that was generated + /// + /// + public static int LastId() => _lastId; + + /// + /// Generate a random string of specified length + /// + /// Length of the random string + /// + public static string RandomString(int length) + { + var randomChars = new char[length]; #if NETSTANDARD2_1_OR_GREATER || NET9_0_OR_GREATER - for (int i = 0; i < length; i++) - randomChars[i] = _allowedRandomChars[RandomNumberGenerator.GetInt32(0, _allowedRandomChars.Length)]; + for (int i = 0; i < length; i++) + randomChars[i] = _allowedRandomChars[RandomNumberGenerator.GetInt32(0, _allowedRandomChars.Length)]; #else - var random = new Random(); - for (int i = 0; i < length; i++) - randomChars[i] = _allowedRandomChars[random.Next(0, _allowedRandomChars.Length)]; + var random = new Random(); + for (int i = 0; i < length; i++) + randomChars[i] = _allowedRandomChars[random.Next(0, _allowedRandomChars.Length)]; #endif - return new string(randomChars); - } + return new string(randomChars); + } - /// - /// Generate a random string of specified length - /// - /// Length of the random string - /// - public static string RandomHexString(int length) - { + /// + /// Generate a random string of specified length + /// + /// Length of the random string + /// + public static string RandomHexString(int length) + { #if NET9_0_OR_GREATER - return "0x" + RandomNumberGenerator.GetHexString(length * 2); + 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); + 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 - } + } - /// - /// Generate a long value - /// - /// Max character length - /// - public static long RandomLong(int maxLength) - { + /// + /// Generate a long value + /// + /// Max character length + /// + public static long RandomLong(int maxLength) + { #if NETSTANDARD2_1_OR_GREATER || NET9_0_OR_GREATER - var value = RandomNumberGenerator.GetInt32(0, int.MaxValue); + var value = RandomNumberGenerator.GetInt32(0, int.MaxValue); #else - var random = new Random(); - var value = random.Next(0, int.MaxValue); + 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; - } + var val = value.ToString(); + if (val.Length > maxLength) + return int.Parse(val.Substring(0, maxLength)); + else + return value; + } - /// - /// Generate a random string of specified length - /// - /// The initial string - /// Total length of the resulting string - /// - public static string AppendRandomString(string source, int totalLength) + /// + /// Generate a random string of specified length + /// + /// The initial string + /// Total length of the resulting string + /// + public static string AppendRandomString(string source, int totalLength) + { + if (totalLength < source.Length) + throw new ArgumentException("Total length smaller than source string length", nameof(totalLength)); + + if (totalLength == source.Length) + return source; + + return source + RandomString(totalLength - source.Length); + } + + /// + /// Get the month representation for futures symbol based on the delivery month + /// + /// Delivery time + /// + public static string GetDeliveryMonthSymbol(DateTime time) => _monthSymbols[time.Month]; + + /// + /// Execute multiple requests to retrieve multiple pages of the result set + /// + /// Type of the client + /// Type of the request + /// The func to execute with each request + /// The request parameters + /// Cancellation token + /// + public static async IAsyncEnumerable> ExecutePages(Func>> paginatedFunc, TRequest request, [EnumeratorCancellation]CancellationToken ct = default) + { + var result = new List(); + ExchangeWebResult batch; + INextPageToken? nextPageToken = null; + while (true) { - if (totalLength < source.Length) - throw new ArgumentException("Total length smaller than source string length", nameof(totalLength)); + batch = await paginatedFunc(request, nextPageToken, ct).ConfigureAwait(false); + yield return batch; + if (!batch || ct.IsCancellationRequested) + break; - if (totalLength == source.Length) - return source; - - return source + RandomString(totalLength - source.Length); - } - - /// - /// Get the month representation for futures symbol based on the delivery month - /// - /// Delivery time - /// - public static string GetDeliveryMonthSymbol(DateTime time) => _monthSymbols[time.Month]; - - /// - /// Execute multiple requests to retrieve multiple pages of the result set - /// - /// Type of the client - /// Type of the request - /// The func to execute with each request - /// The request parameters - /// Cancellation token - /// - public static async IAsyncEnumerable> ExecutePages(Func>> paginatedFunc, U request, [EnumeratorCancellation]CancellationToken ct = default) - { - var result = new List(); - ExchangeWebResult 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; - } - } - - /// - /// Apply the rules (price and quantity step size and decimals precision, min/max quantity) from the symbol to the quantity and price - /// - /// The symbol as retrieved from the exchange - /// Quantity to trade - /// Price to trade at - /// Quantity adjusted to match all trading rules - /// Price adjusted to match all trading rules - 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; - - } - - /// - /// Parse a decimal value from a string - /// - public static decimal? ParseDecimal(string? value) - { - // Value is null or empty is the most common case to return null so check before trying to parse - if (string.IsNullOrEmpty(value)) - return null; - - // Try parse, only fails for these reasons: - // 1. string is null or empty - // 2. value is larger or smaller than decimal max/min - // 3. unparsable format - if (decimal.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var decValue)) - return decValue; - - // Check for values which should be parsed to null - if (string.Equals("null", value, StringComparison.OrdinalIgnoreCase) - || string.Equals("NaN", value, StringComparison.OrdinalIgnoreCase)) - { - return null; - } - - // Infinity value should be parsed to min/max value - if (string.Equals("Infinity", value, StringComparison.OrdinalIgnoreCase)) - return decimal.MaxValue; - else if(string.Equals("-Infinity", value, StringComparison.OrdinalIgnoreCase)) - return decimal.MinValue; - - if (value!.Length > 27 && decimal.TryParse(value.Substring(0, 27), out var overflowValue)) - { - // Not a valid decimal value and more than 27 chars, from which the first part can be parsed correctly. - // assume overflow - if (overflowValue < 0) - return decimal.MinValue; - else - return decimal.MaxValue; - } - - // Unknown decimal format, return null - return null; + result.AddRange(batch.Data); + nextPageToken = batch.NextPageToken; + if (nextPageToken == null) + break; } } + + /// + /// Apply the rules (price and quantity step size and decimals precision, min/max quantity) from the symbol to the quantity and price + /// + /// The symbol as retrieved from the exchange + /// Quantity to trade + /// Price to trade at + /// Quantity adjusted to match all trading rules + /// Price adjusted to match all trading rules + 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; + + } + + /// + /// Parse a decimal value from a string + /// + public static decimal? ParseDecimal(string? value) + { + // Value is null or empty is the most common case to return null so check before trying to parse + if (string.IsNullOrEmpty(value)) + return null; + + // Try parse, only fails for these reasons: + // 1. string is null or empty + // 2. value is larger or smaller than decimal max/min + // 3. unparsable format + if (decimal.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var decValue)) + return decValue; + + // Check for values which should be parsed to null + if (string.Equals("null", value, StringComparison.OrdinalIgnoreCase) + || string.Equals("NaN", value, StringComparison.OrdinalIgnoreCase)) + { + return null; + } + + // Infinity value should be parsed to min/max value + if (string.Equals("Infinity", value, StringComparison.OrdinalIgnoreCase)) + return decimal.MaxValue; + else if(string.Equals("-Infinity", value, StringComparison.OrdinalIgnoreCase)) + return decimal.MinValue; + + if (value!.Length > 27 && decimal.TryParse(value.Substring(0, 27), out var overflowValue)) + { + // Not a valid decimal value and more than 27 chars, from which the first part can be parsed correctly. + // assume overflow + if (overflowValue < 0) + return decimal.MinValue; + else + return decimal.MaxValue; + } + + // Unknown decimal format, return null + return null; + } } diff --git a/CryptoExchange.Net/ExchangeSymbolCache.cs b/CryptoExchange.Net/ExchangeSymbolCache.cs index 765b1e1..1df583e 100644 --- a/CryptoExchange.Net/ExchangeSymbolCache.cs +++ b/CryptoExchange.Net/ExchangeSymbolCache.cs @@ -1,70 +1,68 @@ -using CryptoExchange.Net.SharedApis; +using CryptoExchange.Net.SharedApis; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; -using System.Text; -namespace CryptoExchange.Net +namespace CryptoExchange.Net; + +/// +/// Cache for symbol parsing +/// +public static class ExchangeSymbolCache { + private static ConcurrentDictionary _symbolInfos = new ConcurrentDictionary(); + /// - /// Cache for symbol parsing + /// Update the cached symbol data for an exchange /// - public static class ExchangeSymbolCache + /// Id for the provided data + /// Symbol data + public static void UpdateSymbolInfo(string topicId, SharedSpotSymbol[] updateData) { - private static ConcurrentDictionary _symbolInfos = new ConcurrentDictionary(); - - /// - /// Update the cached symbol data for an exchange - /// - /// Id for the provided data - /// Symbol data - public static void UpdateSymbolInfo(string topicId, SharedSpotSymbol[] updateData) + if(!_symbolInfos.TryGetValue(topicId, out var exchangeInfo)) { - if(!_symbolInfos.TryGetValue(topicId, out var exchangeInfo)) - { - exchangeInfo = new ExchangeInfo(DateTime.UtcNow, updateData.ToDictionary(x => x.Name, x => x.SharedSymbol)); - _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 => x.SharedSymbol)); + exchangeInfo = new ExchangeInfo(DateTime.UtcNow, updateData.ToDictionary(x => x.Name, x => x.SharedSymbol)); + _symbolInfos.TryAdd(topicId, exchangeInfo); } - /// - /// Parse a symbol name to a SharedSymbol - /// - /// Id for the provided data - /// Symbol name - public static SharedSymbol? ParseSymbol(string topicId, string? symbolName) + if (DateTime.UtcNow - exchangeInfo.UpdateTime < TimeSpan.FromMinutes(60)) + return; + + _symbolInfos[topicId] = new ExchangeInfo(DateTime.UtcNow, updateData.ToDictionary(x => x.Name, x => x.SharedSymbol)); + } + + /// + /// Parse a symbol name to a SharedSymbol + /// + /// Id for the provided data + /// Symbol name + 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) { - if (symbolName == null) - return null; + DeliverTime = symbolInfo.DeliverTime + }; + } - if (!_symbolInfos.TryGetValue(topicId, out var exchangeInfo)) - return null; + class ExchangeInfo + { + public DateTime UpdateTime { get; set; } + public Dictionary Symbols { get; set; } - 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 ExchangeInfo(DateTime updateTime, Dictionary symbols) { - public DateTime UpdateTime { get; set; } - public Dictionary Symbols { get; set; } - - public ExchangeInfo(DateTime updateTime, Dictionary symbols) - { - UpdateTime = updateTime; - Symbols = symbols; - } + UpdateTime = updateTime; + Symbols = symbols; } } } diff --git a/CryptoExchange.Net/ExtensionMethods.cs b/CryptoExchange.Net/ExtensionMethods.cs index dbe6cf9..9be78c2 100644 --- a/CryptoExchange.Net/ExtensionMethods.cs +++ b/CryptoExchange.Net/ExtensionMethods.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO.Compression; using System.IO; @@ -10,515 +10,511 @@ 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 +namespace CryptoExchange.Net; + +/// +/// Helper methods +/// +public static class ExtensionMethods { /// - /// Helper methods + /// Add a parameter /// - public static class ExtensionMethods + /// + /// + /// + public static void AddParameter(this Dictionary parameters, string key, string value) { - /// - /// Add a parameter - /// - /// - /// - /// - public static void AddParameter(this Dictionary parameters, string key, string value) - { + parameters.Add(key, value); + } + + /// + /// Add a parameter + /// + /// + /// + /// + public static void AddParameter(this Dictionary parameters, string key, object value) + { + parameters.Add(key, value); + } + + /// + /// Add an optional parameter. Not added if value is null + /// + /// + /// + /// + public static void AddOptionalParameter(this Dictionary parameters, string key, object? value) + { + if (value != null) parameters.Add(key, value); - } + } - /// - /// Add a parameter - /// - /// - /// - /// - public static void AddParameter(this Dictionary parameters, string key, object value) + /// + /// Create a query string of the specified parameters + /// + /// The parameters to use + /// Whether or not the values should be url encoded + /// How to serialize array parameters + /// + public static string CreateParamString(this IDictionary parameters, bool urlEncodeValues, ArrayParametersSerialization serializationType) + { + var uriString = string.Empty; + var arraysParameters = parameters.Where(p => p.Value.GetType().IsArray).ToList(); + foreach (var arrayEntry in arraysParameters) { - parameters.Add(key, value); - } - - /// - /// Add an optional parameter. Not added if value is null - /// - /// - /// - /// - public static void AddOptionalParameter(this Dictionary parameters, string key, object? value) - { - if (value != null) - parameters.Add(key, value); - } - - /// - /// Create a query string of the specified parameters - /// - /// The parameters to use - /// Whether or not the values should be url encoded - /// How to serialize array parameters - /// - public static string CreateParamString(this IDictionary parameters, bool urlEncodeValues, ArrayParametersSerialization serializationType) - { - var uriString = string.Empty; - var arraysParameters = parameters.Where(p => p.Value.GetType().IsArray).ToList(); - foreach (var arrayEntry in arraysParameters) + if (serializationType == ArrayParametersSerialization.Array) { - 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)}"))}&"; + } + else if (serializationType == ArrayParametersSerialization.MultipleValues) + { + var array = (Array)arrayEntry.Value; + uriString += string.Join("&", array.OfType().Select(a => $"{arrayEntry.Key}={Uri.EscapeDataString(string.Format(CultureInfo.InvariantCulture, "{0}", a))}")); + uriString += "&"; + } + else + { + var array = (Array)arrayEntry.Value; + uriString += $"{arrayEntry.Key}=[{string.Join(",", array.OfType().Select(a => string.Format(CultureInfo.InvariantCulture, "{0}", a)))}]&"; + } + } + + uriString += $"{string.Join("&", parameters.Where(p => !p.Value.GetType().IsArray).Select(s => $"{s.Key}={(urlEncodeValues ? Uri.EscapeDataString(string.Format(CultureInfo.InvariantCulture, "{0}", s.Value)) : string.Format(CultureInfo.InvariantCulture, "{0}", s.Value))}"))}"; + uriString = uriString.TrimEnd('&'); + return uriString; + } + + /// + /// Convert a dictionary to formdata string + /// + /// + /// + public static string ToFormData(this IDictionary parameters) + { + var formData = HttpUtility.ParseQueryString(string.Empty); + foreach (var kvp in parameters) + { + if (kvp.Value is null) + continue; + + if (kvp.Value.GetType().IsArray) + { + var array = (Array)kvp.Value; + foreach (var value in array) + formData.Add(kvp.Key, string.Format(CultureInfo.InvariantCulture, "{0}", value)); + } + else + { + formData.Add(kvp.Key, string.Format(CultureInfo.InvariantCulture, "{0}", kvp.Value)); + } + } + + return formData.ToString()!; + } + + /// + /// Validates an int is one of the allowed values + /// + /// Value of the int + /// Name of the parameter + /// Allowed values + public static void ValidateIntValues(this int value, string argumentName, params int[] allowedValues) + { + if (!allowedValues.Contains(value)) + { + throw new ArgumentException( + $"{value} not allowed for parameter {argumentName}, allowed values: {string.Join(", ", allowedValues)}", argumentName); + } + } + + /// + /// Validates an int is between two values + /// + /// The value of the int + /// Name of the parameter + /// Min value + /// Max value + public static void ValidateIntBetween(this int value, string argumentName, int minValue, int maxValue) + { + if (value < minValue || value > maxValue) + { + throw new ArgumentException( + $"{value} not allowed for parameter {argumentName}, min: {minValue}, max: {maxValue}", argumentName); + } + } + + /// + /// Validates a string is not null or empty + /// + /// The value of the string + /// Name of the parameter + public static void ValidateNotNull(this string value, string argumentName) + { + if (string.IsNullOrEmpty(value)) + throw new ArgumentException($"No value provided for parameter {argumentName}", argumentName); + } + + /// + /// Validates a string is null or not empty + /// + /// + /// + public static void ValidateNullOrNotEmpty(this string value, string argumentName) + { + if (value != null && string.IsNullOrEmpty(value)) + throw new ArgumentException($"No value provided for parameter {argumentName}", argumentName); + } + + /// + /// Validates an object is not null + /// + /// The value of the object + /// Name of the parameter + public static void ValidateNotNull(this object value, string argumentName) + { + if (value == null) + throw new ArgumentException($"No value provided for parameter {argumentName}", argumentName); + } + + /// + /// Validates a list is not null or empty + /// + /// The value of the object + /// Name of the parameter + public static void ValidateNotNull(this IEnumerable value, string argumentName) + { + if (value == null || !value.Any()) + throw new ArgumentException($"No values provided for parameter {argumentName}", argumentName); + } + + /// + /// Format a string to RFC3339/ISO8601 string + /// + /// + /// + public static string ToRfc3339String(this DateTime dateTime) + { + return dateTime.ToString("yyyy-MM-dd'T'HH:mm:ss.fffzzz", DateTimeFormatInfo.InvariantInfo); + } + + /// + /// Format an exception and inner exception to a readable string + /// + /// + /// + public static string ToLogString(this Exception? exception) + { + var message = new StringBuilder(); + var indent = 0; + while (exception != null) + { + for (var i = 0; i < indent; i++) + message.Append(' '); + message.Append(exception.GetType().Name); + message.Append(" - "); + message.AppendLine(exception.Message); + for (var i = 0; i < indent; i++) + message.Append(' '); + message.AppendLine(exception.StackTrace); + + indent += 2; + exception = exception.InnerException; + } + + return message.ToString(); + } + + /// + /// Append a base url with provided path + /// + /// + /// + /// + public static string AppendPath(this string url, params string[] path) + { + if (!url.EndsWith("/")) + url += "/"; + + foreach (var item in path) + url += item.Trim('/') + "/"; + + return url.TrimEnd('/'); + } + + /// + /// Create a new uri with the provided parameters as query + /// + /// + /// + /// + /// + public static Uri SetParameters(this Uri baseUri, IDictionary parameters, ArrayParametersSerialization arraySerialization) + { + var uriBuilder = new UriBuilder(); + uriBuilder.Scheme = baseUri.Scheme; + uriBuilder.Host = baseUri.Host; + uriBuilder.Port = baseUri.Port; + uriBuilder.Path = baseUri.AbsolutePath; + var httpValueCollection = HttpUtility.ParseQueryString(string.Empty); + foreach (var parameter in parameters) + { + if (parameter.Value.GetType().IsArray) + { + if (arraySerialization == ArrayParametersSerialization.JsonArray) { - 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) - { - var array = (Array)arrayEntry.Value; - uriString += string.Join("&", array.OfType().Select(a => $"{arrayEntry.Key}={Uri.EscapeDataString(string.Format(CultureInfo.InvariantCulture, "{0}", a))}")); - uriString += "&"; + httpValueCollection.Add(parameter.Key, $"[{string.Join(",", (object[])parameter.Value)}]"); } else { - var array = (Array)arrayEntry.Value; - uriString += $"{arrayEntry.Key}=[{string.Join(",", array.OfType().Select(a => string.Format(CultureInfo.InvariantCulture, "{0}", a)))}]&"; - } - } - - uriString += $"{string.Join("&", parameters.Where(p => !p.Value.GetType().IsArray).Select(s => $"{s.Key}={(urlEncodeValues ? Uri.EscapeDataString(string.Format(CultureInfo.InvariantCulture, "{0}", s.Value)) : string.Format(CultureInfo.InvariantCulture, "{0}", s.Value))}"))}"; - uriString = uriString.TrimEnd('&'); - return uriString; - } - - /// - /// Convert a dictionary to formdata string - /// - /// - /// - public static string ToFormData(this IDictionary parameters) - { - var formData = HttpUtility.ParseQueryString(string.Empty); - foreach (var kvp in parameters) - { - if (kvp.Value is null) - continue; - - if (kvp.Value.GetType().IsArray) - { - var array = (Array)kvp.Value; - foreach (var value in array) - formData.Add(kvp.Key, string.Format(CultureInfo.InvariantCulture, "{0}", value)); - } - else - { - formData.Add(kvp.Key, string.Format(CultureInfo.InvariantCulture, "{0}", kvp.Value)); - } - } - - return formData.ToString()!; - } - - /// - /// Validates an int is one of the allowed values - /// - /// Value of the int - /// Name of the parameter - /// Allowed values - public static void ValidateIntValues(this int value, string argumentName, params int[] allowedValues) - { - if (!allowedValues.Contains(value)) - { - throw new ArgumentException( - $"{value} not allowed for parameter {argumentName}, allowed values: {string.Join(", ", allowedValues)}", argumentName); - } - } - - /// - /// Validates an int is between two values - /// - /// The value of the int - /// Name of the parameter - /// Min value - /// Max value - public static void ValidateIntBetween(this int value, string argumentName, int minValue, int maxValue) - { - if (value < minValue || value > maxValue) - { - throw new ArgumentException( - $"{value} not allowed for parameter {argumentName}, min: {minValue}, max: {maxValue}", argumentName); - } - } - - /// - /// Validates a string is not null or empty - /// - /// The value of the string - /// Name of the parameter - public static void ValidateNotNull(this string value, string argumentName) - { - if (string.IsNullOrEmpty(value)) - throw new ArgumentException($"No value provided for parameter {argumentName}", argumentName); - } - - /// - /// Validates a string is null or not empty - /// - /// - /// - public static void ValidateNullOrNotEmpty(this string value, string argumentName) - { - if (value != null && string.IsNullOrEmpty(value)) - throw new ArgumentException($"No value provided for parameter {argumentName}", argumentName); - } - - /// - /// Validates an object is not null - /// - /// The value of the object - /// Name of the parameter - public static void ValidateNotNull(this object value, string argumentName) - { - if (value == null) - throw new ArgumentException($"No value provided for parameter {argumentName}", argumentName); - } - - /// - /// Validates a list is not null or empty - /// - /// The value of the object - /// Name of the parameter - public static void ValidateNotNull(this IEnumerable value, string argumentName) - { - if (value == null || !value.Any()) - throw new ArgumentException($"No values provided for parameter {argumentName}", argumentName); - } - - /// - /// Format a string to RFC3339/ISO8601 string - /// - /// - /// - public static string ToRfc3339String(this DateTime dateTime) - { - return dateTime.ToString("yyyy-MM-dd'T'HH:mm:ss.fffzzz", DateTimeFormatInfo.InvariantInfo); - } - - /// - /// Format an exception and inner exception to a readable string - /// - /// - /// - public static string ToLogString(this Exception? exception) - { - var message = new StringBuilder(); - var indent = 0; - while (exception != null) - { - for (var i = 0; i < indent; i++) - message.Append(' '); - message.Append(exception.GetType().Name); - message.Append(" - "); - message.AppendLine(exception.Message); - for (var i = 0; i < indent; i++) - message.Append(' '); - message.AppendLine(exception.StackTrace); - - indent += 2; - exception = exception.InnerException; - } - - return message.ToString(); - } - - /// - /// Append a base url with provided path - /// - /// - /// - /// - public static string AppendPath(this string url, params string[] path) - { - if (!url.EndsWith("/")) - url += "/"; - - foreach (var item in path) - url += item.Trim('/') + "/"; - - return url.TrimEnd('/'); - } - - /// - /// Create a new uri with the provided parameters as query - /// - /// - /// - /// - /// - public static Uri SetParameters(this Uri baseUri, IDictionary parameters, ArrayParametersSerialization arraySerialization) - { - var uriBuilder = new UriBuilder(); - uriBuilder.Scheme = baseUri.Scheme; - uriBuilder.Host = baseUri.Host; - uriBuilder.Port = baseUri.Port; - uriBuilder.Path = baseUri.AbsolutePath; - var httpValueCollection = HttpUtility.ParseQueryString(string.Empty); - foreach (var parameter in parameters) - { - if (parameter.Value.GetType().IsArray) - { - if (arraySerialization == ArrayParametersSerialization.JsonArray) + foreach (var item in (object[])parameter.Value) { - httpValueCollection.Add(parameter.Key, $"[{string.Join(",", (object[])parameter.Value)}]"); - } - else - { - foreach (var item in (object[])parameter.Value) + if (arraySerialization == ArrayParametersSerialization.Array) { - if (arraySerialization == ArrayParametersSerialization.Array) - { - httpValueCollection.Add(parameter.Key + "[]", item.ToString()); - } - else - { - httpValueCollection.Add(parameter.Key, item.ToString()); - } + httpValueCollection.Add(parameter.Key + "[]", item.ToString()); + } + else + { + httpValueCollection.Add(parameter.Key, item.ToString()); } } } - else - { - httpValueCollection.Add(parameter.Key, parameter.Value.ToString()); - } } - - uriBuilder.Query = httpValueCollection.ToString(); - return uriBuilder.Uri; + else + { + httpValueCollection.Add(parameter.Key, parameter.Value.ToString()); + } } - /// - /// Create a new uri with the provided parameters as query - /// - /// - /// - /// - /// - public static Uri SetParameters(this Uri baseUri, IOrderedEnumerable> parameters, ArrayParametersSerialization arraySerialization) + uriBuilder.Query = httpValueCollection.ToString(); + return uriBuilder.Uri; + } + + /// + /// Create a new uri with the provided parameters as query + /// + /// + /// + /// + /// + public static Uri SetParameters(this Uri baseUri, IOrderedEnumerable> parameters, ArrayParametersSerialization arraySerialization) + { + var uriBuilder = new UriBuilder(); + uriBuilder.Scheme = baseUri.Scheme; + uriBuilder.Host = baseUri.Host; + uriBuilder.Port = baseUri.Port; + uriBuilder.Path = baseUri.AbsolutePath; + var httpValueCollection = HttpUtility.ParseQueryString(string.Empty); + foreach (var parameter in parameters) { - var uriBuilder = new UriBuilder(); - uriBuilder.Scheme = baseUri.Scheme; - uriBuilder.Host = baseUri.Host; - uriBuilder.Port = baseUri.Port; - uriBuilder.Path = baseUri.AbsolutePath; - var httpValueCollection = HttpUtility.ParseQueryString(string.Empty); - foreach (var parameter in parameters) + if (parameter.Value.GetType().IsArray) { - if (parameter.Value.GetType().IsArray) + if (arraySerialization == ArrayParametersSerialization.JsonArray) { - if (arraySerialization == ArrayParametersSerialization.JsonArray) + httpValueCollection.Add(parameter.Key, $"[{string.Join(",", (object[])parameter.Value)}]"); + } + else + { + foreach (var item in (object[])parameter.Value) { - httpValueCollection.Add(parameter.Key, $"[{string.Join(",", (object[])parameter.Value)}]"); - } - else - { - foreach (var item in (object[])parameter.Value) + if (arraySerialization == ArrayParametersSerialization.Array) { - if (arraySerialization == ArrayParametersSerialization.Array) - { - httpValueCollection.Add(parameter.Key + "[]", item.ToString()); - } - else - { - httpValueCollection.Add(parameter.Key, item.ToString()); - } + httpValueCollection.Add(parameter.Key + "[]", item.ToString()); + } + else + { + httpValueCollection.Add(parameter.Key, item.ToString()); } } } - else - { - httpValueCollection.Add(parameter.Key, parameter.Value.ToString()); - } } - - uriBuilder.Query = httpValueCollection.ToString(); - return uriBuilder.Uri; + else + { + httpValueCollection.Add(parameter.Key, parameter.Value.ToString()); + } } - /// - /// Add parameter to URI - /// - /// - /// - /// - /// - public static Uri AddQueryParameter(this Uri uri, string name, string value) - { - var httpValueCollection = HttpUtility.ParseQueryString(uri.Query); + uriBuilder.Query = httpValueCollection.ToString(); + return uriBuilder.Uri; + } - httpValueCollection.Remove(name); - httpValueCollection.Add(name, value); + /// + /// Add parameter to URI + /// + /// + /// + /// + /// + public static Uri AddQueryParameter(this Uri uri, string name, string value) + { + var httpValueCollection = HttpUtility.ParseQueryString(uri.Query); - var ub = new UriBuilder(uri); - ub.Query = httpValueCollection.ToString(); + httpValueCollection.Remove(name); + httpValueCollection.Add(name, value); - return ub.Uri; - } + var ub = new UriBuilder(uri); + ub.Query = httpValueCollection.ToString(); - /// - /// Decompress using GzipStream - /// - /// - /// - public static ReadOnlyMemory DecompressGzip(this ReadOnlyMemory data) - { - using var decompressedStream = new MemoryStream(); - using var dataStream = MemoryMarshal.TryGetArray(data, out var arraySegment) - ? new MemoryStream(arraySegment.Array!, arraySegment.Offset, arraySegment.Count) - : new MemoryStream(data.ToArray()); - using var deflateStream = new GZipStream(new MemoryStream(data.ToArray()), CompressionMode.Decompress); - deflateStream.CopyTo(decompressedStream); - return new ReadOnlyMemory(decompressedStream.GetBuffer(), 0, (int)decompressedStream.Length); - } + return ub.Uri; + } - /// - /// Decompress using DeflateStream - /// - /// - /// - public static ReadOnlyMemory Decompress(this ReadOnlyMemory input) - { - var output = new MemoryStream(); + /// + /// Decompress using GzipStream + /// + /// + /// + public static ReadOnlyMemory DecompressGzip(this ReadOnlyMemory data) + { + using var decompressedStream = new MemoryStream(); + using var dataStream = MemoryMarshal.TryGetArray(data, out var arraySegment) + ? new MemoryStream(arraySegment.Array!, arraySegment.Offset, arraySegment.Count) + : new MemoryStream(data.ToArray()); + using var deflateStream = new GZipStream(new MemoryStream(data.ToArray()), CompressionMode.Decompress); + deflateStream.CopyTo(decompressedStream); + return new ReadOnlyMemory(decompressedStream.GetBuffer(), 0, (int)decompressedStream.Length); + } - using (var compressStream = new MemoryStream(input.ToArray())) - using (var decompressor = new DeflateStream(compressStream, CompressionMode.Decompress)) - decompressor.CopyTo(output); + /// + /// Decompress using DeflateStream + /// + /// + /// + public static ReadOnlyMemory Decompress(this ReadOnlyMemory input) + { + var output = new MemoryStream(); - output.Position = 0; - return new ReadOnlyMemory(output.GetBuffer(), 0, (int)output.Length); - } + using (var compressStream = new MemoryStream(input.ToArray())) + using (var decompressor = new DeflateStream(compressStream, CompressionMode.Decompress)) + decompressor.CopyTo(output); - /// - /// Whether the trading mode is linear - /// - public static bool IsLinear(this TradingMode type) => type == TradingMode.PerpetualLinear || type == TradingMode.DeliveryLinear; + output.Position = 0; + return new ReadOnlyMemory(output.GetBuffer(), 0, (int)output.Length); + } - /// - /// Whether the trading mode is inverse - /// - public static bool IsInverse(this TradingMode type) => type == TradingMode.PerpetualInverse || type == TradingMode.DeliveryInverse; - - /// - /// Whether the trading mode is perpetual - /// - public static bool IsPerpetual(this TradingMode type) => type == TradingMode.PerpetualInverse || type == TradingMode.PerpetualLinear; + /// + /// Whether the trading mode is linear + /// + public static bool IsLinear(this TradingMode type) => type == TradingMode.PerpetualLinear || type == TradingMode.DeliveryLinear; - /// - /// Whether the trading mode is delivery - /// - public static bool IsDelivery(this TradingMode type) => type == TradingMode.DeliveryInverse || type == TradingMode.DeliveryLinear; + /// + /// Whether the trading mode is inverse + /// + public static bool IsInverse(this TradingMode type) => type == TradingMode.PerpetualInverse || type == TradingMode.DeliveryInverse; + + /// + /// Whether the trading mode is perpetual + /// + public static bool IsPerpetual(this TradingMode type) => type == TradingMode.PerpetualInverse || type == TradingMode.PerpetualLinear; - /// - /// Register rest client interfaces - /// - public static IServiceCollection RegisterSharedRestInterfaces(this IServiceCollection services, Func 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)!); + /// + /// Whether the trading mode is delivery + /// + public static bool IsDelivery(this TradingMode type) => type == TradingMode.DeliveryInverse || type == TradingMode.DeliveryLinear; - 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)!); + /// + /// Register rest client interfaces + /// + public static IServiceCollection RegisterSharedRestInterfaces(this IServiceCollection services, Func 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(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)!); + 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)!); - return services; - } + 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)!); - /// - /// Register socket client interfaces - /// - public static IServiceCollection RegisterSharedSocketInterfaces(this IServiceCollection services, Func 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(IOrderBookSocketClient).IsAssignableFrom(typeof(T))) - services.AddTransient(x => (IOrderBookSocketClient)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)!); + return services; + } - if (typeof(ISpotOrderSocketClient).IsAssignableFrom(typeof(T))) - services.AddTransient(x => (ISpotOrderSocketClient)client(x)!); + /// + /// Register socket client interfaces + /// + public static IServiceCollection RegisterSharedSocketInterfaces(this IServiceCollection services, Func 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(IOrderBookSocketClient).IsAssignableFrom(typeof(T))) + services.AddTransient(x => (IOrderBookSocketClient)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(IFuturesOrderSocketClient).IsAssignableFrom(typeof(T))) - services.AddTransient(x => (IFuturesOrderSocketClient)client(x)!); - if (typeof(IPositionSocketClient).IsAssignableFrom(typeof(T))) - services.AddTransient(x => (IPositionSocketClient)client(x)!); + if (typeof(ISpotOrderSocketClient).IsAssignableFrom(typeof(T))) + services.AddTransient(x => (ISpotOrderSocketClient)client(x)!); - return services; - } + 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; } } diff --git a/CryptoExchange.Net/Interfaces/IAuthTimeProvider.cs b/CryptoExchange.Net/Interfaces/IAuthTimeProvider.cs index 07357fe..9df2ea5 100644 --- a/CryptoExchange.Net/Interfaces/IAuthTimeProvider.cs +++ b/CryptoExchange.Net/Interfaces/IAuthTimeProvider.cs @@ -1,16 +1,15 @@ -using System; +using System; -namespace CryptoExchange.Net.Interfaces +namespace CryptoExchange.Net.Interfaces; + +/// +/// Time provider +/// +internal interface IAuthTimeProvider { /// - /// Time provider + /// Get current time /// - internal interface IAuthTimeProvider - { - /// - /// Get current time - /// - /// - DateTime GetTime(); - } + /// + DateTime GetTime(); } diff --git a/CryptoExchange.Net/Interfaces/IBaseApiClient.cs b/CryptoExchange.Net/Interfaces/IBaseApiClient.cs index 21867c8..2721401 100644 --- a/CryptoExchange.Net/Interfaces/IBaseApiClient.cs +++ b/CryptoExchange.Net/Interfaces/IBaseApiClient.cs @@ -1,48 +1,46 @@ -using CryptoExchange.Net.Authentication; -using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Objects.Options; using CryptoExchange.Net.SharedApis; using System; -namespace CryptoExchange.Net.Interfaces +namespace CryptoExchange.Net.Interfaces; + +/// +/// Base api client +/// +public interface IBaseApiClient { /// - /// Base api client + /// Base address /// - public interface IBaseApiClient - { - /// - /// Base address - /// - string BaseAddress { get; } + string BaseAddress { get; } - /// - /// Whether or not API credentials have been configured for this client. Does not check the credentials are actually valid. - /// - bool Authenticated { get; } + /// + /// Whether or not API credentials have been configured for this client. Does not check the credentials are actually valid. + /// + bool Authenticated { get; } - /// - /// Format a base and quote asset to an exchange accepted symbol - /// - /// The base asset - /// The quote asset - /// The trading mode - /// The deliver date for a delivery futures symbol - /// - string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverDate = null); + /// + /// Format a base and quote asset to an exchange accepted symbol + /// + /// The base asset + /// The quote asset + /// The trading mode + /// The deliver date for a delivery futures symbol + /// + string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverDate = null); - /// - /// Set the API credentials for this API client - /// - /// - /// - void SetApiCredentials(T credentials) where T : ApiCredentials; + /// + /// Set the API credentials for this API client + /// + /// + /// + void SetApiCredentials(T credentials) where T : ApiCredentials; - /// - /// 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. - /// - /// Api credentials type - /// Options to set - void SetOptions(UpdateOptions options) where T : ApiCredentials; - } + /// + /// 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. + /// + /// Api credentials type + /// Options to set + void SetOptions(UpdateOptions options) where T : ApiCredentials; } \ No newline at end of file diff --git a/CryptoExchange.Net/Interfaces/ICryptoRestClient.cs b/CryptoExchange.Net/Interfaces/ICryptoRestClient.cs index c3966eb..c52bfd5 100644 --- a/CryptoExchange.Net/Interfaces/ICryptoRestClient.cs +++ b/CryptoExchange.Net/Interfaces/ICryptoRestClient.cs @@ -1,17 +1,16 @@ -using System; +using System; -namespace CryptoExchange.Net.Interfaces +namespace CryptoExchange.Net.Interfaces; + +/// +/// Client for accessing REST API's for different exchanges +/// +public interface ICryptoRestClient { /// - /// Client for accessing REST API's for different exchanges + /// Try get /// - public interface ICryptoRestClient - { - /// - /// Try get - /// - /// - /// - T TryGet(Func createFunc); - } + /// + /// + T TryGet(Func createFunc); } \ No newline at end of file diff --git a/CryptoExchange.Net/Interfaces/ICryptoSocketClient.cs b/CryptoExchange.Net/Interfaces/ICryptoSocketClient.cs index 867448c..27f64a3 100644 --- a/CryptoExchange.Net/Interfaces/ICryptoSocketClient.cs +++ b/CryptoExchange.Net/Interfaces/ICryptoSocketClient.cs @@ -1,17 +1,16 @@ -using System; +using System; -namespace CryptoExchange.Net.Interfaces +namespace CryptoExchange.Net.Interfaces; + +/// +/// Client for accessing Websocket API's for different exchanges +/// +public interface ICryptoSocketClient { /// - /// Client for accessing Websocket API's for different exchanges + /// Try get a client by type for the service collection /// - public interface ICryptoSocketClient - { - /// - /// Try get a client by type for the service collection - /// - /// - /// - T TryGet(Func createFunc); - } + /// + /// + T TryGet(Func createFunc); } diff --git a/CryptoExchange.Net/Interfaces/IMessageAccessor.cs b/CryptoExchange.Net/Interfaces/IMessageAccessor.cs index 1c3c28c..a7de80b 100644 --- a/CryptoExchange.Net/Interfaces/IMessageAccessor.cs +++ b/CryptoExchange.Net/Interfaces/IMessageAccessor.cs @@ -1,110 +1,110 @@ -using CryptoExchange.Net.Converters.MessageParsing; +using CryptoExchange.Net.Converters.MessageParsing; using CryptoExchange.Net.Objects; using System; -using System.Collections.Generic; +#if NET5_0_OR_GREATER using System.Diagnostics.CodeAnalysis; +#endif using System.IO; using System.Threading.Tasks; -namespace CryptoExchange.Net.Interfaces +namespace CryptoExchange.Net.Interfaces; + +/// +/// Message accessor +/// +public interface IMessageAccessor { /// - /// Message accessor + /// Is this a valid message /// - public interface IMessageAccessor - { - /// - /// Is this a valid message - /// - bool IsValid { get; } - /// - /// Is the original data available for retrieval - /// - bool OriginalDataAvailable { get; } - /// - /// The underlying data object - /// - object? Underlying { get; } - /// - /// Clear internal data structure - /// - void Clear(); - /// - /// Get the type of node - /// - /// - NodeType? GetNodeType(); - /// - /// Get the type of node - /// - /// Access path - /// - NodeType? GetNodeType(MessagePath path); - /// - /// Get the value of a path - /// - /// - /// - /// - T? GetValue(MessagePath path); - /// - /// Get the values of an array - /// - /// - /// - /// - T?[]? GetValues(MessagePath path); - /// - /// Deserialize the message into this type - /// - /// - /// - /// + bool IsValid { get; } + /// + /// Is the original data available for retrieval + /// + bool OriginalDataAvailable { get; } + /// + /// The underlying data object + /// + object? Underlying { get; } + /// + /// Clear internal data structure + /// + void Clear(); + /// + /// Get the type of node + /// + /// + NodeType? GetNodeType(); + /// + /// Get the type of node + /// + /// Access path + /// + NodeType? GetNodeType(MessagePath path); + /// + /// Get the value of a path + /// + /// + /// + /// + T? GetValue(MessagePath path); + /// + /// Get the values of an array + /// + /// + /// + /// + T?[]? GetValues(MessagePath path); + /// + /// Deserialize the message into this type + /// + /// + /// + /// #if NET5_0_OR_GREATER - [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")] - [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")] + [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")] + [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")] #endif - CallResult Deserialize(Type type, MessagePath? path = null); - /// - /// Deserialize the message into this type - /// - /// - /// + CallResult Deserialize(Type type, MessagePath? path = null); + /// + /// Deserialize the message into this type + /// + /// + /// #if NET5_0_OR_GREATER - [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")] - [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")] + [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")] + [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")] #endif - CallResult Deserialize(MessagePath? path = null); - - /// - /// Get the original string value - /// - /// - string GetOriginalString(); - } + CallResult Deserialize(MessagePath? path = null); /// - /// Stream message accessor + /// Get the original string value /// - public interface IStreamMessageAccessor : IMessageAccessor - { - /// - /// Load a stream message - /// - /// - /// - Task Read(Stream stream, bool bufferStream); - } - - /// - /// Byte message accessor - /// - public interface IByteMessageAccessor : IMessageAccessor - { - /// - /// Load a data message - /// - /// - CallResult Read(ReadOnlyMemory data); - } + /// + string GetOriginalString(); +} + +/// +/// Stream message accessor +/// +public interface IStreamMessageAccessor : IMessageAccessor +{ + /// + /// Load a stream message + /// + /// + /// + Task Read(Stream stream, bool bufferStream); +} + +/// +/// Byte message accessor +/// +public interface IByteMessageAccessor : IMessageAccessor +{ + /// + /// Load a data message + /// + /// + CallResult Read(ReadOnlyMemory data); } diff --git a/CryptoExchange.Net/Interfaces/IMessageProcessor.cs b/CryptoExchange.Net/Interfaces/IMessageProcessor.cs index 749a04f..d804d33 100644 --- a/CryptoExchange.Net/Interfaces/IMessageProcessor.cs +++ b/CryptoExchange.Net/Interfaces/IMessageProcessor.cs @@ -1,35 +1,33 @@ -using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects.Sockets; using CryptoExchange.Net.Sockets; using System; -using System.Collections.Generic; using System.Threading.Tasks; -namespace CryptoExchange.Net.Interfaces +namespace CryptoExchange.Net.Interfaces; + +/// +/// Message processor +/// +public interface IMessageProcessor { /// - /// Message processor + /// Id of the processor /// - public interface IMessageProcessor - { - /// - /// Id of the processor - /// - public int Id { get; } - /// - /// The matcher for this listener - /// - public MessageMatcher MessageMatcher { get; } - /// - /// Handle a message - /// - Task Handle(SocketConnection connection, DataEvent message, MessageHandlerLink matchedHandler); - /// - /// Deserialize a message into object of type - /// - /// - /// - /// - CallResult Deserialize(IMessageAccessor accessor, Type type); - } + public int Id { get; } + /// + /// The matcher for this listener + /// + public MessageMatcher MessageMatcher { get; } + /// + /// Handle a message + /// + Task Handle(SocketConnection connection, DataEvent message, MessageHandlerLink matchedHandler); + /// + /// Deserialize a message into object of type + /// + /// + /// + /// + CallResult Deserialize(IMessageAccessor accessor, Type type); } diff --git a/CryptoExchange.Net/Interfaces/IMessageSerializer.cs b/CryptoExchange.Net/Interfaces/IMessageSerializer.cs index 01009a8..86d053f 100644 --- a/CryptoExchange.Net/Interfaces/IMessageSerializer.cs +++ b/CryptoExchange.Net/Interfaces/IMessageSerializer.cs @@ -1,37 +1,34 @@ -using System.Diagnostics.CodeAnalysis; +namespace CryptoExchange.Net.Interfaces; -namespace CryptoExchange.Net.Interfaces +/// +/// Serializer interface +/// +public interface IMessageSerializer +{ +} + +/// +/// Serialize to byte array +/// +public interface IByteMessageSerializer: IMessageSerializer { /// - /// Serializer interface + /// Serialize an object to a string /// - public interface IMessageSerializer - { - } - - /// - /// Serialize to byte array - /// - public interface IByteMessageSerializer: IMessageSerializer - { - /// - /// Serialize an object to a string - /// - /// - /// - byte[] Serialize(T message); - } - - /// - /// Serialize to string - /// - public interface IStringMessageSerializer: IMessageSerializer - { - /// - /// Serialize an object to a string - /// - /// - /// - string Serialize(T message); - } + /// + /// + byte[] Serialize(T message); +} + +/// +/// Serialize to string +/// +public interface IStringMessageSerializer: IMessageSerializer +{ + /// + /// Serialize an object to a string + /// + /// + /// + string Serialize(T message); } diff --git a/CryptoExchange.Net/Interfaces/INonceProvider.cs b/CryptoExchange.Net/Interfaces/INonceProvider.cs index 6b57672..c22b8ed 100644 --- a/CryptoExchange.Net/Interfaces/INonceProvider.cs +++ b/CryptoExchange.Net/Interfaces/INonceProvider.cs @@ -1,14 +1,13 @@ -namespace CryptoExchange.Net.Interfaces +namespace CryptoExchange.Net.Interfaces; + +/// +/// A provider for a nonce value used when signing requests +/// +public interface INonceProvider { /// - /// A provider for a nonce value used when signing requests + /// Get nonce value. Nonce value should be unique and incremental for each call /// - public interface INonceProvider - { - /// - /// Get nonce value. Nonce value should be unique and incremental for each call - /// - /// Nonce value - long GetNonce(); - } + /// Nonce value + long GetNonce(); } diff --git a/CryptoExchange.Net/Interfaces/IOrderBookFactory.cs b/CryptoExchange.Net/Interfaces/IOrderBookFactory.cs index 5ca48d1..de15fe5 100644 --- a/CryptoExchange.Net/Interfaces/IOrderBookFactory.cs +++ b/CryptoExchange.Net/Interfaces/IOrderBookFactory.cs @@ -1,35 +1,34 @@ -using CryptoExchange.Net.Objects.Options; +using CryptoExchange.Net.Objects.Options; using CryptoExchange.Net.SharedApis; using System; -namespace CryptoExchange.Net.Interfaces +namespace CryptoExchange.Net.Interfaces; + +/// +/// Factory for ISymbolOrderBook instances +/// +public interface IOrderBookFactory where TOptions : OrderBookOptions { /// - /// Factory for ISymbolOrderBook instances + /// Create a new order book by symbol name /// - public interface IOrderBookFactory where TOptions : OrderBookOptions - { - /// - /// Create a new order book by symbol name - /// - /// Symbol name - /// Options for the order book - /// - public ISymbolOrderBook Create(string symbol, Action? options = null); - /// - /// Create a new order book by base and quote asset names - /// - /// Base asset name - /// Quote asset name - /// Options for the order book - /// - public ISymbolOrderBook Create(string baseAsset, string quoteAsset, Action? options = null); - /// - /// Create a new order book by base and quote asset names - /// - /// Symbol - /// Options for the order book - /// - public ISymbolOrderBook Create(SharedSymbol symbol, Action? options = null); - } + /// Symbol name + /// Options for the order book + /// + public ISymbolOrderBook Create(string symbol, Action? options = null); + /// + /// Create a new order book by base and quote asset names + /// + /// Base asset name + /// Quote asset name + /// Options for the order book + /// + public ISymbolOrderBook Create(string baseAsset, string quoteAsset, Action? options = null); + /// + /// Create a new order book by base and quote asset names + /// + /// Symbol + /// Options for the order book + /// + public ISymbolOrderBook Create(SharedSymbol symbol, Action? options = null); } diff --git a/CryptoExchange.Net/Interfaces/IRateLimiter.cs b/CryptoExchange.Net/Interfaces/IRateLimiter.cs index ec8a422..811213b 100644 --- a/CryptoExchange.Net/Interfaces/IRateLimiter.cs +++ b/CryptoExchange.Net/Interfaces/IRateLimiter.cs @@ -4,25 +4,24 @@ using System.Net.Http; using System.Threading; using System.Threading.Tasks; -namespace CryptoExchange.Net.Interfaces +namespace CryptoExchange.Net.Interfaces; + +/// +/// Rate limiter interface +/// +public interface IRateLimiter { /// - /// Rate limiter interface + /// Limit a request based on previous requests made /// - public interface IRateLimiter - { - /// - /// Limit a request based on previous requests made - /// - /// The logger - /// The endpoint the request is for - /// The Http request method - /// Whether the request is singed(private) or not - /// The api key making this request - /// The limit behavior for when the limit is reached - /// The weight of the request - /// Cancellation token to cancel waiting - /// The time in milliseconds spend waiting - Task> LimitRequestAsync(ILogger log, string endpoint, HttpMethod method, bool signed, string? apiKey, RateLimitingBehaviour limitBehaviour, int requestWeight, CancellationToken ct); - } + /// The logger + /// The endpoint the request is for + /// The Http request method + /// Whether the request is singed(private) or not + /// The api key making this request + /// The limit behavior for when the limit is reached + /// The weight of the request + /// Cancellation token to cancel waiting + /// The time in milliseconds spend waiting + Task> LimitRequestAsync(ILogger log, string endpoint, HttpMethod method, bool signed, string? apiKey, RateLimitingBehaviour limitBehaviour, int requestWeight, CancellationToken ct); } diff --git a/CryptoExchange.Net/Interfaces/IRequest.cs b/CryptoExchange.Net/Interfaces/IRequest.cs index 72ebe59..d170e91 100644 --- a/CryptoExchange.Net/Interfaces/IRequest.cs +++ b/CryptoExchange.Net/Interfaces/IRequest.cs @@ -1,66 +1,65 @@ -using System; +using System; using System.Collections.Generic; using System.Net.Http; using System.Threading; using System.Threading.Tasks; -namespace CryptoExchange.Net.Interfaces +namespace CryptoExchange.Net.Interfaces; + +/// +/// Request interface +/// +public interface IRequest { /// - /// Request interface + /// Accept header /// - public interface IRequest - { - /// - /// Accept header - /// - string Accept { set; } - /// - /// Content - /// - string? Content { get; } - /// - /// Method - /// - HttpMethod Method { get; set; } - /// - /// Uri - /// - Uri Uri { get; } - /// - /// internal request id for tracing - /// - int RequestId { get; } - /// - /// Set byte content - /// - /// - void SetContent(byte[] data); - /// - /// Set string content - /// - /// - /// - void SetContent(string data, string contentType); + string Accept { set; } + /// + /// Content + /// + string? Content { get; } + /// + /// Method + /// + HttpMethod Method { get; set; } + /// + /// Uri + /// + Uri Uri { get; } + /// + /// internal request id for tracing + /// + int RequestId { get; } + /// + /// Set byte content + /// + /// + void SetContent(byte[] data); + /// + /// Set string content + /// + /// + /// + void SetContent(string data, string contentType); - /// - /// Add a header to the request - /// - /// - /// - void AddHeader(string key, string value); + /// + /// Add a header to the request + /// + /// + /// + void AddHeader(string key, string value); - /// - /// Get all headers - /// - /// - KeyValuePair[] GetHeaders(); + /// + /// Get all headers + /// + /// + KeyValuePair[] GetHeaders(); - /// - /// Get the response - /// - /// - /// - Task GetResponseAsync(CancellationToken cancellationToken); - } + /// + /// Get the response + /// + /// + /// + Task GetResponseAsync(CancellationToken cancellationToken); } diff --git a/CryptoExchange.Net/Interfaces/IRequestFactory.cs b/CryptoExchange.Net/Interfaces/IRequestFactory.cs index f3cc827..8ed6f4e 100644 --- a/CryptoExchange.Net/Interfaces/IRequestFactory.cs +++ b/CryptoExchange.Net/Interfaces/IRequestFactory.cs @@ -1,36 +1,35 @@ -using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects; using System; using System.Net.Http; -namespace CryptoExchange.Net.Interfaces +namespace CryptoExchange.Net.Interfaces; + +/// +/// Request factory interface +/// +public interface IRequestFactory { /// - /// Request factory interface + /// Create a request for an uri /// - public interface IRequestFactory - { - /// - /// Create a request for an uri - /// - /// - /// - /// - /// - IRequest Create(HttpMethod method, Uri uri, int requestId); + /// + /// + /// + /// + IRequest Create(HttpMethod method, Uri uri, int requestId); - /// - /// Configure the requests created by this factory - /// - /// Request timeout to use - /// Optional shared http client instance - /// Optional proxy to use when no http client is provided - void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? httpClient = null); + /// + /// Configure the requests created by this factory + /// + /// Request timeout to use + /// Optional shared http client instance + /// Optional proxy to use when no http client is provided + void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? httpClient = null); - /// - /// Update settings - /// - /// Proxy to use - /// Request timeout to use - void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout); - } + /// + /// Update settings + /// + /// Proxy to use + /// Request timeout to use + void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout); } diff --git a/CryptoExchange.Net/Interfaces/IResponse.cs b/CryptoExchange.Net/Interfaces/IResponse.cs index 55f9921..67dc184 100644 --- a/CryptoExchange.Net/Interfaces/IResponse.cs +++ b/CryptoExchange.Net/Interfaces/IResponse.cs @@ -1,44 +1,43 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using System.Net; using System.Threading.Tasks; -namespace CryptoExchange.Net.Interfaces +namespace CryptoExchange.Net.Interfaces; + +/// +/// Response object interface +/// +public interface IResponse { /// - /// Response object interface + /// The response status code /// - public interface IResponse - { - /// - /// The response status code - /// - HttpStatusCode StatusCode { get; } + HttpStatusCode StatusCode { get; } - /// - /// Whether the status code indicates a success status - /// - bool IsSuccessStatusCode { get; } + /// + /// Whether the status code indicates a success status + /// + bool IsSuccessStatusCode { get; } - /// - /// The length of the response in bytes - /// - long? ContentLength { get; } + /// + /// The length of the response in bytes + /// + long? ContentLength { get; } - /// - /// The response headers - /// - KeyValuePair[] ResponseHeaders { get; } + /// + /// The response headers + /// + KeyValuePair[] ResponseHeaders { get; } - /// - /// Get the response stream - /// - /// - Task GetResponseStreamAsync(); + /// + /// Get the response stream + /// + /// + Task GetResponseStreamAsync(); - /// - /// Close the response - /// - void Close(); - } + /// + /// Close the response + /// + void Close(); } diff --git a/CryptoExchange.Net/Interfaces/IRestApiClient.cs b/CryptoExchange.Net/Interfaces/IRestApiClient.cs index 9d96fbd..3c53428 100644 --- a/CryptoExchange.Net/Interfaces/IRestApiClient.cs +++ b/CryptoExchange.Net/Interfaces/IRestApiClient.cs @@ -1,18 +1,17 @@ -namespace CryptoExchange.Net.Interfaces +namespace CryptoExchange.Net.Interfaces; + +/// +/// Base rest API client +/// +public interface IRestApiClient : IBaseApiClient { /// - /// Base rest API client + /// The factory for creating requests. Used for unit testing /// - public interface IRestApiClient : IBaseApiClient - { - /// - /// The factory for creating requests. Used for unit testing - /// - IRequestFactory RequestFactory { get; set; } + IRequestFactory RequestFactory { get; set; } - /// - /// Total amount of requests made with this API client - /// - int TotalRequestsMade { get; set; } - } + /// + /// Total amount of requests made with this API client + /// + int TotalRequestsMade { get; set; } } \ No newline at end of file diff --git a/CryptoExchange.Net/Interfaces/IRestClient.cs b/CryptoExchange.Net/Interfaces/IRestClient.cs index a2592a3..ff1a0f9 100644 --- a/CryptoExchange.Net/Interfaces/IRestClient.cs +++ b/CryptoExchange.Net/Interfaces/IRestClient.cs @@ -1,26 +1,25 @@ -using System; +using System; using CryptoExchange.Net.Objects.Options; -namespace CryptoExchange.Net.Interfaces +namespace CryptoExchange.Net.Interfaces; + +/// +/// Base class for rest API implementations +/// +public interface IRestClient: IDisposable { /// - /// Base class for rest API implementations + /// The options provided for this client /// - public interface IRestClient: IDisposable - { - /// - /// The options provided for this client - /// - ExchangeOptions ClientOptions { get; } + ExchangeOptions ClientOptions { get; } - /// - /// The total amount of requests made with this client - /// - int TotalRequestsMade { get; } + /// + /// The total amount of requests made with this client + /// + int TotalRequestsMade { get; } - /// - /// The exchange name - /// - string Exchange { get; } - } + /// + /// The exchange name + /// + string Exchange { get; } } \ No newline at end of file diff --git a/CryptoExchange.Net/Interfaces/ISocketApiClient.cs b/CryptoExchange.Net/Interfaces/ISocketApiClient.cs index fdc49bd..a43f85d 100644 --- a/CryptoExchange.Net/Interfaces/ISocketApiClient.cs +++ b/CryptoExchange.Net/Interfaces/ISocketApiClient.cs @@ -1,70 +1,69 @@ -using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects.Options; using CryptoExchange.Net.Objects.Sockets; using System.Threading.Tasks; -namespace CryptoExchange.Net.Interfaces +namespace CryptoExchange.Net.Interfaces; + +/// +/// Socket API client +/// +public interface ISocketApiClient: IBaseApiClient { /// - /// Socket API client + /// The current amount of socket connections on the API client /// - public interface ISocketApiClient: IBaseApiClient - { - /// - /// The current amount of socket connections on the API client - /// - int CurrentConnections { get; } - /// - /// The current amount of subscriptions over all connections - /// - int CurrentSubscriptions { get; } - /// - /// Incoming data Kbps - /// - double IncomingKbps { get; } - /// - /// The factory for creating sockets. Used for unit testing - /// - IWebsocketFactory SocketFactory { get; set; } - /// - /// Current client options - /// - SocketExchangeOptions ClientOptions { get; } - /// - /// Current API options - /// - SocketApiOptions ApiOptions { get; } - /// - /// Log the current state of connections and subscriptions - /// - string GetSubscriptionsState(bool includeSubDetails = true); - /// - /// Reconnect all connections - /// - /// - Task ReconnectAsync(); - /// - /// Unsubscribe all subscriptions - /// - /// - Task UnsubscribeAllAsync(); - /// - /// Unsubscribe an update subscription - /// - /// The id of the subscription to unsubscribe - /// - Task UnsubscribeAsync(int subscriptionId); - /// - /// Unsubscribe an update subscription - /// - /// The subscription to unsubscribe - /// - Task UnsubscribeAsync(UpdateSubscription subscription); + int CurrentConnections { get; } + /// + /// The current amount of subscriptions over all connections + /// + int CurrentSubscriptions { get; } + /// + /// Incoming data Kbps + /// + double IncomingKbps { get; } + /// + /// The factory for creating sockets. Used for unit testing + /// + IWebsocketFactory SocketFactory { get; set; } + /// + /// Current client options + /// + SocketExchangeOptions ClientOptions { get; } + /// + /// Current API options + /// + SocketApiOptions ApiOptions { get; } + /// + /// Log the current state of connections and subscriptions + /// + string GetSubscriptionsState(bool includeSubDetails = true); + /// + /// Reconnect all connections + /// + /// + Task ReconnectAsync(); + /// + /// Unsubscribe all subscriptions + /// + /// + Task UnsubscribeAllAsync(); + /// + /// Unsubscribe an update subscription + /// + /// The id of the subscription to unsubscribe + /// + Task UnsubscribeAsync(int subscriptionId); + /// + /// Unsubscribe an update subscription + /// + /// The subscription to unsubscribe + /// + Task UnsubscribeAsync(UpdateSubscription subscription); - /// - /// 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. - /// - /// - Task PrepareConnectionsAsync(); - } + /// + /// 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. + /// + /// + Task PrepareConnectionsAsync(); } \ No newline at end of file diff --git a/CryptoExchange.Net/Interfaces/ISocketClient.cs b/CryptoExchange.Net/Interfaces/ISocketClient.cs index 42337ec..7e2cca2 100644 --- a/CryptoExchange.Net/Interfaces/ISocketClient.cs +++ b/CryptoExchange.Net/Interfaces/ISocketClient.cs @@ -1,58 +1,57 @@ -using System; +using System; using System.Threading.Tasks; using CryptoExchange.Net.Objects.Options; using CryptoExchange.Net.Objects.Sockets; -namespace CryptoExchange.Net.Interfaces +namespace CryptoExchange.Net.Interfaces; + +/// +/// Base class for socket API implementations +/// +public interface ISocketClient: IDisposable { /// - /// Base class for socket API implementations + /// The exchange name /// - public interface ISocketClient: IDisposable - { - /// - /// The exchange name - /// - string Exchange { get; } + string Exchange { get; } - /// - /// The options provided for this client - /// - ExchangeOptions ClientOptions { get; } + /// + /// The options provided for this client + /// + ExchangeOptions ClientOptions { get; } - /// - /// Incoming kilobytes per second of data - /// - public double IncomingKbps { get; } + /// + /// Incoming kilobytes per second of data + /// + public double IncomingKbps { get; } - /// - /// The current amount of connections to the API from this client. A connection can have multiple subscriptions. - /// - public int CurrentConnections { get; } - - /// - /// The current amount of subscriptions running from the client - /// - public int CurrentSubscriptions { get; } + /// + /// The current amount of connections to the API from this client. A connection can have multiple subscriptions. + /// + public int CurrentConnections { get; } + + /// + /// The current amount of subscriptions running from the client + /// + public int CurrentSubscriptions { get; } - /// - /// Unsubscribe from a stream using the subscription id received when starting the subscription - /// - /// The id of the subscription to unsubscribe - /// - Task UnsubscribeAsync(int subscriptionId); + /// + /// Unsubscribe from a stream using the subscription id received when starting the subscription + /// + /// The id of the subscription to unsubscribe + /// + Task UnsubscribeAsync(int subscriptionId); - /// - /// Unsubscribe from a stream - /// - /// The subscription to unsubscribe - /// - Task UnsubscribeAsync(UpdateSubscription subscription); + /// + /// Unsubscribe from a stream + /// + /// The subscription to unsubscribe + /// + Task UnsubscribeAsync(UpdateSubscription subscription); - /// - /// Unsubscribe all subscriptions - /// - /// - Task UnsubscribeAllAsync(); - } + /// + /// Unsubscribe all subscriptions + /// + /// + Task UnsubscribeAllAsync(); } \ No newline at end of file diff --git a/CryptoExchange.Net/Interfaces/ISymbolOrderBook.cs b/CryptoExchange.Net/Interfaces/ISymbolOrderBook.cs index 21c2bb7..a33a30c 100644 --- a/CryptoExchange.Net/Interfaces/ISymbolOrderBook.cs +++ b/CryptoExchange.Net/Interfaces/ISymbolOrderBook.cs @@ -1,131 +1,129 @@ -using System; -using System.Collections.Generic; +using System; using System.Threading; using System.Threading.Tasks; using CryptoExchange.Net.Objects; -namespace CryptoExchange.Net.Interfaces +namespace CryptoExchange.Net.Interfaces; + +/// +/// Interface for order book +/// +public interface ISymbolOrderBook { /// - /// Interface for order book + /// The exchange the book is for /// - public interface ISymbolOrderBook - { - /// - /// The exchange the book is for - /// - string Exchange { get; } + string Exchange { get; } - /// - /// The Api the book is for - /// - string Api { get; } + /// + /// The Api the book is for + /// + string Api { get; } - /// - /// The status of the order book. Order book is up to date when the status is `Synced` - /// - OrderBookStatus Status { get; set; } + /// + /// The status of the order book. Order book is up to date when the status is `Synced` + /// + OrderBookStatus Status { get; set; } - /// - /// Last update identifier - /// - long LastSequenceNumber { get; } - /// - /// The symbol of the order book - /// - string Symbol { get; } + /// + /// Last update identifier + /// + long LastSequenceNumber { get; } + /// + /// The symbol of the order book + /// + string Symbol { get; } - /// - /// Event when the state changes - /// - event Action OnStatusChange; - /// - /// Event when order book was updated. Be careful! It can generate a lot of events at high-liquidity markets - /// - event Action<(ISymbolOrderBookEntry[] Bids, ISymbolOrderBookEntry[] Asks)> OnOrderBookUpdate; - /// - /// Event when the BestBid or BestAsk changes ie a Pricing Tick - /// - event Action<(ISymbolOrderBookEntry BestBid, ISymbolOrderBookEntry BestAsk)> OnBestOffersChanged; - /// - /// Timestamp of the last update - /// - DateTime UpdateTime { get; } + /// + /// Event when the state changes + /// + event Action OnStatusChange; + /// + /// Event when order book was updated. Be careful! It can generate a lot of events at high-liquidity markets + /// + event Action<(ISymbolOrderBookEntry[] Bids, ISymbolOrderBookEntry[] Asks)> OnOrderBookUpdate; + /// + /// Event when the BestBid or BestAsk changes ie a Pricing Tick + /// + event Action<(ISymbolOrderBookEntry BestBid, ISymbolOrderBookEntry BestAsk)> OnBestOffersChanged; + /// + /// Timestamp of the last update + /// + DateTime UpdateTime { get; } - /// - /// The number of asks in the book - /// - int AskCount { get; } - /// - /// The number of bids in the book - /// - int BidCount { get; } + /// + /// The number of asks in the book + /// + int AskCount { get; } + /// + /// The number of bids in the book + /// + int BidCount { get; } - /// - /// Get a snapshot of the book at this moment - /// - (ISymbolOrderBookEntry[] bids, ISymbolOrderBookEntry[] asks) Book { get; } + /// + /// Get a snapshot of the book at this moment + /// + (ISymbolOrderBookEntry[] bids, ISymbolOrderBookEntry[] asks) Book { get; } - /// - /// The list of asks - /// - ISymbolOrderBookEntry[] Asks { get; } + /// + /// The list of asks + /// + ISymbolOrderBookEntry[] Asks { get; } - /// - /// The list of bids - /// - ISymbolOrderBookEntry[] Bids { get; } + /// + /// The list of bids + /// + ISymbolOrderBookEntry[] Bids { get; } - /// - /// The best bid currently in the order book - /// - ISymbolOrderBookEntry BestBid { get; } + /// + /// The best bid currently in the order book + /// + ISymbolOrderBookEntry BestBid { get; } - /// - /// The best ask currently in the order book - /// - ISymbolOrderBookEntry BestAsk { get; } + /// + /// The best ask currently in the order book + /// + ISymbolOrderBookEntry BestAsk { get; } - /// - /// BestBid/BesAsk returned as a pair - /// - (ISymbolOrderBookEntry Bid, ISymbolOrderBookEntry Ask) BestOffers { get; } + /// + /// BestBid/BesAsk returned as a pair + /// + (ISymbolOrderBookEntry Bid, ISymbolOrderBookEntry Ask) BestOffers { get; } - /// - /// Start connecting and synchronizing the order book - /// - /// A cancellation token to stop the order book when canceled - /// - Task> StartAsync(CancellationToken? ct = null); + /// + /// Start connecting and synchronizing the order book + /// + /// A cancellation token to stop the order book when canceled + /// + Task> StartAsync(CancellationToken? ct = null); - /// - /// Stop syncing the order book - /// - /// - Task StopAsync(); + /// + /// Stop syncing the order book + /// + /// + Task StopAsync(); - /// - /// 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. - /// - /// The quantity in base asset to fill - /// The type - /// Average fill price - CallResult CalculateAverageFillPrice(decimal quantity, OrderBookEntryType type); + /// + /// 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. + /// + /// The quantity in base asset to fill + /// The type + /// Average fill price + CallResult CalculateAverageFillPrice(decimal quantity, OrderBookEntryType type); - /// - /// 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 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. - /// - /// The quantity in quote asset looking to trade - /// The type - /// Amount of base asset tradable with the specified amount of quote asset - CallResult CalculateTradableAmount(decimal quoteQuantity, OrderBookEntryType type); + /// + /// 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 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. + /// + /// The quantity in quote asset looking to trade + /// The type + /// Amount of base asset tradable with the specified amount of quote asset + CallResult CalculateTradableAmount(decimal quoteQuantity, OrderBookEntryType type); - /// - /// String representation of the top x entries - /// - /// - string ToString(int rows); - } + /// + /// String representation of the top x entries + /// + /// + string ToString(int rows); } diff --git a/CryptoExchange.Net/Interfaces/ISymbolOrderBookEntry.cs b/CryptoExchange.Net/Interfaces/ISymbolOrderBookEntry.cs index 3aa9f97..273cf3d 100644 --- a/CryptoExchange.Net/Interfaces/ISymbolOrderBookEntry.cs +++ b/CryptoExchange.Net/Interfaces/ISymbolOrderBookEntry.cs @@ -1,28 +1,27 @@ -namespace CryptoExchange.Net.Interfaces +namespace CryptoExchange.Net.Interfaces; + +/// +/// Interface for order book entries +/// +public interface ISymbolOrderBookEntry { /// - /// Interface for order book entries + /// The quantity of the entry /// - public interface ISymbolOrderBookEntry - { - /// - /// The quantity of the entry - /// - decimal Quantity { get; set; } - /// - /// The price of the entry - /// - decimal Price { get; set; } - } - + decimal Quantity { get; set; } /// - /// Interface for order book entries + /// The price of the entry /// - public interface ISymbolOrderSequencedBookEntry: ISymbolOrderBookEntry - { - /// - /// Sequence of the update - /// - long Sequence { get; set; } - } + decimal Price { get; set; } +} + +/// +/// Interface for order book entries +/// +public interface ISymbolOrderSequencedBookEntry: ISymbolOrderBookEntry +{ + /// + /// Sequence of the update + /// + long Sequence { get; set; } } diff --git a/CryptoExchange.Net/Interfaces/IWebsocket.cs b/CryptoExchange.Net/Interfaces/IWebsocket.cs index bc360d0..bdf217c 100644 --- a/CryptoExchange.Net/Interfaces/IWebsocket.cs +++ b/CryptoExchange.Net/Interfaces/IWebsocket.cs @@ -1,110 +1,109 @@ -using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects; using System; using System.Net.WebSockets; using System.Threading; using System.Threading.Tasks; -namespace CryptoExchange.Net.Interfaces +namespace CryptoExchange.Net.Interfaces; + +/// +/// Websocket connection interface +/// +public interface IWebsocket: IDisposable { /// - /// Websocket connection interface + /// Websocket closed event /// - public interface IWebsocket: IDisposable - { - /// - /// Websocket closed event - /// - event Func OnClose; - /// - /// Websocket message received event - /// - event Func, Task> OnStreamMessage; - /// - /// Websocket sent event, RequestId as parameter - /// - event Func OnRequestSent; - /// - /// Websocket query was ratelimited and couldn't be send - /// - event Func? OnRequestRateLimited; - /// - /// Connection was ratelimited and couldn't be established - /// - event Func? OnConnectRateLimited; - /// - /// Websocket error event - /// - event Func OnError; - /// - /// Websocket opened event - /// - event Func OnOpen; - /// - /// Websocket has lost connection to the server and is attempting to reconnect - /// - event Func OnReconnecting; - /// - /// Websocket has reconnected to the server - /// - event Func OnReconnected; - /// - /// Get reconnection url - /// - Func>? GetReconnectionUrl { get; set; } + event Func OnClose; + /// + /// Websocket message received event + /// + event Func, Task> OnStreamMessage; + /// + /// Websocket sent event, RequestId as parameter + /// + event Func OnRequestSent; + /// + /// Websocket query was ratelimited and couldn't be send + /// + event Func? OnRequestRateLimited; + /// + /// Connection was ratelimited and couldn't be established + /// + event Func? OnConnectRateLimited; + /// + /// Websocket error event + /// + event Func OnError; + /// + /// Websocket opened event + /// + event Func OnOpen; + /// + /// Websocket has lost connection to the server and is attempting to reconnect + /// + event Func OnReconnecting; + /// + /// Websocket has reconnected to the server + /// + event Func OnReconnected; + /// + /// Get reconnection url + /// + Func>? GetReconnectionUrl { get; set; } - /// - /// Unique id for this socket - /// - int Id { get; } - /// - /// The current kilobytes per second of data being received, averaged over the last 3 seconds - /// - double IncomingKbps { get; } - /// - /// The uri the socket connects to - /// - Uri Uri { get; } - /// - /// Whether the socket connection is closed - /// - bool IsClosed { get; } - /// - /// Whether the socket connection is open - /// - bool IsOpen { get; } - /// - /// Connect the socket - /// - /// - Task ConnectAsync(CancellationToken ct); - /// - /// Send string data - /// - /// - /// - /// - bool Send(int id, string data, int weight); - /// - /// Send byte data - /// - /// - /// - /// - bool Send(int id, byte[] data, int weight); - /// - /// Reconnect the socket - /// - /// - Task ReconnectAsync(); - /// - /// Close the connection - /// - /// - Task CloseAsync(); + /// + /// Unique id for this socket + /// + int Id { get; } + /// + /// The current kilobytes per second of data being received, averaged over the last 3 seconds + /// + double IncomingKbps { get; } + /// + /// The uri the socket connects to + /// + Uri Uri { get; } + /// + /// Whether the socket connection is closed + /// + bool IsClosed { get; } + /// + /// Whether the socket connection is open + /// + bool IsOpen { get; } + /// + /// Connect the socket + /// + /// + Task ConnectAsync(CancellationToken ct); + /// + /// Send string data + /// + /// + /// + /// + bool Send(int id, string data, int weight); + /// + /// Send byte data + /// + /// + /// + /// + bool Send(int id, byte[] data, int weight); + /// + /// Reconnect the socket + /// + /// + Task ReconnectAsync(); + /// + /// Close the connection + /// + /// + Task CloseAsync(); - /// - /// Update proxy setting - /// - void UpdateProxy(ApiProxy? proxy); - } + /// + /// Update proxy setting + /// + void UpdateProxy(ApiProxy? proxy); } diff --git a/CryptoExchange.Net/Interfaces/IWebsocketFactory.cs b/CryptoExchange.Net/Interfaces/IWebsocketFactory.cs index 3fe70c1..f149e48 100644 --- a/CryptoExchange.Net/Interfaces/IWebsocketFactory.cs +++ b/CryptoExchange.Net/Interfaces/IWebsocketFactory.cs @@ -1,19 +1,18 @@ -using CryptoExchange.Net.Objects.Sockets; +using CryptoExchange.Net.Objects.Sockets; using Microsoft.Extensions.Logging; -namespace CryptoExchange.Net.Interfaces +namespace CryptoExchange.Net.Interfaces; + +/// +/// Websocket factory interface +/// +public interface IWebsocketFactory { /// - /// Websocket factory interface + /// Create a websocket for an url /// - public interface IWebsocketFactory - { - /// - /// Create a websocket for an url - /// - /// The logger - /// The parameters to use for the connection - /// - IWebsocket CreateWebsocket(ILogger logger, WebSocketParameters parameters); - } + /// The logger + /// The parameters to use for the connection + /// + IWebsocket CreateWebsocket(ILogger logger, WebSocketParameters parameters); } diff --git a/CryptoExchange.Net/LibraryHelpers.cs b/CryptoExchange.Net/LibraryHelpers.cs index f96fdc3..3519bac 100644 --- a/CryptoExchange.Net/LibraryHelpers.cs +++ b/CryptoExchange.Net/LibraryHelpers.cs @@ -1,47 +1,42 @@ -using System; -using System.Collections.Generic; -using System.Text; +namespace CryptoExchange.Net; -namespace CryptoExchange.Net +/// +/// Helpers for client libraries +/// +public static class LibraryHelpers { /// - /// Helpers for client libraries + /// Client order id separator /// - public static class LibraryHelpers + public const string ClientOrderIdSeparator = "JK"; + + /// + /// Apply broker id to a client order id + /// + /// + /// + /// + /// + /// + public static string ApplyBrokerId(string? clientOrderId, string brokerId, int maxLength, bool allowValueAdjustment) { - /// - /// Client order id separator - /// - public const string ClientOrderIdSeparator = "JK"; + var reservedLength = brokerId.Length + ClientOrderIdSeparator.Length; - /// - /// Apply broker id to a client order id - /// - /// - /// - /// - /// - /// - public static string ApplyBrokerId(string? clientOrderId, string brokerId, int maxLength, bool allowValueAdjustment) + if ((clientOrderId?.Length + reservedLength) > maxLength) + return clientOrderId!; + + if (!string.IsNullOrEmpty(clientOrderId)) { - var reservedLength = brokerId.Length + ClientOrderIdSeparator.Length; + if (allowValueAdjustment) + clientOrderId = brokerId + ClientOrderIdSeparator + clientOrderId; - 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; + return clientOrderId!; } + else + { + clientOrderId = ExchangeHelpers.AppendRandomString(brokerId + ClientOrderIdSeparator, maxLength); + } + + return clientOrderId; } } diff --git a/CryptoExchange.Net/Logging/Extensions/CryptoExchangeWebSocketClientLoggingExtension.cs b/CryptoExchange.Net/Logging/Extensions/CryptoExchangeWebSocketClientLoggingExtension.cs index 3a27529..a390c9e 100644 --- a/CryptoExchange.Net/Logging/Extensions/CryptoExchangeWebSocketClientLoggingExtension.cs +++ b/CryptoExchange.Net/Logging/Extensions/CryptoExchangeWebSocketClientLoggingExtension.cs @@ -1,387 +1,386 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using System; -namespace CryptoExchange.Net.Logging.Extensions -{ +namespace CryptoExchange.Net.Logging.Extensions; + #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member - public static class CryptoExchangeWebSocketClientLoggingExtension +public static class CryptoExchangeWebSocketClientLoggingExtension +{ + private static readonly Action _connecting; + private static readonly Action _connectionFailed; + private static readonly Action _connectingCanceled; + private static readonly Action _connected; + private static readonly Action _startingProcessing; + private static readonly Action _finishedProcessing; + private static readonly Action _attemptReconnect; + private static readonly Action _setReconnectUri; + private static readonly Action _addingBytesToSendBuffer; + private static readonly Action _reconnectRequested; + private static readonly Action _closeAsyncWaitingForExistingCloseTask; + private static readonly Action _closeAsyncSocketNotOpen; + private static readonly Action _closing; + private static readonly Action _closed; + private static readonly Action _disposing; + private static readonly Action _disposed; + private static readonly Action _sentBytes; + private static readonly Action _sendLoopStoppedWithException; + private static readonly Action _sendLoopFinished; + private static readonly Action _receivedCloseMessage; + private static readonly Action _receivedCloseConfirmation; + private static readonly Action _receivedPartialMessage; + private static readonly Action _receivedSingleMessage; + private static readonly Action _reassembledMessage; + private static readonly Action _discardIncompleteMessage; + private static readonly Action _receiveLoopStoppedWithException; + private static readonly Action _receiveLoopFinished; + private static readonly Action _startingTaskForNoDataReceivedCheck; + private static readonly Action _noDataReceiveTimeoutReconnect; + private static readonly Action _socketProcessingStateChanged; + private static readonly Action _socketPingTimeout; + + static CryptoExchangeWebSocketClientLoggingExtension() { - private static readonly Action _connecting; - private static readonly Action _connectionFailed; - private static readonly Action _connectingCanceled; - private static readonly Action _connected; - private static readonly Action _startingProcessing; - private static readonly Action _finishedProcessing; - private static readonly Action _attemptReconnect; - private static readonly Action _setReconnectUri; - private static readonly Action _addingBytesToSendBuffer; - private static readonly Action _reconnectRequested; - private static readonly Action _closeAsyncWaitingForExistingCloseTask; - private static readonly Action _closeAsyncSocketNotOpen; - private static readonly Action _closing; - private static readonly Action _closed; - private static readonly Action _disposing; - private static readonly Action _disposed; - private static readonly Action _sentBytes; - private static readonly Action _sendLoopStoppedWithException; - private static readonly Action _sendLoopFinished; - private static readonly Action _receivedCloseMessage; - private static readonly Action _receivedCloseConfirmation; - private static readonly Action _receivedPartialMessage; - private static readonly Action _receivedSingleMessage; - private static readonly Action _reassembledMessage; - private static readonly Action _discardIncompleteMessage; - private static readonly Action _receiveLoopStoppedWithException; - private static readonly Action _receiveLoopFinished; - private static readonly Action _startingTaskForNoDataReceivedCheck; - private static readonly Action _noDataReceiveTimeoutReconnect; - private static readonly Action _socketProcessingStateChanged; - private static readonly Action _socketPingTimeout; + _connecting = LoggerMessage.Define( + LogLevel.Debug, + new EventId(1000, "Connecting"), + "[Sckt {SocketId}] connecting"); - static CryptoExchangeWebSocketClientLoggingExtension() - { - _connecting = LoggerMessage.Define( - LogLevel.Debug, - new EventId(1000, "Connecting"), - "[Sckt {SocketId}] connecting"); + _connectionFailed = LoggerMessage.Define( + LogLevel.Error, + new EventId(1001, "ConnectionFailed"), + "[Sckt {SocketId}] connection failed: {ErrorMessage}"); - _connectionFailed = LoggerMessage.Define( - LogLevel.Error, - new EventId(1001, "ConnectionFailed"), - "[Sckt {SocketId}] connection failed: {ErrorMessage}"); + _connected = LoggerMessage.Define( + LogLevel.Debug, + new EventId(1002, "Connected"), + "[Sckt {SocketId}] connected to {Uri}"); - _connected = LoggerMessage.Define( - LogLevel.Debug, - new EventId(1002, "Connected"), - "[Sckt {SocketId}] connected to {Uri}"); + _startingProcessing = LoggerMessage.Define( + LogLevel.Debug, + new EventId(1003, "StartingProcessing"), + "[Sckt {SocketId}] starting processing tasks"); - _startingProcessing = LoggerMessage.Define( - LogLevel.Debug, - new EventId(1003, "StartingProcessing"), - "[Sckt {SocketId}] starting processing tasks"); + _finishedProcessing = LoggerMessage.Define( + LogLevel.Debug, + new EventId(1004, "FinishedProcessing"), + "[Sckt {SocketId}] processing tasks finished"); - _finishedProcessing = LoggerMessage.Define( - LogLevel.Debug, - new EventId(1004, "FinishedProcessing"), - "[Sckt {SocketId}] processing tasks finished"); + _attemptReconnect = LoggerMessage.Define( + LogLevel.Debug, + new EventId(1005, "AttemptReconnect"), + "[Sckt {SocketId}] attempting to reconnect"); - _attemptReconnect = LoggerMessage.Define( - LogLevel.Debug, - new EventId(1005, "AttemptReconnect"), - "[Sckt {SocketId}] attempting to reconnect"); + _setReconnectUri = LoggerMessage.Define( + LogLevel.Debug, + new EventId(1006, "SetReconnectUri"), + "[Sckt {SocketId}] reconnect URI set to {ReconnectUri}"); - _setReconnectUri = LoggerMessage.Define( - LogLevel.Debug, - new EventId(1006, "SetReconnectUri"), - "[Sckt {SocketId}] reconnect URI set to {ReconnectUri}"); + _addingBytesToSendBuffer = LoggerMessage.Define( + LogLevel.Trace, + new EventId(1007, "AddingBytesToSendBuffer"), + "[Sckt {SocketId}] [Req {RequestId}] adding {NumBytes} bytes to send buffer"); - _addingBytesToSendBuffer = LoggerMessage.Define( - LogLevel.Trace, - new EventId(1007, "AddingBytesToSendBuffer"), - "[Sckt {SocketId}] [Req {RequestId}] adding {NumBytes} bytes to send buffer"); + _reconnectRequested = LoggerMessage.Define( + LogLevel.Debug, + new EventId(1008, "ReconnectRequested"), + "[Sckt {SocketId}] reconnect requested"); - _reconnectRequested = LoggerMessage.Define( - LogLevel.Debug, - new EventId(1008, "ReconnectRequested"), - "[Sckt {SocketId}] reconnect requested"); + _closeAsyncWaitingForExistingCloseTask = LoggerMessage.Define( + LogLevel.Debug, + new EventId(1009, "CloseAsyncWaitForExistingCloseTask"), + "[Sckt {SocketId}] CloseAsync() waiting for existing close task"); - _closeAsyncWaitingForExistingCloseTask = LoggerMessage.Define( - LogLevel.Debug, - new EventId(1009, "CloseAsyncWaitForExistingCloseTask"), - "[Sckt {SocketId}] CloseAsync() waiting for existing close task"); + _closeAsyncSocketNotOpen = LoggerMessage.Define( + LogLevel.Debug, + new EventId(1010, "CloseAsyncSocketNotOpen"), + "[Sckt {SocketId}] CloseAsync() socket not open"); - _closeAsyncSocketNotOpen = LoggerMessage.Define( - LogLevel.Debug, - new EventId(1010, "CloseAsyncSocketNotOpen"), - "[Sckt {SocketId}] CloseAsync() socket not open"); + _closing = LoggerMessage.Define( + LogLevel.Debug, + new EventId(1011, "Closing"), + "[Sckt {SocketId}] closing"); - _closing = LoggerMessage.Define( - LogLevel.Debug, - new EventId(1011, "Closing"), - "[Sckt {SocketId}] closing"); + _closed = LoggerMessage.Define( + LogLevel.Debug, + new EventId(1012, "Closed"), + "[Sckt {SocketId}] closed"); - _closed = LoggerMessage.Define( - LogLevel.Debug, - new EventId(1012, "Closed"), - "[Sckt {SocketId}] closed"); + _disposing = LoggerMessage.Define( + LogLevel.Debug, + new EventId(1013, "Disposing"), + "[Sckt {SocketId}] disposing"); - _disposing = LoggerMessage.Define( - LogLevel.Debug, - new EventId(1013, "Disposing"), - "[Sckt {SocketId}] disposing"); + _disposed = LoggerMessage.Define( + LogLevel.Trace, + new EventId(1014, "Disposed"), + "[Sckt {SocketId}] disposed"); - _disposed = LoggerMessage.Define( - LogLevel.Trace, - new EventId(1014, "Disposed"), - "[Sckt {SocketId}] disposed"); + _sentBytes = LoggerMessage.Define( + LogLevel.Trace, + new EventId(1016, "SentBytes"), + "[Sckt {SocketId}] [Req {RequestId}] sent {NumBytes} bytes"); - _sentBytes = LoggerMessage.Define( - LogLevel.Trace, - new EventId(1016, "SentBytes"), - "[Sckt {SocketId}] [Req {RequestId}] sent {NumBytes} bytes"); + _sendLoopStoppedWithException = LoggerMessage.Define( + LogLevel.Warning, + new EventId(1017, "SendLoopStoppedWithException"), + "[Sckt {SocketId}] send loop stopped with exception: {ErrorMessage}"); - _sendLoopStoppedWithException = LoggerMessage.Define( - LogLevel.Warning, - new EventId(1017, "SendLoopStoppedWithException"), - "[Sckt {SocketId}] send loop stopped with exception: {ErrorMessage}"); + _sendLoopFinished = LoggerMessage.Define( + LogLevel.Debug, + new EventId(1018, "SendLoopFinished"), + "[Sckt {SocketId}] send loop finished"); - _sendLoopFinished = LoggerMessage.Define( - LogLevel.Debug, - new EventId(1018, "SendLoopFinished"), - "[Sckt {SocketId}] send loop finished"); + _receivedCloseMessage = LoggerMessage.Define( + LogLevel.Debug, + new EventId(1019, "ReceivedCloseMessage"), + "[Sckt {SocketId}] received `Close` message, CloseStatus: {CloseStatus}, CloseStatusDescription: {CloseStatusDescription}"); - _receivedCloseMessage = LoggerMessage.Define( - LogLevel.Debug, - new EventId(1019, "ReceivedCloseMessage"), - "[Sckt {SocketId}] received `Close` message, CloseStatus: {CloseStatus}, CloseStatusDescription: {CloseStatusDescription}"); + _receivedPartialMessage = LoggerMessage.Define( + LogLevel.Trace, + new EventId(1020, "ReceivedPartialMessage"), + "[Sckt {SocketId}] received {NumBytes} bytes in partial message"); - _receivedPartialMessage = LoggerMessage.Define( - LogLevel.Trace, - new EventId(1020, "ReceivedPartialMessage"), - "[Sckt {SocketId}] received {NumBytes} bytes in partial message"); + _receivedSingleMessage = LoggerMessage.Define( + LogLevel.Trace, + new EventId(1021, "ReceivedSingleMessage"), + "[Sckt {SocketId}] received {NumBytes} bytes in single message"); - _receivedSingleMessage = LoggerMessage.Define( - LogLevel.Trace, - new EventId(1021, "ReceivedSingleMessage"), - "[Sckt {SocketId}] received {NumBytes} bytes in single message"); + _reassembledMessage = LoggerMessage.Define( + LogLevel.Trace, + new EventId(1022, "ReassembledMessage"), + "[Sckt {SocketId}] reassembled message of {NumBytes} bytes"); - _reassembledMessage = LoggerMessage.Define( - LogLevel.Trace, - new EventId(1022, "ReassembledMessage"), - "[Sckt {SocketId}] reassembled message of {NumBytes} bytes"); + _discardIncompleteMessage = LoggerMessage.Define( + LogLevel.Trace, + new EventId(1023, "DiscardIncompleteMessage"), + "[Sckt {SocketId}] discarding incomplete message of {NumBytes} bytes"); - _discardIncompleteMessage = LoggerMessage.Define( - LogLevel.Trace, - new EventId(1023, "DiscardIncompleteMessage"), - "[Sckt {SocketId}] discarding incomplete message of {NumBytes} bytes"); + _receiveLoopStoppedWithException = LoggerMessage.Define( + LogLevel.Error, + new EventId(1024, "ReceiveLoopStoppedWithException"), + "[Sckt {SocketId}] receive loop stopped with exception"); - _receiveLoopStoppedWithException = LoggerMessage.Define( - LogLevel.Error, - new EventId(1024, "ReceiveLoopStoppedWithException"), - "[Sckt {SocketId}] receive loop stopped with exception"); + _receiveLoopFinished = LoggerMessage.Define( + LogLevel.Debug, + new EventId(1025, "ReceiveLoopFinished"), + "[Sckt {SocketId}] receive loop finished"); - _receiveLoopFinished = LoggerMessage.Define( - LogLevel.Debug, - new EventId(1025, "ReceiveLoopFinished"), - "[Sckt {SocketId}] receive loop finished"); + _startingTaskForNoDataReceivedCheck = LoggerMessage.Define( + LogLevel.Debug, + new EventId(1026, "StartingTaskForNoDataReceivedCheck"), + "[Sckt {SocketId}] starting task checking for no data received for {Timeout}"); - _startingTaskForNoDataReceivedCheck = LoggerMessage.Define( - LogLevel.Debug, - new EventId(1026, "StartingTaskForNoDataReceivedCheck"), - "[Sckt {SocketId}] starting task checking for no data received for {Timeout}"); + _noDataReceiveTimeoutReconnect = LoggerMessage.Define( + LogLevel.Warning, + new EventId(1027, "NoDataReceiveTimeoutReconnect"), + "[Sckt {SocketId}] no data received for {Timeout}, reconnecting socket"); - _noDataReceiveTimeoutReconnect = LoggerMessage.Define( - LogLevel.Warning, - new EventId(1027, "NoDataReceiveTimeoutReconnect"), - "[Sckt {SocketId}] no data received for {Timeout}, reconnecting socket"); + _receivedCloseConfirmation = LoggerMessage.Define( + LogLevel.Debug, + new EventId(1028, "ReceivedCloseMessage"), + "[Sckt {SocketId}] received `Close` message confirming our close request, CloseStatus: {CloseStatus}, CloseStatusDescription: {CloseStatusDescription}"); - _receivedCloseConfirmation = LoggerMessage.Define( - LogLevel.Debug, - new EventId(1028, "ReceivedCloseMessage"), - "[Sckt {SocketId}] received `Close` message confirming our close request, CloseStatus: {CloseStatus}, CloseStatusDescription: {CloseStatusDescription}"); + _socketProcessingStateChanged = LoggerMessage.Define( + LogLevel.Trace, + new EventId(1029, "SocketProcessingStateChanged"), + "[Sckt {Id}] processing state change: {PreviousState} -> {NewState}"); - _socketProcessingStateChanged = LoggerMessage.Define( - LogLevel.Trace, - new EventId(1029, "SocketProcessingStateChanged"), - "[Sckt {Id}] processing state change: {PreviousState} -> {NewState}"); + _socketPingTimeout = LoggerMessage.Define( + LogLevel.Warning, + new EventId(1030, "SocketPingTimeout"), + "[Sckt {Id}] ping frame timeout; reconnecting socket"); - _socketPingTimeout = LoggerMessage.Define( - LogLevel.Warning, - new EventId(1030, "SocketPingTimeout"), - "[Sckt {Id}] ping frame timeout; reconnecting socket"); - - _connectingCanceled = LoggerMessage.Define( - LogLevel.Debug, - new EventId(1031, "ConnectingCanceled"), - "[Sckt {SocketId}] connecting canceled"); + _connectingCanceled = LoggerMessage.Define( + LogLevel.Debug, + new EventId(1031, "ConnectingCanceled"), + "[Sckt {SocketId}] connecting canceled"); - } + } - public static void SocketConnecting( - this ILogger logger, int socketId) - { - _connecting(logger, socketId, null); - } + public static void SocketConnecting( + this ILogger logger, int socketId) + { + _connecting(logger, socketId, null); + } - public static void SocketConnectionFailed( - this ILogger logger, int socketId, string message, Exception e) - { - _connectionFailed(logger, socketId, message, e); - } + public static void SocketConnectionFailed( + this ILogger logger, int socketId, string message, Exception e) + { + _connectionFailed(logger, socketId, message, e); + } - public static void SocketConnected( - this ILogger logger, int socketId, Uri uri) - { - _connected(logger, socketId, uri, null); - } + public static void SocketConnected( + this ILogger logger, int socketId, Uri uri) + { + _connected(logger, socketId, uri, null); + } - public static void SocketStartingProcessing( - this ILogger logger, int socketId) - { - _startingProcessing(logger, socketId, null); - } + public static void SocketStartingProcessing( + this ILogger logger, int socketId) + { + _startingProcessing(logger, socketId, null); + } - public static void SocketFinishedProcessing( - this ILogger logger, int socketId) - { - _finishedProcessing(logger, socketId, null); - } + public static void SocketFinishedProcessing( + this ILogger logger, int socketId) + { + _finishedProcessing(logger, socketId, null); + } - public static void SocketAttemptReconnect( - this ILogger logger, int socketId) - { - _attemptReconnect(logger, socketId, null); - } + public static void SocketAttemptReconnect( + this ILogger logger, int socketId) + { + _attemptReconnect(logger, socketId, null); + } - public static void SocketSetReconnectUri( - this ILogger logger, int socketId, Uri uri) - { - _setReconnectUri(logger, socketId, uri, null); - } + public static void SocketSetReconnectUri( + this ILogger logger, int socketId, Uri uri) + { + _setReconnectUri(logger, socketId, uri, null); + } - public static void SocketAddingBytesToSendBuffer( - this ILogger logger, int socketId, int requestId, byte[] bytes) - { - _addingBytesToSendBuffer(logger, socketId, requestId, bytes.Length, null); - } + public static void SocketAddingBytesToSendBuffer( + this ILogger logger, int socketId, int requestId, byte[] bytes) + { + _addingBytesToSendBuffer(logger, socketId, requestId, bytes.Length, null); + } - public static void SocketReconnectRequested( - this ILogger logger, int socketId) - { - _reconnectRequested(logger, socketId, null); - } + public static void SocketReconnectRequested( + this ILogger logger, int socketId) + { + _reconnectRequested(logger, socketId, null); + } - public static void SocketCloseAsyncWaitingForExistingCloseTask( - this ILogger logger, int socketId) - { - _closeAsyncWaitingForExistingCloseTask(logger, socketId, null); - } + public static void SocketCloseAsyncWaitingForExistingCloseTask( + this ILogger logger, int socketId) + { + _closeAsyncWaitingForExistingCloseTask(logger, socketId, null); + } - public static void SocketCloseAsyncSocketNotOpen( - this ILogger logger, int socketId) - { - _closeAsyncSocketNotOpen(logger, socketId, null); - } + public static void SocketCloseAsyncSocketNotOpen( + this ILogger logger, int socketId) + { + _closeAsyncSocketNotOpen(logger, socketId, null); + } - public static void SocketClosing( - this ILogger logger, int socketId) - { - _closing(logger, socketId, null); - } + public static void SocketClosing( + this ILogger logger, int socketId) + { + _closing(logger, socketId, null); + } - public static void SocketClosed( - this ILogger logger, int socketId) - { - _closed(logger, socketId, null); - } + public static void SocketClosed( + this ILogger logger, int socketId) + { + _closed(logger, socketId, null); + } - public static void SocketDisposing( - this ILogger logger, int socketId) - { - _disposing(logger, socketId, null); - } + public static void SocketDisposing( + this ILogger logger, int socketId) + { + _disposing(logger, socketId, null); + } - public static void SocketDisposed( - this ILogger logger, int socketId) - { - _disposed(logger, socketId, null); - } + public static void SocketDisposed( + this ILogger logger, int socketId) + { + _disposed(logger, socketId, null); + } - public static void SocketSentBytes( - this ILogger logger, int socketId, int requestId, int numBytes) - { - _sentBytes(logger, socketId, requestId, numBytes, null); - } + public static void SocketSentBytes( + this ILogger logger, int socketId, int requestId, int numBytes) + { + _sentBytes(logger, socketId, requestId, numBytes, null); + } - public static void SocketSendLoopStoppedWithException( - this ILogger logger, int socketId, string message, Exception e) - { - _sendLoopStoppedWithException(logger, socketId, message, e); - } + public static void SocketSendLoopStoppedWithException( + this ILogger logger, int socketId, string message, Exception e) + { + _sendLoopStoppedWithException(logger, socketId, message, e); + } - public static void SocketSendLoopFinished( - this ILogger logger, int socketId) - { - _sendLoopFinished(logger, socketId, null); - } + public static void SocketSendLoopFinished( + this ILogger logger, int socketId) + { + _sendLoopFinished(logger, socketId, null); + } - public static void SocketReceivedCloseMessage( - this ILogger logger, int socketId, string webSocketCloseStatus, string closeStatusDescription) - { - _receivedCloseMessage(logger, socketId, webSocketCloseStatus, closeStatusDescription, null); - } + public static void SocketReceivedCloseMessage( + this ILogger logger, int socketId, string webSocketCloseStatus, string closeStatusDescription) + { + _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 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) - { - _receivedPartialMessage(logger, socketId, countBytes, null); - } + public static void SocketReceivedPartialMessage( + this ILogger logger, int socketId, int countBytes) + { + _receivedPartialMessage(logger, socketId, countBytes, null); + } - public static void SocketReceivedSingleMessage( - this ILogger logger, int socketId, int countBytes) - { - _receivedSingleMessage(logger, socketId, countBytes, null); - } + public static void SocketReceivedSingleMessage( + this ILogger logger, int socketId, int countBytes) + { + _receivedSingleMessage(logger, socketId, countBytes, null); + } - public static void SocketReassembledMessage( - this ILogger logger, int socketId, long countBytes) - { - _reassembledMessage(logger, socketId, countBytes, null); - } + public static void SocketReassembledMessage( + this ILogger logger, int socketId, long countBytes) + { + _reassembledMessage(logger, socketId, countBytes, null); + } - public static void SocketDiscardIncompleteMessage( - this ILogger logger, int socketId, long countBytes) - { - _discardIncompleteMessage(logger, socketId, countBytes, null); - } + public static void SocketDiscardIncompleteMessage( + this ILogger logger, int socketId, long countBytes) + { + _discardIncompleteMessage(logger, socketId, countBytes, null); + } - public static void SocketReceiveLoopStoppedWithException( - this ILogger logger, int socketId, Exception e) - { - _receiveLoopStoppedWithException(logger, socketId, e); - } + public static void SocketReceiveLoopStoppedWithException( + this ILogger logger, int socketId, Exception e) + { + _receiveLoopStoppedWithException(logger, socketId, e); + } - public static void SocketReceiveLoopFinished( - this ILogger logger, int socketId) - { - _receiveLoopFinished(logger, socketId, null); - } + public static void SocketReceiveLoopFinished( + this ILogger logger, int socketId) + { + _receiveLoopFinished(logger, socketId, null); + } - public static void SocketStartingTaskForNoDataReceivedCheck( - this ILogger logger, int socketId, TimeSpan? timeSpan) - { - _startingTaskForNoDataReceivedCheck(logger, socketId, timeSpan, null); - } + public static void SocketStartingTaskForNoDataReceivedCheck( + this ILogger logger, int socketId, TimeSpan? timeSpan) + { + _startingTaskForNoDataReceivedCheck(logger, socketId, timeSpan, null); + } - public static void SocketNoDataReceiveTimoutReconnect( - this ILogger logger, int socketId, TimeSpan? timeSpan) - { - _noDataReceiveTimeoutReconnect(logger, socketId, timeSpan, null); - } + public static void SocketNoDataReceiveTimoutReconnect( + this ILogger logger, int socketId, TimeSpan? timeSpan) + { + _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 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 SocketPingTimeout( + this ILogger logger, int socketId) + { + _socketPingTimeout(logger, socketId, null); + } - public static void SocketConnectingCanceled( - this ILogger logger, int socketId) - { - _connectingCanceled(logger, socketId, null); - } + public static void SocketConnectingCanceled( + this ILogger logger, int socketId) + { + _connectingCanceled(logger, socketId, null); } } diff --git a/CryptoExchange.Net/Logging/Extensions/RateLimitGateLoggingExtensions.cs b/CryptoExchange.Net/Logging/Extensions/RateLimitGateLoggingExtensions.cs index a327f18..9689386 100644 --- a/CryptoExchange.Net/Logging/Extensions/RateLimitGateLoggingExtensions.cs +++ b/CryptoExchange.Net/Logging/Extensions/RateLimitGateLoggingExtensions.cs @@ -1,79 +1,78 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using System; -namespace CryptoExchange.Net.Logging.Extensions -{ +namespace CryptoExchange.Net.Logging.Extensions; + #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member - public static class RateLimitGateLoggingExtensions +public static class RateLimitGateLoggingExtensions +{ + private static readonly Action _rateLimitRequestFailed; + private static readonly Action _rateLimitConnectionFailed; + private static readonly Action _rateLimitDelayingRequest; + private static readonly Action _rateLimitDelayingConnection; + private static readonly Action _rateLimitAppliedRequest; + private static readonly Action _rateLimitAppliedConnection; + + static RateLimitGateLoggingExtensions() { - private static readonly Action _rateLimitRequestFailed; - private static readonly Action _rateLimitConnectionFailed; - private static readonly Action _rateLimitDelayingRequest; - private static readonly Action _rateLimitDelayingConnection; - private static readonly Action _rateLimitAppliedRequest; - private static readonly Action _rateLimitAppliedConnection; + _rateLimitRequestFailed = LoggerMessage.Define( + LogLevel.Warning, + new EventId(6000, "RateLimitRequestFailed"), + "[Req {Id}] Call to {Path} failed because of ratelimit guard {Guard}; {Limit}"); - static RateLimitGateLoggingExtensions() - { - _rateLimitRequestFailed = LoggerMessage.Define( - LogLevel.Warning, - new EventId(6000, "RateLimitRequestFailed"), - "[Req {Id}] Call to {Path} failed because of ratelimit guard {Guard}; {Limit}"); + _rateLimitConnectionFailed = LoggerMessage.Define( + LogLevel.Warning, + new EventId(6001, "RateLimitConnectionFailed"), + "[Sckt {Id}] Connection failed because of ratelimit guard {Guard}; {Limit}"); - _rateLimitConnectionFailed = LoggerMessage.Define( - LogLevel.Warning, - new EventId(6001, "RateLimitConnectionFailed"), - "[Sckt {Id}] Connection failed because of ratelimit guard {Guard}; {Limit}"); + _rateLimitDelayingRequest = LoggerMessage.Define( + LogLevel.Warning, + new EventId(6002, "RateLimitDelayingRequest"), + "[Req {Id}] Delaying call to {Path} by {Delay} because of ratelimit guard {Guard}; {Limit}"); - _rateLimitDelayingRequest = LoggerMessage.Define( - LogLevel.Warning, - new EventId(6002, "RateLimitDelayingRequest"), - "[Req {Id}] Delaying call to {Path} by {Delay} because of ratelimit guard {Guard}; {Limit}"); + _rateLimitDelayingConnection = LoggerMessage.Define( + LogLevel.Warning, + new EventId(6003, "RateLimitDelayingConnection"), + "[Sckt {Id}] Delaying connection by {Delay} because of ratelimit guard {Guard}; {Limit}"); - _rateLimitDelayingConnection = LoggerMessage.Define( - LogLevel.Warning, - new EventId(6003, "RateLimitDelayingConnection"), - "[Sckt {Id}] Delaying connection by {Delay} because of ratelimit guard {Guard}; {Limit}"); + _rateLimitAppliedConnection = LoggerMessage.Define( + LogLevel.Trace, + new EventId(6004, "RateLimitDelayingConnection"), + "[Sckt {Id}] Connection passed ratelimit guard {Guard}; {Limit}, New count: {Current}"); - _rateLimitAppliedConnection = LoggerMessage.Define( - LogLevel.Trace, - new EventId(6004, "RateLimitDelayingConnection"), - "[Sckt {Id}] Connection passed ratelimit guard {Guard}; {Limit}, New count: {Current}"); + _rateLimitAppliedRequest = LoggerMessage.Define( + LogLevel.Trace, + new EventId(6005, "RateLimitAppliedRequest"), + "[Req {Id}] Call to {Path} passed ratelimit guard {Guard}; {Limit}, New count: {Current}"); + } - _rateLimitAppliedRequest = LoggerMessage.Define( - LogLevel.Trace, - new EventId(6005, "RateLimitAppliedRequest"), - "[Req {Id}] Call to {Path} passed ratelimit guard {Guard}; {Limit}, New count: {Current}"); - } + public static void RateLimitRequestFailed(this ILogger logger, int requestId, string path, string guard, string limit) + { + _rateLimitRequestFailed(logger, requestId, path, guard, limit, null); + } - public static void RateLimitRequestFailed(this ILogger logger, int requestId, string path, string guard, string limit) - { - _rateLimitRequestFailed(logger, requestId, path, guard, limit, null); - } + public static void RateLimitConnectionFailed(this ILogger logger, int connectionId, string guard, string limit) + { + _rateLimitConnectionFailed(logger, connectionId, guard, limit, null); + } - public static void RateLimitConnectionFailed(this ILogger logger, int connectionId, string guard, string limit) - { - _rateLimitConnectionFailed(logger, connectionId, guard, limit, null); - } + public static void RateLimitDelayingRequest(this ILogger logger, int requestId, string path, TimeSpan delay, string guard, string limit) + { + _rateLimitDelayingRequest(logger, requestId, path, delay, guard, limit, null); + } - public static void RateLimitDelayingRequest(this ILogger logger, int requestId, string path, TimeSpan delay, string guard, string limit) - { - _rateLimitDelayingRequest(logger, requestId, path, delay, guard, limit, null); - } + public static void RateLimitDelayingConnection(this ILogger logger, int connectionId, TimeSpan delay, string guard, string limit) + { + _rateLimitDelayingConnection(logger, connectionId, delay, guard, limit, null); + } - public static void RateLimitDelayingConnection(this ILogger logger, int connectionId, TimeSpan delay, string guard, string limit) - { - _rateLimitDelayingConnection(logger, connectionId, delay, guard, limit, null); - } + public static void RateLimitAppliedConnection(this ILogger logger, int connectionId, string guard, string limit, int current) + { + _rateLimitAppliedConnection(logger, connectionId, guard, limit, current, null); + } - public static void RateLimitAppliedConnection(this ILogger logger, int connectionId, string guard, string limit, int current) - { - _rateLimitAppliedConnection(logger, connectionId, guard, limit, current, null); - } - - public static void RateLimitAppliedRequest(this ILogger logger, int requestIdId, string path, string guard, string limit, int current) - { - _rateLimitAppliedRequest(logger, requestIdId, path, guard, limit, current, null); - } + public static void RateLimitAppliedRequest(this ILogger logger, int requestIdId, string path, string guard, string limit, int current) + { + _rateLimitAppliedRequest(logger, requestIdId, path, guard, limit, current, null); } } diff --git a/CryptoExchange.Net/Logging/Extensions/RestApiClientLoggingExtensions.cs b/CryptoExchange.Net/Logging/Extensions/RestApiClientLoggingExtensions.cs index f5078d3..b0a61b9 100644 --- a/CryptoExchange.Net/Logging/Extensions/RestApiClientLoggingExtensions.cs +++ b/CryptoExchange.Net/Logging/Extensions/RestApiClientLoggingExtensions.cs @@ -1,159 +1,158 @@ -using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects; using Microsoft.Extensions.Logging; using System; using System.Net; using System.Net.Http; -namespace CryptoExchange.Net.Logging.Extensions -{ +namespace CryptoExchange.Net.Logging.Extensions; + #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member - public static class RestApiClientLoggingExtensions +public static class RestApiClientLoggingExtensions +{ + private static readonly Action _restApiErrorReceived; + private static readonly Action _restApiResponseReceived; + private static readonly Action _restApiFailedToSyncTime; + private static readonly Action _restApiNoApiCredentials; + private static readonly Action _restApiCreatingRequest; + private static readonly Action _restApiSendingRequest; + private static readonly Action _restApiRateLimitRetry; + private static readonly Action _restApiRateLimitPauseUntil; + private static readonly Action _restApiSendRequest; + private static readonly Action _restApiCheckingCache; + private static readonly Action _restApiCacheHit; + private static readonly Action _restApiCacheNotHit; + private static readonly Action _restApiCancellationRequested; + + static RestApiClientLoggingExtensions() { - private static readonly Action _restApiErrorReceived; - private static readonly Action _restApiResponseReceived; - private static readonly Action _restApiFailedToSyncTime; - private static readonly Action _restApiNoApiCredentials; - private static readonly Action _restApiCreatingRequest; - private static readonly Action _restApiSendingRequest; - private static readonly Action _restApiRateLimitRetry; - private static readonly Action _restApiRateLimitPauseUntil; - private static readonly Action _restApiSendRequest; - private static readonly Action _restApiCheckingCache; - private static readonly Action _restApiCacheHit; - private static readonly Action _restApiCacheNotHit; - private static readonly Action _restApiCancellationRequested; + _restApiErrorReceived = LoggerMessage.Define( + LogLevel.Warning, + new EventId(4000, "RestApiErrorReceived"), + "[Req {RequestId}] {ResponseStatusCode} - Error received in {ResponseTime}ms: {ErrorMessage}, Data: {OriginalData}"); - static RestApiClientLoggingExtensions() - { - _restApiErrorReceived = LoggerMessage.Define( - LogLevel.Warning, - new EventId(4000, "RestApiErrorReceived"), - "[Req {RequestId}] {ResponseStatusCode} - Error received in {ResponseTime}ms: {ErrorMessage}, Data: {OriginalData}"); + _restApiResponseReceived = LoggerMessage.Define( + LogLevel.Debug, + new EventId(4001, "RestApiResponseReceived"), + "[Req {RequestId}] {ResponseStatusCode} - Response received in {ResponseTime}ms: {OriginalData}"); - _restApiResponseReceived = LoggerMessage.Define( - LogLevel.Debug, - new EventId(4001, "RestApiResponseReceived"), - "[Req {RequestId}] {ResponseStatusCode} - Response received in {ResponseTime}ms: {OriginalData}"); + _restApiFailedToSyncTime = LoggerMessage.Define( + LogLevel.Debug, + new EventId(4002, "RestApiFailedToSyncTime"), + "[Req {RequestId}] Failed to sync time, aborting request: {ErrorMessage}"); - _restApiFailedToSyncTime = LoggerMessage.Define( - LogLevel.Debug, - new EventId(4002, "RestApiFailedToSyncTime"), - "[Req {RequestId}] Failed to sync time, aborting request: {ErrorMessage}"); + _restApiNoApiCredentials = LoggerMessage.Define( + LogLevel.Warning, + new EventId(4003, "RestApiNoApiCredentials"), + "[Req {RequestId}] Request {RestApiUri} failed because no ApiCredentials were provided"); - _restApiNoApiCredentials = LoggerMessage.Define( - LogLevel.Warning, - new EventId(4003, "RestApiNoApiCredentials"), - "[Req {RequestId}] Request {RestApiUri} failed because no ApiCredentials were provided"); + _restApiCreatingRequest = LoggerMessage.Define( + LogLevel.Information, + new EventId(4004, "RestApiCreatingRequest"), + "[Req {RequestId}] Creating request for {RestApiUri}"); - _restApiCreatingRequest = LoggerMessage.Define( - LogLevel.Information, - new EventId(4004, "RestApiCreatingRequest"), - "[Req {RequestId}] Creating request for {RestApiUri}"); + _restApiSendingRequest = LoggerMessage.Define( + LogLevel.Trace, + new EventId(4005, "RestApiSendingRequest"), + "[Req {RequestId}] Sending {Method} {Signed} request to {RestApiUri}{Query}"); - _restApiSendingRequest = LoggerMessage.Define( - LogLevel.Trace, - new EventId(4005, "RestApiSendingRequest"), - "[Req {RequestId}] Sending {Method} {Signed} request to {RestApiUri}{Query}"); + _restApiRateLimitRetry = LoggerMessage.Define( + LogLevel.Warning, + new EventId(4006, "RestApiRateLimitRetry"), + "[Req {RequestId}] Received ratelimit error, retrying after {Timestamp}"); - _restApiRateLimitRetry = LoggerMessage.Define( - LogLevel.Warning, - new EventId(4006, "RestApiRateLimitRetry"), - "[Req {RequestId}] Received ratelimit error, retrying after {Timestamp}"); + _restApiRateLimitPauseUntil = LoggerMessage.Define( + LogLevel.Warning, + new EventId(4007, "RestApiRateLimitPauseUntil"), + "[Req {RequestId}] Ratelimit error from server, pausing requests until {Until}"); - _restApiRateLimitPauseUntil = LoggerMessage.Define( - LogLevel.Warning, - new EventId(4007, "RestApiRateLimitPauseUntil"), - "[Req {RequestId}] Ratelimit error from server, pausing requests until {Until}"); + _restApiSendRequest = LoggerMessage.Define( + LogLevel.Debug, + new EventId(4008, "RestApiSendRequest"), + "[Req {RequestId}] Sending {Definition} request with body {Body}, query parameters {Query} and headers {Headers}"); - _restApiSendRequest = LoggerMessage.Define( - LogLevel.Debug, - new EventId(4008, "RestApiSendRequest"), - "[Req {RequestId}] Sending {Definition} request with body {Body}, query parameters {Query} and headers {Headers}"); + _restApiCheckingCache = LoggerMessage.Define( + LogLevel.Trace, + new EventId(4009, "RestApiCheckingCache"), + "Checking cache for key {Key}"); - _restApiCheckingCache = LoggerMessage.Define( - LogLevel.Trace, - new EventId(4009, "RestApiCheckingCache"), - "Checking cache for key {Key}"); + _restApiCacheHit = LoggerMessage.Define( + LogLevel.Trace, + new EventId(4010, "RestApiCacheHit"), + "Cache hit for key {Key}"); - _restApiCacheHit = LoggerMessage.Define( - LogLevel.Trace, - new EventId(4010, "RestApiCacheHit"), - "Cache hit for key {Key}"); + _restApiCacheNotHit = LoggerMessage.Define( + LogLevel.Trace, + new EventId(4011, "RestApiCacheNotHit"), + "Cache not hit for key {Key}"); - _restApiCacheNotHit = LoggerMessage.Define( - LogLevel.Trace, - new EventId(4011, "RestApiCacheNotHit"), - "Cache not hit for key {Key}"); + _restApiCancellationRequested = LoggerMessage.Define( + LogLevel.Debug, + new EventId(4012, "RestApiCancellationRequested"), + "[Req {RequestId}] Request cancelled by user"); - _restApiCancellationRequested = LoggerMessage.Define( - LogLevel.Debug, - new EventId(4012, "RestApiCancellationRequested"), - "[Req {RequestId}] Request cancelled by user"); + } - } + public static void RestApiErrorReceived(this ILogger logger, int? requestId, HttpStatusCode? responseStatusCode, long responseTime, string? error, string? originalData, Exception? exception) + { + _restApiErrorReceived(logger, requestId, (int?)responseStatusCode, responseTime, error, originalData, exception); + } - 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, originalData, exception); - } + public static void RestApiResponseReceived(this ILogger logger, int? requestId, HttpStatusCode? responseStatusCode, long responseTime, string? originalData) + { + _restApiResponseReceived(logger, requestId, (int?)responseStatusCode, responseTime, originalData, null); + } - public static void RestApiResponseReceived(this ILogger logger, int? requestId, HttpStatusCode? responseStatusCode, long responseTime, string? originalData) - { - _restApiResponseReceived(logger, requestId, (int?)responseStatusCode, responseTime, originalData, null); - } + public static void RestApiFailedToSyncTime(this ILogger logger, int requestId, string error) + { + _restApiFailedToSyncTime(logger, requestId, error, null); + } - public static void RestApiFailedToSyncTime(this ILogger logger, int requestId, string error) - { - _restApiFailedToSyncTime(logger, requestId, error, null); - } + public static void RestApiNoApiCredentials(this ILogger logger, int requestId, string uri) + { + _restApiNoApiCredentials(logger, requestId, uri, null); + } - public static void RestApiNoApiCredentials(this ILogger logger, int requestId, string uri) - { - _restApiNoApiCredentials(logger, requestId, uri, null); - } + public static void RestApiCreatingRequest(this ILogger logger, int requestId, Uri uri) + { + _restApiCreatingRequest(logger, requestId, uri, null); + } - public static void RestApiCreatingRequest(this ILogger logger, int requestId, Uri uri) - { - _restApiCreatingRequest(logger, requestId, uri, null); - } + public static void RestApiSendingRequest(this ILogger logger, int requestId, HttpMethod method, string signed, Uri uri, string paramString) + { + _restApiSendingRequest(logger, requestId, method, signed, uri, paramString, null); + } - public static void RestApiSendingRequest(this ILogger logger, int requestId, HttpMethod method, string signed, Uri uri, string paramString) - { - _restApiSendingRequest(logger, requestId, method, signed, uri, paramString, null); - } + public static void RestApiRateLimitRetry(this ILogger logger, int requestId, DateTime retryAfter) + { + _restApiRateLimitRetry(logger, requestId, retryAfter, null); + } - public static void RestApiRateLimitRetry(this ILogger logger, int requestId, DateTime retryAfter) - { - _restApiRateLimitRetry(logger, requestId, retryAfter, null); - } + public static void RestApiRateLimitPauseUntil(this ILogger logger, int requestId, DateTime retryAfter) + { + _restApiRateLimitPauseUntil(logger, requestId, retryAfter, null); + } - public static void RestApiRateLimitPauseUntil(this ILogger logger, int requestId, DateTime retryAfter) - { - _restApiRateLimitPauseUntil(logger, requestId, retryAfter, null); - } + public static void RestApiSendRequest(this ILogger logger, int requestId, RequestDefinition definition, string? body, string query, string headers) + { + _restApiSendRequest(logger, requestId, definition, body, query, headers, null); + } - public static void RestApiSendRequest(this ILogger logger, int requestId, RequestDefinition definition, string? body, string query, string headers) - { - _restApiSendRequest(logger, requestId, definition, body, query, headers, null); - } + public static void CheckingCache(this ILogger logger, string key) + { + _restApiCheckingCache(logger, key, null); + } - public static void CheckingCache(this ILogger logger, string key) - { - _restApiCheckingCache(logger, key, null); - } + public static void CacheHit(this ILogger logger, string key) + { + _restApiCacheHit(logger, key, null); + } - public static void CacheHit(this ILogger logger, string key) - { - _restApiCacheHit(logger, key, null); - } - - public static void CacheNotHit(this ILogger logger, string key) - { - _restApiCacheNotHit(logger, key, null); - } - public static void RestApiCancellationRequested(this ILogger logger, int? requestId) - { - _restApiCancellationRequested(logger, requestId, null); - } + public static void CacheNotHit(this ILogger logger, string key) + { + _restApiCacheNotHit(logger, key, null); + } + public static void RestApiCancellationRequested(this ILogger logger, int? requestId) + { + _restApiCancellationRequested(logger, requestId, null); } } diff --git a/CryptoExchange.Net/Logging/Extensions/SocketApiClientLoggingExtension.cs b/CryptoExchange.Net/Logging/Extensions/SocketApiClientLoggingExtension.cs index c8e553a..838276b 100644 --- a/CryptoExchange.Net/Logging/Extensions/SocketApiClientLoggingExtension.cs +++ b/CryptoExchange.Net/Logging/Extensions/SocketApiClientLoggingExtension.cs @@ -1,200 +1,199 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using System; -namespace CryptoExchange.Net.Logging.Extensions -{ +namespace CryptoExchange.Net.Logging.Extensions; + #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member - public static class SocketApiClientLoggingExtension +public static class SocketApiClientLoggingExtension +{ + private static readonly Action _failedToAddSubscriptionRetryOnDifferentConnection; + private static readonly Action _hasBeenPausedCantSubscribeAtThisMoment; + private static readonly Action _failedToSubscribe; + private static readonly Action _cancellationTokenSetClosingSubscription; + private static readonly Action _subscriptionCompletedSuccessfully; + private static readonly Action _hasBeenPausedCantSendQueryAtThisMoment; + private static readonly Action _attemptingToAuthenticate; + private static readonly Action _authenticationFailed; + private static readonly Action _authenticated; + private static readonly Action _failedToDetermineConnectionUrl; + private static readonly Action _connectionAddressSetTo; + private static readonly Action _socketCreatedForAddress; + private static readonly Action _unsubscribingAll; + private static readonly Action _disposingSocketClient; + private static readonly Action _unsubscribingSubscription; + private static readonly Action _reconnectingAllConnections; + private static readonly Action _addingRetryAfterGuard; + + static SocketApiClientLoggingExtension() { - private static readonly Action _failedToAddSubscriptionRetryOnDifferentConnection; - private static readonly Action _hasBeenPausedCantSubscribeAtThisMoment; - private static readonly Action _failedToSubscribe; - private static readonly Action _cancellationTokenSetClosingSubscription; - private static readonly Action _subscriptionCompletedSuccessfully; - private static readonly Action _hasBeenPausedCantSendQueryAtThisMoment; - private static readonly Action _attemptingToAuthenticate; - private static readonly Action _authenticationFailed; - private static readonly Action _authenticated; - private static readonly Action _failedToDetermineConnectionUrl; - private static readonly Action _connectionAddressSetTo; - private static readonly Action _socketCreatedForAddress; - private static readonly Action _unsubscribingAll; - private static readonly Action _disposingSocketClient; - private static readonly Action _unsubscribingSubscription; - private static readonly Action _reconnectingAllConnections; - private static readonly Action _addingRetryAfterGuard; + _failedToAddSubscriptionRetryOnDifferentConnection = LoggerMessage.Define( + LogLevel.Trace, + new EventId(3000, "FailedToAddSubscriptionRetryOnDifferentConnection"), + "[Sckt {SocketId}] failed to add subscription, retrying on different connection"); - static SocketApiClientLoggingExtension() - { - _failedToAddSubscriptionRetryOnDifferentConnection = LoggerMessage.Define( - LogLevel.Trace, - new EventId(3000, "FailedToAddSubscriptionRetryOnDifferentConnection"), - "[Sckt {SocketId}] failed to add subscription, retrying on different connection"); + _hasBeenPausedCantSubscribeAtThisMoment = LoggerMessage.Define( + LogLevel.Warning, + new EventId(3001, "HasBeenPausedCantSubscribeAtThisMoment"), + "[Sckt {SocketId}] has been paused, can't subscribe at this moment"); - _hasBeenPausedCantSubscribeAtThisMoment = LoggerMessage.Define( - LogLevel.Warning, - new EventId(3001, "HasBeenPausedCantSubscribeAtThisMoment"), - "[Sckt {SocketId}] has been paused, can't subscribe at this moment"); + _failedToSubscribe = LoggerMessage.Define( + LogLevel.Warning, + new EventId(3002, "FailedToSubscribe"), + "[Sckt {SocketId}] failed to subscribe: {ErrorMessage}"); - _failedToSubscribe = LoggerMessage.Define( - LogLevel.Warning, - new EventId(3002, "FailedToSubscribe"), - "[Sckt {SocketId}] failed to subscribe: {ErrorMessage}"); + _cancellationTokenSetClosingSubscription = LoggerMessage.Define( + LogLevel.Information, + new EventId(3003, "CancellationTokenSetClosingSubscription"), + "[Sckt {SocketId}] Cancellation token set, closing subscription {SubscriptionId}"); - _cancellationTokenSetClosingSubscription = LoggerMessage.Define( - LogLevel.Information, - new EventId(3003, "CancellationTokenSetClosingSubscription"), - "[Sckt {SocketId}] Cancellation token set, closing subscription {SubscriptionId}"); + _subscriptionCompletedSuccessfully = LoggerMessage.Define( + LogLevel.Information, + new EventId(3004, "SubscriptionCompletedSuccessfully"), + "[Sckt {SocketId}] subscription {SubscriptionId} completed successfully"); - _subscriptionCompletedSuccessfully = LoggerMessage.Define( - LogLevel.Information, - new EventId(3004, "SubscriptionCompletedSuccessfully"), - "[Sckt {SocketId}] subscription {SubscriptionId} completed successfully"); + _hasBeenPausedCantSendQueryAtThisMoment = LoggerMessage.Define( + LogLevel.Warning, + new EventId(3005, "HasBeenPausedCantSendQueryAtThisMoment"), + "[Sckt {SocketId}] has been paused, can't send query at this moment"); - _hasBeenPausedCantSendQueryAtThisMoment = LoggerMessage.Define( - LogLevel.Warning, - new EventId(3005, "HasBeenPausedCantSendQueryAtThisMoment"), - "[Sckt {SocketId}] has been paused, can't send query at this moment"); + _attemptingToAuthenticate = LoggerMessage.Define( + LogLevel.Debug, + new EventId(3006, "AttemptingToAuthenticate"), + "[Sckt {SocketId}] Attempting to authenticate"); - _attemptingToAuthenticate = LoggerMessage.Define( - LogLevel.Debug, - new EventId(3006, "AttemptingToAuthenticate"), - "[Sckt {SocketId}] Attempting to authenticate"); + _authenticationFailed = LoggerMessage.Define( + LogLevel.Warning, + new EventId(3007, "AuthenticationFailed"), + "[Sckt {SocketId}] authentication failed"); - _authenticationFailed = LoggerMessage.Define( - LogLevel.Warning, - new EventId(3007, "AuthenticationFailed"), - "[Sckt {SocketId}] authentication failed"); + _authenticated = LoggerMessage.Define( + LogLevel.Debug, + new EventId(3008, "Authenticated"), + "[Sckt {SocketId}] authenticated"); - _authenticated = LoggerMessage.Define( - LogLevel.Debug, - new EventId(3008, "Authenticated"), - "[Sckt {SocketId}] authenticated"); + _failedToDetermineConnectionUrl = LoggerMessage.Define( + LogLevel.Warning, + new EventId(3009, "FailedToDetermineConnectionUrl"), + "Failed to determine connection url: {ErrorMessage}"); - _failedToDetermineConnectionUrl = LoggerMessage.Define( - LogLevel.Warning, - new EventId(3009, "FailedToDetermineConnectionUrl"), - "Failed to determine connection url: {ErrorMessage}"); + _connectionAddressSetTo = LoggerMessage.Define( + LogLevel.Debug, + new EventId(3010, "ConnectionAddressSetTo"), + "Connection address set to {ConnectionAddress}"); - _connectionAddressSetTo = LoggerMessage.Define( - LogLevel.Debug, - new EventId(3010, "ConnectionAddressSetTo"), - "Connection address set to {ConnectionAddress}"); + _socketCreatedForAddress = LoggerMessage.Define( + LogLevel.Debug, + new EventId(3011, "SocketCreatedForAddress"), + "[Sckt {SocketId}] created for {Address}"); - _socketCreatedForAddress = LoggerMessage.Define( - LogLevel.Debug, - new EventId(3011, "SocketCreatedForAddress"), - "[Sckt {SocketId}] created for {Address}"); + _unsubscribingAll = LoggerMessage.Define( + LogLevel.Information, + new EventId(3013, "UnsubscribingAll"), + "Unsubscribing all {SubscriptionCount} subscriptions"); - _unsubscribingAll = LoggerMessage.Define( - LogLevel.Information, - new EventId(3013, "UnsubscribingAll"), - "Unsubscribing all {SubscriptionCount} subscriptions"); + _disposingSocketClient = LoggerMessage.Define( + LogLevel.Debug, + new EventId(3015, "DisposingSocketClient"), + "Disposing socket client, closing all subscriptions"); - _disposingSocketClient = LoggerMessage.Define( - LogLevel.Debug, - new EventId(3015, "DisposingSocketClient"), - "Disposing socket client, closing all subscriptions"); + _unsubscribingSubscription = LoggerMessage.Define( + LogLevel.Information, + new EventId(3016, "UnsubscribingSubscription"), + "[Sckt {SocketId}] Unsubscribing subscription {SubscriptionId}"); - _unsubscribingSubscription = LoggerMessage.Define( - LogLevel.Information, - new EventId(3016, "UnsubscribingSubscription"), - "[Sckt {SocketId}] Unsubscribing subscription {SubscriptionId}"); + _reconnectingAllConnections = LoggerMessage.Define( + LogLevel.Information, + new EventId(3017, "ReconnectingAll"), + "Reconnecting all {ConnectionCount} connections"); - _reconnectingAllConnections = LoggerMessage.Define( - LogLevel.Information, - new EventId(3017, "ReconnectingAll"), - "Reconnecting all {ConnectionCount} connections"); + _addingRetryAfterGuard = LoggerMessage.Define( + LogLevel.Warning, + new EventId(3018, "AddRetryAfterGuard"), + "Adding RetryAfterGuard ({RetryAfter}) because the connection attempt was rate limited"); + } - _addingRetryAfterGuard = LoggerMessage.Define( - 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) + { + _failedToAddSubscriptionRetryOnDifferentConnection(logger, socketId, null); + } - public static void FailedToAddSubscriptionRetryOnDifferentConnection(this ILogger logger, int socketId) - { - _failedToAddSubscriptionRetryOnDifferentConnection(logger, socketId, null); - } + public static void HasBeenPausedCantSubscribeAtThisMoment(this ILogger logger, int socketId) + { + _hasBeenPausedCantSubscribeAtThisMoment(logger, socketId, null); + } - public static void HasBeenPausedCantSubscribeAtThisMoment(this ILogger logger, int socketId) - { - _hasBeenPausedCantSubscribeAtThisMoment(logger, socketId, null); - } + public static void FailedToSubscribe(this ILogger logger, int socketId, string? error) + { + _failedToSubscribe(logger, socketId, error, null); + } - public static void FailedToSubscribe(this ILogger logger, int socketId, string? error) - { - _failedToSubscribe(logger, socketId, error, null); - } + public static void CancellationTokenSetClosingSubscription(this ILogger logger, int socketId, int subscriptionId) + { + _cancellationTokenSetClosingSubscription(logger, socketId, subscriptionId, null); + } - public static void CancellationTokenSetClosingSubscription(this ILogger logger, int socketId, int subscriptionId) - { - _cancellationTokenSetClosingSubscription(logger, socketId, subscriptionId, null); - } + public static void SubscriptionCompletedSuccessfully(this ILogger logger, int socketId, int subscriptionId) + { + _subscriptionCompletedSuccessfully(logger, socketId, subscriptionId, null); + } - public static void SubscriptionCompletedSuccessfully(this ILogger logger, int socketId, int subscriptionId) - { - _subscriptionCompletedSuccessfully(logger, socketId, subscriptionId, null); - } + public static void HasBeenPausedCantSendQueryAtThisMoment(this ILogger logger, int socketId) + { + _hasBeenPausedCantSendQueryAtThisMoment(logger, socketId, null); + } - public static void HasBeenPausedCantSendQueryAtThisMoment(this ILogger logger, int socketId) - { - _hasBeenPausedCantSendQueryAtThisMoment(logger, socketId, null); - } + public static void AttemptingToAuthenticate(this ILogger logger, int socketId) + { + _attemptingToAuthenticate(logger, socketId, null); + } - public static void AttemptingToAuthenticate(this ILogger logger, int socketId) - { - _attemptingToAuthenticate(logger, socketId, null); - } + public static void AuthenticationFailed(this ILogger logger, int socketId) + { + _authenticationFailed(logger, socketId, null); + } - public static void AuthenticationFailed(this ILogger logger, int socketId) - { - _authenticationFailed(logger, socketId, null); - } + public static void Authenticated(this ILogger logger, int socketId) + { + _authenticated(logger, socketId, null); + } - public static void Authenticated(this ILogger logger, int socketId) - { - _authenticated(logger, socketId, null); - } + public static void FailedToDetermineConnectionUrl(this ILogger logger, string? error) + { + _failedToDetermineConnectionUrl(logger, error, null); + } - public static void FailedToDetermineConnectionUrl(this ILogger logger, string? error) - { - _failedToDetermineConnectionUrl(logger, error, null); - } + public static void ConnectionAddressSetTo(this ILogger logger, string connectionAddress) + { + _connectionAddressSetTo(logger, connectionAddress, null); + } - public static void ConnectionAddressSetTo(this ILogger logger, string connectionAddress) - { - _connectionAddressSetTo(logger, connectionAddress, null); - } + public static void SocketCreatedForAddress(this ILogger logger, int socketId, string address) + { + _socketCreatedForAddress(logger, socketId, address, null); + } - public static void SocketCreatedForAddress(this ILogger logger, int socketId, string address) - { - _socketCreatedForAddress(logger, socketId, address, null); - } + public static void UnsubscribingAll(this ILogger logger, int subscriptionCount) + { + _unsubscribingAll(logger, subscriptionCount, null); + } - public static void UnsubscribingAll(this ILogger logger, int subscriptionCount) - { - _unsubscribingAll(logger, subscriptionCount, null); - } + public static void DisposingSocketClient(this ILogger logger) + { + _disposingSocketClient(logger, null); + } - public static void DisposingSocketClient(this ILogger logger) - { - _disposingSocketClient(logger, null); - } + public static void UnsubscribingSubscription(this ILogger logger, int socketId, int subscriptionId) + { + _unsubscribingSubscription(logger, socketId, subscriptionId, null); + } - public static void UnsubscribingSubscription(this ILogger logger, int socketId, int subscriptionId) - { - _unsubscribingSubscription(logger, socketId, subscriptionId, null); - } + public static void ReconnectingAllConnections(this ILogger logger, int connectionCount) + { + _reconnectingAllConnections(logger, connectionCount, null); + } - public static void ReconnectingAllConnections(this ILogger logger, int connectionCount) - { - _reconnectingAllConnections(logger, connectionCount, null); - } - - public static void AddingRetryAfterGuard(this ILogger logger, DateTime retryAfter) - { - _addingRetryAfterGuard(logger, retryAfter, null); - } + public static void AddingRetryAfterGuard(this ILogger logger, DateTime retryAfter) + { + _addingRetryAfterGuard(logger, retryAfter, null); } } diff --git a/CryptoExchange.Net/Logging/Extensions/SocketConnectionLoggingExtension.cs b/CryptoExchange.Net/Logging/Extensions/SocketConnectionLoggingExtension.cs index af6a61b..31add4a 100644 --- a/CryptoExchange.Net/Logging/Extensions/SocketConnectionLoggingExtension.cs +++ b/CryptoExchange.Net/Logging/Extensions/SocketConnectionLoggingExtension.cs @@ -1,349 +1,348 @@ -using System; +using System; using System.Net.WebSockets; using Microsoft.Extensions.Logging; -namespace CryptoExchange.Net.Logging.Extensions -{ +namespace CryptoExchange.Net.Logging.Extensions; + #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member - public static class SocketConnectionLoggingExtension +public static class SocketConnectionLoggingExtension +{ + private static readonly Action _activityPaused; + private static readonly Action _socketStatusChanged; + private static readonly Action _failedReconnectProcessing; + private static readonly Action _unknownExceptionWhileProcessingReconnection; + private static readonly Action _webSocketErrorCodeAndDetails; + private static readonly Action _webSocketError; + private static readonly Action _messageSentNotPending; + private static readonly Action _receivedData; + private static readonly Action _failedToParse; + private static readonly Action _failedToEvaluateMessage; + private static readonly Action _errorProcessingMessage; + private static readonly Action _processorMatched; + private static readonly Action _receivedMessageNotRecognized; + private static readonly Action _failedToDeserializeMessage; + private static readonly Action _userMessageProcessingFailed; + private static readonly Action _messageProcessed; + private static readonly Action _closingSubscription; + private static readonly Action _notUnsubscribingSubscriptionBecauseDuplicateRunning; + private static readonly Action _alreadyClosing; + private static readonly Action _closingNoMoreSubscriptions; + private static readonly Action _addingNewSubscription; + private static readonly Action _nothingToResubscribeCloseConnection; + private static readonly Action _failedAuthenticationDisconnectAndReconnect; + private static readonly Action _authenticationSucceeded; + private static readonly Action _failedRequestRevitalization; + private static readonly Action _allSubscriptionResubscribed; + private static readonly Action _subscriptionUnsubscribed; + private static readonly Action _sendingPeriodic; + private static readonly Action _periodicSendFailed; + private static readonly Action _sendingData; + private static readonly Action _receivedMessageNotMatchedToAnyListener; + private static readonly Action _sendingByteData; + + static SocketConnectionLoggingExtension() { - private static readonly Action _activityPaused; - private static readonly Action _socketStatusChanged; - private static readonly Action _failedReconnectProcessing; - private static readonly Action _unknownExceptionWhileProcessingReconnection; - private static readonly Action _webSocketErrorCodeAndDetails; - private static readonly Action _webSocketError; - private static readonly Action _messageSentNotPending; - private static readonly Action _receivedData; - private static readonly Action _failedToParse; - private static readonly Action _failedToEvaluateMessage; - private static readonly Action _errorProcessingMessage; - private static readonly Action _processorMatched; - private static readonly Action _receivedMessageNotRecognized; - private static readonly Action _failedToDeserializeMessage; - private static readonly Action _userMessageProcessingFailed; - private static readonly Action _messageProcessed; - private static readonly Action _closingSubscription; - private static readonly Action _notUnsubscribingSubscriptionBecauseDuplicateRunning; - private static readonly Action _alreadyClosing; - private static readonly Action _closingNoMoreSubscriptions; - private static readonly Action _addingNewSubscription; - private static readonly Action _nothingToResubscribeCloseConnection; - private static readonly Action _failedAuthenticationDisconnectAndReconnect; - private static readonly Action _authenticationSucceeded; - private static readonly Action _failedRequestRevitalization; - private static readonly Action _allSubscriptionResubscribed; - private static readonly Action _subscriptionUnsubscribed; - private static readonly Action _sendingPeriodic; - private static readonly Action _periodicSendFailed; - private static readonly Action _sendingData; - private static readonly Action _receivedMessageNotMatchedToAnyListener; - private static readonly Action _sendingByteData; + _activityPaused = LoggerMessage.Define( + LogLevel.Information, + new EventId(2000, "ActivityPaused"), + "[Sckt {SocketId}] paused activity: {Paused}"); - static SocketConnectionLoggingExtension() - { - _activityPaused = LoggerMessage.Define( - LogLevel.Information, - new EventId(2000, "ActivityPaused"), - "[Sckt {SocketId}] paused activity: {Paused}"); + _socketStatusChanged = LoggerMessage.Define( + LogLevel.Debug, + new EventId(2001, "SocketStatusChanged"), + "[Sckt {SocketId}] status changed from {OldStatus} to {NewStatus}"); - _socketStatusChanged = LoggerMessage.Define( - LogLevel.Debug, - new EventId(2001, "SocketStatusChanged"), - "[Sckt {SocketId}] status changed from {OldStatus} to {NewStatus}"); + _failedReconnectProcessing = LoggerMessage.Define( + LogLevel.Warning, + new EventId(2002, "FailedReconnectProcessing"), + "[Sckt {SocketId}] failed reconnect processing: {ErrorMessage}, reconnecting again"); - _failedReconnectProcessing = LoggerMessage.Define( - LogLevel.Warning, - new EventId(2002, "FailedReconnectProcessing"), - "[Sckt {SocketId}] failed reconnect processing: {ErrorMessage}, reconnecting again"); + _unknownExceptionWhileProcessingReconnection = LoggerMessage.Define( + LogLevel.Warning, + new EventId(2003, "UnknownExceptionWhileProcessingReconnection"), + "[Sckt {SocketId}] Unknown exception while processing reconnection, reconnecting again"); - _unknownExceptionWhileProcessingReconnection = LoggerMessage.Define( - LogLevel.Warning, - new EventId(2003, "UnknownExceptionWhileProcessingReconnection"), - "[Sckt {SocketId}] Unknown exception while processing reconnection, reconnecting again"); + _webSocketErrorCodeAndDetails = LoggerMessage.Define( + LogLevel.Warning, + new EventId(2004, "WebSocketErrorCode"), + "[Sckt {SocketId}] error: Websocket error code {WebSocketErrorCode}, details: {Details}"); - _webSocketErrorCodeAndDetails = LoggerMessage.Define( - LogLevel.Warning, - new EventId(2004, "WebSocketErrorCode"), - "[Sckt {SocketId}] error: Websocket error code {WebSocketErrorCode}, details: {Details}"); + _webSocketError = LoggerMessage.Define( + LogLevel.Warning, + new EventId(2005, "WebSocketError"), + "[Sckt {SocketId}] error: {ErrorMessage}"); - _webSocketError = LoggerMessage.Define( - LogLevel.Warning, - new EventId(2005, "WebSocketError"), - "[Sckt {SocketId}] error: {ErrorMessage}"); + _messageSentNotPending = LoggerMessage.Define( + LogLevel.Debug, + new EventId(2006, "MessageSentNotPending"), + "[Sckt {SocketId}] [Req {RequestId}] message sent, but not pending"); - _messageSentNotPending = LoggerMessage.Define( - LogLevel.Debug, - new EventId(2006, "MessageSentNotPending"), - "[Sckt {SocketId}] [Req {RequestId}] message sent, but not pending"); + _receivedData = LoggerMessage.Define( + LogLevel.Trace, + new EventId(2007, "ReceivedData"), + "[Sckt {SocketId}] received {OriginalData}"); - _receivedData = LoggerMessage.Define( - LogLevel.Trace, - new EventId(2007, "ReceivedData"), - "[Sckt {SocketId}] received {OriginalData}"); + _failedToEvaluateMessage = LoggerMessage.Define( + LogLevel.Warning, + new EventId(2008, "FailedToEvaluateMessage"), + "[Sckt {SocketId}] failed to evaluate message. {OriginalData}"); - _failedToEvaluateMessage = LoggerMessage.Define( - LogLevel.Warning, - new EventId(2008, "FailedToEvaluateMessage"), - "[Sckt {SocketId}] failed to evaluate message. {OriginalData}"); + _errorProcessingMessage = LoggerMessage.Define( + LogLevel.Error, + new EventId(2009, "ErrorProcessingMessage"), + "[Sckt {SocketId}] error processing message"); - _errorProcessingMessage = LoggerMessage.Define( - LogLevel.Error, - new EventId(2009, "ErrorProcessingMessage"), - "[Sckt {SocketId}] error processing message"); + _receivedMessageNotRecognized = LoggerMessage.Define( + LogLevel.Warning, + new EventId(2011, "ReceivedMessageNotRecognized"), + "[Sckt {SocketId}] received message not recognized by handler {ProcessorId}"); - _receivedMessageNotRecognized = LoggerMessage.Define( - LogLevel.Warning, - new EventId(2011, "ReceivedMessageNotRecognized"), - "[Sckt {SocketId}] received message not recognized by handler {ProcessorId}"); + _failedToDeserializeMessage = LoggerMessage.Define( + LogLevel.Warning, + new EventId(2012, "FailedToDeserializeMessage"), + "[Sckt {SocketId}] deserialization failed: {ErrorMessage}"); - _failedToDeserializeMessage = LoggerMessage.Define( - LogLevel.Warning, - new EventId(2012, "FailedToDeserializeMessage"), - "[Sckt {SocketId}] deserialization failed: {ErrorMessage}"); + _userMessageProcessingFailed = LoggerMessage.Define( + LogLevel.Warning, + new EventId(2013, "UserMessageProcessingFailed"), + "[Sckt {SocketId}] user message processing failed: {ErrorMessage}"); - _userMessageProcessingFailed = LoggerMessage.Define( - LogLevel.Warning, - new EventId(2013, "UserMessageProcessingFailed"), - "[Sckt {SocketId}] user message processing failed: {ErrorMessage}"); + _messageProcessed = LoggerMessage.Define( + LogLevel.Trace, + new EventId(2014, "MessageProcessed"), + "[Sckt {SocketId}] message processed in {ProcessingTime}ms, {ParsingTime}ms parsing"); - _messageProcessed = LoggerMessage.Define( - LogLevel.Trace, - new EventId(2014, "MessageProcessed"), - "[Sckt {SocketId}] message processed in {ProcessingTime}ms, {ParsingTime}ms parsing"); + _closingSubscription = LoggerMessage.Define( + LogLevel.Debug, + new EventId(2015, "ClosingSubscription"), + "[Sckt {SocketId}] closing subscription {SubscriptionId}"); - _closingSubscription = LoggerMessage.Define( - LogLevel.Debug, - new EventId(2015, "ClosingSubscription"), - "[Sckt {SocketId}] closing subscription {SubscriptionId}"); + _notUnsubscribingSubscriptionBecauseDuplicateRunning = LoggerMessage.Define( + LogLevel.Debug, + new EventId(2016, "NotUnsubscribingSubscription"), + "[Sckt {SocketId}] not unsubscribing subscription as there is still a duplicate subscription running"); - _notUnsubscribingSubscriptionBecauseDuplicateRunning = LoggerMessage.Define( - LogLevel.Debug, - new EventId(2016, "NotUnsubscribingSubscription"), - "[Sckt {SocketId}] not unsubscribing subscription as there is still a duplicate subscription running"); + _alreadyClosing = LoggerMessage.Define( + LogLevel.Debug, + new EventId(2017, "AlreadyClosing"), + "[Sckt {SocketId}] already closing"); - _alreadyClosing = LoggerMessage.Define( - LogLevel.Debug, - new EventId(2017, "AlreadyClosing"), - "[Sckt {SocketId}] already closing"); + _closingNoMoreSubscriptions = LoggerMessage.Define( + LogLevel.Debug, + new EventId(2018, "ClosingNoMoreSubscriptions"), + "[Sckt {SocketId}] closing as there are no more subscriptions"); - _closingNoMoreSubscriptions = LoggerMessage.Define( - LogLevel.Debug, - new EventId(2018, "ClosingNoMoreSubscriptions"), - "[Sckt {SocketId}] closing as there are no more subscriptions"); + _addingNewSubscription = LoggerMessage.Define( + LogLevel.Debug, + new EventId(2019, "AddingNewSubscription"), + "[Sckt {SocketId}] adding new subscription with id {SubscriptionId}, total subscriptions on connection: {UserSubscriptionCount}"); - _addingNewSubscription = LoggerMessage.Define( - LogLevel.Debug, - new EventId(2019, "AddingNewSubscription"), - "[Sckt {SocketId}] adding new subscription with id {SubscriptionId}, total subscriptions on connection: {UserSubscriptionCount}"); + _nothingToResubscribeCloseConnection = LoggerMessage.Define( + LogLevel.Debug, + new EventId(2020, "NothingToResubscribe"), + "[Sckt {SocketId}] nothing to resubscribe, closing connection"); - _nothingToResubscribeCloseConnection = LoggerMessage.Define( - LogLevel.Debug, - new EventId(2020, "NothingToResubscribe"), - "[Sckt {SocketId}] nothing to resubscribe, closing connection"); + _failedAuthenticationDisconnectAndReconnect = LoggerMessage.Define( + LogLevel.Warning, + new EventId(2021, "FailedAuthentication"), + "[Sckt {SocketId}] authentication failed on reconnected socket. Disconnecting and reconnecting"); - _failedAuthenticationDisconnectAndReconnect = LoggerMessage.Define( - LogLevel.Warning, - new EventId(2021, "FailedAuthentication"), - "[Sckt {SocketId}] authentication failed on reconnected socket. Disconnecting and reconnecting"); + _authenticationSucceeded = LoggerMessage.Define( + LogLevel.Debug, + new EventId(2022, "AuthenticationSucceeded"), + "[Sckt {SocketId}] authentication succeeded on reconnected socket"); - _authenticationSucceeded = LoggerMessage.Define( - LogLevel.Debug, - new EventId(2022, "AuthenticationSucceeded"), - "[Sckt {SocketId}] authentication succeeded on reconnected socket"); + _failedRequestRevitalization = LoggerMessage.Define( + LogLevel.Warning, + new EventId(2023, "FailedRequestRevitalization"), + "[Sckt {SocketId}] failed request revitalization: {ErrorMessage}"); - _failedRequestRevitalization = LoggerMessage.Define( - LogLevel.Warning, - new EventId(2023, "FailedRequestRevitalization"), - "[Sckt {SocketId}] failed request revitalization: {ErrorMessage}"); + _allSubscriptionResubscribed = LoggerMessage.Define( + LogLevel.Debug, + new EventId(2024, "AllSubscriptionResubscribed"), + "[Sckt {SocketId}] all subscription successfully resubscribed on reconnected socket"); - _allSubscriptionResubscribed = LoggerMessage.Define( - LogLevel.Debug, - new EventId(2024, "AllSubscriptionResubscribed"), - "[Sckt {SocketId}] all subscription successfully resubscribed on reconnected socket"); + _subscriptionUnsubscribed = LoggerMessage.Define( + LogLevel.Information, + new EventId(2025, "SubscriptionUnsubscribed"), + "[Sckt {SocketId}] subscription {SubscriptionId} unsubscribed"); - _subscriptionUnsubscribed = LoggerMessage.Define( - LogLevel.Information, - new EventId(2025, "SubscriptionUnsubscribed"), - "[Sckt {SocketId}] subscription {SubscriptionId} unsubscribed"); + _sendingPeriodic = LoggerMessage.Define( + LogLevel.Trace, + new EventId(2026, "SendingPeriodic"), + "[Sckt {SocketId}] sending periodic {Identifier}"); - _sendingPeriodic = LoggerMessage.Define( - LogLevel.Trace, - new EventId(2026, "SendingPeriodic"), - "[Sckt {SocketId}] sending periodic {Identifier}"); + _periodicSendFailed = LoggerMessage.Define( + LogLevel.Warning, + new EventId(2027, "PeriodicSendFailed"), + "[Sckt {SocketId}] periodic send {Identifier} failed: {ErrorMessage}"); - _periodicSendFailed = LoggerMessage.Define( - LogLevel.Warning, - new EventId(2027, "PeriodicSendFailed"), - "[Sckt {SocketId}] periodic send {Identifier} failed: {ErrorMessage}"); + _sendingData = LoggerMessage.Define( + LogLevel.Trace, + new EventId(2028, "SendingData"), + "[Sckt {SocketId}] [Req {RequestId}] sending message: {Data}"); - _sendingData = LoggerMessage.Define( - LogLevel.Trace, - new EventId(2028, "SendingData"), - "[Sckt {SocketId}] [Req {RequestId}] sending message: {Data}"); + _receivedMessageNotMatchedToAnyListener = LoggerMessage.Define( + LogLevel.Warning, + new EventId(2029, "ReceivedMessageNotMatchedToAnyListener"), + "[Sckt {SocketId}] received message not matched to any listener. ListenId: {ListenId}, current listeners: [{ListenIds}]"); - _receivedMessageNotMatchedToAnyListener = LoggerMessage.Define( - LogLevel.Warning, - new EventId(2029, "ReceivedMessageNotMatchedToAnyListener"), - "[Sckt {SocketId}] received message not matched to any listener. ListenId: {ListenId}, current listeners: [{ListenIds}]"); + _failedToParse = LoggerMessage.Define( + LogLevel.Warning, + new EventId(2030, "FailedToParse"), + "[Sckt {SocketId}] failed to parse data: {Error}"); - _failedToParse = LoggerMessage.Define( - LogLevel.Warning, - new EventId(2030, "FailedToParse"), - "[Sckt {SocketId}] failed to parse data: {Error}"); + _sendingByteData = LoggerMessage.Define( + LogLevel.Trace, + new EventId(2031, "SendingByteData"), + "[Sckt {SocketId}] [Req {RequestId}] sending byte message of length: {Length}"); - _sendingByteData = LoggerMessage.Define( - LogLevel.Trace, - new EventId(2031, "SendingByteData"), - "[Sckt {SocketId}] [Req {RequestId}] sending byte message of length: {Length}"); + _processorMatched = LoggerMessage.Define( + LogLevel.Trace, + new EventId(2032, "ProcessorMatched"), + "[Sckt {SocketId}] listener '{ListenId}' matched to message with listener identifier {ListenerId}"); - _processorMatched = LoggerMessage.Define( - LogLevel.Trace, - new EventId(2032, "ProcessorMatched"), - "[Sckt {SocketId}] listener '{ListenId}' matched to message with listener identifier {ListenerId}"); + } - } + public static void ActivityPaused(this ILogger logger, int socketId, bool paused) + { + _activityPaused(logger, socketId, paused, null); + } - public static void ActivityPaused(this ILogger logger, int socketId, bool paused) - { - _activityPaused(logger, socketId, paused, null); - } + public static void SocketStatusChanged(this ILogger logger, int socketId, Sockets.SocketConnection.SocketStatus oldStatus, Sockets.SocketConnection.SocketStatus newStatus) + { + _socketStatusChanged(logger, socketId, oldStatus, newStatus, null); + } - public static void SocketStatusChanged(this ILogger logger, int socketId, Sockets.SocketConnection.SocketStatus oldStatus, Sockets.SocketConnection.SocketStatus newStatus) - { - _socketStatusChanged(logger, socketId, oldStatus, newStatus, null); - } + public static void FailedReconnectProcessing(this ILogger logger, int socketId, string? error) + { + _failedReconnectProcessing(logger, socketId, error, null); + } - public static void FailedReconnectProcessing(this ILogger logger, int socketId, string? error) - { - _failedReconnectProcessing(logger, socketId, error, null); - } + public static void UnknownExceptionWhileProcessingReconnection(this ILogger logger, int socketId, Exception e) + { + _unknownExceptionWhileProcessingReconnection(logger, socketId, e); + } - public static void UnknownExceptionWhileProcessingReconnection(this ILogger logger, int socketId, Exception e) - { - _unknownExceptionWhileProcessingReconnection(logger, socketId, e); - } + public static void WebSocketErrorCodeAndDetails(this ILogger logger, int socketId, WebSocketError error, string? details, Exception e) + { + _webSocketErrorCodeAndDetails(logger, socketId, error, details, e); + } - public static void WebSocketErrorCodeAndDetails(this ILogger logger, int socketId, WebSocketError error, string? details, Exception e) - { - _webSocketErrorCodeAndDetails(logger, socketId, error, details, e); - } + public static void WebSocketError(this ILogger logger, int socketId, string? errorMessage, Exception e) + { + _webSocketError(logger, socketId, errorMessage, e); + } - public static void WebSocketError(this ILogger logger, int socketId, string? errorMessage, Exception e) - { - _webSocketError(logger, socketId, errorMessage, e); - } + public static void MessageSentNotPending(this ILogger logger, int socketId, int requestId) + { + _messageSentNotPending(logger, socketId, requestId, null); + } - public static void MessageSentNotPending(this ILogger logger, int socketId, int requestId) - { - _messageSentNotPending(logger, socketId, requestId, null); - } + public static void ReceivedData(this ILogger logger, int socketId, string originalData) + { + _receivedData(logger, socketId, originalData, null); + } - public static void ReceivedData(this ILogger logger, int socketId, string originalData) - { - _receivedData(logger, socketId, originalData, null); - } + public static void FailedToParse(this ILogger logger, int socketId, string error) + { + _failedToParse(logger, socketId, error, null); + } - public static void FailedToParse(this ILogger logger, int socketId, string error) - { - _failedToParse(logger, socketId, error, null); - } + public static void FailedToEvaluateMessage(this ILogger logger, int socketId, string originalData) + { + _failedToEvaluateMessage(logger, socketId, originalData, null); + } + public static void ErrorProcessingMessage(this ILogger logger, int socketId, Exception e) + { + _errorProcessingMessage(logger, socketId, e); + } + public static void ProcessorMatched(this ILogger logger, int socketId, string listener, string listenerId) + { + _processorMatched(logger, socketId, listener, listenerId, null); + } + public static void ReceivedMessageNotRecognized(this ILogger logger, int socketId, int id) + { + _receivedMessageNotRecognized(logger, socketId, id, null); + } + public static void FailedToDeserializeMessage(this ILogger logger, int socketId, string? errorMessage, Exception? ex) + { + _failedToDeserializeMessage(logger, socketId, errorMessage, ex); + } + public static void UserMessageProcessingFailed(this ILogger logger, int socketId, string errorMessage, Exception e) + { + _userMessageProcessingFailed(logger, socketId, errorMessage, e); + } + public static void MessageProcessed(this ILogger logger, int socketId, long processingTime, long parsingTime) + { + _messageProcessed(logger, socketId, processingTime, parsingTime, null); + } + public static void ClosingSubscription(this ILogger logger, int socketId, int subscriptionId) + { + _closingSubscription(logger, socketId, subscriptionId, null); + } + public static void NotUnsubscribingSubscriptionBecauseDuplicateRunning(this ILogger logger, int socketId) + { + _notUnsubscribingSubscriptionBecauseDuplicateRunning(logger, socketId, null); + } + public static void AlreadyClosing(this ILogger logger, int socketId) + { + _alreadyClosing(logger, socketId, null); + } + public static void ClosingNoMoreSubscriptions(this ILogger logger, int socketId) + { + _closingNoMoreSubscriptions(logger, socketId, null); + } + public static void AddingNewSubscription(this ILogger logger, int socketId, int subscriptionId, int userSubscriptionCount) + { + _addingNewSubscription(logger, socketId, subscriptionId, userSubscriptionCount, null); + } - public static void FailedToEvaluateMessage(this ILogger logger, int socketId, string originalData) - { - _failedToEvaluateMessage(logger, socketId, originalData, null); - } - public static void ErrorProcessingMessage(this ILogger logger, int socketId, Exception e) - { - _errorProcessingMessage(logger, socketId, e); - } - public static void ProcessorMatched(this ILogger logger, int socketId, string listener, string listenerId) - { - _processorMatched(logger, socketId, listener, listenerId, null); - } - public static void ReceivedMessageNotRecognized(this ILogger logger, int socketId, int id) - { - _receivedMessageNotRecognized(logger, socketId, id, null); - } - public static void FailedToDeserializeMessage(this ILogger logger, int socketId, string? errorMessage, Exception? ex) - { - _failedToDeserializeMessage(logger, socketId, errorMessage, ex); - } - public static void UserMessageProcessingFailed(this ILogger logger, int socketId, string errorMessage, Exception e) - { - _userMessageProcessingFailed(logger, socketId, errorMessage, e); - } - public static void MessageProcessed(this ILogger logger, int socketId, long processingTime, long parsingTime) - { - _messageProcessed(logger, socketId, processingTime, parsingTime, null); - } - public static void ClosingSubscription(this ILogger logger, int socketId, int subscriptionId) - { - _closingSubscription(logger, socketId, subscriptionId, null); - } - public static void NotUnsubscribingSubscriptionBecauseDuplicateRunning(this ILogger logger, int socketId) - { - _notUnsubscribingSubscriptionBecauseDuplicateRunning(logger, socketId, null); - } - public static void AlreadyClosing(this ILogger logger, int socketId) - { - _alreadyClosing(logger, socketId, null); - } - public static void ClosingNoMoreSubscriptions(this ILogger logger, int socketId) - { - _closingNoMoreSubscriptions(logger, socketId, null); - } - public static void AddingNewSubscription(this ILogger logger, int socketId, int subscriptionId, int userSubscriptionCount) - { - _addingNewSubscription(logger, socketId, subscriptionId, userSubscriptionCount, null); - } + public static void NothingToResubscribeCloseConnection(this ILogger logger, int socketId) + { + _nothingToResubscribeCloseConnection(logger, socketId, null); + } + public static void FailedAuthenticationDisconnectAndRecoonect(this ILogger logger, int socketId) + { + _failedAuthenticationDisconnectAndReconnect(logger, socketId, null); + } + public static void AuthenticationSucceeded(this ILogger logger, int socketId) + { + _authenticationSucceeded(logger, socketId, null); + } + public static void FailedRequestRevitalization(this ILogger logger, int socketId, string? errorMessage) + { + _failedRequestRevitalization(logger, socketId, errorMessage, null); + } + public static void AllSubscriptionResubscribed(this ILogger logger, int socketId) + { + _allSubscriptionResubscribed(logger, socketId, null); + } + public static void SubscriptionUnsubscribed(this ILogger logger, int socketId, int subscriptionId) + { + _subscriptionUnsubscribed(logger, socketId, subscriptionId, null); + } + public static void SendingPeriodic(this ILogger logger, int socketId, string identifier) + { + _sendingPeriodic(logger, socketId, identifier, null); + } + public static void PeriodicSendFailed(this ILogger logger, int socketId, string identifier, string errorMessage, Exception e) + { + _periodicSendFailed(logger, socketId, identifier, errorMessage, e); + } - public static void NothingToResubscribeCloseConnection(this ILogger logger, int socketId) - { - _nothingToResubscribeCloseConnection(logger, socketId, null); - } - public static void FailedAuthenticationDisconnectAndRecoonect(this ILogger logger, int socketId) - { - _failedAuthenticationDisconnectAndReconnect(logger, socketId, null); - } - public static void AuthenticationSucceeded(this ILogger logger, int socketId) - { - _authenticationSucceeded(logger, socketId, null); - } - public static void FailedRequestRevitalization(this ILogger logger, int socketId, string? errorMessage) - { - _failedRequestRevitalization(logger, socketId, errorMessage, null); - } - public static void AllSubscriptionResubscribed(this ILogger logger, int socketId) - { - _allSubscriptionResubscribed(logger, socketId, null); - } - public static void SubscriptionUnsubscribed(this ILogger logger, int socketId, int subscriptionId) - { - _subscriptionUnsubscribed(logger, socketId, subscriptionId, null); - } - public static void SendingPeriodic(this ILogger logger, int socketId, string identifier) - { - _sendingPeriodic(logger, socketId, identifier, null); - } - public static void PeriodicSendFailed(this ILogger logger, int socketId, string identifier, string errorMessage, Exception e) - { - _periodicSendFailed(logger, socketId, identifier, errorMessage, e); - } + public static void SendingData(this ILogger logger, int socketId, int requestId, string data) + { + _sendingData(logger, socketId, requestId, data, null); + } - public static void SendingData(this ILogger logger, int socketId, int requestId, string data) - { - _sendingData(logger, socketId, requestId, data, null); - } + public static void ReceivedMessageNotMatchedToAnyListener(this ILogger logger, int socketId, string listenId, string listenIds) + { + _receivedMessageNotMatchedToAnyListener(logger, socketId, listenId, listenIds, null); + } - public static void ReceivedMessageNotMatchedToAnyListener(this ILogger logger, int socketId, string listenId, string listenIds) - { - _receivedMessageNotMatchedToAnyListener(logger, socketId, listenId, listenIds, null); - } - - public static void SendingByteData(this ILogger logger, int socketId, int requestId, int length) - { - _sendingByteData(logger, socketId, requestId, length, null); - } + public static void SendingByteData(this ILogger logger, int socketId, int requestId, int length) + { + _sendingByteData(logger, socketId, requestId, length, null); } } \ No newline at end of file diff --git a/CryptoExchange.Net/Logging/Extensions/SymbolOrderBookLoggingExtensions.cs b/CryptoExchange.Net/Logging/Extensions/SymbolOrderBookLoggingExtensions.cs index 413f12b..c922d9f 100644 --- a/CryptoExchange.Net/Logging/Extensions/SymbolOrderBookLoggingExtensions.cs +++ b/CryptoExchange.Net/Logging/Extensions/SymbolOrderBookLoggingExtensions.cs @@ -1,237 +1,236 @@ -using System; +using System; using CryptoExchange.Net.Objects; using Microsoft.Extensions.Logging; -namespace CryptoExchange.Net.Logging.Extensions -{ +namespace CryptoExchange.Net.Logging.Extensions; + #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member - public static class SymbolOrderBookLoggingExtensions +public static class SymbolOrderBookLoggingExtensions +{ + private static readonly Action _orderBookStatusChanged; + private static readonly Action _orderBookStarting; + private static readonly Action _orderBookStoppedStarting; + private static readonly Action _orderBookStopping; + private static readonly Action _orderBookStopped; + private static readonly Action _orderBookConnectionLost; + private static readonly Action _orderBookDisconnected; + private static readonly Action _orderBookProcessingBufferedUpdates; + private static readonly Action _orderBookUpdateSkipped; + private static readonly Action _orderBookOutOfSyncChecksum; + private static readonly Action _orderBookResyncFailed; + private static readonly Action _orderBookResyncing; + private static readonly Action _orderBookResynced; + private static readonly Action _orderBookMessageSkippedBecauseOfResubscribing; + private static readonly Action _orderBookDataSet; + private static readonly Action _orderBookUpdateBuffered; + private static readonly Action _orderBookOutOfSyncDetected; + private static readonly Action _orderBookReconnectingSocket; + private static readonly Action _orderBookSkippedMessage; + private static readonly Action _orderBookProcessedMessage; + private static readonly Action _orderBookOutOfSync; + + static SymbolOrderBookLoggingExtensions() { - private static readonly Action _orderBookStatusChanged; - private static readonly Action _orderBookStarting; - private static readonly Action _orderBookStoppedStarting; - private static readonly Action _orderBookStopping; - private static readonly Action _orderBookStopped; - private static readonly Action _orderBookConnectionLost; - private static readonly Action _orderBookDisconnected; - private static readonly Action _orderBookProcessingBufferedUpdates; - private static readonly Action _orderBookUpdateSkipped; - private static readonly Action _orderBookOutOfSyncChecksum; - private static readonly Action _orderBookResyncFailed; - private static readonly Action _orderBookResyncing; - private static readonly Action _orderBookResynced; - private static readonly Action _orderBookMessageSkippedBecauseOfResubscribing; - private static readonly Action _orderBookDataSet; - private static readonly Action _orderBookUpdateBuffered; - private static readonly Action _orderBookOutOfSyncDetected; - private static readonly Action _orderBookReconnectingSocket; - private static readonly Action _orderBookSkippedMessage; - private static readonly Action _orderBookProcessedMessage; - private static readonly Action _orderBookOutOfSync; + _orderBookStatusChanged = LoggerMessage.Define( + LogLevel.Information, + new EventId(5000, "OrderBookStatusChanged"), + "{Api} order book {Symbol} status changed: {PreviousStatus} => {NewStatus}"); - static SymbolOrderBookLoggingExtensions() - { - _orderBookStatusChanged = LoggerMessage.Define( - LogLevel.Information, - new EventId(5000, "OrderBookStatusChanged"), - "{Api} order book {Symbol} status changed: {PreviousStatus} => {NewStatus}"); + _orderBookStarting = LoggerMessage.Define( + LogLevel.Debug, + new EventId(5001, "OrderBookStarting"), + "{Api} order book {Symbol} starting"); - _orderBookStarting = LoggerMessage.Define( - LogLevel.Debug, - new EventId(5001, "OrderBookStarting"), - "{Api} order book {Symbol} starting"); + _orderBookStoppedStarting = LoggerMessage.Define( + LogLevel.Debug, + new EventId(5002, "OrderBookStoppedStarting"), + "{Api} order book {Symbol} stopped while starting"); - _orderBookStoppedStarting = LoggerMessage.Define( - LogLevel.Debug, - new EventId(5002, "OrderBookStoppedStarting"), - "{Api} order book {Symbol} stopped while starting"); + _orderBookConnectionLost = LoggerMessage.Define( + LogLevel.Warning, + new EventId(5003, "OrderBookConnectionLost"), + "{Api} order book {Symbol} connection lost"); - _orderBookConnectionLost = LoggerMessage.Define( - LogLevel.Warning, - new EventId(5003, "OrderBookConnectionLost"), - "{Api} order book {Symbol} connection lost"); + _orderBookDisconnected = LoggerMessage.Define( + LogLevel.Debug, + new EventId(5004, "OrderBookDisconnected"), + "{Api} order book {Symbol} disconnected"); - _orderBookDisconnected = LoggerMessage.Define( - LogLevel.Debug, - new EventId(5004, "OrderBookDisconnected"), - "{Api} order book {Symbol} disconnected"); + _orderBookStopping = LoggerMessage.Define( + LogLevel.Debug, + new EventId(5005, "OrderBookStopping"), + "{Api} order book {Symbol} stopping"); - _orderBookStopping = LoggerMessage.Define( - LogLevel.Debug, - new EventId(5005, "OrderBookStopping"), - "{Api} order book {Symbol} stopping"); + _orderBookStopped = LoggerMessage.Define( + LogLevel.Trace, + new EventId(5006, "OrderBookStopped"), + "{Api} order book {Symbol} stopped"); - _orderBookStopped = LoggerMessage.Define( - LogLevel.Trace, - new EventId(5006, "OrderBookStopped"), - "{Api} order book {Symbol} stopped"); + _orderBookProcessingBufferedUpdates = LoggerMessage.Define( + LogLevel.Debug, + new EventId(5007, "OrderBookProcessingBufferedUpdates"), + "{Api} order book {Symbol} Processing {NumberBufferedUpdated} buffered updates"); - _orderBookProcessingBufferedUpdates = LoggerMessage.Define( - LogLevel.Debug, - new EventId(5007, "OrderBookProcessingBufferedUpdates"), - "{Api} order book {Symbol} Processing {NumberBufferedUpdated} buffered updates"); + _orderBookUpdateSkipped = LoggerMessage.Define( + LogLevel.Debug, + new EventId(5008, "OrderBookUpdateSkipped"), + "{Api} order book {Symbol} update skipped #{SequenceNumber}, currently at #{LastSequenceNumber}"); - _orderBookUpdateSkipped = LoggerMessage.Define( - LogLevel.Debug, - new EventId(5008, "OrderBookUpdateSkipped"), - "{Api} order book {Symbol} update skipped #{SequenceNumber}, currently at #{LastSequenceNumber}"); + _orderBookOutOfSync = LoggerMessage.Define( + LogLevel.Warning, + new EventId(5009, "OrderBookOutOfSync"), + "{Api} order book {Symbol} out of sync (expected {ExpectedSequenceNumber}, was {SequenceNumber}), reconnecting"); - _orderBookOutOfSync = LoggerMessage.Define( - LogLevel.Warning, - new EventId(5009, "OrderBookOutOfSync"), - "{Api} order book {Symbol} out of sync (expected {ExpectedSequenceNumber}, was {SequenceNumber}), reconnecting"); + _orderBookResynced = LoggerMessage.Define( + LogLevel.Information, + new EventId(5010, "OrderBookResynced"), + "{Api} order book {Symbol} successfully resynchronized"); - _orderBookResynced = LoggerMessage.Define( - LogLevel.Information, - new EventId(5010, "OrderBookResynced"), - "{Api} order book {Symbol} successfully resynchronized"); + _orderBookMessageSkippedBecauseOfResubscribing = LoggerMessage.Define( + LogLevel.Trace, + new EventId(5011, "OrderBookMessageSkippedResubscribing"), + "{Api} order book {Symbol} Skipping message because of resubscribing"); - _orderBookMessageSkippedBecauseOfResubscribing = LoggerMessage.Define( - LogLevel.Trace, - new EventId(5011, "OrderBookMessageSkippedResubscribing"), - "{Api} order book {Symbol} Skipping message because of resubscribing"); + _orderBookDataSet = LoggerMessage.Define( + LogLevel.Debug, + new EventId(5012, "OrderBookDataSet"), + "{Api} order book {Symbol} data set: {BidCount} bids, {AskCount} asks. #{EndUpdateId}"); - _orderBookDataSet = LoggerMessage.Define( - LogLevel.Debug, - new EventId(5012, "OrderBookDataSet"), - "{Api} order book {Symbol} data set: {BidCount} bids, {AskCount} asks. #{EndUpdateId}"); + _orderBookUpdateBuffered = LoggerMessage.Define( + LogLevel.Trace, + new EventId(5013, "OrderBookUpdateBuffered"), + "{Api} order book {Symbol} update buffered #{StartUpdateId}-#{EndUpdateId} [{AsksCount} asks, {BidsCount} bids]"); - _orderBookUpdateBuffered = LoggerMessage.Define( - LogLevel.Trace, - new EventId(5013, "OrderBookUpdateBuffered"), - "{Api} order book {Symbol} update buffered #{StartUpdateId}-#{EndUpdateId} [{AsksCount} asks, {BidsCount} bids]"); + _orderBookOutOfSyncDetected = LoggerMessage.Define( + LogLevel.Warning, + new EventId(5014, "OrderBookOutOfSyncDetected"), + "{Api} order book {Symbol} detected out of sync order book. First ask: {FirstAsk}, first bid: {FirstBid}. Resyncing"); - _orderBookOutOfSyncDetected = LoggerMessage.Define( - LogLevel.Warning, - new EventId(5014, "OrderBookOutOfSyncDetected"), - "{Api} order book {Symbol} detected out of sync order book. First ask: {FirstAsk}, first bid: {FirstBid}. Resyncing"); + _orderBookReconnectingSocket = LoggerMessage.Define( + LogLevel.Warning, + new EventId(5015, "OrderBookReconnectingSocket"), + "{Api} order book {Symbol} out of sync. Reconnecting socket"); - _orderBookReconnectingSocket = LoggerMessage.Define( - LogLevel.Warning, - new EventId(5015, "OrderBookReconnectingSocket"), - "{Api} order book {Symbol} out of sync. Reconnecting socket"); + _orderBookResyncing = LoggerMessage.Define( + LogLevel.Warning, + new EventId(5016, "OrderBookResyncing"), + "{Api} order book {Symbol} out of sync. Resyncing"); - _orderBookResyncing = LoggerMessage.Define( - LogLevel.Warning, - new EventId(5016, "OrderBookResyncing"), - "{Api} order book {Symbol} out of sync. Resyncing"); + _orderBookResyncFailed = LoggerMessage.Define( + LogLevel.Warning, + new EventId(5017, "OrderBookResyncFailed"), + "{Api} order book {Symbol} resync failed, reconnecting socket"); - _orderBookResyncFailed = LoggerMessage.Define( - LogLevel.Warning, - new EventId(5017, "OrderBookResyncFailed"), - "{Api} order book {Symbol} resync failed, reconnecting socket"); + _orderBookSkippedMessage = LoggerMessage.Define( + LogLevel.Trace, + new EventId(5018, "OrderBookSkippedMessage"), + "{Api} order book {Symbol} update skipped #{FirstUpdateId}-{LastUpdateId}"); - _orderBookSkippedMessage = LoggerMessage.Define( - LogLevel.Trace, - new EventId(5018, "OrderBookSkippedMessage"), - "{Api} order book {Symbol} update skipped #{FirstUpdateId}-{LastUpdateId}"); + _orderBookProcessedMessage = LoggerMessage.Define( + LogLevel.Trace, + new EventId(5019, "OrderBookProcessedMessage"), + "{Api} order book {Symbol} update processed #{FirstUpdateId}-{LastUpdateId}"); - _orderBookProcessedMessage = LoggerMessage.Define( - LogLevel.Trace, - new EventId(5019, "OrderBookProcessedMessage"), - "{Api} order book {Symbol} update processed #{FirstUpdateId}-{LastUpdateId}"); + _orderBookOutOfSyncChecksum = LoggerMessage.Define( + LogLevel.Warning, + new EventId(5020, "OrderBookOutOfSyncChecksum"), + "{Api} order book {Symbol} out of sync. Checksum mismatch, resyncing"); + } - _orderBookOutOfSyncChecksum = LoggerMessage.Define( - LogLevel.Warning, - new EventId(5020, "OrderBookOutOfSyncChecksum"), - "{Api} order book {Symbol} out of sync. Checksum mismatch, resyncing"); - } + public static void OrderBookStatusChanged(this ILogger logger, string api, string symbol, OrderBookStatus previousStatus, OrderBookStatus newStatus) + { + _orderBookStatusChanged(logger, api, symbol, previousStatus, newStatus, null); + } + public static void OrderBookStarting(this ILogger logger, string api, string symbol) + { + _orderBookStarting(logger, api, symbol, null); + } + public static void OrderBookStoppedStarting(this ILogger logger, string api, string symbol) + { + _orderBookStoppedStarting(logger, api, symbol, null); + } + public static void OrderBookConnectionLost(this ILogger logger, string api, string symbol) + { + _orderBookConnectionLost(logger, api, symbol, null); + } - public static void OrderBookStatusChanged(this ILogger logger, string api, string symbol, OrderBookStatus previousStatus, OrderBookStatus newStatus) - { - _orderBookStatusChanged(logger, api, symbol, previousStatus, newStatus, null); - } - public static void OrderBookStarting(this ILogger logger, string api, string symbol) - { - _orderBookStarting(logger, api, symbol, null); - } - public static void OrderBookStoppedStarting(this ILogger logger, string api, string symbol) - { - _orderBookStoppedStarting(logger, api, symbol, null); - } - public static void OrderBookConnectionLost(this ILogger logger, string api, string symbol) - { - _orderBookConnectionLost(logger, api, symbol, null); - } + public static void OrderBookDisconnected(this ILogger logger, string api, string symbol) + { + _orderBookDisconnected(logger, api, symbol, null); + } - public static void OrderBookDisconnected(this ILogger logger, string api, string symbol) - { - _orderBookDisconnected(logger, api, symbol, null); - } + public static void OrderBookStopping(this ILogger logger, string api, string symbol) + { + _orderBookStopping(logger, api, symbol, null); + } - public static void OrderBookStopping(this ILogger logger, string api, string symbol) - { - _orderBookStopping(logger, api, symbol, null); - } + public static void OrderBookStopped(this ILogger logger, string api, string symbol) + { + _orderBookStopped(logger, api, symbol, null); + } - public static void OrderBookStopped(this ILogger logger, string api, string symbol) - { - _orderBookStopped(logger, api, symbol, null); - } + public static void OrderBookProcessingBufferedUpdates(this ILogger logger, string api, string symbol, int numberBufferedUpdated) + { + _orderBookProcessingBufferedUpdates(logger, api, symbol, numberBufferedUpdated, null); + } - public static void OrderBookProcessingBufferedUpdates(this ILogger logger, string api, string symbol, int numberBufferedUpdated) - { - _orderBookProcessingBufferedUpdates(logger, api, symbol, numberBufferedUpdated, null); - } + public static void OrderBookUpdateSkipped(this ILogger logger, string api, string symbol, long sequence, long lastSequenceNumber) + { + _orderBookUpdateSkipped(logger, api, symbol, sequence, lastSequenceNumber, null); + } - public static void OrderBookUpdateSkipped(this ILogger logger, string api, string symbol, long sequence, long lastSequenceNumber) - { - _orderBookUpdateSkipped(logger, api, symbol, sequence, lastSequenceNumber, null); - } + public static void OrderBookOutOfSync(this ILogger logger, string api, string symbol, long expectedSequenceNumber, long sequenceNumber) + { + _orderBookOutOfSync(logger, api, symbol, expectedSequenceNumber, sequenceNumber, null); + } - public static void OrderBookOutOfSync(this ILogger logger, string api, string symbol, long expectedSequenceNumber, long sequenceNumber) - { - _orderBookOutOfSync(logger, api, symbol, expectedSequenceNumber, sequenceNumber, null); - } + public static void OrderBookResynced(this ILogger logger, string api, string symbol) + { + _orderBookResynced(logger, api, symbol, null); + } - public static void OrderBookResynced(this ILogger logger, string api, string symbol) - { - _orderBookResynced(logger, api, symbol, null); - } + public static void OrderBookMessageSkippedResubscribing(this ILogger logger, string api, string symbol) + { + _orderBookMessageSkippedBecauseOfResubscribing(logger, api, symbol, null); + } + public static void OrderBookDataSet(this ILogger logger, string api, string symbol, long bidCount, long askCount, long endUpdateId) + { + _orderBookDataSet(logger, api, symbol, bidCount, askCount, endUpdateId, null); + } + public static void OrderBookUpdateBuffered(this ILogger logger, string api, string symbol, long startUpdateId, long endUpdateId, long asksCount, long bidsCount) + { + _orderBookUpdateBuffered(logger, api, symbol, startUpdateId, endUpdateId, asksCount, bidsCount, null); + } + public static void OrderBookOutOfSyncDetected(this ILogger logger, string api, string symbol, decimal firstAsk, decimal firstBid) + { + _orderBookOutOfSyncDetected(logger, api, symbol, firstAsk, firstBid, null); + } - public static void OrderBookMessageSkippedResubscribing(this ILogger logger, string api, string symbol) - { - _orderBookMessageSkippedBecauseOfResubscribing(logger, api, symbol, null); - } - public static void OrderBookDataSet(this ILogger logger, string api, string symbol, long bidCount, long askCount, long endUpdateId) - { - _orderBookDataSet(logger, api, symbol, bidCount, askCount, endUpdateId, null); - } - public static void OrderBookUpdateBuffered(this ILogger logger, string api, string symbol, long startUpdateId, long endUpdateId, long asksCount, long bidsCount) - { - _orderBookUpdateBuffered(logger, api, symbol, startUpdateId, endUpdateId, asksCount, bidsCount, null); - } - public static void OrderBookOutOfSyncDetected(this ILogger logger, string api, string symbol, decimal firstAsk, decimal firstBid) - { - _orderBookOutOfSyncDetected(logger, api, symbol, firstAsk, firstBid, null); - } + public static void OrderBookReconnectingSocket(this ILogger logger, string api, string symbol) + { + _orderBookReconnectingSocket(logger, api, symbol, null); + } - public static void OrderBookReconnectingSocket(this ILogger logger, string api, string symbol) - { - _orderBookReconnectingSocket(logger, api, symbol, null); - } + public static void OrderBookResyncing(this ILogger logger, string api, string symbol) + { + _orderBookResyncing(logger, api, symbol, null); + } + public static void OrderBookResyncFailed(this ILogger logger, string api, string symbol) + { + _orderBookResyncFailed(logger, api, symbol, null); + } + public static void OrderBookSkippedMessage(this ILogger logger, string api, string symbol, long firstUpdateId, long lastUpdateId) + { + _orderBookSkippedMessage(logger, api, symbol, firstUpdateId, lastUpdateId, null); + } + public static void OrderBookProcessedMessage(this ILogger logger, string api, string symbol, long firstUpdateId, long lastUpdateId) + { + _orderBookProcessedMessage(logger, api, symbol, firstUpdateId, lastUpdateId, null); + } - public static void OrderBookResyncing(this ILogger logger, string api, string symbol) - { - _orderBookResyncing(logger, api, symbol, null); - } - public static void OrderBookResyncFailed(this ILogger logger, string api, string symbol) - { - _orderBookResyncFailed(logger, api, symbol, null); - } - public static void OrderBookSkippedMessage(this ILogger logger, string api, string symbol, long firstUpdateId, long lastUpdateId) - { - _orderBookSkippedMessage(logger, api, symbol, firstUpdateId, lastUpdateId, null); - } - public static void OrderBookProcessedMessage(this ILogger logger, string api, string symbol, long firstUpdateId, long lastUpdateId) - { - _orderBookProcessedMessage(logger, api, symbol, firstUpdateId, lastUpdateId, null); - } - - public static void OrderBookOutOfSyncChecksum(this ILogger logger, string api, string symbol) - { - _orderBookOutOfSyncChecksum(logger, api, symbol, null); - } + public static void OrderBookOutOfSyncChecksum(this ILogger logger, string api, string symbol) + { + _orderBookOutOfSyncChecksum(logger, api, symbol, null); } } diff --git a/CryptoExchange.Net/Logging/Extensions/TrackerLoggingExtensions.cs b/CryptoExchange.Net/Logging/Extensions/TrackerLoggingExtensions.cs index 8015f5b..cb16712 100644 --- a/CryptoExchange.Net/Logging/Extensions/TrackerLoggingExtensions.cs +++ b/CryptoExchange.Net/Logging/Extensions/TrackerLoggingExtensions.cs @@ -1,291 +1,290 @@ -using System; +using System; using CryptoExchange.Net.Objects; using Microsoft.Extensions.Logging; -namespace CryptoExchange.Net.Logging.Extensions -{ +namespace CryptoExchange.Net.Logging.Extensions; + #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member - public static class TrackerLoggingExtensions +public static class TrackerLoggingExtensions +{ + private static readonly Action _klineTrackerStatusChanged; + private static readonly Action _klineTrackerStarting; + private static readonly Action _klineTrackerStartFailed; + private static readonly Action _klineTrackerStarted; + private static readonly Action _klineTrackerStopping; + private static readonly Action _klineTrackerStopped; + private static readonly Action _klineTrackerInitialDataSet; + private static readonly Action _klineTrackerKlineUpdated; + private static readonly Action _klineTrackerKlineAdded; + private static readonly Action _klineTrackerConnectionLost; + private static readonly Action _klineTrackerConnectionClosed; + private static readonly Action _klineTrackerConnectionRestored; + + private static readonly Action _tradeTrackerStatusChanged; + private static readonly Action _tradeTrackerStarting; + private static readonly Action _tradeTrackerStartFailed; + private static readonly Action _tradeTrackerStarted; + private static readonly Action _tradeTrackerStopping; + private static readonly Action _tradeTrackerStopped; + private static readonly Action _tradeTrackerInitialDataSet; + private static readonly Action _tradeTrackerPreSnapshotSkip; + private static readonly Action _tradeTrackerPreSnapshotApplied; + private static readonly Action _tradeTrackerTradeAdded; + private static readonly Action _tradeTrackerConnectionLost; + private static readonly Action _tradeTrackerConnectionClosed; + private static readonly Action _tradeTrackerConnectionRestored; + + static TrackerLoggingExtensions() { - private static readonly Action _klineTrackerStatusChanged; - private static readonly Action _klineTrackerStarting; - private static readonly Action _klineTrackerStartFailed; - private static readonly Action _klineTrackerStarted; - private static readonly Action _klineTrackerStopping; - private static readonly Action _klineTrackerStopped; - private static readonly Action _klineTrackerInitialDataSet; - private static readonly Action _klineTrackerKlineUpdated; - private static readonly Action _klineTrackerKlineAdded; - private static readonly Action _klineTrackerConnectionLost; - private static readonly Action _klineTrackerConnectionClosed; - private static readonly Action _klineTrackerConnectionRestored; + _klineTrackerStatusChanged = LoggerMessage.Define( + LogLevel.Debug, + new EventId(6001, "KlineTrackerStatusChanged"), + "Kline tracker for {Symbol} status changed: {OldStatus} => {NewStatus}"); - private static readonly Action _tradeTrackerStatusChanged; - private static readonly Action _tradeTrackerStarting; - private static readonly Action _tradeTrackerStartFailed; - private static readonly Action _tradeTrackerStarted; - private static readonly Action _tradeTrackerStopping; - private static readonly Action _tradeTrackerStopped; - private static readonly Action _tradeTrackerInitialDataSet; - private static readonly Action _tradeTrackerPreSnapshotSkip; - private static readonly Action _tradeTrackerPreSnapshotApplied; - private static readonly Action _tradeTrackerTradeAdded; - private static readonly Action _tradeTrackerConnectionLost; - private static readonly Action _tradeTrackerConnectionClosed; - private static readonly Action _tradeTrackerConnectionRestored; + _klineTrackerStarting = LoggerMessage.Define( + LogLevel.Debug, + new EventId(6002, "KlineTrackerStarting"), + "Kline tracker for {Symbol} starting"); - static TrackerLoggingExtensions() - { - _klineTrackerStatusChanged = LoggerMessage.Define( - LogLevel.Debug, - new EventId(6001, "KlineTrackerStatusChanged"), - "Kline tracker for {Symbol} status changed: {OldStatus} => {NewStatus}"); + _klineTrackerStartFailed = LoggerMessage.Define( + LogLevel.Warning, + new EventId(6003, "KlineTrackerStartFailed"), + "Kline tracker for {Symbol} failed to start: {Error}"); - _klineTrackerStarting = LoggerMessage.Define( - LogLevel.Debug, - new EventId(6002, "KlineTrackerStarting"), - "Kline tracker for {Symbol} starting"); + _klineTrackerStarted = LoggerMessage.Define( + LogLevel.Information, + new EventId(6004, "KlineTrackerStarted"), + "Kline tracker for {Symbol} started"); - _klineTrackerStartFailed = LoggerMessage.Define( - LogLevel.Warning, - new EventId(6003, "KlineTrackerStartFailed"), - "Kline tracker for {Symbol} failed to start: {Error}"); + _klineTrackerStopping = LoggerMessage.Define( + LogLevel.Debug, + new EventId(6005, "KlineTrackerStopping"), + "Kline tracker for {Symbol} stopping"); - _klineTrackerStarted = LoggerMessage.Define( - LogLevel.Information, - new EventId(6004, "KlineTrackerStarted"), - "Kline tracker for {Symbol} started"); + _klineTrackerStopped = LoggerMessage.Define( + LogLevel.Information, + new EventId(6006, "KlineTrackerStopped"), + "Kline tracker for {Symbol} stopped"); - _klineTrackerStopping = LoggerMessage.Define( - LogLevel.Debug, - new EventId(6005, "KlineTrackerStopping"), - "Kline tracker for {Symbol} stopping"); + _klineTrackerInitialDataSet = LoggerMessage.Define( + LogLevel.Debug, + new EventId(6007, "KlineTrackerInitialDataSet"), + "Kline tracker for {Symbol} initial data set, last timestamp: {LastTime}"); - _klineTrackerStopped = LoggerMessage.Define( - LogLevel.Information, - new EventId(6006, "KlineTrackerStopped"), - "Kline tracker for {Symbol} stopped"); + _klineTrackerKlineUpdated = LoggerMessage.Define( + LogLevel.Trace, + new EventId(6008, "KlineTrackerKlineUpdated"), + "Kline tracker for {Symbol} kline updated for open time: {LastTime}"); - _klineTrackerInitialDataSet = LoggerMessage.Define( - LogLevel.Debug, - new EventId(6007, "KlineTrackerInitialDataSet"), - "Kline tracker for {Symbol} initial data set, last timestamp: {LastTime}"); + _klineTrackerKlineAdded = LoggerMessage.Define( + LogLevel.Trace, + new EventId(6009, "KlineTrackerKlineAdded"), + "Kline tracker for {Symbol} new kline for open time: {LastTime}"); - _klineTrackerKlineUpdated = LoggerMessage.Define( - LogLevel.Trace, - new EventId(6008, "KlineTrackerKlineUpdated"), - "Kline tracker for {Symbol} kline updated for open time: {LastTime}"); + _klineTrackerConnectionLost = LoggerMessage.Define( + LogLevel.Warning, + new EventId(6010, "KlineTrackerConnectionLost"), + "Kline tracker for {Symbol} connection lost"); - _klineTrackerKlineAdded = LoggerMessage.Define( - LogLevel.Trace, - new EventId(6009, "KlineTrackerKlineAdded"), - "Kline tracker for {Symbol} new kline for open time: {LastTime}"); + _klineTrackerConnectionClosed = LoggerMessage.Define( + LogLevel.Warning, + new EventId(6011, "KlineTrackerConnectionClosed"), + "Kline tracker for {Symbol} disconnected"); - _klineTrackerConnectionLost = LoggerMessage.Define( - LogLevel.Warning, - new EventId(6010, "KlineTrackerConnectionLost"), - "Kline tracker for {Symbol} connection lost"); + _klineTrackerConnectionRestored = LoggerMessage.Define( + LogLevel.Information, + new EventId(6012, "KlineTrackerConnectionRestored"), + "Kline tracker for {Symbol} successfully resynchronized"); - _klineTrackerConnectionClosed = LoggerMessage.Define( - LogLevel.Warning, - new EventId(6011, "KlineTrackerConnectionClosed"), - "Kline tracker for {Symbol} disconnected"); + _tradeTrackerStatusChanged = LoggerMessage.Define( + LogLevel.Debug, + new EventId(6013, "KlineTrackerStatusChanged"), + "Trade tracker for {Symbol} status changed: {OldStatus} => {NewStatus}"); - _klineTrackerConnectionRestored = LoggerMessage.Define( - LogLevel.Information, - new EventId(6012, "KlineTrackerConnectionRestored"), - "Kline tracker for {Symbol} successfully resynchronized"); + _tradeTrackerStarting = LoggerMessage.Define( + LogLevel.Debug, + new EventId(6014, "KlineTrackerStarting"), + "Trade tracker for {Symbol} starting"); - _tradeTrackerStatusChanged = LoggerMessage.Define( - LogLevel.Debug, - new EventId(6013, "KlineTrackerStatusChanged"), - "Trade tracker for {Symbol} status changed: {OldStatus} => {NewStatus}"); + _tradeTrackerStartFailed = LoggerMessage.Define( + LogLevel.Warning, + new EventId(6015, "KlineTrackerStartFailed"), + "Trade tracker for {Symbol} failed to start: {Error}"); - _tradeTrackerStarting = LoggerMessage.Define( - LogLevel.Debug, - new EventId(6014, "KlineTrackerStarting"), - "Trade tracker for {Symbol} starting"); + _tradeTrackerStarted = LoggerMessage.Define( + LogLevel.Information, + new EventId(6016, "KlineTrackerStarted"), + "Trade tracker for {Symbol} started"); - _tradeTrackerStartFailed = LoggerMessage.Define( - LogLevel.Warning, - new EventId(6015, "KlineTrackerStartFailed"), - "Trade tracker for {Symbol} failed to start: {Error}"); + _tradeTrackerStopping = LoggerMessage.Define( + LogLevel.Debug, + new EventId(6017, "KlineTrackerStopping"), + "Trade tracker for {Symbol} stopping"); - _tradeTrackerStarted = LoggerMessage.Define( - LogLevel.Information, - new EventId(6016, "KlineTrackerStarted"), - "Trade tracker for {Symbol} started"); + _tradeTrackerStopped = LoggerMessage.Define( + LogLevel.Information, + new EventId(6018, "KlineTrackerStopped"), + "Trade tracker for {Symbol} stopped"); - _tradeTrackerStopping = LoggerMessage.Define( - LogLevel.Debug, - new EventId(6017, "KlineTrackerStopping"), - "Trade tracker for {Symbol} stopping"); + _tradeTrackerInitialDataSet = LoggerMessage.Define( + LogLevel.Debug, + new EventId(6019, "TradeTrackerInitialDataSet"), + "Trade tracker for {Symbol} snapshot set, Count: {Count}, Last id: {LastId}"); - _tradeTrackerStopped = LoggerMessage.Define( - LogLevel.Information, - new EventId(6018, "KlineTrackerStopped"), - "Trade tracker for {Symbol} stopped"); + _tradeTrackerPreSnapshotSkip = LoggerMessage.Define( + LogLevel.Trace, + new EventId(6020, "TradeTrackerPreSnapshotSkip"), + "Trade tracker for {Symbol} skipping {Id}, already in snapshot"); - _tradeTrackerInitialDataSet = LoggerMessage.Define( - LogLevel.Debug, - new EventId(6019, "TradeTrackerInitialDataSet"), - "Trade tracker for {Symbol} snapshot set, Count: {Count}, Last id: {LastId}"); + _tradeTrackerPreSnapshotApplied = LoggerMessage.Define( + LogLevel.Trace, + new EventId(6021, "TradeTrackerPreSnapshotApplied"), + "Trade tracker for {Symbol} adding {Id} from pre-snapshot"); - _tradeTrackerPreSnapshotSkip = LoggerMessage.Define( - LogLevel.Trace, - new EventId(6020, "TradeTrackerPreSnapshotSkip"), - "Trade tracker for {Symbol} skipping {Id}, already in snapshot"); + _tradeTrackerTradeAdded = LoggerMessage.Define( + LogLevel.Trace, + new EventId(6022, "TradeTrackerTradeAdded"), + "Trade tracker for {Symbol} adding trade {Id}"); - _tradeTrackerPreSnapshotApplied = LoggerMessage.Define( - LogLevel.Trace, - new EventId(6021, "TradeTrackerPreSnapshotApplied"), - "Trade tracker for {Symbol} adding {Id} from pre-snapshot"); + _tradeTrackerConnectionLost = LoggerMessage.Define( + LogLevel.Warning, + new EventId(6023, "TradeTrackerConnectionLost"), + "Trade tracker for {Symbol} connection lost"); - _tradeTrackerTradeAdded = LoggerMessage.Define( - LogLevel.Trace, - new EventId(6022, "TradeTrackerTradeAdded"), - "Trade tracker for {Symbol} adding trade {Id}"); + _tradeTrackerConnectionClosed = LoggerMessage.Define( + LogLevel.Warning, + new EventId(6024, "TradeTrackerConnectionClosed"), + "Trade tracker for {Symbol} disconnected"); - _tradeTrackerConnectionLost = LoggerMessage.Define( - LogLevel.Warning, - new EventId(6023, "TradeTrackerConnectionLost"), - "Trade tracker for {Symbol} connection lost"); + _tradeTrackerConnectionRestored = LoggerMessage.Define( + LogLevel.Information, + new EventId(6025, "TradeTrackerConnectionRestored"), + "Trade tracker for {Symbol} successfully resynchronized"); + } - _tradeTrackerConnectionClosed = LoggerMessage.Define( - LogLevel.Warning, - new EventId(6024, "TradeTrackerConnectionClosed"), - "Trade tracker for {Symbol} disconnected"); + public static void KlineTrackerStatusChanged(this ILogger logger, string symbol, SyncStatus oldStatus, SyncStatus newStatus) + { + _klineTrackerStatusChanged(logger, symbol, oldStatus, newStatus, null); + } - _tradeTrackerConnectionRestored = LoggerMessage.Define( - LogLevel.Information, - new EventId(6025, "TradeTrackerConnectionRestored"), - "Trade tracker for {Symbol} successfully resynchronized"); - } + public static void KlineTrackerStarting(this ILogger logger, string symbol) + { + _klineTrackerStarting(logger, symbol, null); + } - public static void KlineTrackerStatusChanged(this ILogger logger, string symbol, SyncStatus oldStatus, SyncStatus newStatus) - { - _klineTrackerStatusChanged(logger, symbol, oldStatus, newStatus, null); - } + public static void KlineTrackerStartFailed(this ILogger logger, string symbol, string error, Exception? exception) + { + _klineTrackerStartFailed(logger, symbol, error, exception); + } - public static void KlineTrackerStarting(this ILogger logger, string symbol) - { - _klineTrackerStarting(logger, symbol, null); - } + public static void KlineTrackerStarted(this ILogger logger, string symbol) + { + _klineTrackerStarted(logger, symbol, null); + } - public static void KlineTrackerStartFailed(this ILogger logger, string symbol, string error, Exception? exception) - { - _klineTrackerStartFailed(logger, symbol, error, exception); - } + public static void KlineTrackerStopping(this ILogger logger, string symbol) + { + _klineTrackerStopping(logger, symbol, null); + } - public static void KlineTrackerStarted(this ILogger logger, string symbol) - { - _klineTrackerStarted(logger, symbol, null); - } + public static void KlineTrackerStopped(this ILogger logger, string symbol) + { + _klineTrackerStopped(logger, symbol, null); + } - public static void KlineTrackerStopping(this ILogger logger, string symbol) - { - _klineTrackerStopping(logger, symbol, null); - } + public static void KlineTrackerInitialDataSet(this ILogger logger, string symbol, DateTime lastTime) + { + _klineTrackerInitialDataSet(logger, symbol, lastTime, null); + } - public static void KlineTrackerStopped(this ILogger logger, string symbol) - { - _klineTrackerStopped(logger, symbol, null); - } + public static void KlineTrackerKlineUpdated(this ILogger logger, string symbol, DateTime lastTime) + { + _klineTrackerKlineUpdated(logger, symbol, lastTime, null); + } - public static void KlineTrackerInitialDataSet(this ILogger logger, string symbol, DateTime lastTime) - { - _klineTrackerInitialDataSet(logger, symbol, lastTime, null); - } + public static void KlineTrackerKlineAdded(this ILogger logger, string symbol, DateTime lastTime) + { + _klineTrackerKlineAdded(logger, symbol, lastTime, null); + } - public static void KlineTrackerKlineUpdated(this ILogger logger, string symbol, DateTime lastTime) - { - _klineTrackerKlineUpdated(logger, symbol, lastTime, null); - } + public static void KlineTrackerConnectionLost(this ILogger logger, string symbol) + { + _klineTrackerConnectionLost(logger, symbol, null); + } - public static void KlineTrackerKlineAdded(this ILogger logger, string symbol, DateTime lastTime) - { - _klineTrackerKlineAdded(logger, symbol, lastTime, null); - } + public static void KlineTrackerConnectionClosed(this ILogger logger, string symbol) + { + _klineTrackerConnectionClosed(logger, symbol, null); + } - public static void KlineTrackerConnectionLost(this ILogger logger, string symbol) - { - _klineTrackerConnectionLost(logger, symbol, null); - } + public static void KlineTrackerConnectionRestored(this ILogger logger, string symbol) + { + _klineTrackerConnectionRestored(logger, symbol, null); + } - public static void KlineTrackerConnectionClosed(this ILogger logger, string symbol) - { - _klineTrackerConnectionClosed(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 KlineTrackerConnectionRestored(this ILogger logger, string symbol) - { - _klineTrackerConnectionRestored(logger, symbol, null); - } + public static void TradeTrackerStarting(this ILogger logger, string symbol) + { + _tradeTrackerStarting(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 TradeTrackerStartFailed(this ILogger logger, string symbol, string error, Exception? ex) + { + _tradeTrackerStartFailed(logger, symbol, error, ex); + } - public static void TradeTrackerStarting(this ILogger logger, string symbol) - { - _tradeTrackerStarting(logger, symbol, null); - } + public static void TradeTrackerStarted(this ILogger logger, string symbol) + { + _tradeTrackerStarted(logger, symbol, null); + } - public static void TradeTrackerStartFailed(this ILogger logger, string symbol, string error, Exception? ex) - { - _tradeTrackerStartFailed(logger, symbol, error, ex); - } + public static void TradeTrackerStopping(this ILogger logger, string symbol) + { + _tradeTrackerStopping(logger, symbol, null); + } - public static void TradeTrackerStarted(this ILogger logger, string symbol) - { - _tradeTrackerStarted(logger, symbol, null); - } + public static void TradeTrackerStopped(this ILogger logger, string symbol) + { + _tradeTrackerStopped(logger, symbol, null); + } - public static void TradeTrackerStopping(this ILogger logger, string symbol) - { - _tradeTrackerStopping(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 TradeTrackerStopped(this ILogger logger, string symbol) - { - _tradeTrackerStopped(logger, symbol, null); - } + public static void TradeTrackerPreSnapshotSkip(this ILogger logger, string symbol, long lastId) + { + _tradeTrackerPreSnapshotSkip(logger, symbol, lastId, null); + } - public static void TradeTrackerInitialDataSet(this ILogger logger, string symbol, int count, long lastId) - { - _tradeTrackerInitialDataSet(logger, symbol, count, lastId, null); - } + public static void TradeTrackerPreSnapshotApplied(this ILogger logger, string symbol, long lastId) + { + _tradeTrackerPreSnapshotApplied(logger, symbol, lastId, null); + } - public static void TradeTrackerPreSnapshotSkip(this ILogger logger, string symbol, long lastId) - { - _tradeTrackerPreSnapshotSkip(logger, symbol, lastId, null); - } + public static void TradeTrackerTradeAdded(this ILogger logger, string symbol, long lastId) + { + _tradeTrackerTradeAdded(logger, symbol, lastId, null); + } - public static void TradeTrackerPreSnapshotApplied(this ILogger logger, string symbol, long lastId) - { - _tradeTrackerPreSnapshotApplied(logger, symbol, lastId, null); - } + public static void TradeTrackerConnectionLost(this ILogger logger, string symbol) + { + _tradeTrackerConnectionLost(logger, symbol, null); + } - public static void TradeTrackerTradeAdded(this ILogger logger, string symbol, long lastId) - { - _tradeTrackerTradeAdded(logger, symbol, lastId, null); - } + public static void TradeTrackerConnectionClosed(this ILogger logger, string symbol) + { + _tradeTrackerConnectionClosed(logger, symbol, 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); - } + public static void TradeTrackerConnectionRestored(this ILogger logger, string symbol) + { + _tradeTrackerConnectionRestored(logger, symbol, null); } } diff --git a/CryptoExchange.Net/Objects/ApiProxy.cs b/CryptoExchange.Net/Objects/ApiProxy.cs index bc0da4e..ab64738 100644 --- a/CryptoExchange.Net/Objects/ApiProxy.cs +++ b/CryptoExchange.Net/Objects/ApiProxy.cs @@ -1,42 +1,41 @@ -namespace CryptoExchange.Net.Objects +namespace CryptoExchange.Net.Objects; + +/// +/// Proxy info +/// +public class ApiProxy { /// - /// Proxy info + /// The host address of the proxy /// - public class ApiProxy + public string Host { get; set; } + /// + /// The port of the proxy + /// + public int Port { get; set; } + + /// + /// The login of the proxy + /// + public string? Login { get; set; } + + /// + /// The password of the proxy + /// + public string? Password { get; set; } + + /// + /// Create new settings for a proxy + /// + /// The proxy hostname/ip + /// The proxy port + /// The proxy login + /// The proxy password + public ApiProxy(string host, int port, string? login = null, string? password = null) { - /// - /// The host address of the proxy - /// - public string Host { get; set; } - /// - /// The port of the proxy - /// - public int Port { get; set; } - - /// - /// The login of the proxy - /// - public string? Login { get; set; } - - /// - /// The password of the proxy - /// - public string? Password { get; set; } - - /// - /// Create new settings for a proxy - /// - /// The proxy hostname/ip - /// The proxy port - /// The proxy login - /// The proxy password - public ApiProxy(string host, int port, string? login = null, string? password = null) - { - Host = host; - Port = port; - Login = login; - Password = password; - } + Host = host; + Port = port; + Login = login; + Password = password; } } diff --git a/CryptoExchange.Net/Objects/AssetAlias.cs b/CryptoExchange.Net/Objects/AssetAlias.cs index 5829d09..1187d88 100644 --- a/CryptoExchange.Net/Objects/AssetAlias.cs +++ b/CryptoExchange.Net/Objects/AssetAlias.cs @@ -1,30 +1,25 @@ -using System; -using System.Collections.Generic; -using System.Text; +namespace CryptoExchange.Net.Objects; -namespace CryptoExchange.Net.Objects +/// +/// An alias used by the exchange for an asset commonly known by another name +/// +public class AssetAlias { /// - /// An alias used by the exchange for an asset commonly known by another name + /// The name of the asset on the exchange /// - public class AssetAlias - { - /// - /// The name of the asset on the exchange - /// - public string ExchangeAssetName { get; set; } - /// - /// The name of the asset as it's commonly known - /// - public string CommonAssetName { get; set; } + public string ExchangeAssetName { get; set; } + /// + /// The name of the asset as it's commonly known + /// + public string CommonAssetName { get; set; } - /// - /// ctor - /// - public AssetAlias(string exchangeName, string commonName) - { - ExchangeAssetName = exchangeName; - CommonAssetName = commonName; - } + /// + /// ctor + /// + public AssetAlias(string exchangeName, string commonName) + { + ExchangeAssetName = exchangeName; + CommonAssetName = commonName; } } diff --git a/CryptoExchange.Net/Objects/AssetAliasConfiguration.cs b/CryptoExchange.Net/Objects/AssetAliasConfiguration.cs index ab5622f..d59b55f 100644 --- a/CryptoExchange.Net/Objects/AssetAliasConfiguration.cs +++ b/CryptoExchange.Net/Objects/AssetAliasConfiguration.cs @@ -1,34 +1,30 @@ -using System; -using System.Collections.Generic; using System.Linq; -using System.Text; -namespace CryptoExchange.Net.Objects +namespace CryptoExchange.Net.Objects; + +/// +/// Exchange configuration for asset aliases +/// +public class AssetAliasConfiguration { /// - /// Exchange configuration for asset aliases + /// Defined aliases /// - public class AssetAliasConfiguration - { - /// - /// Defined aliases - /// - public AssetAlias[] Aliases { get; set; } = []; + public AssetAlias[] Aliases { get; set; } = []; - /// - /// Auto convert asset names when using the Shared interfaces. Defaults to true - /// - public bool AutoConvertEnabled { get; set; } = true; + /// + /// Auto convert asset names when using the Shared interfaces. Defaults to true + /// + public bool AutoConvertEnabled { get; set; } = true; - /// - /// Map the common name to an exchange name for an asset. If there is no alias the input name is returned - /// - public string CommonToExchangeName(string commonName) => !AutoConvertEnabled ? commonName : Aliases.SingleOrDefault(x => x.CommonAssetName == commonName)?.ExchangeAssetName ?? commonName; + /// + /// Map the common name to an exchange name for an asset. If there is no alias the input name is returned + /// + public string CommonToExchangeName(string commonName) => !AutoConvertEnabled ? commonName : Aliases.SingleOrDefault(x => x.CommonAssetName == commonName)?.ExchangeAssetName ?? commonName; - /// - /// Map the exchange name to a common name for an asset. If there is no alias the input name is returned - /// - public string ExchangeToCommonName(string exchangeName) => !AutoConvertEnabled ? exchangeName : Aliases.SingleOrDefault(x => x.ExchangeAssetName == exchangeName)?.CommonAssetName ?? exchangeName; + /// + /// Map the exchange name to a common name for an asset. If there is no alias the input name is returned + /// + public string ExchangeToCommonName(string exchangeName) => !AutoConvertEnabled ? exchangeName : Aliases.SingleOrDefault(x => x.ExchangeAssetName == exchangeName)?.CommonAssetName ?? exchangeName; - } } diff --git a/CryptoExchange.Net/Objects/AsyncAutoResetEvent.cs b/CryptoExchange.Net/Objects/AsyncAutoResetEvent.cs index ff4895d..96ecc7c 100644 --- a/CryptoExchange.Net/Objects/AsyncAutoResetEvent.cs +++ b/CryptoExchange.Net/Objects/AsyncAutoResetEvent.cs @@ -1,122 +1,123 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; -namespace CryptoExchange.Net.Objects +namespace CryptoExchange.Net.Objects; + +/// +/// Async auto reset based on Stephen Toub`s implementation +/// https://devblogs.microsoft.com/pfxteam/building-async-coordination-primitives-part-2-asyncautoresetevent/ +/// +public class AsyncResetEvent : IDisposable { + private static readonly Task _completed = Task.FromResult(true); + private Queue> _waits = new Queue>(); + private bool _signaled; + private readonly bool _reset; + /// - /// Async auto reset based on Stephen Toub`s implementation - /// https://devblogs.microsoft.com/pfxteam/building-async-coordination-primitives-part-2-asyncautoresetevent/ + /// New AsyncResetEvent /// - public class AsyncResetEvent : IDisposable + /// + /// + public AsyncResetEvent(bool initialState = false, bool reset = true) { - private static readonly Task _completed = Task.FromResult(true); - private Queue> _waits = new Queue>(); - private bool _signaled; - private readonly bool _reset; + _signaled = initialState; + _reset = reset; + } - /// - /// New AsyncResetEvent - /// - /// - /// - public AsyncResetEvent(bool initialState = false, bool reset = true) - { - _signaled = initialState; - _reset = reset; - } - - /// - /// Wait for the AutoResetEvent to be set - /// - /// - public async Task WaitAsync(TimeSpan? timeout = null, CancellationToken ct = default) - { - CancellationTokenRegistration registration = default; - try - { - Task waiter = _completed; - lock (_waits) - { - if (_signaled) - { - if (_reset) - _signaled = false; - } - else if (!ct.IsCancellationRequested) - { - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - if (timeout.HasValue) - { - var timeoutSource = new CancellationTokenSource(timeout.Value); - var cancellationSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutSource.Token, ct); - ct = cancellationSource.Token; - } - - registration = ct.Register(() => - { - lock (_waits) - { - tcs.TrySetResult(false); - - // Not the cleanest but it works - _waits = new Queue>(_waits.Where(i => i != tcs)); - } - }, useSynchronizationContext: false); - - - _waits.Enqueue(tcs); - waiter = tcs.Task; - } - } - - return await waiter.ConfigureAwait(false); - } - finally - { - registration.Dispose(); - } - } - - /// - /// Signal a waiter - /// - public void Set() + /// + /// Wait for the AutoResetEvent to be set + /// + /// + public async Task WaitAsync(TimeSpan? timeout = null, CancellationToken ct = default) + { + CancellationTokenRegistration registration = default; + try { + Task waiter = _completed; lock (_waits) { - if (!_reset) + if (_signaled) { - // Act as ManualResetEvent. Once set keep it signaled and signal everyone who is waiting - _signaled = true; - while (_waits.Count > 0) - { - var toRelease = _waits.Dequeue(); - toRelease.TrySetResult(true); - } + if (_reset) + _signaled = false; } - else + else if (!ct.IsCancellationRequested) { - // Act as AutoResetEvent. When set signal 1 waiter - if (_waits.Count > 0) + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + if (timeout.HasValue) { - var toRelease = _waits.Dequeue(); - toRelease.TrySetResult(true); + var timeoutSource = new CancellationTokenSource(timeout.Value); + var cancellationSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutSource.Token, ct); + ct = cancellationSource.Token; } - else if (!_signaled) - _signaled = true; + + registration = ct.Register(() => + { + lock (_waits) + { + tcs.TrySetResult(false); + + // Not the cleanest but it works + _waits = new Queue>(_waits.Where(i => i != tcs)); + } + }, useSynchronizationContext: false); + + + _waits.Enqueue(tcs); + waiter = tcs.Task; + } + } + + return await waiter.ConfigureAwait(false); + } + finally + { + registration.Dispose(); + } + } + + /// + /// Signal a waiter + /// + public void Set() + { + lock (_waits) + { + if (!_reset) + { + // Act as ManualResetEvent. Once set keep it signaled and signal everyone who is waiting + _signaled = true; + while (_waits.Count > 0) + { + var toRelease = _waits.Dequeue(); + toRelease.TrySetResult(true); + } + } + else + { + // Act as AutoResetEvent. When set signal 1 waiter + if (_waits.Count > 0) + { + var toRelease = _waits.Dequeue(); + toRelease.TrySetResult(true); + } + else if (!_signaled) + { + _signaled = true; } } } + } - /// - /// Dispose - /// - public void Dispose() - { - _waits.Clear(); - } + /// + /// Dispose + /// + public void Dispose() + { + _waits.Clear(); } } diff --git a/CryptoExchange.Net/Objects/AuthTimeProvider.cs b/CryptoExchange.Net/Objects/AuthTimeProvider.cs index 9cdeb96..68e423c 100644 --- a/CryptoExchange.Net/Objects/AuthTimeProvider.cs +++ b/CryptoExchange.Net/Objects/AuthTimeProvider.cs @@ -1,10 +1,9 @@ -using CryptoExchange.Net.Interfaces; +using CryptoExchange.Net.Interfaces; using System; -namespace CryptoExchange.Net.Objects +namespace CryptoExchange.Net.Objects; + +internal class AuthTimeProvider : IAuthTimeProvider { - internal class AuthTimeProvider : IAuthTimeProvider - { - public DateTime GetTime() => DateTime.UtcNow; - } + public DateTime GetTime() => DateTime.UtcNow; } diff --git a/CryptoExchange.Net/Objects/ByteOrderComparer.cs b/CryptoExchange.Net/Objects/ByteOrderComparer.cs index e2b694b..9a3ff92 100644 --- a/CryptoExchange.Net/Objects/ByteOrderComparer.cs +++ b/CryptoExchange.Net/Objects/ByteOrderComparer.cs @@ -1,58 +1,57 @@ -using System; +using System; using System.Collections.Generic; -namespace CryptoExchange.Net.Objects +namespace CryptoExchange.Net.Objects; + +/// +/// Comparer for byte order +/// +public class ByteOrderComparer : IComparer { /// - /// Comparer for byte order + /// Compare function /// - public class ByteOrderComparer : IComparer + /// + /// + /// + public int Compare(byte[]? x, byte[]? y) { - /// - /// Compare function - /// - /// - /// - /// - public int Compare(byte[]? x, byte[]? y) + // Shortcuts: If both are null, they are the same. + if (x == null && y == null) return 0; + + // If one is null and the other isn't, then the + // one that is null is "lesser". + if (x == null) return -1; + if (y == null) return 1; + + // Both arrays are non-null. Find the shorter + // of the two lengths. + var bytesToCompare = Math.Min(x.Length, y.Length); + + // Compare the bytes. + for (var index = 0; index < bytesToCompare; ++index) { - // Shortcuts: If both are null, they are the same. - if (x == null && y == null) return 0; + // The x and y bytes. + var xByte = x[index]; + var yByte = y[index]; - // If one is null and the other isn't, then the - // one that is null is "lesser". - if (x == null) return -1; - if (y == null) return 1; + // Compare result. + var compareResult = Comparer.Default.Compare(xByte, yByte); - // Both arrays are non-null. Find the shorter - // of the two lengths. - var bytesToCompare = Math.Min(x.Length, y.Length); + // If not the same, then return the result of the + // comparison of the bytes, as they were the same + // up until now. + if (compareResult != 0) return compareResult; - // Compare the bytes. - for (var index = 0; index < bytesToCompare; ++index) - { - // The x and y bytes. - var xByte = x[index]; - var yByte = y[index]; - - // Compare result. - var compareResult = Comparer.Default.Compare(xByte, yByte); - - // If not the same, then return the result of the - // comparison of the bytes, as they were the same - // up until now. - if (compareResult != 0) return compareResult; - - // They are the same, continue. - } - - // The first n bytes are the same. Compare lengths. - // If the lengths are the same, the arrays - // are the same. - if (x.Length == y.Length) return 0; - - // Compare lengths. - return x.Length < y.Length ? -1 : 1; + // They are the same, continue. } + + // The first n bytes are the same. Compare lengths. + // If the lengths are the same, the arrays + // are the same. + if (x.Length == y.Length) return 0; + + // Compare lengths. + return x.Length < y.Length ? -1 : 1; } } diff --git a/CryptoExchange.Net/Objects/CallResult.cs b/CryptoExchange.Net/Objects/CallResult.cs index e847bfc..2b6b9f5 100644 --- a/CryptoExchange.Net/Objects/CallResult.cs +++ b/CryptoExchange.Net/Objects/CallResult.cs @@ -1,4 +1,4 @@ -using CryptoExchange.Net.SharedApis; +using CryptoExchange.Net.SharedApis; using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; @@ -6,583 +6,582 @@ using System.Net; using System.Net.Http; using System.Text; -namespace CryptoExchange.Net.Objects +namespace CryptoExchange.Net.Objects; + +/// +/// The result of an operation +/// +public class CallResult { /// - /// The result of an operation + /// Static success result /// - public class CallResult + public static CallResult SuccessResult { get; } = new CallResult(null); + + /// + /// An error if the call didn't succeed, will always be filled if Success = false + /// + public Error? Error { get; internal set; } + + /// + /// Whether the call was successful + /// + public bool Success => Error == null; + + /// + /// ctor + /// + /// + public CallResult(Error? error) { - /// - /// Static success result - /// - public static CallResult SuccessResult { get; } = new CallResult(null); - - /// - /// An error if the call didn't succeed, will always be filled if Success = false - /// - public Error? Error { get; internal set; } - - /// - /// Whether the call was successful - /// - public bool Success => Error == null; - - /// - /// ctor - /// - /// - public CallResult(Error? error) - { - Error = error; - } - - /// - /// Overwrite bool check so we can use if(callResult) instead of if(callResult.Success) - /// - /// - public static implicit operator bool(CallResult obj) - { - return obj?.Success == true; - } - - /// - public override string ToString() - { - return Success ? $"Success" : $"Error: {Error}"; - } + Error = error; } /// - /// The result of an operation + /// Overwrite bool check so we can use if(callResult) instead of if(callResult.Success) /// - /// - public class CallResult: CallResult + /// + public static implicit operator bool(CallResult obj) { - /// - /// The data returned by the call, only available when Success = true - /// - public T Data { get; internal set; } - - /// - /// The original data returned by the call, only available when `OutputOriginalData` is set to `true` in the client options - /// - public string? OriginalData { get; internal set; } - - /// - /// ctor - /// - /// - /// - /// -#pragma warning disable 8618 - public CallResult([AllowNull]T data, string? originalData, Error? error): base(error) -#pragma warning restore 8618 - { - OriginalData = originalData; -#pragma warning disable 8601 - Data = data; -#pragma warning restore 8601 - } - - /// - /// Create a new data result - /// - /// The data to return - public CallResult(T data) : this(data, null, null) { } - - /// - /// Create a new error result - /// - /// The error to return - public CallResult(Error error) : this(default, null, error) { } - - /// - /// Create a new error result - /// - /// The error to return - /// The original response data - public CallResult(Error error, string? originalData) : this(default, originalData, error) { } - - /// - /// Overwrite bool check so we can use if(callResult) instead of if(callResult.Success) - /// - /// - public static implicit operator bool(CallResult obj) - { - return obj?.Success == true; - } - - /// - /// Whether the call was successful or not. Useful for nullability checking. - /// - /// The data returned by the call. - /// on failure. - /// true when succeeded, false otherwise. - public bool GetResultOrError([MaybeNullWhen(false)] out T data, [NotNullWhen(false)] out Error? error) - { - if (Success) - { - data = Data!; - error = null; - - return true; - } - else - { - data = default; - error = Error!; - - return false; - } - } - - /// - /// Copy the WebCallResult to a new data type - /// - /// The new type - /// The data of the new type - /// - public CallResult As([AllowNull] K data) - { - return new CallResult(data, OriginalData, Error); - } - - /// - /// Copy as a dataless result - /// - /// - public CallResult AsDataless() - { - return SuccessResult; - } - - /// - /// Copy as a dataless result - /// - /// - public CallResult AsDatalessError(Error error) - { - return new CallResult(error); - } - - /// - /// Copy the CallResult to a new data type - /// - /// The new type - /// The data - /// The error returned - /// - public CallResult AsErrorWithData(Error error, K data) - { - return new CallResult(data, OriginalData, error); - } - - /// - /// Copy the WebCallResult to a new data type - /// - /// The new type - /// The error to return - /// - public CallResult AsError(Error error) - { - return new CallResult(default, OriginalData, error); - } - - /// - public override string ToString() - { - return Success ? $"Success" : $"Error: {Error}"; - } + return obj?.Success == true; } - /// - /// The result of a request - /// - public class WebCallResult : CallResult + /// + public override string ToString() { - /// - /// The request http method - /// - public HttpMethod? RequestMethod { get; set; } - - /// - /// The headers sent with the request - /// - public KeyValuePair[]? RequestHeaders { get; set; } - - /// - /// The request id - /// - public int? RequestId { get; set; } - - /// - /// The url which was requested - /// - public string? RequestUrl { get; set; } - - /// - /// The body of the request - /// - public string? RequestBody { get; set; } - - /// - /// The original data returned by the call, only available when `OutputOriginalData` is set to `true` in the client options - /// - public string? OriginalData { get; internal set; } - - /// - /// The status code of the response. Note that a OK status does not always indicate success, check the Success parameter for this. - /// - public HttpStatusCode? ResponseStatusCode { get; set; } - - /// - /// The response headers - /// - public KeyValuePair[]? ResponseHeaders { get; set; } - - /// - /// The time between sending the request and receiving the response - /// - public TimeSpan? ResponseTime { get; set; } - - /// - /// ctor - /// - public WebCallResult( - HttpStatusCode? code, - KeyValuePair[]? responseHeaders, - TimeSpan? responseTime, - string? originalData, - int? requestId, - string? requestUrl, - string? requestBody, - HttpMethod? requestMethod, - KeyValuePair[]? requestHeaders, - Error? error) : base(error) - { - ResponseStatusCode = code; - ResponseHeaders = responseHeaders; - ResponseTime = responseTime; - RequestId = requestId; - OriginalData = originalData; - - RequestUrl = requestUrl; - RequestBody = requestBody; - RequestHeaders = requestHeaders; - RequestMethod = requestMethod; - } - - /// - /// ctor - /// - /// - public WebCallResult(Error error): base(error) { } - - /// - /// Return the result as an error result - /// - /// The error returned - /// - public WebCallResult AsError(Error error) - { - return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error); - } - - /// - /// Copy the WebCallResult to a new data type - /// - /// The new type - /// The data of the new type - /// - public WebCallResult As([AllowNull] K data) - { - return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, 0, null, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, ResultDataSource.Server, data, Error); - } - - /// - /// Copy the WebCallResult to an ExchangeWebResult of a new data type - /// - /// The new type - /// The exchange - /// Trade mode the result applies to - /// The data - /// - public ExchangeWebResult AsExchangeResult(string exchange, TradingMode tradeMode, [AllowNull] K data) - { - return new ExchangeWebResult(exchange, tradeMode, this.As(data)); - } - - /// - /// Copy the WebCallResult to an ExchangeWebResult of a new data type - /// - /// The new type - /// The exchange - /// Trade modes the result applies to - /// The data - /// - public ExchangeWebResult AsExchangeResult(string exchange, TradingMode[]? tradeModes, [AllowNull] K data) - { - return new ExchangeWebResult(exchange, tradeModes, this.As(data)); - } - - /// - /// Copy the WebCallResult to a new data type - /// - /// The new type - /// The error returned - /// - public WebCallResult AsError(Error error) - { - return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, 0, null, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, ResultDataSource.Server, default, error); - } - - /// - public override string ToString() - { - return (Success ? $"Success" : $"Error: {Error}") + $" in {ResponseTime}"; - } - } - - /// - /// The result of a request - /// - /// - public class WebCallResult: CallResult - { - /// - /// The request http method - /// - public HttpMethod? RequestMethod { get; set; } - - /// - /// The headers sent with the request - /// - public KeyValuePair[]? RequestHeaders { get; set; } - - /// - /// The request id - /// - public int? RequestId { get; set; } - - /// - /// The url which was requested - /// - public string? RequestUrl { get; set; } - - /// - /// The body of the request - /// - public string? RequestBody { get; set; } - - /// - /// The status code of the response. Note that a OK status does not always indicate success, check the Success parameter for this. - /// - public HttpStatusCode? ResponseStatusCode { get; set; } - - /// - /// Length in bytes of the response - /// - public long? ResponseLength { get; set; } - - /// - /// The response headers - /// - public KeyValuePair[]? ResponseHeaders { get; set; } - - /// - /// The time between sending the request and receiving the response - /// - public TimeSpan? ResponseTime { get; set; } - - /// - /// The data source of this result - /// - public ResultDataSource DataSource { get; set; } = ResultDataSource.Server; - - /// - /// Create a new result - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - public WebCallResult( - HttpStatusCode? code, - KeyValuePair[]? responseHeaders, - TimeSpan? responseTime, - long? responseLength, - string? originalData, - int? requestId, - string? requestUrl, - string? requestBody, - HttpMethod? requestMethod, - KeyValuePair[]? requestHeaders, - ResultDataSource dataSource, - [AllowNull] T data, - Error? error) : base(data, originalData, error) - { - ResponseStatusCode = code; - ResponseHeaders = responseHeaders; - ResponseTime = responseTime; - ResponseLength = responseLength; - - RequestId = requestId; - RequestUrl = requestUrl; - RequestBody = requestBody; - RequestHeaders = requestHeaders; - RequestMethod = requestMethod; - DataSource = dataSource; - } - - /// - /// Copy as a dataless result - /// - /// - public new WebCallResult AsDataless() - { - return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, Error); - } - /// - /// Copy as a dataless result - /// - /// - public new WebCallResult AsDatalessError(Error error) - { - return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error); - } - - /// - /// Create a new error result - /// - /// The error - public WebCallResult(Error? error) : this(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, default, error) { } - - /// - /// Copy the WebCallResult to a new data type - /// - /// The new type - /// The data of the new type - /// - public new WebCallResult As([AllowNull] K data) - { - return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, data, Error); - } - - /// - /// Copy the WebCallResult to a new data type - /// - /// The new type - /// The error returned - /// - public new WebCallResult AsError(Error error) - { - return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, default, error); - } - - /// - /// Copy the WebCallResult to a new data type - /// - /// The new type - /// The data - /// The error returned - /// - public new WebCallResult AsErrorWithData(Error error, K data) - { - return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, data, error); - } - - /// - /// Copy the WebCallResult to an ExchangeWebResult of a new data type - /// - /// The exchange - /// Trade mode the result applies to - /// - public ExchangeWebResult AsExchangeResult(string exchange, TradingMode tradeMode) - { - return new ExchangeWebResult(exchange, tradeMode, this); - } - - /// - /// Copy the WebCallResult to an ExchangeWebResult of a new data type - /// - /// The exchange - /// Trade modes the result applies to - /// - public ExchangeWebResult AsExchangeResult(string exchange, TradingMode[] tradeModes) - { - return new ExchangeWebResult(exchange, tradeModes, this); - } - - /// - /// Copy the WebCallResult to an ExchangeWebResult of a new data type - /// - /// The new type - /// The exchange - /// Trade mode the result applies to - /// Data - /// Next page token - /// - public ExchangeWebResult AsExchangeResult(string exchange, TradingMode tradeMode, [AllowNull] K data, INextPageToken? nextPageToken = null) - { - return new ExchangeWebResult(exchange, tradeMode, As(data), nextPageToken); - } - - /// - /// Copy the WebCallResult to an ExchangeWebResult of a new data type - /// - /// The new type - /// The exchange - /// Trade modes the result applies to - /// Data - /// Next page token - /// - public ExchangeWebResult AsExchangeResult(string exchange, TradingMode[]? tradeModes, [AllowNull] K data, INextPageToken? nextPageToken = null) - { - return new ExchangeWebResult(exchange, tradeModes, As(data), nextPageToken); - } - - /// - /// Copy the WebCallResult to an ExchangeWebResult with a specific error - /// - /// The new type - /// The exchange - /// The error returned - /// - public ExchangeWebResult AsExchangeError(string exchange, Error error) - { - return new ExchangeWebResult(exchange, null, AsError(error)); - } - - /// - /// Return a copy of this result with data source set to cache - /// - /// - internal WebCallResult Cached() - { - return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, ResultDataSource.Cache, Data, Error); - } - - /// - public override string ToString() - { - var sb = new StringBuilder(); - sb.Append(Success ? $"Success response" : $"Error response: {Error}"); - if (ResponseLength != null) - sb.Append($", {ResponseLength} bytes"); - if (ResponseTime != null) - sb.Append($", received in {Math.Round(ResponseTime?.TotalMilliseconds ?? 0)}ms"); - - return sb.ToString(); - } + return Success ? $"Success" : $"Error: {Error}"; + } +} + +/// +/// The result of an operation +/// +/// +public class CallResult: CallResult +{ + /// + /// The data returned by the call, only available when Success = true + /// + public T Data { get; internal set; } + + /// + /// The original data returned by the call, only available when `OutputOriginalData` is set to `true` in the client options + /// + public string? OriginalData { get; internal set; } + + /// + /// ctor + /// + /// + /// + /// +#pragma warning disable 8618 + public CallResult([AllowNull]T data, string? originalData, Error? error): base(error) +#pragma warning restore 8618 + { + OriginalData = originalData; +#pragma warning disable 8601 + Data = data; +#pragma warning restore 8601 + } + + /// + /// Create a new data result + /// + /// The data to return + public CallResult(T data) : this(data, null, null) { } + + /// + /// Create a new error result + /// + /// The error to return + public CallResult(Error error) : this(default, null, error) { } + + /// + /// Create a new error result + /// + /// The error to return + /// The original response data + public CallResult(Error error, string? originalData) : this(default, originalData, error) { } + + /// + /// Overwrite bool check so we can use if(callResult) instead of if(callResult.Success) + /// + /// + public static implicit operator bool(CallResult obj) + { + return obj?.Success == true; + } + + /// + /// Whether the call was successful or not. Useful for nullability checking. + /// + /// The data returned by the call. + /// on failure. + /// true when succeeded, false otherwise. + public bool GetResultOrError([MaybeNullWhen(false)] out T data, [NotNullWhen(false)] out Error? error) + { + if (Success) + { + data = Data!; + error = null; + + return true; + } + else + { + data = default; + error = Error!; + + return false; + } + } + + /// + /// Copy the WebCallResult to a new data type + /// + /// The new type + /// The data of the new type + /// + public CallResult As([AllowNull] TNew data) + { + return new CallResult(data, OriginalData, Error); + } + + /// + /// Copy as a dataless result + /// + /// + public CallResult AsDataless() + { + return SuccessResult; + } + + /// + /// Copy as a dataless result + /// + /// + public CallResult AsDatalessError(Error error) + { + return new CallResult(error); + } + + /// + /// Copy the CallResult to a new data type + /// + /// The new type + /// The data + /// The error returned + /// + public CallResult AsErrorWithData(Error error, TNew data) + { + return new CallResult(data, OriginalData, error); + } + + /// + /// Copy the WebCallResult to a new data type + /// + /// The new type + /// The error to return + /// + public CallResult AsError(Error error) + { + return new CallResult(default, OriginalData, error); + } + + /// + public override string ToString() + { + return Success ? $"Success" : $"Error: {Error}"; + } +} + +/// +/// The result of a request +/// +public class WebCallResult : CallResult +{ + /// + /// The request http method + /// + public HttpMethod? RequestMethod { get; set; } + + /// + /// The headers sent with the request + /// + public KeyValuePair[]? RequestHeaders { get; set; } + + /// + /// The request id + /// + public int? RequestId { get; set; } + + /// + /// The url which was requested + /// + public string? RequestUrl { get; set; } + + /// + /// The body of the request + /// + public string? RequestBody { get; set; } + + /// + /// The original data returned by the call, only available when `OutputOriginalData` is set to `true` in the client options + /// + public string? OriginalData { get; internal set; } + + /// + /// The status code of the response. Note that a OK status does not always indicate success, check the Success parameter for this. + /// + public HttpStatusCode? ResponseStatusCode { get; set; } + + /// + /// The response headers + /// + public KeyValuePair[]? ResponseHeaders { get; set; } + + /// + /// The time between sending the request and receiving the response + /// + public TimeSpan? ResponseTime { get; set; } + + /// + /// ctor + /// + public WebCallResult( + HttpStatusCode? code, + KeyValuePair[]? responseHeaders, + TimeSpan? responseTime, + string? originalData, + int? requestId, + string? requestUrl, + string? requestBody, + HttpMethod? requestMethod, + KeyValuePair[]? requestHeaders, + Error? error) : base(error) + { + ResponseStatusCode = code; + ResponseHeaders = responseHeaders; + ResponseTime = responseTime; + RequestId = requestId; + OriginalData = originalData; + + RequestUrl = requestUrl; + RequestBody = requestBody; + RequestHeaders = requestHeaders; + RequestMethod = requestMethod; + } + + /// + /// ctor + /// + /// + public WebCallResult(Error error): base(error) { } + + /// + /// Return the result as an error result + /// + /// The error returned + /// + public WebCallResult AsError(Error error) + { + return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error); + } + + /// + /// Copy the WebCallResult to a new data type + /// + /// The new type + /// The data of the new type + /// + public WebCallResult As([AllowNull] TNew data) + { + return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, 0, null, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, ResultDataSource.Server, data, Error); + } + + /// + /// Copy the WebCallResult to an ExchangeWebResult of a new data type + /// + /// The new type + /// The exchange + /// Trade mode the result applies to + /// The data + /// + public ExchangeWebResult AsExchangeResult(string exchange, TradingMode tradeMode, [AllowNull] TNew data) + { + return new ExchangeWebResult(exchange, tradeMode, this.As(data)); + } + + /// + /// Copy the WebCallResult to an ExchangeWebResult of a new data type + /// + /// The new type + /// The exchange + /// Trade modes the result applies to + /// The data + /// + public ExchangeWebResult AsExchangeResult(string exchange, TradingMode[]? tradeModes, [AllowNull] TNew data) + { + return new ExchangeWebResult(exchange, tradeModes, this.As(data)); + } + + /// + /// Copy the WebCallResult to a new data type + /// + /// The new type + /// The error returned + /// + public WebCallResult AsError(Error error) + { + return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, 0, null, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, ResultDataSource.Server, default, error); + } + + /// + public override string ToString() + { + return (Success ? $"Success" : $"Error: {Error}") + $" in {ResponseTime}"; + } +} + +/// +/// The result of a request +/// +/// +public class WebCallResult: CallResult +{ + /// + /// The request http method + /// + public HttpMethod? RequestMethod { get; set; } + + /// + /// The headers sent with the request + /// + public KeyValuePair[]? RequestHeaders { get; set; } + + /// + /// The request id + /// + public int? RequestId { get; set; } + + /// + /// The url which was requested + /// + public string? RequestUrl { get; set; } + + /// + /// The body of the request + /// + public string? RequestBody { get; set; } + + /// + /// The status code of the response. Note that a OK status does not always indicate success, check the Success parameter for this. + /// + public HttpStatusCode? ResponseStatusCode { get; set; } + + /// + /// Length in bytes of the response + /// + public long? ResponseLength { get; set; } + + /// + /// The response headers + /// + public KeyValuePair[]? ResponseHeaders { get; set; } + + /// + /// The time between sending the request and receiving the response + /// + public TimeSpan? ResponseTime { get; set; } + + /// + /// The data source of this result + /// + public ResultDataSource DataSource { get; set; } = ResultDataSource.Server; + + /// + /// Create a new result + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public WebCallResult( + HttpStatusCode? code, + KeyValuePair[]? responseHeaders, + TimeSpan? responseTime, + long? responseLength, + string? originalData, + int? requestId, + string? requestUrl, + string? requestBody, + HttpMethod? requestMethod, + KeyValuePair[]? requestHeaders, + ResultDataSource dataSource, + [AllowNull] T data, + Error? error) : base(data, originalData, error) + { + ResponseStatusCode = code; + ResponseHeaders = responseHeaders; + ResponseTime = responseTime; + ResponseLength = responseLength; + + RequestId = requestId; + RequestUrl = requestUrl; + RequestBody = requestBody; + RequestHeaders = requestHeaders; + RequestMethod = requestMethod; + DataSource = dataSource; + } + + /// + /// Copy as a dataless result + /// + /// + public new WebCallResult AsDataless() + { + return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, Error); + } + /// + /// Copy as a dataless result + /// + /// + public new WebCallResult AsDatalessError(Error error) + { + return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error); + } + + /// + /// Create a new error result + /// + /// The error + public WebCallResult(Error? error) : this(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, default, error) { } + + /// + /// Copy the WebCallResult to a new data type + /// + /// The new type + /// The data of the new type + /// + public new WebCallResult As([AllowNull] TNew data) + { + return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, data, Error); + } + + /// + /// Copy the WebCallResult to a new data type + /// + /// The new type + /// The error returned + /// + public new WebCallResult AsError(Error error) + { + return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, default, error); + } + + /// + /// Copy the WebCallResult to a new data type + /// + /// The new type + /// The data + /// The error returned + /// + public new WebCallResult AsErrorWithData(Error error, TNew data) + { + return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, data, error); + } + + /// + /// Copy the WebCallResult to an ExchangeWebResult of a new data type + /// + /// The exchange + /// Trade mode the result applies to + /// + public ExchangeWebResult AsExchangeResult(string exchange, TradingMode tradeMode) + { + return new ExchangeWebResult(exchange, tradeMode, this); + } + + /// + /// Copy the WebCallResult to an ExchangeWebResult of a new data type + /// + /// The exchange + /// Trade modes the result applies to + /// + public ExchangeWebResult AsExchangeResult(string exchange, TradingMode[] tradeModes) + { + return new ExchangeWebResult(exchange, tradeModes, this); + } + + /// + /// Copy the WebCallResult to an ExchangeWebResult of a new data type + /// + /// The new type + /// The exchange + /// Trade mode the result applies to + /// Data + /// Next page token + /// + public ExchangeWebResult AsExchangeResult(string exchange, TradingMode tradeMode, [AllowNull] TNew data, INextPageToken? nextPageToken = null) + { + return new ExchangeWebResult(exchange, tradeMode, As(data), nextPageToken); + } + + /// + /// Copy the WebCallResult to an ExchangeWebResult of a new data type + /// + /// The new type + /// The exchange + /// Trade modes the result applies to + /// Data + /// Next page token + /// + public ExchangeWebResult AsExchangeResult(string exchange, TradingMode[]? tradeModes, [AllowNull] TNew data, INextPageToken? nextPageToken = null) + { + return new ExchangeWebResult(exchange, tradeModes, As(data), nextPageToken); + } + + /// + /// Copy the WebCallResult to an ExchangeWebResult with a specific error + /// + /// The new type + /// The exchange + /// The error returned + /// + public ExchangeWebResult AsExchangeError(string exchange, Error error) + { + return new ExchangeWebResult(exchange, null, AsError(error)); + } + + /// + /// Return a copy of this result with data source set to cache + /// + /// + internal WebCallResult Cached() + { + return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, ResultDataSource.Cache, Data, Error); + } + + /// + public override string ToString() + { + var sb = new StringBuilder(); + sb.Append(Success ? $"Success response" : $"Error response: {Error}"); + if (ResponseLength != null) + sb.Append($", {ResponseLength} bytes"); + if (ResponseTime != null) + sb.Append($", received in {Math.Round(ResponseTime?.TotalMilliseconds ?? 0)}ms"); + + return sb.ToString(); } } diff --git a/CryptoExchange.Net/Objects/Constants.cs b/CryptoExchange.Net/Objects/Constants.cs index 386897b..b4c5e9e 100644 --- a/CryptoExchange.Net/Objects/Constants.cs +++ b/CryptoExchange.Net/Objects/Constants.cs @@ -1,21 +1,20 @@ -namespace CryptoExchange.Net.Objects +namespace CryptoExchange.Net.Objects; + +/// +/// Constants +/// +public class Constants { /// - /// Constants + /// Json content type header /// - public class Constants - { - /// - /// Json content type header - /// - public const string JsonContentHeader = "application/json"; - /// - /// Form content type header - /// - public const string FormContentHeader = "application/x-www-form-urlencoded"; - /// - /// Placeholder key for when request body should be set to the value of this KVP - /// - public const string BodyPlaceHolderKey = "_BODY_"; - } + public const string JsonContentHeader = "application/json"; + /// + /// Form content type header + /// + public const string FormContentHeader = "application/x-www-form-urlencoded"; + /// + /// Placeholder key for when request body should be set to the value of this KVP + /// + public const string BodyPlaceHolderKey = "_BODY_"; } diff --git a/CryptoExchange.Net/Objects/Enums.cs b/CryptoExchange.Net/Objects/Enums.cs index 3efc973..fc375e9 100644 --- a/CryptoExchange.Net/Objects/Enums.cs +++ b/CryptoExchange.Net/Objects/Enums.cs @@ -1,270 +1,266 @@ -using CryptoExchange.Net.Attributes; +namespace CryptoExchange.Net.Objects; -namespace CryptoExchange.Net.Objects +/// +/// What to do when a request would exceed the rate limit +/// +public enum RateLimitingBehaviour { /// - /// What to do when a request would exceed the rate limit + /// Fail the request /// - public enum RateLimitingBehaviour - { - /// - /// Fail the request - /// - Fail, - /// - /// Wait till the request can be send - /// - Wait - } - + Fail, /// - /// What to do when a request would exceed the rate limit + /// Wait till the request can be send /// - public enum RateLimitWindowType - { - /// - /// A sliding window - /// - Sliding, - /// - /// A fixed interval window - /// - Fixed, - /// - /// A fixed interval starting after the first request - /// - FixedAfterFirst, - /// - /// Decaying window - /// - Decay - } - - /// - /// Where the parameters for a HttpMethod should be added in a request - /// - public enum HttpMethodParameterPosition - { - /// - /// Parameters in body - /// - InBody, - /// - /// Parameters in url - /// - InUri - } - - /// - /// The format of the request body - /// - public enum RequestBodyFormat - { - /// - /// Form data - /// - FormData, - /// - /// Json - /// - Json - } - - /// - /// Tracker sync status - /// - public enum SyncStatus - { - /// - /// Not connected - /// - Disconnected, - /// - /// Syncing, data connection is being made - /// - Syncing, - /// - /// The connection is active, but the full data backlog is not yet reached. For example, a tracker set to retain 10 minutes of data only has 8 minutes of data at this moment. - /// - PartiallySynced, - /// - /// Synced - /// - Synced, - /// - /// Disposed - /// - Disposed - } - - /// - /// Status of the order book - /// - public enum OrderBookStatus - { - /// - /// Not connected - /// - Disconnected, - /// - /// Connecting - /// - Connecting, - /// - /// Reconnecting - /// - Reconnecting, - /// - /// Syncing data - /// - Syncing, - /// - /// Data synced, order book is up to date - /// - Synced, - /// - /// Disposing - /// - Disposing, - /// - /// Disposed - /// - Disposed - } - - /// - /// Order book entry type - /// - public enum OrderBookEntryType - { - /// - /// Ask - /// - Ask, - /// - /// Bid - /// - Bid - } - - /// - /// Define how array parameters should be send - /// - public enum ArrayParametersSerialization -#pragma warning disable CS1570 // XML comment has badly formed XML - { - /// - /// Send as key=value1&key=value2 - /// - MultipleValues, - - /// - /// Send as key[]=value1&key[]=value2 - /// - Array, - /// - /// Send as key=[value1, value2] - /// - JsonArray -#pragma warning restore CS1570 // XML comment has badly formed XML - } - - /// - /// How to round - /// - public enum RoundingType - { - /// - /// Round down (flooring) - /// - Down, - /// - /// Round to closest value - /// - Closest, - /// - /// Round up (ceil) - /// - Up - } - - /// - /// Type of the update - /// - public enum SocketUpdateType - { - /// - /// A update - /// - Update, - /// - /// A snapshot, generally send at the start of the connection - /// - Snapshot - } - - /// - /// Reconnect policy - /// - public enum ReconnectPolicy - { - /// - /// Reconnect is disabled - /// - Disabled, - /// - /// Fixed delay of `ReconnectInterval` between retries - /// - FixedDelay, - /// - /// Backoff policy of 2^`reconnectAttempt`, where `reconnectAttempt` has a max value of 5 - /// - ExponentialBackoff - } - - /// - /// The data source of the result - /// - public enum ResultDataSource - { - /// - /// From server - /// - Server, - /// - /// From cache - /// - Cache - } - - /// - /// Type of exchange - /// - public enum ExchangeType - { - /// - /// Centralized - /// - CEX, - /// - /// Decentralized - /// - DEX - } - - /// - /// Timeout behavior for queries - /// - public enum TimeoutBehavior - { - /// - /// Fail the request - /// - Fail, - /// - /// Mark the query as successful - /// - Succeed - } - + Wait +} + +/// +/// What to do when a request would exceed the rate limit +/// +public enum RateLimitWindowType +{ + /// + /// A sliding window + /// + Sliding, + /// + /// A fixed interval window + /// + Fixed, + /// + /// A fixed interval starting after the first request + /// + FixedAfterFirst, + /// + /// Decaying window + /// + Decay +} + +/// +/// Where the parameters for a HttpMethod should be added in a request +/// +public enum HttpMethodParameterPosition +{ + /// + /// Parameters in body + /// + InBody, + /// + /// Parameters in url + /// + InUri +} + +/// +/// The format of the request body +/// +public enum RequestBodyFormat +{ + /// + /// Form data + /// + FormData, + /// + /// Json + /// + Json +} + +/// +/// Tracker sync status +/// +public enum SyncStatus +{ + /// + /// Not connected + /// + Disconnected, + /// + /// Syncing, data connection is being made + /// + Syncing, + /// + /// The connection is active, but the full data backlog is not yet reached. For example, a tracker set to retain 10 minutes of data only has 8 minutes of data at this moment. + /// + PartiallySynced, + /// + /// Synced + /// + Synced, + /// + /// Disposed + /// + Disposed +} + +/// +/// Status of the order book +/// +public enum OrderBookStatus +{ + /// + /// Not connected + /// + Disconnected, + /// + /// Connecting + /// + Connecting, + /// + /// Reconnecting + /// + Reconnecting, + /// + /// Syncing data + /// + Syncing, + /// + /// Data synced, order book is up to date + /// + Synced, + /// + /// Disposing + /// + Disposing, + /// + /// Disposed + /// + Disposed +} + +/// +/// Order book entry type +/// +public enum OrderBookEntryType +{ + /// + /// Ask + /// + Ask, + /// + /// Bid + /// + Bid +} + +/// +/// Define how array parameters should be send +/// +public enum ArrayParametersSerialization +#pragma warning disable CS1570 // XML comment has badly formed XML +{ + /// + /// Send as key=value1&key=value2 + /// + MultipleValues, + + /// + /// Send as key[]=value1&key[]=value2 + /// + Array, + /// + /// Send as key=[value1, value2] + /// + JsonArray +#pragma warning restore CS1570 // XML comment has badly formed XML +} + +/// +/// How to round +/// +public enum RoundingType +{ + /// + /// Round down (flooring) + /// + Down, + /// + /// Round to closest value + /// + Closest, + /// + /// Round up (ceil) + /// + Up +} + +/// +/// Type of the update +/// +public enum SocketUpdateType +{ + /// + /// A update + /// + Update, + /// + /// A snapshot, generally send at the start of the connection + /// + Snapshot +} + +/// +/// Reconnect policy +/// +public enum ReconnectPolicy +{ + /// + /// Reconnect is disabled + /// + Disabled, + /// + /// Fixed delay of `ReconnectInterval` between retries + /// + FixedDelay, + /// + /// Backoff policy of 2^`reconnectAttempt`, where `reconnectAttempt` has a max value of 5 + /// + ExponentialBackoff +} + +/// +/// The data source of the result +/// +public enum ResultDataSource +{ + /// + /// From server + /// + Server, + /// + /// From cache + /// + Cache +} + +/// +/// Type of exchange +/// +public enum ExchangeType +{ + /// + /// Centralized + /// + CEX, + /// + /// Decentralized + /// + DEX +} + +/// +/// Timeout behavior for queries +/// +public enum TimeoutBehavior +{ + /// + /// Fail the request + /// + Fail, + /// + /// Mark the query as successful + /// + Succeed } diff --git a/CryptoExchange.Net/Objects/Error.cs b/CryptoExchange.Net/Objects/Error.cs index 99bcaa5..6cac4be 100644 --- a/CryptoExchange.Net/Objects/Error.cs +++ b/CryptoExchange.Net/Objects/Error.cs @@ -1,333 +1,331 @@ -using CryptoExchange.Net.Objects.Errors; +using CryptoExchange.Net.Objects.Errors; using System; -namespace CryptoExchange.Net.Objects +namespace CryptoExchange.Net.Objects; + +/// +/// Base class for errors +/// +public abstract class Error { + private int? _code; /// - /// Base class for errors + /// The int error code the server returned; or the http status code int value if there was no error code.
+ ///
+ /// Note:
+ /// The property should be used for more generic error checking; it might contain a string error code if the server does not return an int code. ///
- public abstract class Error - { - - private int? _code; - /// - /// The int error code the server returned; or the http status code int value if there was no error code.
- ///
- /// Note:
- /// The property should be used for more generic error checking; it might contain a string error code if the server does not return an int code. - ///
- public int? Code - { - get - { - if (_code.HasValue) - return _code; - - return int.TryParse(ErrorCode, out var r) ? r : null; - } - set - { - _code = value; - } - } - - /// - /// The error code returned by the server - /// - public string? ErrorCode { get; set; } - /// - /// The error description - /// - public string? ErrorDescription { get; set; } - - /// - /// Error type - /// - public ErrorType ErrorType { get; set; } - - /// - /// Whether the error is transient and can be retried - /// - public bool IsTransient { get; set; } - - /// - /// The server message for the error that occurred - /// - public string? Message { get; set; } - - /// - /// Underlying exception - /// - public Exception? Exception { get; set; } - - /// - /// ctor - /// - protected Error(string? errorCode, ErrorInfo errorInfo, Exception? exception) + public int? Code + { + get { - ErrorCode = errorCode; - ErrorType = errorInfo.ErrorType; - Message = errorInfo.Message; - ErrorDescription = errorInfo.ErrorDescription; - IsTransient = errorInfo.IsTransient; - Exception = exception; - } + if (_code.HasValue) + return _code; - /// - /// String representation - /// - /// - public override string ToString() + return int.TryParse(ErrorCode, out var r) ? r : null; + } + set { - return ErrorCode != null ? $"[{GetType().Name}.{ErrorType}] {ErrorCode}: {Message ?? ErrorDescription}" : $"[{GetType().Name}.{ErrorType}] {Message ?? ErrorDescription}"; + _code = value; } } /// - /// Cant reach server error + /// The error code returned by the server /// - public class CantConnectError : Error + public string? ErrorCode { get; set; } + /// + /// The error description + /// + public string? ErrorDescription { get; set; } + + /// + /// Error type + /// + public ErrorType ErrorType { get; set; } + + /// + /// Whether the error is transient and can be retried + /// + public bool IsTransient { get; set; } + + /// + /// The server message for the error that occurred + /// + public string? Message { get; set; } + + /// + /// Underlying exception + /// + public Exception? Exception { get; set; } + + /// + /// ctor + /// + protected Error(string? errorCode, ErrorInfo errorInfo, Exception? exception) { - /// - /// Default error info - /// - protected static readonly ErrorInfo _errorInfo = new ErrorInfo(ErrorType.UnableToConnect, false, "Can't connect to the server"); - - /// - /// ctor - /// - public CantConnectError() : base(null, _errorInfo, null) { } - - /// - /// ctor - /// - public CantConnectError(Exception? exception) : base(null, _errorInfo, exception) { } - - /// - /// ctor - /// - protected CantConnectError(ErrorInfo info, Exception? exception) : base(null, info, exception) { } + ErrorCode = errorCode; + ErrorType = errorInfo.ErrorType; + Message = errorInfo.Message; + ErrorDescription = errorInfo.ErrorDescription; + IsTransient = errorInfo.IsTransient; + Exception = exception; } /// - /// No api credentials provided while trying to access a private endpoint + /// String representation /// - public class NoApiCredentialsError : Error + /// + public override string ToString() { - /// - /// Default error info - /// - protected static readonly ErrorInfo _errorInfo = new ErrorInfo(ErrorType.MissingCredentials, false, "No credentials provided for private endpoint"); - - /// - /// ctor - /// - public NoApiCredentialsError() : base(null, _errorInfo, null) { } - - /// - /// ctor - /// - protected NoApiCredentialsError(ErrorInfo info, Exception? exception) : base(null, info, exception) { } - } - - /// - /// Error returned by the server - /// - public class ServerError : Error - { - /// - /// ctor - /// - public ServerError(ErrorInfo errorInfo, Exception? exception = null) - : base(null, errorInfo, exception) { } - - /// - /// ctor - /// - public ServerError(int errorCode, ErrorInfo errorInfo, Exception? exception = null) - : this(errorCode.ToString(), errorInfo, exception) { } - - /// - /// ctor - /// - public ServerError(string errorCode, ErrorInfo errorInfo, Exception? exception = null) : base(errorCode, errorInfo, exception) { } - } - - /// - /// Web error returned by the server - /// - public class WebError : Error - { - /// - /// Default error info - /// - protected static readonly ErrorInfo _errorInfo = new ErrorInfo(ErrorType.NetworkError, true, "Failed to complete the request to the server due to a network error"); - - /// - /// ctor - /// - public WebError(string? message = null, Exception? exception = null) : base(null, _errorInfo with { Message = (message?.Length > 0 ? _errorInfo.Message + ": " + message : _errorInfo.Message) }, exception) { } - } - - /// - /// Timeout error waiting for a response from the server - /// - public class TimeoutError : Error - { - /// - /// Default error info - /// - protected static readonly ErrorInfo _errorInfo = new ErrorInfo(ErrorType.Timeout, false, "Failed to receive a response from the server in time"); - - /// - /// ctor - /// - public TimeoutError(string? message = null, Exception? exception = null) : base(null, _errorInfo with { Message = (message?.Length > 0 ? _errorInfo.Message + ": " + message : _errorInfo.Message) }, exception) { } - } - - /// - /// Error while deserializing data - /// - public class DeserializeError : Error - { - /// - /// Default error info - /// - protected static readonly ErrorInfo _errorInfo = new ErrorInfo(ErrorType.DeserializationFailed, false, "Failed to deserialize data"); - - /// - /// ctor - /// - public DeserializeError(string? message = null, Exception? exception = null) : base(null, _errorInfo with { Message = (message?.Length > 0 ? _errorInfo.Message + ": " + message : _errorInfo.Message) }, exception) { } - } - - /// - /// An invalid parameter has been provided - /// - public class ArgumentError : Error - { - /// - /// Default error info for missing parameter - /// - protected static readonly ErrorInfo _missingInfo = new ErrorInfo(ErrorType.MissingParameter, false, "Missing parameter"); - /// - /// Default error info for invalid parameter - /// - protected static readonly ErrorInfo _invalidInfo = new ErrorInfo(ErrorType.InvalidParameter, false, "Invalid parameter"); - - /// - /// ctor - /// - public static ArgumentError Missing(string parameterName, string? message = null) => new ArgumentError(_missingInfo with { Message = message == null ? $"{_missingInfo.Message} '{parameterName}'" : $"{_missingInfo.Message} '{parameterName}': {message}" }, null); - - /// - /// ctor - /// - public static ArgumentError Invalid(string parameterName, string message) => new ArgumentError(_invalidInfo with { Message = $"{_invalidInfo.Message} '{parameterName}': {message}" }, null); - - /// - /// ctor - /// - protected ArgumentError(ErrorInfo info, Exception? exception) : base(null, info, exception) { } - } - - /// - /// Rate limit exceeded (client side) - /// - public abstract class BaseRateLimitError : Error - { - /// - /// When the request can be retried - /// - public DateTime? RetryAfter { get; set; } - - /// - /// ctor - /// - protected BaseRateLimitError(ErrorInfo errorInfo, Exception? exception) : base(null, errorInfo, exception) { } - } - - /// - /// Rate limit exceeded (client side) - /// - public class ClientRateLimitError : BaseRateLimitError - { - /// - /// Default error info - /// - protected static readonly ErrorInfo _errorInfo = new ErrorInfo(ErrorType.RateLimitRequest, false, "Client rate limit exceeded"); - - /// - /// ctor - /// - public ClientRateLimitError(string? message = null, Exception? exception = null) : base(_errorInfo with { Message = (message?.Length > 0 ? _errorInfo.Message + ": " + message : _errorInfo.Message) }, exception) { } - - /// - /// ctor - /// - protected ClientRateLimitError(ErrorInfo info, Exception? exception) : base(info, exception) { } - } - - /// - /// Rate limit exceeded (server side) - /// - public class ServerRateLimitError : BaseRateLimitError - { - /// - /// Default error info - /// - protected static readonly ErrorInfo _errorInfo = new ErrorInfo(ErrorType.RateLimitRequest, false, "Server rate limit exceeded"); - - /// - /// ctor - /// - public ServerRateLimitError(string? message = null, Exception? exception = null) : base(_errorInfo with { Message = (message?.Length > 0 ? _errorInfo.Message + ": " + message : _errorInfo.Message) }, exception) { } - - /// - /// ctor - /// - protected ServerRateLimitError(ErrorInfo info, Exception? exception) : base(info, exception) { } - } - - /// - /// Cancellation requested - /// - public class CancellationRequestedError : Error - { - /// - /// Default error info - /// - protected static readonly ErrorInfo _errorInfo = new ErrorInfo(ErrorType.CancellationRequested, false, "Cancellation requested"); - - /// - /// ctor - /// - public CancellationRequestedError(Exception? exception = null) : base(null, _errorInfo, null) { } - - /// - /// ctor - /// - protected CancellationRequestedError(ErrorInfo info, Exception? exception) : base(null, info, exception) { } - } - - /// - /// Invalid operation requested - /// - public class InvalidOperationError : Error - { - /// - /// Default error info - /// - protected static readonly ErrorInfo _errorInfo = new ErrorInfo(ErrorType.InvalidOperation, false, "Operation invalid"); - - /// - /// ctor - /// - public InvalidOperationError(string message) : base(null, _errorInfo with { Message = message }, null) { } - - /// - /// ctor - /// - protected InvalidOperationError(ErrorInfo info, Exception? exception) : base(null, info, exception) { } + return ErrorCode != null ? $"[{GetType().Name}.{ErrorType}] {ErrorCode}: {Message ?? ErrorDescription}" : $"[{GetType().Name}.{ErrorType}] {Message ?? ErrorDescription}"; } } + +/// +/// Cant reach server error +/// +public class CantConnectError : Error +{ + /// + /// Default error info + /// + protected static readonly ErrorInfo errorInfo = new ErrorInfo(ErrorType.UnableToConnect, false, "Can't connect to the server"); + + /// + /// ctor + /// + public CantConnectError() : base(null, errorInfo, null) { } + + /// + /// ctor + /// + public CantConnectError(Exception? exception) : base(null, errorInfo, exception) { } + + /// + /// ctor + /// + protected CantConnectError(ErrorInfo info, Exception? exception) : base(null, info, exception) { } +} + +/// +/// No api credentials provided while trying to access a private endpoint +/// +public class NoApiCredentialsError : Error +{ + /// + /// Default error info + /// + protected static readonly ErrorInfo errorInfo = new ErrorInfo(ErrorType.MissingCredentials, false, "No credentials provided for private endpoint"); + + /// + /// ctor + /// + public NoApiCredentialsError() : base(null, errorInfo, null) { } + + /// + /// ctor + /// + protected NoApiCredentialsError(ErrorInfo info, Exception? exception) : base(null, info, exception) { } +} + +/// +/// Error returned by the server +/// +public class ServerError : Error +{ + /// + /// ctor + /// + public ServerError(ErrorInfo errorInfo, Exception? exception = null) + : base(null, errorInfo, exception) { } + + /// + /// ctor + /// + public ServerError(int errorCode, ErrorInfo errorInfo, Exception? exception = null) + : this(errorCode.ToString(), errorInfo, exception) { } + + /// + /// ctor + /// + public ServerError(string errorCode, ErrorInfo errorInfo, Exception? exception = null) : base(errorCode, errorInfo, exception) { } +} + +/// +/// Web error returned by the server +/// +public class WebError : Error +{ + /// + /// Default error info + /// + protected static readonly ErrorInfo errorInfo = new ErrorInfo(ErrorType.NetworkError, true, "Failed to complete the request to the server due to a network error"); + + /// + /// ctor + /// + public WebError(string? message = null, Exception? exception = null) : base(null, errorInfo with { Message = (message?.Length > 0 ? errorInfo.Message + ": " + message : errorInfo.Message) }, exception) { } +} + +/// +/// Timeout error waiting for a response from the server +/// +public class TimeoutError : Error +{ + /// + /// Default error info + /// + protected static readonly ErrorInfo errorInfo = new ErrorInfo(ErrorType.Timeout, false, "Failed to receive a response from the server in time"); + + /// + /// ctor + /// + public TimeoutError(string? message = null, Exception? exception = null) : base(null, errorInfo with { Message = (message?.Length > 0 ? errorInfo.Message + ": " + message : errorInfo.Message) }, exception) { } +} + +/// +/// Error while deserializing data +/// +public class DeserializeError : Error +{ + /// + /// Default error info + /// + protected static readonly ErrorInfo errorInfo = new ErrorInfo(ErrorType.DeserializationFailed, false, "Failed to deserialize data"); + + /// + /// ctor + /// + public DeserializeError(string? message = null, Exception? exception = null) : base(null, errorInfo with { Message = (message?.Length > 0 ? errorInfo.Message + ": " + message : errorInfo.Message) }, exception) { } +} + +/// +/// An invalid parameter has been provided +/// +public class ArgumentError : Error +{ + /// + /// Default error info for missing parameter + /// + protected static readonly ErrorInfo missingInfo = new ErrorInfo(ErrorType.MissingParameter, false, "Missing parameter"); + /// + /// Default error info for invalid parameter + /// + protected static readonly ErrorInfo invalidInfo = new ErrorInfo(ErrorType.InvalidParameter, false, "Invalid parameter"); + + /// + /// ctor + /// + public static ArgumentError Missing(string parameterName, string? message = null) => new ArgumentError(missingInfo with { Message = message == null ? $"{missingInfo.Message} '{parameterName}'" : $"{missingInfo.Message} '{parameterName}': {message}" }, null); + + /// + /// ctor + /// + public static ArgumentError Invalid(string parameterName, string message) => new ArgumentError(invalidInfo with { Message = $"{invalidInfo.Message} '{parameterName}': {message}" }, null); + + /// + /// ctor + /// + protected ArgumentError(ErrorInfo info, Exception? exception) : base(null, info, exception) { } +} + +/// +/// Rate limit exceeded (client side) +/// +public abstract class BaseRateLimitError : Error +{ + /// + /// When the request can be retried + /// + public DateTime? RetryAfter { get; set; } + + /// + /// ctor + /// + protected BaseRateLimitError(ErrorInfo errorInfo, Exception? exception) : base(null, errorInfo, exception) { } +} + +/// +/// Rate limit exceeded (client side) +/// +public class ClientRateLimitError : BaseRateLimitError +{ + /// + /// Default error info + /// + protected static readonly ErrorInfo errorInfo = new ErrorInfo(ErrorType.RateLimitRequest, false, "Client rate limit exceeded"); + + /// + /// ctor + /// + public ClientRateLimitError(string? message = null, Exception? exception = null) : base(errorInfo with { Message = (message?.Length > 0 ? errorInfo.Message + ": " + message : errorInfo.Message) }, exception) { } + + /// + /// ctor + /// + protected ClientRateLimitError(ErrorInfo info, Exception? exception) : base(info, exception) { } +} + +/// +/// Rate limit exceeded (server side) +/// +public class ServerRateLimitError : BaseRateLimitError +{ + /// + /// Default error info + /// + protected static readonly ErrorInfo errorInfo = new ErrorInfo(ErrorType.RateLimitRequest, false, "Server rate limit exceeded"); + + /// + /// ctor + /// + public ServerRateLimitError(string? message = null, Exception? exception = null) : base(errorInfo with { Message = (message?.Length > 0 ? errorInfo.Message + ": " + message : errorInfo.Message) }, exception) { } + + /// + /// ctor + /// + protected ServerRateLimitError(ErrorInfo info, Exception? exception) : base(info, exception) { } +} + +/// +/// Cancellation requested +/// +public class CancellationRequestedError : Error +{ + /// + /// Default error info + /// + protected static readonly ErrorInfo errorInfo = new ErrorInfo(ErrorType.CancellationRequested, false, "Cancellation requested"); + + /// + /// ctor + /// + public CancellationRequestedError(Exception? exception = null) : base(null, errorInfo, null) { } + + /// + /// ctor + /// + protected CancellationRequestedError(ErrorInfo info, Exception? exception) : base(null, info, exception) { } +} + +/// +/// Invalid operation requested +/// +public class InvalidOperationError : Error +{ + /// + /// Default error info + /// + protected static readonly ErrorInfo errorInfo = new ErrorInfo(ErrorType.InvalidOperation, false, "Operation invalid"); + + /// + /// ctor + /// + public InvalidOperationError(string message) : base(null, errorInfo with { Message = message }, null) { } + + /// + /// ctor + /// + protected InvalidOperationError(ErrorInfo info, Exception? exception) : base(null, info, exception) { } +} diff --git a/CryptoExchange.Net/Objects/Errors/ErrorEvaluator.cs b/CryptoExchange.Net/Objects/Errors/ErrorEvaluator.cs index b245115..03a7f8d 100644 --- a/CryptoExchange.Net/Objects/Errors/ErrorEvaluator.cs +++ b/CryptoExchange.Net/Objects/Errors/ErrorEvaluator.cs @@ -1,40 +1,37 @@ -using System; -using System.Collections.Generic; -using System.Text; +using System; -namespace CryptoExchange.Net.Objects.Errors +namespace CryptoExchange.Net.Objects.Errors; + +/// +/// Error evaluator +/// +public class ErrorEvaluator { /// - /// Error evaluator + /// Error code /// - public class ErrorEvaluator + public string[] ErrorCodes { get; set; } + + /// + /// Evaluation callback for determining the error type + /// + public Func ErrorTypeEvaluator { get; set; } + + /// + /// ctor + /// + public ErrorEvaluator(string errorCode, Func errorTypeEvaluator) { - /// - /// Error code - /// - public string[] ErrorCodes { get; set; } + ErrorCodes = [errorCode]; + ErrorTypeEvaluator = errorTypeEvaluator; + } - /// - /// Evaluation callback for determining the error type - /// - public Func ErrorTypeEvaluator { get; set; } - - /// - /// ctor - /// - public ErrorEvaluator(string errorCode, Func errorTypeEvaluator) - { - ErrorCodes = [errorCode]; - ErrorTypeEvaluator = errorTypeEvaluator; - } - - /// - /// ctor - /// - public ErrorEvaluator(string[] errorCodes, Func errorTypeEvaluator) - { - ErrorCodes = errorCodes; - ErrorTypeEvaluator = errorTypeEvaluator; - } + /// + /// ctor + /// + public ErrorEvaluator(string[] errorCodes, Func errorTypeEvaluator) + { + ErrorCodes = errorCodes; + ErrorTypeEvaluator = errorTypeEvaluator; } } diff --git a/CryptoExchange.Net/Objects/Errors/ErrorInfo.cs b/CryptoExchange.Net/Objects/Errors/ErrorInfo.cs index 0dd38c6..55f6fa8 100644 --- a/CryptoExchange.Net/Objects/Errors/ErrorInfo.cs +++ b/CryptoExchange.Net/Objects/Errors/ErrorInfo.cs @@ -1,58 +1,55 @@ -using System; +namespace CryptoExchange.Net.Objects.Errors; -namespace CryptoExchange.Net.Objects.Errors +/// +/// Error info +/// +public record ErrorInfo { /// - /// Error info + /// Unknown error info /// - public record ErrorInfo + public static ErrorInfo Unknown { get; } = new ErrorInfo(ErrorType.Unknown, false, "Unknown error", []); + + /// + /// The server error code + /// + public string[] ErrorCodes { get; set; } + /// + /// Error description + /// + public string? ErrorDescription { get; set; } + /// + /// The error type + /// + public ErrorType ErrorType { get; set; } + /// + /// Whether the error is transient and can be retried + /// + public bool IsTransient { get; set; } + /// + /// Server response message + /// + public string? Message { get; set; } + + /// + /// ctor + /// + public ErrorInfo(ErrorType errorType, string description) { - /// - /// Unknown error info - /// - public static ErrorInfo Unknown { get; } = new ErrorInfo(ErrorType.Unknown, false, "Unknown error", []); + ErrorCodes = []; + ErrorType = errorType; + IsTransient = false; + ErrorDescription = description; + } - /// - /// The server error code - /// - public string[] ErrorCodes { get; set; } - /// - /// Error description - /// - public string? ErrorDescription { get; set; } - /// - /// The error type - /// - public ErrorType ErrorType { get; set; } - /// - /// Whether the error is transient and can be retried - /// - public bool IsTransient { get; set; } - /// - /// Server response message - /// - public string? Message { get; set; } - - /// - /// ctor - /// - public ErrorInfo(ErrorType errorType, string description) - { - ErrorCodes = []; - ErrorType = errorType; - IsTransient = false; - ErrorDescription = description; - } - - /// - /// ctor - /// - public ErrorInfo(ErrorType errorType, bool isTransient, string description, params string[] errorCodes) - { - ErrorCodes = errorCodes; - ErrorType = errorType; - IsTransient = isTransient; - ErrorDescription = description; - } + /// + /// ctor + /// + public ErrorInfo(ErrorType errorType, bool isTransient, string description, params string[] errorCodes) + { + ErrorCodes = errorCodes; + ErrorType = errorType; + IsTransient = isTransient; + ErrorDescription = description; } } diff --git a/CryptoExchange.Net/Objects/Errors/ErrorMapping.cs b/CryptoExchange.Net/Objects/Errors/ErrorMapping.cs index 219b3c9..8bb9a33 100644 --- a/CryptoExchange.Net/Objects/Errors/ErrorMapping.cs +++ b/CryptoExchange.Net/Objects/Errors/ErrorMapping.cs @@ -1,54 +1,51 @@ -using System; +using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -namespace CryptoExchange.Net.Objects.Errors +namespace CryptoExchange.Net.Objects.Errors; + +/// +/// Error mapping collection +/// +public class ErrorMapping { + private Dictionary _evaluators = new Dictionary(); + private Dictionary _directMapping = new Dictionary(); + /// - /// Error mapping collection + /// ctor /// - public class ErrorMapping + public ErrorMapping(ErrorInfo[] errorMappings, ErrorEvaluator[]? errorTypeEvaluators = null) { - private Dictionary _evaluators = new Dictionary(); - private Dictionary _directMapping = new Dictionary(); - - /// - /// ctor - /// - public ErrorMapping(ErrorInfo[] errorMappings, ErrorEvaluator[]? errorTypeEvaluators = null) + foreach (var item in errorMappings) { - foreach (var item in errorMappings) - { - if (!item.ErrorCodes.Any()) - throw new Exception("Error codes can't be null in error mapping"); + if (item.ErrorCodes.Length == 0) + throw new Exception("Error codes can't be null in error mapping"); - foreach(var code in item.ErrorCodes!) - _directMapping.Add(code, item); - } - - if (errorTypeEvaluators == null) - return; - - foreach (var item in errorTypeEvaluators) - { - foreach(var code in item.ErrorCodes) - _evaluators.Add(code, item); - } + foreach(var code in item.ErrorCodes!) + _directMapping.Add(code, item); } - /// - /// Get error info for an error code - /// - public ErrorInfo GetErrorInfo(string code, string? message) - { - if (_directMapping.TryGetValue(code!, out var info)) - return info with { Message = message }; - - if (_evaluators.TryGetValue(code!, out var eva)) - return eva.ErrorTypeEvaluator.Invoke(code!, message) with { Message = message }; + if (errorTypeEvaluators == null) + return; - return ErrorInfo.Unknown with { Message = message }; + foreach (var item in errorTypeEvaluators) + { + foreach(var code in item.ErrorCodes) + _evaluators.Add(code, item); } } + + /// + /// Get error info for an error code + /// + public ErrorInfo GetErrorInfo(string code, string? message) + { + if (_directMapping.TryGetValue(code!, out var info)) + return info with { Message = message }; + + if (_evaluators.TryGetValue(code!, out var eva)) + return eva.ErrorTypeEvaluator.Invoke(code!, message) with { Message = message }; + + return ErrorInfo.Unknown with { Message = message }; + } } diff --git a/CryptoExchange.Net/Objects/Errors/ErrorType.cs b/CryptoExchange.Net/Objects/Errors/ErrorType.cs index 7af5017..901e54d 100644 --- a/CryptoExchange.Net/Objects/Errors/ErrorType.cs +++ b/CryptoExchange.Net/Objects/Errors/ErrorType.cs @@ -1,162 +1,157 @@ -using System; -using System.Collections.Generic; -using System.Text; +namespace CryptoExchange.Net.Objects.Errors; -namespace CryptoExchange.Net.Objects.Errors +/// +/// Type of error +/// +public enum ErrorType { + #region Library errors + /// - /// Type of error + /// Failed to connect to server /// - public enum ErrorType - { - #region Library errors + UnableToConnect, + /// + /// Failed to complete the request to the server + /// + NetworkError, + /// + /// No API credentials have been specified + /// + MissingCredentials, + /// + /// Invalid parameter value + /// + InvalidParameter, + /// + /// Missing parameter value + /// + MissingParameter, + /// + /// Cancellation requested by user + /// + CancellationRequested, + /// + /// Invalid operation requested + /// + InvalidOperation, + /// + /// Failed to deserialize data + /// + DeserializationFailed, + /// + /// Websocket is temporarily paused + /// + WebsocketPaused, + /// + /// Timeout while waiting for data from the order book subscription + /// + OrderBookTimeout, + /// + /// All orders failed for a multi-order operation + /// + AllOrdersFailed, + /// + /// Request timeout + /// + Timeout, - /// - /// Failed to connect to server - /// - UnableToConnect, - /// - /// Failed to complete the request to the server - /// - NetworkError, - /// - /// No API credentials have been specified - /// - MissingCredentials, - /// - /// Invalid parameter value - /// - InvalidParameter, - /// - /// Missing parameter value - /// - MissingParameter, - /// - /// Cancellation requested by user - /// - CancellationRequested, - /// - /// Invalid operation requested - /// - InvalidOperation, - /// - /// Failed to deserialize data - /// - DeserializationFailed, - /// - /// Websocket is temporarily paused - /// - WebsocketPaused, - /// - /// Timeout while waiting for data from the order book subscription - /// - OrderBookTimeout, - /// - /// All orders failed for a multi-order operation - /// - AllOrdersFailed, - /// - /// Request timeout - /// - Timeout, + #endregion - #endregion + #region Server errors - #region Server errors + /// + /// Unknown error + /// + Unknown, + /// + /// Not authorized or insufficient permissions + /// + Unauthorized, + /// + /// Request rate limit error, too many requests + /// + RateLimitRequest, + /// + /// Connection rate limit error, too many connections + /// + RateLimitConnection, + /// + /// Subscription rate limit error, too many subscriptions + /// + RateLimitSubscription, + /// + /// Order rate limit error, too many orders + /// + RateLimitOrder, + /// + /// Request timestamp invalid + /// + InvalidTimestamp, + /// + /// Unknown symbol + /// + UnknownSymbol, + /// + /// Unknown asset + /// + UnknownAsset, + /// + /// Unknown order + /// + UnknownOrder, + /// + /// Duplicate subscription + /// + DuplicateSubscription, + /// + /// Invalid quantity + /// + InvalidQuantity, + /// + /// Invalid price + /// + InvalidPrice, + /// + /// Parameter(s) for stop or tp/sl order invalid + /// + InvalidStopParameters, + /// + /// Not enough balance to execute request + /// + InsufficientBalance, + /// + /// Client order id already in use + /// + DuplicateClientOrderId, + /// + /// Symbol is not currently trading + /// + UnavailableSymbol, + /// + /// Order rejected due to order configuration such as order type or time in force restrictions + /// + RejectedOrderConfiguration, + /// + /// There is no open position + /// + NoPosition, + /// + /// Max position reached + /// + MaxPosition, + /// + /// Error in the internal system + /// + SystemError, + /// + /// The target object is not in the correct state for an operation + /// + IncorrectState, + /// + /// Risk management error + /// + RiskError - /// - /// Unknown error - /// - Unknown, - /// - /// Not authorized or insufficient permissions - /// - Unauthorized, - /// - /// Request rate limit error, too many requests - /// - RateLimitRequest, - /// - /// Connection rate limit error, too many connections - /// - RateLimitConnection, - /// - /// Subscription rate limit error, too many subscriptions - /// - RateLimitSubscription, - /// - /// Order rate limit error, too many orders - /// - RateLimitOrder, - /// - /// Request timestamp invalid - /// - InvalidTimestamp, - /// - /// Unknown symbol - /// - UnknownSymbol, - /// - /// Unknown asset - /// - UnknownAsset, - /// - /// Unknown order - /// - UnknownOrder, - /// - /// Duplicate subscription - /// - DuplicateSubscription, - /// - /// Invalid quantity - /// - InvalidQuantity, - /// - /// Invalid price - /// - InvalidPrice, - /// - /// Parameter(s) for stop or tp/sl order invalid - /// - InvalidStopParameters, - /// - /// Not enough balance to execute request - /// - InsufficientBalance, - /// - /// Client order id already in use - /// - DuplicateClientOrderId, - /// - /// Symbol is not currently trading - /// - UnavailableSymbol, - /// - /// Order rejected due to order configuration such as order type or time in force restrictions - /// - RejectedOrderConfiguration, - /// - /// There is no open position - /// - NoPosition, - /// - /// Max position reached - /// - MaxPosition, - /// - /// Error in the internal system - /// - SystemError, - /// - /// The target object is not in the correct state for an operation - /// - IncorrectState, - /// - /// Risk management error - /// - RiskError - - #endregion - } + #endregion } diff --git a/CryptoExchange.Net/Objects/Options/ApiOptions.cs b/CryptoExchange.Net/Objects/Options/ApiOptions.cs index 855bb08..c432cbb 100644 --- a/CryptoExchange.Net/Objects/Options/ApiOptions.cs +++ b/CryptoExchange.Net/Objects/Options/ApiOptions.cs @@ -1,20 +1,19 @@ -using CryptoExchange.Net.Authentication; +using CryptoExchange.Net.Authentication; -namespace CryptoExchange.Net.Objects.Options +namespace CryptoExchange.Net.Objects.Options; + +/// +/// Options for API usage +/// +public class ApiOptions { /// - /// Options for API usage + /// If true, the CallResult and DataEvent objects will also include the originally received json data in the OriginalData property /// - public class ApiOptions - { - /// - /// If true, the CallResult and DataEvent objects will also include the originally received json data in the OriginalData property - /// - public bool? OutputOriginalData { get; set; } + public bool? OutputOriginalData { get; set; } - /// - /// The api credentials used for signing requests to this API. Overrides API credentials provided in the client options - /// - public ApiCredentials? ApiCredentials { get; set; } - } + /// + /// The api credentials used for signing requests to this API. Overrides API credentials provided in the client options + /// + public ApiCredentials? ApiCredentials { get; set; } } diff --git a/CryptoExchange.Net/Objects/Options/ExchangeOptions.cs b/CryptoExchange.Net/Objects/Options/ExchangeOptions.cs index 2d55808..79e41e5 100644 --- a/CryptoExchange.Net/Objects/Options/ExchangeOptions.cs +++ b/CryptoExchange.Net/Objects/Options/ExchangeOptions.cs @@ -1,46 +1,45 @@ -using CryptoExchange.Net.Authentication; +using CryptoExchange.Net.Authentication; using System; -namespace CryptoExchange.Net.Objects.Options +namespace CryptoExchange.Net.Objects.Options; + +/// +/// Exchange options +/// +public class ExchangeOptions { /// - /// Exchange options + /// Proxy settings /// - public class ExchangeOptions + public ApiProxy? Proxy { get; set; } + + /// + /// If true, the CallResult and DataEvent objects will also include the originally received json data in the OriginalData property + /// + public bool OutputOriginalData { get; set; } + + /// + /// The max time a request is allowed to take + /// + public TimeSpan RequestTimeout { get; set; } = TimeSpan.FromSeconds(20); + + /// + /// The api credentials used for signing requests to this API. + /// + public ApiCredentials? ApiCredentials { get; set; } + + /// + /// Whether or not client side rate limiting should be applied + /// + public bool RateLimiterEnabled { get; set; } = true; + /// + /// What should happen when a rate limit is reached + /// + public RateLimitingBehaviour RateLimitingBehaviour { get; set; } = RateLimitingBehaviour.Wait; + + /// + public override string ToString() { - /// - /// Proxy settings - /// - public ApiProxy? Proxy { get; set; } - - /// - /// If true, the CallResult and DataEvent objects will also include the originally received json data in the OriginalData property - /// - public bool OutputOriginalData { get; set; } = false; - - /// - /// The max time a request is allowed to take - /// - public TimeSpan RequestTimeout { get; set; } = TimeSpan.FromSeconds(20); - - /// - /// The api credentials used for signing requests to this API. - /// - public ApiCredentials? ApiCredentials { get; set; } - - /// - /// Whether or not client side rate limiting should be applied - /// - public bool RateLimiterEnabled { get; set; } = true; - /// - /// What should happen when a rate limit is reached - /// - public RateLimitingBehaviour RateLimitingBehaviour { get; set; } = RateLimitingBehaviour.Wait; - - /// - public override string ToString() - { - return $"RequestTimeout: {RequestTimeout}, Proxy: {(Proxy == null ? "-" : "set")}, ApiCredentials: {(ApiCredentials == null ? "-" : "set")}"; - } + return $"RequestTimeout: {RequestTimeout}, Proxy: {(Proxy == null ? "-" : "set")}, ApiCredentials: {(ApiCredentials == null ? "-" : "set")}"; } } diff --git a/CryptoExchange.Net/Objects/Options/LibraryOptions.cs b/CryptoExchange.Net/Objects/Options/LibraryOptions.cs index 7cb441e..3ea5f82 100644 --- a/CryptoExchange.Net/Objects/Options/LibraryOptions.cs +++ b/CryptoExchange.Net/Objects/Options/LibraryOptions.cs @@ -1,58 +1,57 @@ -using CryptoExchange.Net.Authentication; +using CryptoExchange.Net.Authentication; using Microsoft.Extensions.DependencyInjection; -namespace CryptoExchange.Net.Objects.Options +namespace CryptoExchange.Net.Objects.Options; + +/// +/// Library options +/// +/// +/// +/// +/// +public class LibraryOptions + where TRestOptions: RestExchangeOptions, new() + where TSocketOptions: SocketExchangeOptions, new() + where TApiCredentials: ApiCredentials + where TEnvironment: TradeEnvironment { /// - /// Library options + /// Rest client options /// - /// - /// - /// - /// - public class LibraryOptions - where TRestOptions: RestExchangeOptions, new() - where TSocketOptions: SocketExchangeOptions, new() - where TApiCredentials: ApiCredentials - where TEnvironment: TradeEnvironment + public TRestOptions Rest { get; set; } = new TRestOptions(); + + /// + /// Socket client options + /// + public TSocketOptions Socket { get; set; } = new TSocketOptions(); + + /// + /// Trade environment. Contains info about URL's to use to connect to the API. + /// + public TEnvironment? Environment { get; set; } + + /// + /// The api credentials used for signing requests. + /// + public TApiCredentials? ApiCredentials { get; set; } + + /// + /// The DI service lifetime for the socket client + /// + public ServiceLifetime? SocketClientLifeTime { get; set; } + + /// + /// Copy values from these options to the target options + /// + public T Set(T targetOptions) where T: LibraryOptions { - /// - /// Rest client options - /// - public TRestOptions Rest { get; set; } = new TRestOptions(); + targetOptions.ApiCredentials = (TApiCredentials?)ApiCredentials?.Copy(); + targetOptions.Environment = Environment; + targetOptions.SocketClientLifeTime = SocketClientLifeTime; + targetOptions.Rest = Rest.Set(targetOptions.Rest); + targetOptions.Socket = Socket.Set(targetOptions.Socket); - /// - /// Socket client options - /// - public TSocketOptions Socket { get; set; } = new TSocketOptions(); - - /// - /// Trade environment. Contains info about URL's to use to connect to the API. - /// - public TEnvironment? Environment { get; set; } - - /// - /// The api credentials used for signing requests. - /// - public TApiCredentials? ApiCredentials { get; set; } - - /// - /// The DI service lifetime for the socket client - /// - public ServiceLifetime? SocketClientLifeTime { get; set; } - - /// - /// Copy values from these options to the target options - /// - public T Set(T targetOptions) where T: LibraryOptions - { - targetOptions.ApiCredentials = (TApiCredentials?)ApiCredentials?.Copy(); - targetOptions.Environment = Environment; - targetOptions.SocketClientLifeTime = SocketClientLifeTime; - targetOptions.Rest = Rest.Set(targetOptions.Rest); - targetOptions.Socket = Socket.Set(targetOptions.Socket); - - return targetOptions; - } + return targetOptions; } } diff --git a/CryptoExchange.Net/Objects/Options/OrderBookOptions.cs b/CryptoExchange.Net/Objects/Options/OrderBookOptions.cs index 9bcc39d..c7de05a 100644 --- a/CryptoExchange.Net/Objects/Options/OrderBookOptions.cs +++ b/CryptoExchange.Net/Objects/Options/OrderBookOptions.cs @@ -1,26 +1,25 @@ -namespace CryptoExchange.Net.Objects.Options +namespace CryptoExchange.Net.Objects.Options; + +/// +/// Base for order book options +/// +public class OrderBookOptions { /// - /// Base for order book options + /// Whether or not checksum validation is enabled. Default is true, disabling will ignore checksum messages. /// - public class OrderBookOptions - { - /// - /// Whether or not checksum validation is enabled. Default is true, disabling will ignore checksum messages. - /// - public bool ChecksumValidationEnabled { get; set; } = true; + public bool ChecksumValidationEnabled { get; set; } = true; - /// - /// Create a copy of this options - /// - /// - /// - public T Copy() where T : OrderBookOptions, new() + /// + /// Create a copy of this options + /// + /// + /// + public T Copy() where T : OrderBookOptions, new() + { + return new T { - return new T - { - ChecksumValidationEnabled = ChecksumValidationEnabled, - }; - } + ChecksumValidationEnabled = ChecksumValidationEnabled, + }; } } diff --git a/CryptoExchange.Net/Objects/Options/RestApiOptions.cs b/CryptoExchange.Net/Objects/Options/RestApiOptions.cs index 244fb43..620bde2 100644 --- a/CryptoExchange.Net/Objects/Options/RestApiOptions.cs +++ b/CryptoExchange.Net/Objects/Options/RestApiOptions.cs @@ -1,49 +1,48 @@ -using CryptoExchange.Net.Authentication; +using CryptoExchange.Net.Authentication; using System; -namespace CryptoExchange.Net.Objects.Options +namespace CryptoExchange.Net.Objects.Options; + +/// +/// Http api options +/// +public class RestApiOptions : ApiOptions { /// - /// Http api options + /// Whether or not to automatically sync the local time with the server time /// - public class RestApiOptions : ApiOptions - { - /// - /// Whether or not to automatically sync the local time with the server time - /// - public bool? AutoTimestamp { get; set; } - - /// - /// How often the timestamp adjustment between client and server is recalculated. If you need a very small TimeSpan here you're probably better of syncing your server time more often - /// - public TimeSpan? TimestampRecalculationInterval { get; set; } - - /// - /// Set the values of this options on the target options - /// - public T Set(T item) where T : RestApiOptions, new() - { - item.ApiCredentials = ApiCredentials?.Copy(); - item.OutputOriginalData = OutputOriginalData; - item.AutoTimestamp = AutoTimestamp; - item.TimestampRecalculationInterval = TimestampRecalculationInterval; - return item; - } - } + public bool? AutoTimestamp { get; set; } /// - /// Http API options + /// How often the timestamp adjustment between client and server is recalculated. If you need a very small TimeSpan here you're probably better of syncing your server time more often /// - /// - public class RestApiOptions: RestApiOptions where TApiCredentials: ApiCredentials + public TimeSpan? TimestampRecalculationInterval { get; set; } + + /// + /// Set the values of this options on the target options + /// + public T Set(T item) where T : RestApiOptions, new() { - /// - /// The api credentials used for signing requests to this API. - /// - public new TApiCredentials? ApiCredentials - { - get => (TApiCredentials?)base.ApiCredentials; - set => base.ApiCredentials = value; - } + item.ApiCredentials = ApiCredentials?.Copy(); + item.OutputOriginalData = OutputOriginalData; + item.AutoTimestamp = AutoTimestamp; + item.TimestampRecalculationInterval = TimestampRecalculationInterval; + return item; + } +} + +/// +/// Http API options +/// +/// +public class RestApiOptions: RestApiOptions where TApiCredentials: ApiCredentials +{ + /// + /// The api credentials used for signing requests to this API. + /// + public new TApiCredentials? ApiCredentials + { + get => (TApiCredentials?)base.ApiCredentials; + set => base.ApiCredentials = value; } } diff --git a/CryptoExchange.Net/Objects/Options/RestExchangeOptions.cs b/CryptoExchange.Net/Objects/Options/RestExchangeOptions.cs index 6235782..a6f829a 100644 --- a/CryptoExchange.Net/Objects/Options/RestExchangeOptions.cs +++ b/CryptoExchange.Net/Objects/Options/RestExchangeOptions.cs @@ -1,91 +1,90 @@ -using CryptoExchange.Net.Authentication; +using CryptoExchange.Net.Authentication; using System; -namespace CryptoExchange.Net.Objects.Options +namespace CryptoExchange.Net.Objects.Options; + +/// +/// Options for a rest exchange client +/// +public class RestExchangeOptions: ExchangeOptions { /// - /// Options for a rest exchange client + /// Whether or not to automatically sync the local time with the server time /// - public class RestExchangeOptions: ExchangeOptions - { - /// - /// Whether or not to automatically sync the local time with the server time - /// - public bool AutoTimestamp { get; set; } - - /// - /// How often the timestamp adjustment between client and server is recalculated. If you need a very small TimeSpan here you're probably better of syncing your server time more often - /// - public TimeSpan TimestampRecalculationInterval { get; set; } = TimeSpan.FromHours(1); - - /// - /// Whether caching is enabled. Caching will only be applied to GET http requests. The lifetime of cached results can be determined by the `CachingMaxAge` option - /// - public bool CachingEnabled { get; set; } = false; - - /// - /// The max age of a cached entry, only used when the `CachingEnabled` options is set to true. When a cached entry is older than the max age it will be discarded and a new server request will be done - /// - public TimeSpan CachingMaxAge { get; set; } = TimeSpan.FromSeconds(5); - - /// - /// Set the values of this options on the target options - /// - public T Set(T item) where T : RestExchangeOptions, new() - { - item.OutputOriginalData = OutputOriginalData; - item.AutoTimestamp = AutoTimestamp; - item.TimestampRecalculationInterval = TimestampRecalculationInterval; - item.ApiCredentials = ApiCredentials?.Copy(); - item.Proxy = Proxy; - item.RequestTimeout = RequestTimeout; - item.RateLimiterEnabled = RateLimiterEnabled; - item.RateLimitingBehaviour = RateLimitingBehaviour; - item.CachingEnabled = CachingEnabled; - item.CachingMaxAge = CachingMaxAge; - return item; - } - } + public bool AutoTimestamp { get; set; } /// - /// Options for a rest exchange client + /// How often the timestamp adjustment between client and server is recalculated. If you need a very small TimeSpan here you're probably better of syncing your server time more often /// - /// - public class RestExchangeOptions : RestExchangeOptions where TEnvironment : TradeEnvironment - { - /// - /// Trade environment. Contains info about URL's to use to connect to the API. To swap environment select another environment for - /// the exchange's environment list or create a custom environment using either `[Exchange]Environment.CreateCustom()` or `[Exchange]Environment.[Environment]`, for example `KucoinEnvironment.TestNet` or `BinanceEnvironment.Live` - /// -#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. - public TEnvironment Environment { get; set; } -#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. - - /// - /// Set the values of this options on the target options - /// - public new T Set(T target) where T : RestExchangeOptions, new() - { - base.Set(target); - target.Environment = Environment; - return target; - } - } + public TimeSpan TimestampRecalculationInterval { get; set; } = TimeSpan.FromHours(1); /// - /// Options for a rest exchange client + /// Whether caching is enabled. Caching will only be applied to GET http requests. The lifetime of cached results can be determined by the `CachingMaxAge` option /// - /// - /// - public class RestExchangeOptions : RestExchangeOptions where TEnvironment : TradeEnvironment where TApiCredentials : ApiCredentials + public bool CachingEnabled { get; set; } + + /// + /// The max age of a cached entry, only used when the `CachingEnabled` options is set to true. When a cached entry is older than the max age it will be discarded and a new server request will be done + /// + public TimeSpan CachingMaxAge { get; set; } = TimeSpan.FromSeconds(5); + + /// + /// Set the values of this options on the target options + /// + public T Set(T item) where T : RestExchangeOptions, new() { - /// - /// The api credentials used for signing requests to this API. - /// - public new TApiCredentials? ApiCredentials - { - get => (TApiCredentials?)base.ApiCredentials; - set => base.ApiCredentials = value; - } + item.OutputOriginalData = OutputOriginalData; + item.AutoTimestamp = AutoTimestamp; + item.TimestampRecalculationInterval = TimestampRecalculationInterval; + item.ApiCredentials = ApiCredentials?.Copy(); + item.Proxy = Proxy; + item.RequestTimeout = RequestTimeout; + item.RateLimiterEnabled = RateLimiterEnabled; + item.RateLimitingBehaviour = RateLimitingBehaviour; + item.CachingEnabled = CachingEnabled; + item.CachingMaxAge = CachingMaxAge; + return item; + } +} + +/// +/// Options for a rest exchange client +/// +/// +public class RestExchangeOptions : RestExchangeOptions where TEnvironment : TradeEnvironment +{ + /// + /// Trade environment. Contains info about URL's to use to connect to the API. To swap environment select another environment for + /// the exchange's environment list or create a custom environment using either `[Exchange]Environment.CreateCustom()` or `[Exchange]Environment.[Environment]`, for example `KucoinEnvironment.TestNet` or `BinanceEnvironment.Live` + /// +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + public TEnvironment Environment { get; set; } +#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + + /// + /// Set the values of this options on the target options + /// + public new T Set(T target) where T : RestExchangeOptions, new() + { + base.Set(target); + target.Environment = Environment; + return target; + } +} + +/// +/// Options for a rest exchange client +/// +/// +/// +public class RestExchangeOptions : RestExchangeOptions where TEnvironment : TradeEnvironment where TApiCredentials : ApiCredentials +{ + /// + /// The api credentials used for signing requests to this API. + /// + public new TApiCredentials? ApiCredentials + { + get => (TApiCredentials?)base.ApiCredentials; + set => base.ApiCredentials = value; } } diff --git a/CryptoExchange.Net/Objects/Options/SocketApiOptions.cs b/CryptoExchange.Net/Objects/Options/SocketApiOptions.cs index b41f144..7a42b52 100644 --- a/CryptoExchange.Net/Objects/Options/SocketApiOptions.cs +++ b/CryptoExchange.Net/Objects/Options/SocketApiOptions.cs @@ -1,50 +1,49 @@ -using CryptoExchange.Net.Authentication; +using CryptoExchange.Net.Authentication; using System; -namespace CryptoExchange.Net.Objects.Options +namespace CryptoExchange.Net.Objects.Options; + +/// +/// Socket api options +/// +public class SocketApiOptions : ApiOptions { /// - /// Socket api options + /// The max time of not receiving any data after which the connection is assumed to be dropped. This can only be used for socket connections where a steady flow of data is expected, + /// for example when the server sends intermittent ping requests /// - public class SocketApiOptions : ApiOptions - { - /// - /// The max time of not receiving any data after which the connection is assumed to be dropped. This can only be used for socket connections where a steady flow of data is expected, - /// for example when the server sends intermittent ping requests - /// - public TimeSpan? SocketNoDataTimeout { get; set; } - - /// - /// The max amount of connections to make to the server. Can be used for API's which only allow a certain number of connections. Changing this to a high value might cause issues. - /// - public int? MaxSocketConnections { get; set; } - - /// - /// Set the values of this options on the target options - /// - public T Set(T item) where T : SocketApiOptions, new() - { - item.ApiCredentials = ApiCredentials?.Copy(); - item.OutputOriginalData = OutputOriginalData; - item.SocketNoDataTimeout = SocketNoDataTimeout; - item.MaxSocketConnections = MaxSocketConnections; - return item; - } - } + public TimeSpan? SocketNoDataTimeout { get; set; } /// - /// Socket API options + /// The max amount of connections to make to the server. Can be used for API's which only allow a certain number of connections. Changing this to a high value might cause issues. /// - /// - public class SocketApiOptions : SocketApiOptions where TApiCredentials : ApiCredentials + public int? MaxSocketConnections { get; set; } + + /// + /// Set the values of this options on the target options + /// + public T Set(T item) where T : SocketApiOptions, new() { - /// - /// The api credentials used for signing requests to this API. - /// - public new TApiCredentials? ApiCredentials - { - get => (TApiCredentials?)base.ApiCredentials; - set => base.ApiCredentials = value; - } + item.ApiCredentials = ApiCredentials?.Copy(); + item.OutputOriginalData = OutputOriginalData; + item.SocketNoDataTimeout = SocketNoDataTimeout; + item.MaxSocketConnections = MaxSocketConnections; + return item; + } +} + +/// +/// Socket API options +/// +/// +public class SocketApiOptions : SocketApiOptions where TApiCredentials : ApiCredentials +{ + /// + /// The api credentials used for signing requests to this API. + /// + public new TApiCredentials? ApiCredentials + { + get => (TApiCredentials?)base.ApiCredentials; + set => base.ApiCredentials = value; } } diff --git a/CryptoExchange.Net/Objects/Options/SocketExchangeOptions.cs b/CryptoExchange.Net/Objects/Options/SocketExchangeOptions.cs index 40e8ea0..5db25fa 100644 --- a/CryptoExchange.Net/Objects/Options/SocketExchangeOptions.cs +++ b/CryptoExchange.Net/Objects/Options/SocketExchangeOptions.cs @@ -1,130 +1,129 @@ -using CryptoExchange.Net.Authentication; +using CryptoExchange.Net.Authentication; using System; -namespace CryptoExchange.Net.Objects.Options +namespace CryptoExchange.Net.Objects.Options; + +/// +/// Options for a websocket exchange client +/// +public class SocketExchangeOptions : ExchangeOptions { /// - /// Options for a websocket exchange client + /// The fixed time to wait between reconnect attempts, only used when `ReconnectPolicy` is set to `ReconnectPolicy.ExponentialBackoff` /// - public class SocketExchangeOptions : ExchangeOptions - { - /// - /// The fixed time to wait between reconnect attempts, only used when `ReconnectPolicy` is set to `ReconnectPolicy.ExponentialBackoff` - /// - public TimeSpan ReconnectInterval { get; set; } = TimeSpan.FromSeconds(5); - - /// - /// Reconnect policy - /// - public ReconnectPolicy ReconnectPolicy { get; set; } = ReconnectPolicy.FixedDelay; - - /// - /// Max number of concurrent resubscription tasks per socket after reconnecting a socket - /// - public int MaxConcurrentResubscriptionsPerSocket { get; set; } = 5; - - /// - /// The max time of not receiving any data after which the connection is assumed to be dropped. This can only be used for socket connections where a steady flow of data is expected, - /// for example when the server sends intermittent ping requests - /// - public TimeSpan SocketNoDataTimeout { get; set; } - - /// - /// The amount of subscriptions that should be made on a single socket connection. Not all API's support multiple subscriptions on a single socket. - /// Setting this to a higher number increases subscription speed because not every subscription needs to connect to the server, but having more subscriptions on a - /// single connection will also increase the amount of traffic on that single connection, potentially leading to issues. - /// - public int? SocketSubscriptionsCombineTarget { get; set; } - - /// - /// The max amount of connections to make to the server. Can be used for API's which only allow a certain number of connections. Changing this to a high value might cause issues. - /// - public int? MaxSocketConnections { get; set; } - - /// - /// The time to wait after connecting a socket before sending messages. Can be used for API's which will rate limit if you subscribe directly after connecting. - /// - public TimeSpan DelayAfterConnect { get; set; } = TimeSpan.Zero; - - /// - /// This delay is used to set a RetryAfter guard on the connection after a rate limit is hit on the server. - /// This is used to prevent the client from reconnecting too quickly after a rate limit is hit. - /// - public TimeSpan? ConnectDelayAfterRateLimited { get; set; } - - /// - /// The buffer size to use for receiving data. Leave unset to use the default buffer size. - /// - /// - /// Only specify this if you are creating a significant amount of connections and understand the typical message length we receive from the exchange. - /// Setting this too low can increase memory consumption and allocations. - /// - public int? ReceiveBufferSize { get; set; } - - /// - /// Create a copy of this options - /// - /// - /// - public T Set(T item) where T : SocketExchangeOptions, new() - { - item.ApiCredentials = ApiCredentials?.Copy(); - item.OutputOriginalData = OutputOriginalData; - item.ReconnectPolicy = ReconnectPolicy; - item.DelayAfterConnect = DelayAfterConnect; - item.MaxConcurrentResubscriptionsPerSocket = MaxConcurrentResubscriptionsPerSocket; - item.ReconnectInterval = ReconnectInterval; - item.SocketNoDataTimeout = SocketNoDataTimeout; - item.SocketSubscriptionsCombineTarget = SocketSubscriptionsCombineTarget; - item.MaxSocketConnections = MaxSocketConnections; - item.Proxy = Proxy; - item.RequestTimeout = RequestTimeout; - item.RateLimitingBehaviour = RateLimitingBehaviour; - item.RateLimiterEnabled = RateLimiterEnabled; - item.ReceiveBufferSize = ReceiveBufferSize; - return item; - } - } + public TimeSpan ReconnectInterval { get; set; } = TimeSpan.FromSeconds(5); /// - /// Options for a socket exchange client + /// Reconnect policy /// - /// - public class SocketExchangeOptions : SocketExchangeOptions where TEnvironment : TradeEnvironment - { - /// - /// Trade environment. Contains info about URL's to use to connect to the API. To swap environment select another environment for - /// the exchange's environment list or create a custom environment using either `[Exchange]Environment.CreateCustom()` or `[Exchange]Environment.[Environment]`, for example `KucoinEnvironment.TestNet` or `BinanceEnvironment.Live` - /// -#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. - public TEnvironment Environment { get; set; } -#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. - - /// - /// Set the values of this options on the target options - /// - public new T Set(T target) where T : SocketExchangeOptions, new() - { - base.Set(target); - target.Environment = Environment; - return target; - } - } + public ReconnectPolicy ReconnectPolicy { get; set; } = ReconnectPolicy.FixedDelay; /// - /// Options for a socket exchange client + /// Max number of concurrent resubscription tasks per socket after reconnecting a socket /// - /// - /// - public class SocketExchangeOptions : SocketExchangeOptions where TEnvironment : TradeEnvironment where TApiCredentials : ApiCredentials + public int MaxConcurrentResubscriptionsPerSocket { get; set; } = 5; + + /// + /// The max time of not receiving any data after which the connection is assumed to be dropped. This can only be used for socket connections where a steady flow of data is expected, + /// for example when the server sends intermittent ping requests + /// + public TimeSpan SocketNoDataTimeout { get; set; } + + /// + /// The amount of subscriptions that should be made on a single socket connection. Not all API's support multiple subscriptions on a single socket. + /// Setting this to a higher number increases subscription speed because not every subscription needs to connect to the server, but having more subscriptions on a + /// single connection will also increase the amount of traffic on that single connection, potentially leading to issues. + /// + public int? SocketSubscriptionsCombineTarget { get; set; } + + /// + /// The max amount of connections to make to the server. Can be used for API's which only allow a certain number of connections. Changing this to a high value might cause issues. + /// + public int? MaxSocketConnections { get; set; } + + /// + /// The time to wait after connecting a socket before sending messages. Can be used for API's which will rate limit if you subscribe directly after connecting. + /// + public TimeSpan DelayAfterConnect { get; set; } = TimeSpan.Zero; + + /// + /// This delay is used to set a RetryAfter guard on the connection after a rate limit is hit on the server. + /// This is used to prevent the client from reconnecting too quickly after a rate limit is hit. + /// + public TimeSpan? ConnectDelayAfterRateLimited { get; set; } + + /// + /// The buffer size to use for receiving data. Leave unset to use the default buffer size. + /// + /// + /// Only specify this if you are creating a significant amount of connections and understand the typical message length we receive from the exchange. + /// Setting this too low can increase memory consumption and allocations. + /// + public int? ReceiveBufferSize { get; set; } + + /// + /// Create a copy of this options + /// + /// + /// + public T Set(T item) where T : SocketExchangeOptions, new() { - /// - /// The api credentials used for signing requests to this API. - /// - public new TApiCredentials? ApiCredentials - { - get => (TApiCredentials?)base.ApiCredentials; - set => base.ApiCredentials = value; - } + item.ApiCredentials = ApiCredentials?.Copy(); + item.OutputOriginalData = OutputOriginalData; + item.ReconnectPolicy = ReconnectPolicy; + item.DelayAfterConnect = DelayAfterConnect; + item.MaxConcurrentResubscriptionsPerSocket = MaxConcurrentResubscriptionsPerSocket; + item.ReconnectInterval = ReconnectInterval; + item.SocketNoDataTimeout = SocketNoDataTimeout; + item.SocketSubscriptionsCombineTarget = SocketSubscriptionsCombineTarget; + item.MaxSocketConnections = MaxSocketConnections; + item.Proxy = Proxy; + item.RequestTimeout = RequestTimeout; + item.RateLimitingBehaviour = RateLimitingBehaviour; + item.RateLimiterEnabled = RateLimiterEnabled; + item.ReceiveBufferSize = ReceiveBufferSize; + return item; + } +} + +/// +/// Options for a socket exchange client +/// +/// +public class SocketExchangeOptions : SocketExchangeOptions where TEnvironment : TradeEnvironment +{ + /// + /// Trade environment. Contains info about URL's to use to connect to the API. To swap environment select another environment for + /// the exchange's environment list or create a custom environment using either `[Exchange]Environment.CreateCustom()` or `[Exchange]Environment.[Environment]`, for example `KucoinEnvironment.TestNet` or `BinanceEnvironment.Live` + /// +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + public TEnvironment Environment { get; set; } +#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + + /// + /// Set the values of this options on the target options + /// + public new T Set(T target) where T : SocketExchangeOptions, new() + { + base.Set(target); + target.Environment = Environment; + return target; + } +} + +/// +/// Options for a socket exchange client +/// +/// +/// +public class SocketExchangeOptions : SocketExchangeOptions where TEnvironment : TradeEnvironment where TApiCredentials : ApiCredentials +{ + /// + /// The api credentials used for signing requests to this API. + /// + public new TApiCredentials? ApiCredentials + { + get => (TApiCredentials?)base.ApiCredentials; + set => base.ApiCredentials = value; } } diff --git a/CryptoExchange.Net/Objects/Options/UpdateOptions.cs b/CryptoExchange.Net/Objects/Options/UpdateOptions.cs index 9fc9ed0..70f05ab 100644 --- a/CryptoExchange.Net/Objects/Options/UpdateOptions.cs +++ b/CryptoExchange.Net/Objects/Options/UpdateOptions.cs @@ -1,29 +1,26 @@ -using CryptoExchange.Net.Authentication; +using CryptoExchange.Net.Authentication; using System; -using System.Collections.Generic; -using System.Text; -namespace CryptoExchange.Net.Objects.Options +namespace CryptoExchange.Net.Objects.Options; + +/// +/// Options to update +/// +public class UpdateOptions where T : ApiCredentials { /// - /// Options to update + /// Proxy setting. Note that if this is not provided any previously set proxy will be reset /// - public class UpdateOptions where T : ApiCredentials - { - /// - /// Proxy setting. Note that if this is not provided any previously set proxy will be reset - /// - public ApiProxy? Proxy { get; set; } - /// - /// Api credentials - /// - public T? ApiCredentials { get; set; } - /// - /// Request timeout - /// - public TimeSpan? RequestTimeout { get; set; } - } - - /// - public class UpdateOptions : UpdateOptions { } + public ApiProxy? Proxy { get; set; } + /// + /// Api credentials + /// + public T? ApiCredentials { get; set; } + /// + /// Request timeout + /// + public TimeSpan? RequestTimeout { get; set; } } + +/// +public class UpdateOptions : UpdateOptions { } diff --git a/CryptoExchange.Net/Objects/OrderedStringComparer.cs b/CryptoExchange.Net/Objects/OrderedStringComparer.cs index 9dfe067..6d0562e 100644 --- a/CryptoExchange.Net/Objects/OrderedStringComparer.cs +++ b/CryptoExchange.Net/Objects/OrderedStringComparer.cs @@ -1,29 +1,28 @@ -using System.Collections.Generic; +using System.Collections.Generic; -namespace CryptoExchange.Net.Objects +namespace CryptoExchange.Net.Objects; + +/// +/// Order string comparer, sorts by alphabetical order +/// +public class OrderedStringComparer : IComparer { /// - /// Order string comparer, sorts by alphabetical order + /// Compare function /// - public class OrderedStringComparer : IComparer + /// + /// + /// + public int Compare(string? x, string? y) { - /// - /// Compare function - /// - /// - /// - /// - public int Compare(string? x, string? y) - { - // Shortcuts: If both are null, they are the same. - if (x == null && y == null) return 0; + // Shortcuts: If both are null, they are the same. + if (x == null && y == null) return 0; - // If one is null and the other isn't, then the - // one that is null is "lesser". - if (x == null) return -1; - if (y == null) return 1; + // If one is null and the other isn't, then the + // one that is null is "lesser". + if (x == null) return -1; + if (y == null) return 1; - return x.CompareTo(y); - } + return x.CompareTo(y); } } diff --git a/CryptoExchange.Net/Objects/ParameterCollection.cs b/CryptoExchange.Net/Objects/ParameterCollection.cs index 1512995..a775953 100644 --- a/CryptoExchange.Net/Objects/ParameterCollection.cs +++ b/CryptoExchange.Net/Objects/ParameterCollection.cs @@ -1,249 +1,250 @@ -using CryptoExchange.Net.Attributes; +using CryptoExchange.Net.Attributes; using CryptoExchange.Net.Converters.SystemTextJson; using System; using System.Collections.Generic; +#if NET5_0_OR_GREATER using System.Diagnostics.CodeAnalysis; +#endif using System.Globalization; using System.Linq; -namespace CryptoExchange.Net.Objects +namespace CryptoExchange.Net.Objects; + +/// +/// Parameters collection +/// +public class ParameterCollection : Dictionary { /// - /// Parameters collection + /// Add an optional parameter. Not added if value is null /// - public class ParameterCollection : Dictionary + /// + /// + public void AddOptional(string key, object? value) { - /// - /// Add an optional parameter. Not added if value is null - /// - /// - /// - public void AddOptional(string key, object? value) - { - if (value != null) - Add(key, value); - } + if (value != null) + Add(key, value); + } - /// - /// Add a decimal value as string - /// - /// - /// - public void AddString(string key, decimal value) - { - Add(key, value.ToString(CultureInfo.InvariantCulture)); - } + /// + /// Add a decimal value as string + /// + /// + /// + public void AddString(string key, decimal value) + { + Add(key, value.ToString(CultureInfo.InvariantCulture)); + } - /// - /// Add a decimal value as string. Not added if value is null - /// - /// - /// - public void AddOptionalString(string key, decimal? value) - { - if (value != null) - Add(key, value.Value.ToString(CultureInfo.InvariantCulture)); - } + /// + /// Add a decimal value as string. Not added if value is null + /// + /// + /// + public void AddOptionalString(string key, decimal? value) + { + if (value != null) + Add(key, value.Value.ToString(CultureInfo.InvariantCulture)); + } - /// - /// Add a int value as string - /// - /// - /// - public void AddString(string key, int value) - { - Add(key, value.ToString(CultureInfo.InvariantCulture)); - } + /// + /// Add a int value as string + /// + /// + /// + public void AddString(string key, int value) + { + Add(key, value.ToString(CultureInfo.InvariantCulture)); + } - /// - /// Add a int value as string. Not added if value is null - /// - /// - /// - public void AddOptionalString(string key, int? value) - { - if (value != null) - Add(key, value.Value.ToString(CultureInfo.InvariantCulture)); - } + /// + /// Add a int value as string. Not added if value is null + /// + /// + /// + public void AddOptionalString(string key, int? value) + { + if (value != null) + Add(key, value.Value.ToString(CultureInfo.InvariantCulture)); + } - /// - /// Add a long value as string - /// - /// - /// - public void AddString(string key, long value) - { - Add(key, value.ToString(CultureInfo.InvariantCulture)); - } + /// + /// Add a long value as string + /// + /// + /// + public void AddString(string key, long value) + { + Add(key, value.ToString(CultureInfo.InvariantCulture)); + } - /// - /// Add a long value as string. Not added if value is null - /// - /// - /// - public void AddOptionalString(string key, long? value) - { - if (value != null) - Add(key, value.Value.ToString(CultureInfo.InvariantCulture)); - } + /// + /// Add a long value as string. Not added if value is null + /// + /// + /// + public void AddOptionalString(string key, long? value) + { + if (value != null) + Add(key, value.Value.ToString(CultureInfo.InvariantCulture)); + } - /// - /// Add a datetime value as milliseconds timestamp - /// - /// - /// - public void AddMilliseconds(string key, DateTime value) - { + /// + /// Add a datetime value as milliseconds timestamp + /// + /// + /// + public void AddMilliseconds(string key, DateTime value) + { + Add(key, DateTimeConverter.ConvertToMilliseconds(value)); + } + + /// + /// Add a datetime value as milliseconds timestamp. Not added if value is null + /// + /// + /// + public void AddOptionalMilliseconds(string key, DateTime? value) + { + if (value != null) Add(key, DateTimeConverter.ConvertToMilliseconds(value)); - } + } - /// - /// Add a datetime value as milliseconds timestamp. Not added if value is null - /// - /// - /// - public void AddOptionalMilliseconds(string key, DateTime? value) - { - if (value != null) - Add(key, DateTimeConverter.ConvertToMilliseconds(value)); - } + /// + /// Add a datetime value as milliseconds timestamp + /// + /// + /// + public void AddMillisecondsString(string key, DateTime value) + { + Add(key, DateTimeConverter.ConvertToMilliseconds(value).Value.ToString(CultureInfo.InvariantCulture)); + } - /// - /// Add a datetime value as milliseconds timestamp - /// - /// - /// - public void AddMillisecondsString(string key, DateTime value) - { + /// + /// Add a datetime value as milliseconds timestamp. Not added if value is null + /// + /// + /// + public void AddOptionalMillisecondsString(string key, DateTime? value) + { + if (value != null) Add(key, DateTimeConverter.ConvertToMilliseconds(value).Value.ToString(CultureInfo.InvariantCulture)); - } + } - /// - /// Add a datetime value as milliseconds timestamp. Not added if value is null - /// - /// - /// - public void AddOptionalMillisecondsString(string key, DateTime? value) - { - if (value != null) - Add(key, DateTimeConverter.ConvertToMilliseconds(value).Value.ToString(CultureInfo.InvariantCulture)); - } + /// + /// Add a datetime value as seconds timestamp + /// + /// + /// + public void AddSeconds(string key, DateTime value) + { + Add(key, DateTimeConverter.ConvertToSeconds(value)); + } - /// - /// Add a datetime value as seconds timestamp - /// - /// - /// - public void AddSeconds(string key, DateTime value) - { + /// + /// Add a datetime value as seconds timestamp. Not added if value is null + /// + /// + /// + public void AddOptionalSeconds(string key, DateTime? value) + { + if (value != null) Add(key, DateTimeConverter.ConvertToSeconds(value)); - } + } - /// - /// Add a datetime value as seconds timestamp. Not added if value is null - /// - /// - /// - public void AddOptionalSeconds(string key, DateTime? value) - { - if (value != null) - Add(key, DateTimeConverter.ConvertToSeconds(value)); - } + /// + /// Add a datetime value as string seconds timestamp + /// + /// + /// + public void AddSecondsString(string key, DateTime value) + { + Add(key, DateTimeConverter.ConvertToSeconds(value).ToString()!); + } - /// - /// Add a datetime value as string seconds timestamp - /// - /// - /// - public void AddSecondsString(string key, DateTime value) - { + /// + /// Add a datetime value as string seconds timestamp. Not added if value is null + /// + /// + /// + public void AddOptionalSecondsString(string key, DateTime? value) + { + if (value != null) Add(key, DateTimeConverter.ConvertToSeconds(value).ToString()!); - } + } - /// - /// Add a datetime value as string seconds timestamp. Not added if value is null - /// - /// - /// - public void AddOptionalSecondsString(string key, DateTime? value) - { - if (value != null) - Add(key, DateTimeConverter.ConvertToSeconds(value).ToString()!); - } - - /// - /// Add an enum value as the string value as mapped using the - /// + /// + /// Add an enum value as the string value as mapped using the + /// #if NET5_0_OR_GREATER - public void AddEnum<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string key, T value) + public void AddEnum<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string key, T value) #else - public void AddEnum(string key, T value) + public void AddEnum(string key, T value) #endif - where T : struct, Enum - { - Add(key, EnumConverter.GetString(value)!); - } + where T : struct, Enum + { + Add(key, EnumConverter.GetString(value)!); + } - /// - /// Add an enum value as the string value as mapped using the - /// - /// - /// + /// + /// Add an enum value as the string value as mapped using the + /// + /// + /// #if NET5_0_OR_GREATER - public void AddEnumAsInt<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string key, T value) + public void AddEnumAsInt<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string key, T value) #else - public void AddEnumAsInt(string key, T value) + public void AddEnumAsInt(string key, T value) #endif - where T : struct, Enum - { - var stringVal = EnumConverter.GetString(value)!; - Add(key, int.Parse(stringVal)!); - } + where T : struct, Enum + { + var stringVal = EnumConverter.GetString(value)!; + Add(key, int.Parse(stringVal)!); + } - /// - /// Add an enum value as the string value as mapped using the . Not added if value is null - /// - /// - /// + /// + /// Add an enum value as the string value as mapped using the . Not added if value is null + /// + /// + /// #if NET5_0_OR_GREATER - public void AddOptionalEnum<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string key, T? value) + public void AddOptionalEnum<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string key, T? value) #else - public void AddOptionalEnum(string key, T? value) + public void AddOptionalEnum(string key, T? value) #endif - where T : struct, Enum - { - if (value != null) - Add(key, EnumConverter.GetString(value)); - } + where T : struct, Enum + { + if (value != null) + Add(key, EnumConverter.GetString(value)); + } - /// - /// Add an enum value as the string value as mapped using the . Not added if value is null - /// + /// + /// Add an enum value as the string value as mapped using the . Not added if value is null + /// #if NET5_0_OR_GREATER - public void AddOptionalEnumAsInt<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string key, T? value) + public void AddOptionalEnumAsInt<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string key, T? value) #else - public void AddOptionalEnumAsInt(string key, T? value) + public void AddOptionalEnumAsInt(string key, T? value) #endif - where T : struct, Enum + where T : struct, Enum + { + if (value != null) { - if (value != null) - { - var stringVal = EnumConverter.GetString(value); - Add(key, int.Parse(stringVal)); - } - } - - /// - /// Set the request body. Can be used to specify a simple value or array as the body instead of an object - /// - /// Body to set - /// - public void SetBody(object body) - { - if (this.Any()) - throw new InvalidOperationException("Can't set body when other parameters already specified"); - - Add(Constants.BodyPlaceHolderKey, body); + var stringVal = EnumConverter.GetString(value); + Add(key, int.Parse(stringVal)); } } + + /// + /// Set the request body. Can be used to specify a simple value or array as the body instead of an object + /// + /// Body to set + /// + public void SetBody(object body) + { + if (this.Any()) + throw new InvalidOperationException("Can't set body when other parameters already specified"); + + Add(Constants.BodyPlaceHolderKey, body); + } } diff --git a/CryptoExchange.Net/Objects/RequestDefinition.cs b/CryptoExchange.Net/Objects/RequestDefinition.cs index 6ce9ab4..93857b9 100644 --- a/CryptoExchange.Net/Objects/RequestDefinition.cs +++ b/CryptoExchange.Net/Objects/RequestDefinition.cs @@ -1,90 +1,89 @@ -using CryptoExchange.Net.RateLimiting.Interfaces; +using CryptoExchange.Net.RateLimiting.Interfaces; using System.Net.Http; -namespace CryptoExchange.Net.Objects +namespace CryptoExchange.Net.Objects; + +/// +/// The definition of a rest request +/// +public class RequestDefinition { + private string? _stringRep; + + // Basics + /// - /// The definition of a rest request + /// Path of the request /// - public class RequestDefinition + public string Path { get; set; } + /// + /// Http method of the request + /// + public HttpMethod Method { get; set; } + /// + /// Is the request authenticated + /// + public bool Authenticated { get; set; } + + // Formatting + + /// + /// The body format for this request + /// + public RequestBodyFormat? RequestBodyFormat { get; set; } + /// + /// The position of parameters for this request + /// + public HttpMethodParameterPosition? ParameterPosition { get; set; } + /// + /// The array serialization type for this request + /// + public ArrayParametersSerialization? ArraySerialization { get; set; } + + // Rate limiting + + /// + /// Request weight + /// + public int Weight { get; set; } = 1; + + /// + /// Rate limit gate to use + /// + public IRateLimitGate? RateLimitGate { get; set; } + + /// + /// Individual endpoint rate limit guard to use + /// + public IRateLimitGuard? LimitGuard { get; set; } + + /// + /// Whether this request should never be cached + /// + public bool PreventCaching { get; set; } + + /// + /// Connection id + /// + public int? ConnectionId { get; set; } + + /// + /// ctor + /// + /// + /// + public RequestDefinition(string path, HttpMethod method) { - private string? _stringRep; + Path = path; + Method = method; - // Basics + if (!Path.StartsWith("/")) + Path = $"/{Path}"; + } - /// - /// Path of the request - /// - public string Path { get; set; } - /// - /// Http method of the request - /// - public HttpMethod Method { get; set; } - /// - /// Is the request authenticated - /// - public bool Authenticated { get; set; } - - // Formatting - - /// - /// The body format for this request - /// - public RequestBodyFormat? RequestBodyFormat { get; set; } - /// - /// The position of parameters for this request - /// - public HttpMethodParameterPosition? ParameterPosition { get; set; } - /// - /// The array serialization type for this request - /// - public ArrayParametersSerialization? ArraySerialization { get; set; } - - // Rate limiting - - /// - /// Request weight - /// - public int Weight { get; set; } = 1; - - /// - /// Rate limit gate to use - /// - public IRateLimitGate? RateLimitGate { get; set; } - - /// - /// Individual endpoint rate limit guard to use - /// - public IRateLimitGuard? LimitGuard { get; set; } - - /// - /// Whether this request should never be cached - /// - public bool PreventCaching { get; set; } - - /// - /// Connection id - /// - public int? ConnectionId { get; set; } - - /// - /// ctor - /// - /// - /// - public RequestDefinition(string path, HttpMethod method) - { - Path = path; - Method = method; - - if (!Path.StartsWith("/")) - Path = $"/{Path}"; - } - - /// - public override string ToString() - { - return _stringRep ??= $"{Method} {Path}{(Authenticated ? " authenticated" : "")}"; - } + /// + public override string ToString() + { + return _stringRep ??= $"{Method} {Path}{(Authenticated ? " authenticated" : "")}"; } } diff --git a/CryptoExchange.Net/Objects/RequestDefinitionCache.cs b/CryptoExchange.Net/Objects/RequestDefinitionCache.cs index 9c50a0b..f4fa791 100644 --- a/CryptoExchange.Net/Objects/RequestDefinitionCache.cs +++ b/CryptoExchange.Net/Objects/RequestDefinitionCache.cs @@ -1,111 +1,110 @@ -using CryptoExchange.Net.RateLimiting.Interfaces; +using CryptoExchange.Net.RateLimiting.Interfaces; using System.Collections.Concurrent; using System.Net.Http; -namespace CryptoExchange.Net.Objects +namespace CryptoExchange.Net.Objects; + +/// +/// Request definitions cache +/// +public class RequestDefinitionCache { + private readonly ConcurrentDictionary _definitions = new(); + /// - /// Request definitions cache + /// Get a definition if it is already in the cache or create a new definition and add it to the cache /// - public class RequestDefinitionCache + /// The HttpMethod + /// Endpoint path + /// Endpoint is authenticated + /// + public RequestDefinition GetOrCreate(HttpMethod method, string path, bool authenticated = false) + => GetOrCreate(method, path, null, 0, authenticated, null, null, null, null, null); + + /// + /// Get a definition if it is already in the cache or create a new definition and add it to the cache + /// + /// The HttpMethod + /// Endpoint path + /// The rate limit gate + /// Request weight + /// Endpoint is authenticated + /// + public RequestDefinition GetOrCreate(HttpMethod method, string path, IRateLimitGate rateLimitGate, int weight = 1, bool authenticated = false) + => GetOrCreate(method, path, rateLimitGate, weight, authenticated, null, null, null, null, null); + + /// + /// Get a definition if it is already in the cache or create a new definition and add it to the cache + /// + /// The HttpMethod + /// Endpoint path + /// The rate limit gate + /// The rate limit guard for this specific endpoint + /// Request weight + /// Endpoint is authenticated + /// Request body format + /// Parameter position + /// Array serialization type + /// Prevent request caching + /// + public RequestDefinition GetOrCreate( + HttpMethod method, + string path, + IRateLimitGate? rateLimitGate, + int weight, + bool authenticated, + IRateLimitGuard? limitGuard = null, + RequestBodyFormat? requestBodyFormat = null, + HttpMethodParameterPosition? parameterPosition = null, + ArrayParametersSerialization? arraySerialization = null, + bool? preventCaching = null) + => GetOrCreate(method + path, method, path, rateLimitGate, weight, authenticated, limitGuard, requestBodyFormat, parameterPosition, arraySerialization, preventCaching); + + /// + /// Get a definition if it is already in the cache or create a new definition and add it to the cache + /// + /// Request identifier + /// The HttpMethod + /// Endpoint path + /// The rate limit gate + /// The rate limit guard for this specific endpoint + /// Request weight + /// Endpoint is authenticated + /// Request body format + /// Parameter position + /// Array serialization type + /// Prevent request caching + /// + public RequestDefinition GetOrCreate( + string identifier, + HttpMethod method, + string path, + IRateLimitGate? rateLimitGate, + int weight, + bool authenticated, + IRateLimitGuard? limitGuard = null, + RequestBodyFormat? requestBodyFormat = null, + HttpMethodParameterPosition? parameterPosition = null, + ArrayParametersSerialization? arraySerialization = null, + bool? preventCaching = null) { - private readonly ConcurrentDictionary _definitions = new(); - /// - /// Get a definition if it is already in the cache or create a new definition and add it to the cache - /// - /// The HttpMethod - /// Endpoint path - /// Endpoint is authenticated - /// - public RequestDefinition GetOrCreate(HttpMethod method, string path, bool authenticated = false) - => GetOrCreate(method, path, null, 0, authenticated, null, null, null, null, null); - - /// - /// Get a definition if it is already in the cache or create a new definition and add it to the cache - /// - /// The HttpMethod - /// Endpoint path - /// The rate limit gate - /// Request weight - /// Endpoint is authenticated - /// - public RequestDefinition GetOrCreate(HttpMethod method, string path, IRateLimitGate rateLimitGate, int weight = 1, bool authenticated = false) - => GetOrCreate(method, path, rateLimitGate, weight, authenticated, null, null, null, null, null); - - /// - /// Get a definition if it is already in the cache or create a new definition and add it to the cache - /// - /// The HttpMethod - /// Endpoint path - /// The rate limit gate - /// The rate limit guard for this specific endpoint - /// Request weight - /// Endpoint is authenticated - /// Request body format - /// Parameter position - /// Array serialization type - /// Prevent request caching - /// - public RequestDefinition GetOrCreate( - HttpMethod method, - string path, - IRateLimitGate? rateLimitGate, - int weight, - bool authenticated, - IRateLimitGuard? limitGuard = null, - RequestBodyFormat? requestBodyFormat = null, - HttpMethodParameterPosition? parameterPosition = null, - ArrayParametersSerialization? arraySerialization = null, - bool? preventCaching = null) - => GetOrCreate(method + path, method, path, rateLimitGate, weight, authenticated, limitGuard, requestBodyFormat, parameterPosition, arraySerialization, preventCaching); - - /// - /// Get a definition if it is already in the cache or create a new definition and add it to the cache - /// - /// Request identifier - /// The HttpMethod - /// Endpoint path - /// The rate limit gate - /// The rate limit guard for this specific endpoint - /// Request weight - /// Endpoint is authenticated - /// Request body format - /// Parameter position - /// Array serialization type - /// Prevent request caching - /// - public RequestDefinition GetOrCreate( - string identifier, - HttpMethod method, - string path, - IRateLimitGate? rateLimitGate, - int weight, - bool authenticated, - IRateLimitGuard? limitGuard = null, - RequestBodyFormat? requestBodyFormat = null, - HttpMethodParameterPosition? parameterPosition = null, - ArrayParametersSerialization? arraySerialization = null, - bool? preventCaching = null) + if (!_definitions.TryGetValue(identifier, out var def)) { - - if (!_definitions.TryGetValue(identifier, out var def)) + def = new RequestDefinition(path, method) { - def = new RequestDefinition(path, method) - { - Authenticated = authenticated, - LimitGuard = limitGuard, - RateLimitGate = rateLimitGate, - Weight = weight, - ArraySerialization = arraySerialization, - RequestBodyFormat = requestBodyFormat, - ParameterPosition = parameterPosition, - PreventCaching = preventCaching ?? false - }; - _definitions.TryAdd(identifier, def); - } - - return def; + Authenticated = authenticated, + LimitGuard = limitGuard, + RateLimitGate = rateLimitGate, + Weight = weight, + ArraySerialization = arraySerialization, + RequestBodyFormat = requestBodyFormat, + ParameterPosition = parameterPosition, + PreventCaching = preventCaching ?? false + }; + _definitions.TryAdd(identifier, def); } + + return def; } } diff --git a/CryptoExchange.Net/Objects/RestRequestConfiguration.cs b/CryptoExchange.Net/Objects/RestRequestConfiguration.cs index e748b9f..2b3344e 100644 --- a/CryptoExchange.Net/Objects/RestRequestConfiguration.cs +++ b/CryptoExchange.Net/Objects/RestRequestConfiguration.cs @@ -1,124 +1,123 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Net.Http; -namespace CryptoExchange.Net.Objects +namespace CryptoExchange.Net.Objects; + +/// +/// Rest request configuration +/// +public class RestRequestConfiguration { + private string? _bodyContent; + private string? _queryString; + /// - /// Rest request configuration + /// Http method /// - public class RestRequestConfiguration + public HttpMethod Method { get; set; } + /// + /// Whether the request needs authentication + /// + public bool Authenticated { get; set; } + /// + /// Base address for the request + /// + public string BaseAddress { get; set; } + /// + /// The request path + /// + public string Path { get; set; } + /// + /// Query parameters + /// + public IDictionary QueryParameters { get; set; } + /// + /// Body parameters + /// + public IDictionary BodyParameters { get; set; } + /// + /// Request headers + /// + public IDictionary Headers { get; set; } + /// + /// Array serialization type + /// + public ArrayParametersSerialization ArraySerialization { get; set; } + /// + /// Position of the parameters + /// + public HttpMethodParameterPosition ParameterPosition { get; set; } + /// + /// Body format + /// + public RequestBodyFormat BodyFormat { get; set; } + + /// + /// ctor + /// + public RestRequestConfiguration( + RequestDefinition requestDefinition, + string baseAddress, + IDictionary queryParams, + IDictionary bodyParams, + IDictionary headers, + ArrayParametersSerialization arraySerialization, + HttpMethodParameterPosition parametersPosition, + RequestBodyFormat bodyFormat) { - private string? _bodyContent; - private string? _queryString; + Method = requestDefinition.Method; + Authenticated = requestDefinition.Authenticated; + Path = requestDefinition.Path; + BaseAddress = baseAddress; + QueryParameters = queryParams; + BodyParameters = bodyParams; + Headers = headers; + ArraySerialization = arraySerialization; + ParameterPosition = parametersPosition; + BodyFormat = bodyFormat; + } - /// - /// Http method - /// - public HttpMethod Method { get; set; } - /// - /// Whether the request needs authentication - /// - public bool Authenticated { get; set; } - /// - /// Base address for the request - /// - public string BaseAddress { get; set; } - /// - /// The request path - /// - public string Path { get; set; } - /// - /// Query parameters - /// - public IDictionary QueryParameters { get; set; } - /// - /// Body parameters - /// - public IDictionary BodyParameters { get; set; } - /// - /// Request headers - /// - public IDictionary Headers { get; set; } - /// - /// Array serialization type - /// - public ArrayParametersSerialization ArraySerialization { get; set; } - /// - /// Position of the parameters - /// - public HttpMethodParameterPosition ParameterPosition { get; set; } - /// - /// Body format - /// - public RequestBodyFormat BodyFormat { get; set; } + /// + /// Get the parameter collection based on the ParameterPosition + /// + public IDictionary GetPositionParameters() + { + if (ParameterPosition == HttpMethodParameterPosition.InBody) + return BodyParameters; - /// - /// ctor - /// - public RestRequestConfiguration( - RequestDefinition requestDefinition, - string baseAddress, - IDictionary queryParams, - IDictionary bodyParams, - IDictionary headers, - ArrayParametersSerialization arraySerialization, - HttpMethodParameterPosition parametersPosition, - RequestBodyFormat bodyFormat) - { - Method = requestDefinition.Method; - Authenticated = requestDefinition.Authenticated; - Path = requestDefinition.Path; - BaseAddress = baseAddress; - QueryParameters = queryParams; - BodyParameters = bodyParams; - Headers = headers; - ArraySerialization = arraySerialization; - ParameterPosition = parametersPosition; - BodyFormat = bodyFormat; - } + return QueryParameters; + } - /// - /// Get the parameter collection based on the ParameterPosition - /// - public IDictionary GetPositionParameters() - { - if (ParameterPosition == HttpMethodParameterPosition.InBody) - return BodyParameters; + /// + /// Get the query string. If it's not previously set it will return a newly formatted query string. If previously set return that. + /// + /// Whether to URL encode the parameter string if creating new + public string GetQueryString(bool urlEncode = true) + { + return _queryString ?? QueryParameters.CreateParamString(urlEncode, ArraySerialization); + } - return QueryParameters; - } + /// + /// Set the query string of the request. Will be returned by subsequent calls + /// + public void SetQueryString(string value) + { + _queryString = value; + } - /// - /// Get the query string. If it's not previously set it will return a newly formatted query string. If previously set return that. - /// - /// Whether to URL encode the parameter string if creating new - public string GetQueryString(bool urlEncode = true) - { - return _queryString ?? QueryParameters.CreateParamString(urlEncode, ArraySerialization); - } + /// + /// Get the body content if it's previously set + /// + public string? GetBodyContent() + { + return _bodyContent; + } - /// - /// Set the query string of the request. Will be returned by subsequent calls - /// - public void SetQueryString(string value) - { - _queryString = value; - } - - /// - /// Get the body content if it's previously set - /// - public string? GetBodyContent() - { - return _bodyContent; - } - - /// - /// Set the body content for the request - /// - public void SetBodyContent(string content) - { - _bodyContent = content; - } + /// + /// Set the body content for the request + /// + public void SetBodyContent(string content) + { + _bodyContent = content; } } diff --git a/CryptoExchange.Net/Objects/Sockets/DataEvent.cs b/CryptoExchange.Net/Objects/Sockets/DataEvent.cs index e808bdf..12344d5 100644 --- a/CryptoExchange.Net/Objects/Sockets/DataEvent.cs +++ b/CryptoExchange.Net/Objects/Sockets/DataEvent.cs @@ -1,196 +1,195 @@ -using CryptoExchange.Net.SharedApis; +using CryptoExchange.Net.SharedApis; using System; -namespace CryptoExchange.Net.Objects.Sockets +namespace CryptoExchange.Net.Objects.Sockets; + +/// +/// An update received from a socket update subscription +/// +/// The type of the data +public class DataEvent { /// - /// An update received from a socket update subscription + /// The timestamp the data was received /// - /// The type of the data - public class DataEvent + public DateTime ReceiveTime { get; set; } + + /// + /// The timestamp of the data as specified by the server. Note that the server time and client time might not be 100% in sync so this value might not be fully comparable to local time. + /// + public DateTime? DataTime { get; set; } + + /// + /// The stream producing the update + /// + public string? StreamId { get; set; } + + /// + /// The symbol the update is for + /// + public string? Symbol { get; set; } + + /// + /// The original data that was received, only available when OutputOriginalData is set to true in the client options + /// + public string? OriginalData { get; set; } + + /// + /// Type of update + /// + public SocketUpdateType? UpdateType { get; set; } + + /// + /// The received data deserialized into an object + /// + public T Data { get; set; } + + /// + /// ctor + /// + public DataEvent(T data, string? streamId, string? symbol, string? originalData, DateTime receiveTimestamp, SocketUpdateType? updateType) { - /// - /// The timestamp the data was received - /// - public DateTime ReceiveTime { get; set; } + Data = data; + StreamId = streamId; + Symbol = symbol; + OriginalData = originalData; + ReceiveTime = receiveTimestamp; + UpdateType = updateType; + } - /// - /// The timestamp of the data as specified by the server. Note that the server time and client time might not be 100% in sync so this value might not be fully comparable to local time. - /// - public DateTime? DataTime { get; set; } - - /// - /// The stream producing the update - /// - public string? StreamId { get; set; } - - /// - /// The symbol the update is for - /// - public string? Symbol { get; set; } - - /// - /// The original data that was received, only available when OutputOriginalData is set to true in the client options - /// - public string? OriginalData { get; set; } - - /// - /// Type of update - /// - public SocketUpdateType? UpdateType { get; set; } - - /// - /// The received data deserialized into an object - /// - public T Data { get; set; } - - /// - /// ctor - /// - public DataEvent(T data, string? streamId, string? symbol, string? originalData, DateTime receiveTimestamp, SocketUpdateType? updateType) + /// + /// Create a new DataEvent with data in the from of type K based on the current DataEvent. Topic, OriginalData and ReceivedTimestamp will be copied over + /// + /// The type of the new data + /// The new data + /// + public DataEvent As(TNew data) + { + return new DataEvent(data, StreamId, Symbol, OriginalData, ReceiveTime, UpdateType) { - Data = data; - StreamId = streamId; - Symbol = symbol; - OriginalData = originalData; - ReceiveTime = receiveTimestamp; - UpdateType = updateType; - } + DataTime = DataTime + }; + } - /// - /// Create a new DataEvent with data in the from of type K based on the current DataEvent. Topic, OriginalData and ReceivedTimestamp will be copied over - /// - /// The type of the new data - /// The new data - /// - public DataEvent As(K data) + /// + /// Create a new DataEvent with data in the from of type K based on the current DataEvent. OriginalData and ReceivedTimestamp will be copied over + /// + /// The type of the new data + /// The new data + /// The new symbol + /// + public DataEvent As(TNew data, string? symbol) + { + return new DataEvent(data, StreamId, symbol, OriginalData, ReceiveTime, UpdateType) { - return new DataEvent(data, StreamId, Symbol, OriginalData, ReceiveTime, UpdateType) - { - DataTime = DataTime - }; - } + DataTime = DataTime + }; + } - /// - /// Create a new DataEvent with data in the from of type K based on the current DataEvent. OriginalData and ReceivedTimestamp will be copied over - /// - /// The type of the new data - /// The new data - /// The new symbol - /// - public DataEvent As(K data, string? symbol) + /// + /// Create a new DataEvent with data in the from of type K based on the current DataEvent. OriginalData and ReceivedTimestamp will be copied over + /// + /// The type of the new data + /// The new data + /// The new stream id + /// The new symbol + /// The type of update + /// + public DataEvent As(TNew data, string streamId, string? symbol, SocketUpdateType updateType) + { + return new DataEvent(data, streamId, symbol, OriginalData, ReceiveTime, updateType) { - return new DataEvent(data, StreamId, symbol, OriginalData, ReceiveTime, UpdateType) - { - DataTime = DataTime - }; - } + DataTime = DataTime + }; + } - /// - /// Create a new DataEvent with data in the from of type K based on the current DataEvent. OriginalData and ReceivedTimestamp will be copied over - /// - /// The type of the new data - /// The new data - /// The new stream id - /// The new symbol - /// The type of update - /// - public DataEvent As(K data, string streamId, string? symbol, SocketUpdateType updateType) + /// + /// Copy the WebCallResult to a new data type + /// + /// The new type + /// The exchange the result is for + /// The data + /// + public ExchangeEvent AsExchangeEvent(string exchange, TNew data) + { + return new ExchangeEvent(exchange, this.As(data)) { - return new DataEvent(data, streamId, symbol, OriginalData, ReceiveTime, updateType) - { - DataTime = DataTime - }; - } + DataTime = DataTime + }; + } - /// - /// Copy the WebCallResult to a new data type - /// - /// The new type - /// The exchange the result is for - /// The data - /// - public ExchangeEvent AsExchangeEvent(string exchange, K data) - { - return new ExchangeEvent(exchange, this.As(data)) - { - DataTime = DataTime - }; - } + /// + /// Specify the symbol + /// + /// + /// + public DataEvent WithSymbol(string symbol) + { + Symbol = symbol; + return this; + } - /// - /// Specify the symbol - /// - /// - /// - public DataEvent WithSymbol(string symbol) - { - Symbol = symbol; - return this; - } + /// + /// Specify the update type + /// + /// + /// + public DataEvent WithUpdateType(SocketUpdateType type) + { + UpdateType = type; + return this; + } - /// - /// Specify the update type - /// - /// - /// - public DataEvent WithUpdateType(SocketUpdateType type) - { - UpdateType = type; - return this; - } + /// + /// Specify the stream id + /// + /// + /// + public DataEvent WithStreamId(string streamId) + { + StreamId = streamId; + return this; + } - /// - /// Specify the stream id - /// - /// - /// - public DataEvent WithStreamId(string streamId) - { - StreamId = streamId; - return this; - } + /// + /// Specify the data timestamp + /// + public DataEvent WithDataTimestamp(DateTime? timestamp) + { + DataTime = timestamp; + return this; + } - /// - /// Specify the data timestamp - /// - public DataEvent WithDataTimestamp(DateTime? timestamp) - { - DataTime = timestamp; - return this; - } + /// + /// Create a CallResult from this DataEvent + /// + /// + public CallResult ToCallResult() + { + return new CallResult(Data, OriginalData, null); + } - /// - /// Create a CallResult from this DataEvent - /// - /// - public CallResult ToCallResult() - { - return new CallResult(Data, OriginalData, null); - } + /// + /// Create a CallResult from this DataEvent + /// + /// + public CallResult ToCallResult(TNew data) + { + return new CallResult(data, OriginalData, null); + } - /// - /// Create a CallResult from this DataEvent - /// - /// - public CallResult ToCallResult(K data) - { - return new CallResult(data, OriginalData, null); - } + /// + /// Create a CallResult from this DataEvent + /// + /// + public CallResult ToCallResult(Error error) + { + return new CallResult(default, OriginalData, error); + } - /// - /// Create a CallResult from this DataEvent - /// - /// - public CallResult ToCallResult(Error error) - { - return new CallResult(default, OriginalData, error); - } - - /// - public override string ToString() - { - return $"{StreamId} - {(Symbol == null ? "" : (Symbol + " - "))}{(UpdateType == null ? "" : (UpdateType + " - "))}{Data}"; - } + /// + public override string ToString() + { + return $"{StreamId} - {(Symbol == null ? "" : (Symbol + " - "))}{(UpdateType == null ? "" : (UpdateType + " - "))}{Data}"; } } diff --git a/CryptoExchange.Net/Objects/Sockets/UpdateSubscription.cs b/CryptoExchange.Net/Objects/Sockets/UpdateSubscription.cs index e627c43..45f5f51 100644 --- a/CryptoExchange.Net/Objects/Sockets/UpdateSubscription.cs +++ b/CryptoExchange.Net/Objects/Sockets/UpdateSubscription.cs @@ -1,137 +1,136 @@ -using CryptoExchange.Net.Sockets; +using CryptoExchange.Net.Sockets; using System; using System.Threading.Tasks; -namespace CryptoExchange.Net.Objects.Sockets +namespace CryptoExchange.Net.Objects.Sockets; + +/// +/// Subscription to a data stream +/// +public class UpdateSubscription { + private readonly SocketConnection _connection; + private readonly Subscription _listener; + /// - /// Subscription to a data stream + /// Event when the connection is lost. The socket will automatically reconnect when possible. /// - public class UpdateSubscription + public event Action ConnectionLost { - private readonly SocketConnection _connection; - private readonly Subscription _listener; + add => _connection.ConnectionLost += value; + remove => _connection.ConnectionLost -= value; + } - /// - /// Event when the connection is lost. The socket will automatically reconnect when possible. - /// - public event Action ConnectionLost - { - add => _connection.ConnectionLost += value; - remove => _connection.ConnectionLost -= value; - } + /// + /// Event when the connection is closed and will not be reconnected + /// + public event Action ConnectionClosed + { + add => _connection.ConnectionClosed += value; + remove => _connection.ConnectionClosed -= value; + } - /// - /// Event when the connection is closed and will not be reconnected - /// - public event Action ConnectionClosed - { - add => _connection.ConnectionClosed += value; - remove => _connection.ConnectionClosed -= value; - } + /// + /// Event when a lost connection is restored, but the resubscribing of update subscriptions failed + /// + public event Action ResubscribingFailed + { + add => _connection.ResubscribingFailed += value; + remove => _connection.ResubscribingFailed -= value; + } - /// - /// Event when a lost connection is restored, but the resubscribing of update subscriptions failed - /// - public event Action ResubscribingFailed - { - add => _connection.ResubscribingFailed += value; - remove => _connection.ResubscribingFailed -= value; - } + /// + /// Event when the connection is restored. Timespan parameter indicates the time the socket has been offline for before reconnecting. + /// Note that when the executing code is suspended and resumed at a later period (for example, a laptop going to sleep) the disconnect time will be incorrect as the disconnect + /// will only be detected after resuming the code, so the initial disconnect time is lost. Use the timespan only for informational purposes. + /// + public event Action ConnectionRestored + { + add => _connection.ConnectionRestored += value; + remove => _connection.ConnectionRestored -= value; + } - /// - /// Event when the connection is restored. Timespan parameter indicates the time the socket has been offline for before reconnecting. - /// Note that when the executing code is suspended and resumed at a later period (for example, a laptop going to sleep) the disconnect time will be incorrect as the disconnect - /// will only be detected after resuming the code, so the initial disconnect time is lost. Use the timespan only for informational purposes. - /// - public event Action ConnectionRestored - { - add => _connection.ConnectionRestored += value; - remove => _connection.ConnectionRestored -= value; - } + /// + /// Event when the connection to the server is paused based on a server indication. No operations can be performed while paused + /// + public event Action ActivityPaused + { + add => _connection.ActivityPaused += value; + remove => _connection.ActivityPaused -= value; + } - /// - /// Event when the connection to the server is paused based on a server indication. No operations can be performed while paused - /// - public event Action ActivityPaused - { - add => _connection.ActivityPaused += value; - remove => _connection.ActivityPaused -= value; - } + /// + /// Event when the connection to the server is unpaused after being paused + /// + public event Action ActivityUnpaused + { + add => _connection.ActivityUnpaused += value; + remove => _connection.ActivityUnpaused -= value; + } - /// - /// Event when the connection to the server is unpaused after being paused - /// - public event Action ActivityUnpaused - { - add => _connection.ActivityUnpaused += value; - remove => _connection.ActivityUnpaused -= value; - } + /// + /// Event when an exception happens during the handling of the data + /// + public event Action Exception + { + add => _listener.Exception += value; + remove => _listener.Exception -= value; + } - /// - /// Event when an exception happens during the handling of the data - /// - public event Action Exception - { - add => _listener.Exception += value; - remove => _listener.Exception -= value; - } + /// + /// The id of the socket + /// + public int SocketId => _connection.SocketId; - /// - /// The id of the socket - /// - public int SocketId => _connection.SocketId; + /// + /// The id of the subscription + /// + public int Id => _listener.Id; - /// - /// The id of the subscription - /// - public int Id => _listener.Id; + /// + /// ctor + /// + /// The socket connection the subscription is on + /// The subscription + public UpdateSubscription(SocketConnection connection, Subscription subscription) + { + _connection = connection; + _listener = subscription; + } - /// - /// ctor - /// - /// The socket connection the subscription is on - /// The subscription - public UpdateSubscription(SocketConnection connection, Subscription subscription) - { - _connection = connection; - _listener = subscription; - } + /// + /// Close the subscription + /// + /// + public Task CloseAsync() + { + return _connection.CloseAsync(_listener); + } - /// - /// Close the subscription - /// - /// - public Task CloseAsync() - { - return _connection.CloseAsync(_listener); - } + /// + /// Close the socket to cause a reconnect + /// + /// + public Task ReconnectAsync() + { + return _connection.TriggerReconnectAsync(); + } - /// - /// Close the socket to cause a reconnect - /// - /// - public Task ReconnectAsync() - { - return _connection.TriggerReconnectAsync(); - } + /// + /// Unsubscribe a subscription + /// + /// + internal async Task UnsubscribeAsync() + { + await _connection.UnsubscribeAsync(_listener).ConfigureAwait(false); + } - /// - /// Unsubscribe a subscription - /// - /// - internal async Task UnsubscribeAsync() - { - await _connection.UnsubscribeAsync(_listener).ConfigureAwait(false); - } - - /// - /// Resubscribe this subscription - /// - /// - internal async Task ResubscribeAsync() - { - return await _connection.ResubscribeAsync(_listener).ConfigureAwait(false); - } + /// + /// Resubscribe this subscription + /// + /// + internal async Task ResubscribeAsync() + { + return await _connection.ResubscribeAsync(_listener).ConfigureAwait(false); } } diff --git a/CryptoExchange.Net/Objects/Sockets/WebSocketParameters.cs b/CryptoExchange.Net/Objects/Sockets/WebSocketParameters.cs index 5ce4c1a..ae6c649 100644 --- a/CryptoExchange.Net/Objects/Sockets/WebSocketParameters.cs +++ b/CryptoExchange.Net/Objects/Sockets/WebSocketParameters.cs @@ -1,88 +1,87 @@ -using CryptoExchange.Net.RateLimiting.Interfaces; +using CryptoExchange.Net.RateLimiting.Interfaces; using System; using System.Collections.Generic; using System.Text; -namespace CryptoExchange.Net.Objects.Sockets +namespace CryptoExchange.Net.Objects.Sockets; + +/// +/// Parameters for a websocket +/// +public class WebSocketParameters { /// - /// Parameters for a websocket + /// The uri to connect to /// - public class WebSocketParameters + public Uri Uri { get; set; } + + /// + /// Headers to send in the connection handshake + /// + public IDictionary Headers { get; set; } = new Dictionary(); + + /// + /// Cookies to send in the connection handshake + /// + public IDictionary Cookies { get; set; } = new Dictionary(); + + /// + /// The fixed time to wait between reconnect attempts, only used when `ReconnectPolicy` is set to `ReconnectPolicy.ExponentialBackoff` + /// + public TimeSpan ReconnectInterval { get; set; } = TimeSpan.FromSeconds(5); + + /// + /// Reconnect policy + /// + public ReconnectPolicy ReconnectPolicy { get; set; } = ReconnectPolicy.FixedDelay; + + /// + /// Proxy for the connection + /// + public ApiProxy? Proxy { get; set; } + + /// + /// The maximum time of no data received before considering the connection lost and closing/reconnecting the socket + /// + public TimeSpan? Timeout { get; set; } + + /// + /// Interval at which to send ping frames + /// + public TimeSpan? KeepAliveInterval { get; set; } + + /// + /// Timeout for keep alive response messages + /// + public TimeSpan? KeepAliveTimeout { get; set; } + + /// + /// The rate limiter for the socket connection + /// + public IRateLimitGate? RateLimiter { get; set; } + /// + /// What to do when rate limit is reached + /// + public RateLimitingBehaviour RateLimitingBehavior { get; set; } + + /// + /// Encoding for sending/receiving data + /// + public Encoding Encoding { get; set; } = Encoding.UTF8; + + /// + /// The buffer size to use for receiving data + /// + public int? ReceiveBufferSize { get; set; } + + /// + /// ctor + /// + /// Uri + /// Reconnect policy + public WebSocketParameters(Uri uri, ReconnectPolicy policy) { - /// - /// The uri to connect to - /// - public Uri Uri { get; set; } - - /// - /// Headers to send in the connection handshake - /// - public IDictionary Headers { get; set; } = new Dictionary(); - - /// - /// Cookies to send in the connection handshake - /// - public IDictionary Cookies { get; set; } = new Dictionary(); - - /// - /// The fixed time to wait between reconnect attempts, only used when `ReconnectPolicy` is set to `ReconnectPolicy.ExponentialBackoff` - /// - public TimeSpan ReconnectInterval { get; set; } = TimeSpan.FromSeconds(5); - - /// - /// Reconnect policy - /// - public ReconnectPolicy ReconnectPolicy { get; set; } = ReconnectPolicy.FixedDelay; - - /// - /// Proxy for the connection - /// - public ApiProxy? Proxy { get; set; } - - /// - /// The maximum time of no data received before considering the connection lost and closing/reconnecting the socket - /// - public TimeSpan? Timeout { get; set; } - - /// - /// Interval at which to send ping frames - /// - public TimeSpan? KeepAliveInterval { get; set; } - - /// - /// Timeout for keep alive response messages - /// - public TimeSpan? KeepAliveTimeout { get; set; } - - /// - /// The rate limiter for the socket connection - /// - public IRateLimitGate? RateLimiter { get; set; } - /// - /// What to do when rate limit is reached - /// - public RateLimitingBehaviour RateLimitingBehavior { get; set; } - - /// - /// Encoding for sending/receiving data - /// - public Encoding Encoding { get; set; } = Encoding.UTF8; - - /// - /// The buffer size to use for receiving data - /// - public int? ReceiveBufferSize { get; set; } = null; - - /// - /// ctor - /// - /// Uri - /// Reconnect policy - public WebSocketParameters(Uri uri, ReconnectPolicy policy) - { - Uri = uri; - ReconnectPolicy = policy; - } + Uri = uri; + ReconnectPolicy = policy; } } diff --git a/CryptoExchange.Net/Objects/TimeSyncState.cs b/CryptoExchange.Net/Objects/TimeSyncState.cs index 021a9fb..1484afd 100644 --- a/CryptoExchange.Net/Objects/TimeSyncState.cs +++ b/CryptoExchange.Net/Objects/TimeSyncState.cs @@ -1,95 +1,94 @@ -using System; +using System; using System.Threading; using Microsoft.Extensions.Logging; -namespace CryptoExchange.Net.Objects +namespace CryptoExchange.Net.Objects; + +/// +/// The time synchronization state of an API client +/// +public class TimeSyncState { /// - /// The time synchronization state of an API client + /// Name of the API /// - public class TimeSyncState - { - /// - /// Name of the API - /// - public string ApiName { get; set; } - /// - /// Semaphore to use for checking the time syncing. Should be shared instance among the API client - /// - public SemaphoreSlim Semaphore { get; } - /// - /// Last sync time for the API client - /// - public DateTime LastSyncTime { get; set; } - /// - /// Time offset for the API client - /// - public TimeSpan TimeOffset { get; set; } + public string ApiName { get; set; } + /// + /// Semaphore to use for checking the time syncing. Should be shared instance among the API client + /// + public SemaphoreSlim Semaphore { get; } + /// + /// Last sync time for the API client + /// + public DateTime LastSyncTime { get; set; } + /// + /// Time offset for the API client + /// + public TimeSpan TimeOffset { get; set; } - /// - /// ctor - /// - public TimeSyncState(string apiName) - { - ApiName = apiName; - Semaphore = new SemaphoreSlim(1, 1); - } + /// + /// ctor + /// + public TimeSyncState(string apiName) + { + ApiName = apiName; + Semaphore = new SemaphoreSlim(1, 1); + } +} + +/// +/// Time synchronization info +/// +public class TimeSyncInfo +{ + /// + /// Logger + /// + public ILogger Logger { get; } + /// + /// Should synchronize time + /// + public bool SyncTime { get; } + /// + /// Timestamp recalculation interval + /// + public TimeSpan RecalculationInterval { get; } + /// + /// Time sync state for the API client + /// + public TimeSyncState TimeSyncState { get; } + + /// + /// ctor + /// + /// + /// + /// + /// + public TimeSyncInfo(ILogger logger, bool syncTime, TimeSpan recalculationInterval, TimeSyncState syncState) + { + Logger = logger; + SyncTime = syncTime; + RecalculationInterval = recalculationInterval; + TimeSyncState = syncState; } /// - /// Time synchronization info + /// Set the time offset /// - public class TimeSyncInfo + /// + public void UpdateTimeOffset(TimeSpan offset) { - /// - /// Logger - /// - public ILogger Logger { get; } - /// - /// Should synchronize time - /// - public bool SyncTime { get; } - /// - /// Timestamp recalulcation interval - /// - public TimeSpan RecalculationInterval { get; } - /// - /// Time sync state for the API client - /// - public TimeSyncState TimeSyncState { get; } - - /// - /// ctor - /// - /// - /// - /// - /// - public TimeSyncInfo(ILogger logger, bool syncTime, TimeSpan recalculationInterval, TimeSyncState syncState) + TimeSyncState.LastSyncTime = DateTime.UtcNow; + if (offset.TotalMilliseconds > 0 && offset.TotalMilliseconds < 500) { - Logger = logger; - SyncTime = syncTime; - RecalculationInterval = recalculationInterval; - TimeSyncState = syncState; + Logger.Log(LogLevel.Information, "{TimeSyncState.ApiName} Time offset within limits, set offset to 0ms", TimeSyncState.ApiName); + TimeSyncState.TimeOffset = TimeSpan.Zero; } - - /// - /// Set the time offset - /// - /// - public void UpdateTimeOffset(TimeSpan offset) + else { - TimeSyncState.LastSyncTime = DateTime.UtcNow; - if (offset.TotalMilliseconds > 0 && offset.TotalMilliseconds < 500) - { - Logger.Log(LogLevel.Information, "{TimeSyncState.ApiName} Time offset within limits, set offset to 0ms", TimeSyncState.ApiName); - TimeSyncState.TimeOffset = TimeSpan.Zero; - } - else - { - Logger.Log(LogLevel.Information, "{TimeSyncState.ApiName} Time offset set to {Offset}ms", TimeSyncState.ApiName, Math.Round(offset.TotalMilliseconds)); - TimeSyncState.TimeOffset = offset; - } + Logger.Log(LogLevel.Information, "{TimeSyncState.ApiName} Time offset set to {Offset}ms", TimeSyncState.ApiName, Math.Round(offset.TotalMilliseconds)); + TimeSyncState.TimeOffset = offset; } } } diff --git a/CryptoExchange.Net/Objects/TraceLogger.cs b/CryptoExchange.Net/Objects/TraceLogger.cs index ee96141..2617e97 100644 --- a/CryptoExchange.Net/Objects/TraceLogger.cs +++ b/CryptoExchange.Net/Objects/TraceLogger.cs @@ -1,63 +1,62 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using System; using System.Diagnostics; -namespace CryptoExchange.Net.Objects +namespace CryptoExchange.Net.Objects; + +/// +/// Trace logger provider for creating trace loggers +/// +public class TraceLoggerProvider : ILoggerProvider { + private readonly LogLevel _logLevel; + /// - /// Trace logger provider for creating trace loggers + /// ctor /// - public class TraceLoggerProvider : ILoggerProvider + /// + public TraceLoggerProvider(LogLevel? logLevel = null) { - private readonly LogLevel _logLevel; - - /// - /// ctor - /// - /// - public TraceLoggerProvider(LogLevel? logLevel = null) - { - _logLevel = logLevel ?? LogLevel.Trace; - } - - /// - public ILogger CreateLogger(string categoryName) => new TraceLogger(categoryName, _logLevel); - /// - public void Dispose() { } + _logLevel = logLevel ?? LogLevel.Trace; } + /// + public ILogger CreateLogger(string categoryName) => new TraceLogger(categoryName, _logLevel); + /// + public void Dispose() { } +} + +/// +/// Trace logger +/// +public class TraceLogger : ILogger +{ + private readonly string? _categoryName; + private readonly LogLevel _logLevel; + /// - /// Trace logger + /// ctor /// - public class TraceLogger : ILogger + /// + /// + public TraceLogger(string? categoryName = null, LogLevel level = LogLevel.Trace) { - private readonly string? _categoryName; - private readonly LogLevel _logLevel; + _categoryName = categoryName; + _logLevel = level; + } - /// - /// ctor - /// - /// - /// - public TraceLogger(string? categoryName = null, LogLevel level = LogLevel.Trace) - { - _categoryName = categoryName; - _logLevel = level; - } + /// + public IDisposable? BeginScope(TState state) where TState : notnull => null!; - /// - public IDisposable? BeginScope(TState state) where TState : notnull => null!; + /// + public bool IsEnabled(LogLevel logLevel) => (int)logLevel >= (int)_logLevel; + /// + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + if (!IsEnabled(logLevel)) + return; - /// - public bool IsEnabled(LogLevel logLevel) => (int)logLevel >= (int)_logLevel; - /// - public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) - { - if (!IsEnabled(logLevel)) - return; - - var logMessage = $"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | {logLevel} | {(_categoryName == null ? "" : $"{_categoryName} | ")}{formatter(state, exception)}{(exception == null ? string.Empty : (", " + exception.ToLogString()))}"; - Trace.WriteLine(logMessage); - } + var logMessage = $"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | {logLevel} | {(_categoryName == null ? "" : $"{_categoryName} | ")}{formatter(state, exception)}{(exception == null ? string.Empty : (", " + exception.ToLogString()))}"; + Trace.WriteLine(logMessage); } } diff --git a/CryptoExchange.Net/Objects/TradeEnvironment.cs b/CryptoExchange.Net/Objects/TradeEnvironment.cs index 3afadaa..69ae447 100644 --- a/CryptoExchange.Net/Objects/TradeEnvironment.cs +++ b/CryptoExchange.Net/Objects/TradeEnvironment.cs @@ -1,37 +1,36 @@ -namespace CryptoExchange.Net.Objects +namespace CryptoExchange.Net.Objects; + +/// +/// Trade environment names +/// +public static class TradeEnvironmentNames { /// - /// Trade environment names + /// Live environment /// - public static class TradeEnvironmentNames - { - /// - /// Live environment - /// - public const string Live = "live"; - /// - /// Testnet environment - /// - public const string Testnet = "testnet"; - } + public const string Live = "live"; + /// + /// Testnet environment + /// + public const string Testnet = "testnet"; +} + +/// +/// Trade environment. Contains info about URL's to use to connect to the API. To swap environment select another environment for +/// the exchange's environment list or create a custom environment using either `[Exchange]Environment.CreateCustom()` or `[Exchange]Environment.[Environment]`, for example `KucoinEnvironment.TestNet` or `BinanceEnvironment.Live` +/// +public class TradeEnvironment +{ + /// + /// Name of the environment + /// + public string Name { get; set; } /// - /// Trade environment. Contains info about URL's to use to connect to the API. To swap environment select another environment for - /// the exchange's environment list or create a custom environment using either `[Exchange]Environment.CreateCustom()` or `[Exchange]Environment.[Environment]`, for example `KucoinEnvironment.TestNet` or `BinanceEnvironment.Live` /// - public class TradeEnvironment + /// + protected TradeEnvironment(string name) { - /// - /// Name of the environment - /// - public string Name { get; set; } - - /// - /// - /// - protected TradeEnvironment(string name) - { - Name = name; - } + Name = name; } } diff --git a/CryptoExchange.Net/OrderBook/OrderBookFactory.cs b/CryptoExchange.Net/OrderBook/OrderBookFactory.cs index 0aafaef..7670728 100644 --- a/CryptoExchange.Net/OrderBook/OrderBookFactory.cs +++ b/CryptoExchange.Net/OrderBook/OrderBookFactory.cs @@ -1,34 +1,33 @@ -using CryptoExchange.Net.Interfaces; +using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Objects.Options; using CryptoExchange.Net.SharedApis; using System; -namespace CryptoExchange.Net.OrderBook +namespace CryptoExchange.Net.OrderBook; + +/// +public class OrderBookFactory : IOrderBookFactory where TOptions: OrderBookOptions { - /// - public class OrderBookFactory : IOrderBookFactory where TOptions: OrderBookOptions + private readonly Func?, ISymbolOrderBook> _symbolCtor; + private readonly Func?, ISymbolOrderBook> _assetsCtor; + + /// + /// ctor + /// + /// + /// + public OrderBookFactory(Func?, ISymbolOrderBook> symbolCtor, Func?, ISymbolOrderBook> assetsCtor) { - private readonly Func?, ISymbolOrderBook> _symbolCtor; - private readonly Func?, ISymbolOrderBook> _assetsCtor; - - /// - /// ctor - /// - /// - /// - public OrderBookFactory(Func?, ISymbolOrderBook> symbolCtor, Func?, ISymbolOrderBook> assetsCtor) - { - _symbolCtor = symbolCtor; - _assetsCtor = assetsCtor; - } - - /// - public ISymbolOrderBook Create(string symbol, Action? options = null) => _symbolCtor(symbol, options); - - /// - public ISymbolOrderBook Create(string baseAsset, string quoteAsset, Action? options = null) => _assetsCtor(new SharedSymbol(TradingMode.Spot, baseAsset, quoteAsset), options); - - /// - public ISymbolOrderBook Create(SharedSymbol symbol, Action? options = null) => _assetsCtor(symbol, options); + _symbolCtor = symbolCtor; + _assetsCtor = assetsCtor; } + + /// + public ISymbolOrderBook Create(string symbol, Action? options = null) => _symbolCtor(symbol, options); + + /// + public ISymbolOrderBook Create(string baseAsset, string quoteAsset, Action? options = null) => _assetsCtor(new SharedSymbol(TradingMode.Spot, baseAsset, quoteAsset), options); + + /// + public ISymbolOrderBook Create(SharedSymbol symbol, Action? options = null) => _assetsCtor(symbol, options); } diff --git a/CryptoExchange.Net/OrderBook/ProcessBufferEntry.cs b/CryptoExchange.Net/OrderBook/ProcessBufferEntry.cs index 5a71974..431a290 100644 --- a/CryptoExchange.Net/OrderBook/ProcessBufferEntry.cs +++ b/CryptoExchange.Net/OrderBook/ProcessBufferEntry.cs @@ -1,32 +1,30 @@ -using CryptoExchange.Net.Interfaces; +using CryptoExchange.Net.Interfaces; using System; -using System.Collections.Generic; -namespace CryptoExchange.Net.OrderBook +namespace CryptoExchange.Net.OrderBook; + +/// +/// Buffer entry with a first and last update id +/// +public class ProcessBufferRangeSequenceEntry { /// - /// Buffer entry with a first and last update id + /// First sequence number in this update /// - public class ProcessBufferRangeSequenceEntry - { - /// - /// First sequence number in this update - /// - public long FirstUpdateId { get; set; } + public long FirstUpdateId { get; set; } - /// - /// Last sequence number in this update - /// - public long LastUpdateId { get; set; } + /// + /// Last sequence number in this update + /// + public long LastUpdateId { get; set; } - /// - /// List of changed/new asks - /// - public ISymbolOrderBookEntry[] Asks { get; set; } = Array.Empty(); + /// + /// List of changed/new asks + /// + public ISymbolOrderBookEntry[] Asks { get; set; } = Array.Empty(); - /// - /// List of changed/new bids - /// - public ISymbolOrderBookEntry[] Bids { get; set; } = Array.Empty(); - } + /// + /// List of changed/new bids + /// + public ISymbolOrderBookEntry[] Bids { get; set; } = Array.Empty(); } diff --git a/CryptoExchange.Net/OrderBook/ProcessQueueItem.cs b/CryptoExchange.Net/OrderBook/ProcessQueueItem.cs index f23110c..0f67ed9 100644 --- a/CryptoExchange.Net/OrderBook/ProcessQueueItem.cs +++ b/CryptoExchange.Net/OrderBook/ProcessQueueItem.cs @@ -1,27 +1,25 @@ -using CryptoExchange.Net.Interfaces; +using CryptoExchange.Net.Interfaces; using System; -using System.Collections.Generic; -namespace CryptoExchange.Net.OrderBook +namespace CryptoExchange.Net.OrderBook; + +internal class ProcessQueueItem { - internal class ProcessQueueItem - { - public long StartUpdateId { get; set; } - public long EndUpdateId { get; set; } - public ISymbolOrderBookEntry[] Bids { get; set; } = Array.Empty(); - public ISymbolOrderBookEntry[] Asks { get; set; } = Array.Empty(); - } - - internal class InitialOrderBookItem - { - public long StartUpdateId { get; set; } - public long EndUpdateId { get; set; } - public ISymbolOrderBookEntry[] Bids { get; set; } = Array.Empty(); - public ISymbolOrderBookEntry[] Asks { get; set; } = Array.Empty(); - } - - internal class ChecksumItem - { - public int Checksum { get; set; } - } + public long StartUpdateId { get; set; } + public long EndUpdateId { get; set; } + public ISymbolOrderBookEntry[] Bids { get; set; } = Array.Empty(); + public ISymbolOrderBookEntry[] Asks { get; set; } = Array.Empty(); +} + +internal class InitialOrderBookItem +{ + public long StartUpdateId { get; set; } + public long EndUpdateId { get; set; } + public ISymbolOrderBookEntry[] Bids { get; set; } = Array.Empty(); + public ISymbolOrderBookEntry[] Asks { get; set; } = Array.Empty(); +} + +internal class ChecksumItem +{ + public int Checksum { get; set; } } diff --git a/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs b/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs index cedb443..c70882a 100644 --- a/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs +++ b/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; @@ -15,840 +15,839 @@ using CryptoExchange.Net.Objects.Sockets; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -namespace CryptoExchange.Net.OrderBook +namespace CryptoExchange.Net.OrderBook; + +/// +/// Base for order book implementations +/// +public abstract class SymbolOrderBook : ISymbolOrderBook, IDisposable { - /// - /// Base for order book implementations - /// - public abstract class SymbolOrderBook : ISymbolOrderBook, IDisposable + private readonly object _bookLock = new object(); + + private OrderBookStatus _status; + private UpdateSubscription? _subscription; + + private bool _stopProcessing; + private Task? _processTask; + private CancellationTokenSource? _cts; + + private readonly AsyncResetEvent _queueEvent; + private readonly ConcurrentQueue _processQueue; + private bool _validateChecksum; + + private class EmptySymbolOrderBookEntry : ISymbolOrderBookEntry { - private readonly object _bookLock = new object(); + public decimal Quantity { get => 0m; + set { } } + public decimal Price { get => 0m; + set { } } + } - private OrderBookStatus _status; - private UpdateSubscription? _subscription; - - private bool _stopProcessing; - private Task? _processTask; - private CancellationTokenSource? _cts; + private static readonly ISymbolOrderBookEntry _emptySymbolOrderBookEntry = new EmptySymbolOrderBookEntry(); - private readonly AsyncResetEvent _queueEvent; - private readonly ConcurrentQueue _processQueue; - private bool _validateChecksum; + /// + /// A buffer to store messages received before the initial book snapshot is processed. These messages + /// will be processed after the book snapshot is set. Any messages in this buffer with sequence numbers lower + /// than the snapshot sequence number will be discarded + /// + protected readonly List _processBuffer; - private class EmptySymbolOrderBookEntry : ISymbolOrderBookEntry + /// + /// The ask list, should only be accessed using the bookLock + /// + protected SortedList _asks; + + /// + /// The bid list, should only be accessed using the bookLock + /// + protected SortedList _bids; + + /// + /// The log + /// + protected ILogger _logger; + + /// + /// Whether update numbers are consecutive. If set to true and an update comes in which isn't the previous sequences number + 1 + /// the book will resynchronize as it is deemed out of sync + /// + protected bool _sequencesAreConsecutive; + + /// + /// Whether levels should be strictly enforced. For example, when an order book has 25 levels and a new update comes in which pushes + /// the current level 25 ask out of the top 25, should the level 26 entry be removed from the book or does the server handle this + /// + protected bool _strictLevels; + + /// + /// If the initial snapshot of the book has been set + /// + protected bool _bookSet; + + /// + /// The amount of levels for this book + /// + protected int? Levels { get; set; } + + /// + public string Exchange { get; } + + /// + public string Api { get; } + + /// + public OrderBookStatus Status + { + get => _status; + set { - public decimal Quantity { get => 0m; - set { } } - public decimal Price { get => 0m; - set { } } + if (value == _status) + return; + + var old = _status; + _status = value; + _logger.OrderBookStatusChanged(Api, Symbol, old, value); + OnStatusChange?.Invoke(old, _status); } + } - private static readonly ISymbolOrderBookEntry _emptySymbolOrderBookEntry = new EmptySymbolOrderBookEntry(); + /// + public long LastSequenceNumber { get; private set; } - /// - /// A buffer to store messages received before the initial book snapshot is processed. These messages - /// will be processed after the book snapshot is set. Any messages in this buffer with sequence numbers lower - /// than the snapshot sequence number will be discarded - /// - protected readonly List _processBuffer; + /// + public string Symbol { get; } - /// - /// The ask list, should only be accessed using the bookLock - /// - protected SortedList _asks; + /// + public event Action? OnStatusChange; - /// - /// The bid list, should only be accessed using the bookLock - /// - protected SortedList _bids; + /// + public event Action<(ISymbolOrderBookEntry BestBid, ISymbolOrderBookEntry BestAsk)>? OnBestOffersChanged; - /// - /// The log - /// - protected ILogger _logger; + /// + public event Action<(ISymbolOrderBookEntry[] Bids, ISymbolOrderBookEntry[] Asks)>? OnOrderBookUpdate; - /// - /// Whether update numbers are consecutive. If set to true and an update comes in which isn't the previous sequences number + 1 - /// the book will resynchronize as it is deemed out of sync - /// - protected bool _sequencesAreConsecutive; - - /// - /// Whether levels should be strictly enforced. For example, when an order book has 25 levels and a new update comes in which pushes - /// the current level 25 ask out of the top 25, should the level 26 entry be removed from the book or does the server handle this - /// - protected bool _strictLevels; + /// + public DateTime UpdateTime { get; private set; } - /// - /// If the initial snapshot of the book has been set - /// - protected bool _bookSet; + /// + public int AskCount { get; private set; } - /// - /// The amount of levels for this book - /// - protected int? Levels { get; set; } = null; + /// + public int BidCount { get; private set; } - /// - public string Exchange { get; } - - /// - public string Api { get; } - - /// - public OrderBookStatus Status + /// + public ISymbolOrderBookEntry[] Asks + { + get { - get => _status; - set - { - if (value == _status) - return; - - var old = _status; - _status = value; - _logger.OrderBookStatusChanged(Api, Symbol, old, value); - OnStatusChange?.Invoke(old, _status); - } - } - - /// - public long LastSequenceNumber { get; private set; } - - /// - public string Symbol { get; } - - /// - public event Action? OnStatusChange; - - /// - public event Action<(ISymbolOrderBookEntry BestBid, ISymbolOrderBookEntry BestAsk)>? OnBestOffersChanged; - - /// - public event Action<(ISymbolOrderBookEntry[] Bids, ISymbolOrderBookEntry[] Asks)>? OnOrderBookUpdate; - - /// - public DateTime UpdateTime { get; private set; } - - /// - public int AskCount { get; private set; } - - /// - public int BidCount { get; private set; } - - /// - public ISymbolOrderBookEntry[] Asks - { - get - { - lock (_bookLock) - return _asks.Select(a => a.Value).ToArray(); - } - } - - /// - public ISymbolOrderBookEntry[] Bids - { - get - { - lock (_bookLock) - return _bids.Select(a => a.Value).ToArray(); - } - } - - /// - public (ISymbolOrderBookEntry[] bids, ISymbolOrderBookEntry[] asks) Book - { - get - { - lock (_bookLock) - return (Bids, Asks); - } - } - - /// - public ISymbolOrderBookEntry BestBid - { - get - { - lock (_bookLock) - return _bids.FirstOrDefault().Value ?? _emptySymbolOrderBookEntry; - } - } - - /// - public ISymbolOrderBookEntry BestAsk - { - get - { - lock (_bookLock) - return _asks.FirstOrDefault().Value ?? _emptySymbolOrderBookEntry; - } - } - - /// - public (ISymbolOrderBookEntry Bid, ISymbolOrderBookEntry Ask) BestOffers { - get { - lock (_bookLock) - return (BestBid,BestAsk); - } - } - - /// - /// ctor - /// - /// Logger to use. If not provided will create a TraceLogger - /// The exchange of the order book - /// The API the book is for, for example Spot - /// The symbol the order book is for - protected SymbolOrderBook(ILoggerFactory? logger, string exchange, string api, string symbol) - { - if (symbol == null) - throw new ArgumentNullException(nameof(symbol)); - - Exchange = exchange; - Api = api; - - _processBuffer = new List(); - _processQueue = new ConcurrentQueue(); - _queueEvent = new AsyncResetEvent(false, true); - - Symbol = symbol; - Status = OrderBookStatus.Disconnected; - - _asks = new SortedList(); - _bids = new SortedList(new DescComparer()); - - _logger = logger?.CreateLogger(Exchange) ?? NullLoggerFactory.Instance.CreateLogger(Exchange); - } - - /// - /// Initialize the order book using the provided options - /// - /// The options - /// - protected void Initialize(OrderBookOptions options) - { - if (options == null) - throw new ArgumentNullException(nameof(options)); - - _validateChecksum = options.ChecksumValidationEnabled; - } - - /// - public async Task> StartAsync(CancellationToken? ct = null) - { - if (Status != OrderBookStatus.Disconnected) - throw new InvalidOperationException($"Can't start book unless state is {OrderBookStatus.Disconnected}. Current state: {Status}"); - - _logger.OrderBookStarting(Api, Symbol); - _cts = new CancellationTokenSource(); - ct?.Register(async () => - { - _cts.Cancel(); - await StopAsync().ConfigureAwait(false); - }, false); - - // Clear any previous messages - while (_processQueue.TryDequeue(out _)) { } - - _processBuffer.Clear(); - _bookSet = false; - - Status = OrderBookStatus.Connecting; - _processTask = Task.Factory.StartNew(ProcessQueue, TaskCreationOptions.LongRunning); - - var startResult = await DoStartAsync(_cts.Token).ConfigureAwait(false); - if (!startResult) - { - Status = OrderBookStatus.Disconnected; - return new CallResult(startResult.Error!); - } - - if (_cts.IsCancellationRequested) - { - _logger.OrderBookStoppedStarting(Api, Symbol); - await startResult.Data.CloseAsync().ConfigureAwait(false); - Status = OrderBookStatus.Disconnected; - return new CallResult(new CancellationRequestedError()); - } - - _subscription = startResult.Data; - _subscription.ConnectionLost += HandleConnectionLost; - _subscription.ConnectionClosed += HandleConnectionClosed; - _subscription.ConnectionRestored += HandleConnectionRestored; - - Status = OrderBookStatus.Synced; - return new CallResult(true); - } - - private void HandleConnectionLost() - { - _logger.OrderBookConnectionLost(Api, Symbol); - if (Status != OrderBookStatus.Disposed) { - Status = OrderBookStatus.Reconnecting; - Reset(); - } - } - - private void HandleConnectionClosed() { - _logger.OrderBookDisconnected(Api, Symbol); - Status = OrderBookStatus.Disconnected; - _ = StopAsync(); - } - - private async void HandleConnectionRestored(TimeSpan _) { - await ResyncAsync().ConfigureAwait(false); - } - - /// - public async Task StopAsync() - { - _logger.OrderBookStopping(Api, Symbol); - Status = OrderBookStatus.Disconnected; - _cts?.Cancel(); - _queueEvent.Set(); - if (_processTask != null) - await _processTask.ConfigureAwait(false); - - if (_subscription != null) { - await _subscription.CloseAsync().ConfigureAwait(false); - _subscription.ConnectionLost -= HandleConnectionLost; - _subscription.ConnectionClosed -= HandleConnectionClosed; - _subscription.ConnectionRestored -= HandleConnectionRestored; - } - - _logger.OrderBookStopped(Api, Symbol); - } - - /// - public CallResult CalculateAverageFillPrice(decimal baseQuantity, OrderBookEntryType type) - { - if (Status != OrderBookStatus.Synced) - return new CallResult(new InvalidOperationError($"{nameof(CalculateAverageFillPrice)} is not available when book is not in Synced state")); - - var totalCost = 0m; - var totalAmount = 0m; - var amountLeft = baseQuantity; lock (_bookLock) - { - var list = type == OrderBookEntryType.Ask ? _asks : _bids; - - var step = 0; - while (amountLeft > 0) - { - if (step == list.Count) - return new CallResult(new InvalidOperationError("Quantity is larger than order in the order book")); - - var element = list.ElementAt(step); - var stepAmount = Math.Min(element.Value.Quantity, amountLeft); - totalCost += stepAmount * element.Value.Price; - totalAmount += stepAmount; - amountLeft -= stepAmount; - step++; - } - } - - return new CallResult(Math.Round(totalCost / totalAmount, 8)); + return _asks.Select(a => a.Value).ToArray(); } + } - /// - public CallResult CalculateTradableAmount(decimal quoteQuantity, OrderBookEntryType type) + /// + public ISymbolOrderBookEntry[] Bids + { + get { - if (Status != OrderBookStatus.Synced) - return new CallResult(new InvalidOperationError($"{nameof(CalculateTradableAmount)} is not available when book is not in Synced state")); - - var quoteQuantityLeft = quoteQuantity; - var totalBaseQuantity = 0m; lock (_bookLock) + return _bids.Select(a => a.Value).ToArray(); + } + } + + /// + public (ISymbolOrderBookEntry[] bids, ISymbolOrderBookEntry[] asks) Book + { + get + { + lock (_bookLock) + return (Bids, Asks); + } + } + + /// + public ISymbolOrderBookEntry BestBid + { + get + { + lock (_bookLock) + return _bids.FirstOrDefault().Value ?? _emptySymbolOrderBookEntry; + } + } + + /// + public ISymbolOrderBookEntry BestAsk + { + get + { + lock (_bookLock) + return _asks.FirstOrDefault().Value ?? _emptySymbolOrderBookEntry; + } + } + + /// + public (ISymbolOrderBookEntry Bid, ISymbolOrderBookEntry Ask) BestOffers { + get { + lock (_bookLock) + return (BestBid,BestAsk); + } + } + + /// + /// ctor + /// + /// Logger to use. If not provided will create a TraceLogger + /// The exchange of the order book + /// The API the book is for, for example Spot + /// The symbol the order book is for + protected SymbolOrderBook(ILoggerFactory? logger, string exchange, string api, string symbol) + { + if (symbol == null) + throw new ArgumentNullException(nameof(symbol)); + + Exchange = exchange; + Api = api; + + _processBuffer = new List(); + _processQueue = new ConcurrentQueue(); + _queueEvent = new AsyncResetEvent(false, true); + + Symbol = symbol; + Status = OrderBookStatus.Disconnected; + + _asks = new SortedList(); + _bids = new SortedList(new DescComparer()); + + _logger = logger?.CreateLogger(Exchange) ?? NullLoggerFactory.Instance.CreateLogger(Exchange); + } + + /// + /// Initialize the order book using the provided options + /// + /// The options + /// + protected void Initialize(OrderBookOptions options) + { + if (options == null) + throw new ArgumentNullException(nameof(options)); + + _validateChecksum = options.ChecksumValidationEnabled; + } + + /// + public async Task> StartAsync(CancellationToken? ct = null) + { + if (Status != OrderBookStatus.Disconnected) + throw new InvalidOperationException($"Can't start book unless state is {OrderBookStatus.Disconnected}. Current state: {Status}"); + + _logger.OrderBookStarting(Api, Symbol); + _cts = new CancellationTokenSource(); + ct?.Register(async () => + { + _cts.Cancel(); + await StopAsync().ConfigureAwait(false); + }, false); + + // Clear any previous messages + while (_processQueue.TryDequeue(out _)) { } + + _processBuffer.Clear(); + _bookSet = false; + + Status = OrderBookStatus.Connecting; + _processTask = Task.Factory.StartNew(ProcessQueue, TaskCreationOptions.LongRunning); + + var startResult = await DoStartAsync(_cts.Token).ConfigureAwait(false); + if (!startResult) + { + Status = OrderBookStatus.Disconnected; + return new CallResult(startResult.Error!); + } + + if (_cts.IsCancellationRequested) + { + _logger.OrderBookStoppedStarting(Api, Symbol); + await startResult.Data.CloseAsync().ConfigureAwait(false); + Status = OrderBookStatus.Disconnected; + return new CallResult(new CancellationRequestedError()); + } + + _subscription = startResult.Data; + _subscription.ConnectionLost += HandleConnectionLost; + _subscription.ConnectionClosed += HandleConnectionClosed; + _subscription.ConnectionRestored += HandleConnectionRestored; + + Status = OrderBookStatus.Synced; + return new CallResult(true); + } + + private void HandleConnectionLost() + { + _logger.OrderBookConnectionLost(Api, Symbol); + if (Status != OrderBookStatus.Disposed) { + Status = OrderBookStatus.Reconnecting; + Reset(); + } + } + + private void HandleConnectionClosed() { + _logger.OrderBookDisconnected(Api, Symbol); + Status = OrderBookStatus.Disconnected; + _ = StopAsync(); + } + + private async void HandleConnectionRestored(TimeSpan _) { + await ResyncAsync().ConfigureAwait(false); + } + + /// + public async Task StopAsync() + { + _logger.OrderBookStopping(Api, Symbol); + Status = OrderBookStatus.Disconnected; + _cts?.Cancel(); + _queueEvent.Set(); + if (_processTask != null) + await _processTask.ConfigureAwait(false); + + if (_subscription != null) { + await _subscription.CloseAsync().ConfigureAwait(false); + _subscription.ConnectionLost -= HandleConnectionLost; + _subscription.ConnectionClosed -= HandleConnectionClosed; + _subscription.ConnectionRestored -= HandleConnectionRestored; + } + + _logger.OrderBookStopped(Api, Symbol); + } + + /// + public CallResult CalculateAverageFillPrice(decimal quantity, OrderBookEntryType type) + { + if (Status != OrderBookStatus.Synced) + return new CallResult(new InvalidOperationError($"{nameof(CalculateAverageFillPrice)} is not available when book is not in Synced state")); + + var totalCost = 0m; + var totalAmount = 0m; + var amountLeft = quantity; + lock (_bookLock) + { + var list = type == OrderBookEntryType.Ask ? _asks : _bids; + + var step = 0; + while (amountLeft > 0) { - var list = type == OrderBookEntryType.Ask ? _asks : _bids; + if (step == list.Count) + return new CallResult(new InvalidOperationError("Quantity is larger than order in the order book")); - var step = 0; - while (quoteQuantityLeft > 0) - { - if (step == list.Count) - return new CallResult(new InvalidOperationError("Quantity is larger than order in the order book")); - - var element = list.ElementAt(step); - var stepAmount = Math.Min(element.Value.Quantity * element.Value.Price, quoteQuantityLeft); - quoteQuantityLeft -= stepAmount; - totalBaseQuantity += stepAmount / element.Value.Price; - step++; - } - } - - return new CallResult(Math.Round(totalBaseQuantity, 8)); - } - - /// - /// Implementation for starting the order book. Should typically have logic for subscribing to the update stream and retrieving - /// and setting the initial order book - /// - /// - protected abstract Task> DoStartAsync(CancellationToken ct); - - /// - /// Reset the order book - /// - protected virtual void DoReset() { } - - /// - /// Resync the order book - /// - /// - protected abstract Task> DoResyncAsync(CancellationToken ct); - - /// - /// Implementation for validating a checksum value with the current order book. If checksum validation fails (returns false) - /// the order book will be resynchronized - /// - /// - /// - protected virtual bool DoChecksum(int checksum) => true; - - /// - /// Set the initial data for the order book. Typically the snapshot which was requested from the Rest API, or the first snapshot - /// received from a socket subscription - /// - /// The last update sequence number until which the snapshot is in sync - /// List of asks - /// List of bids - protected void SetInitialOrderBook(long orderBookSequenceNumber, ISymbolOrderBookEntry[] bidList, ISymbolOrderBookEntry[] askList) - { - _processQueue.Enqueue(new InitialOrderBookItem { StartUpdateId = orderBookSequenceNumber, EndUpdateId = orderBookSequenceNumber, Asks = askList, Bids = bidList }); - _queueEvent.Set(); - } - - /// - /// Add an update to the process queue. Updates the book by providing changed bids and asks, along with an update number which should be higher than the previous update numbers - /// - /// The sequence number - /// List of updated/new bids - /// List of updated/new asks - protected void UpdateOrderBook(long updateId, ISymbolOrderBookEntry[] bids, ISymbolOrderBookEntry[] asks) - { - _processQueue.Enqueue(new ProcessQueueItem { StartUpdateId = updateId, EndUpdateId = updateId, Asks = asks, Bids = bids }); - _queueEvent.Set(); - } - - /// - /// Add an update to the process queue. Updates the book by providing changed bids and asks, along with the first and last sequence number in the update - /// - /// The sequence number of the first update - /// The sequence number of the last update - /// List of updated/new bids - /// List of updated/new asks - protected void UpdateOrderBook(long firstUpdateId, long lastUpdateId, ISymbolOrderBookEntry[] bids, ISymbolOrderBookEntry[] asks) - { - _processQueue.Enqueue(new ProcessQueueItem { StartUpdateId = firstUpdateId, EndUpdateId = lastUpdateId, Asks = asks, Bids = bids }); - _queueEvent.Set(); - } - - /// - /// Add an update to the process queue. Updates the book by providing changed bids and asks, each with its own sequence number - /// - /// List of updated/new bids - /// List of updated/new asks - protected void UpdateOrderBook(ISymbolOrderSequencedBookEntry[] bids, ISymbolOrderSequencedBookEntry[] asks) - { - var highest = Math.Max(bids.Any() ? bids.Max(b => b.Sequence) : 0, asks.Any() ? asks.Max(a => a.Sequence) : 0); - var lowest = Math.Min(bids.Any() ? bids.Min(b => b.Sequence) : long.MaxValue, asks.Any() ? asks.Min(a => a.Sequence) : long.MaxValue); - - _processQueue.Enqueue(new ProcessQueueItem { StartUpdateId = lowest, EndUpdateId = highest, Asks = asks, Bids = bids }); - _queueEvent.Set(); - } - - /// - /// Add a checksum value to the process queue - /// - /// The checksum value - protected void AddChecksum(int checksum) - { - _processQueue.Enqueue(new ChecksumItem() { Checksum = checksum }); - _queueEvent.Set(); - } - - /// - /// Check and empty the process buffer; see what entries to update the book with - /// - protected void CheckProcessBuffer() - { - var pbList = _processBuffer.ToList(); - if (pbList.Count > 0) - _logger.OrderBookProcessingBufferedUpdates(Api, Symbol, pbList.Count); - - foreach (var bufferEntry in pbList) - { - ProcessRangeUpdates(bufferEntry.FirstUpdateId, bufferEntry.LastUpdateId, bufferEntry.Bids, bufferEntry.Asks); - _processBuffer.Remove(bufferEntry); + var element = list.ElementAt(step); + var stepAmount = Math.Min(element.Value.Quantity, amountLeft); + totalCost += stepAmount * element.Value.Price; + totalAmount += stepAmount; + amountLeft -= stepAmount; + step++; } } - /// - /// Update order book with an entry - /// - /// Sequence number of the update - /// Type of entry - /// The entry - protected virtual bool ProcessUpdate(long sequence, OrderBookEntryType type, ISymbolOrderBookEntry entry) + return new CallResult(Math.Round(totalCost / totalAmount, 8)); + } + + /// + public CallResult CalculateTradableAmount(decimal quoteQuantity, OrderBookEntryType type) + { + if (Status != OrderBookStatus.Synced) + return new CallResult(new InvalidOperationError($"{nameof(CalculateTradableAmount)} is not available when book is not in Synced state")); + + var quoteQuantityLeft = quoteQuantity; + var totalBaseQuantity = 0m; + lock (_bookLock) { - if (sequence <= LastSequenceNumber) + var list = type == OrderBookEntryType.Ask ? _asks : _bids; + + var step = 0; + while (quoteQuantityLeft > 0) { - _logger.OrderBookSkippedMessage(Api, Symbol, sequence, LastSequenceNumber); - return false; + if (step == list.Count) + return new CallResult(new InvalidOperationError("Quantity is larger than order in the order book")); + + var element = list.ElementAt(step); + var stepAmount = Math.Min(element.Value.Quantity * element.Value.Price, quoteQuantityLeft); + quoteQuantityLeft -= stepAmount; + totalBaseQuantity += stepAmount / element.Value.Price; + step++; } + } - if (_sequencesAreConsecutive && sequence > LastSequenceNumber + 1) + return new CallResult(Math.Round(totalBaseQuantity, 8)); + } + + /// + /// Implementation for starting the order book. Should typically have logic for subscribing to the update stream and retrieving + /// and setting the initial order book + /// + /// + protected abstract Task> DoStartAsync(CancellationToken ct); + + /// + /// Reset the order book + /// + protected virtual void DoReset() { } + + /// + /// Resync the order book + /// + /// + protected abstract Task> DoResyncAsync(CancellationToken ct); + + /// + /// Implementation for validating a checksum value with the current order book. If checksum validation fails (returns false) + /// the order book will be resynchronized + /// + /// + /// + protected virtual bool DoChecksum(int checksum) => true; + + /// + /// Set the initial data for the order book. Typically the snapshot which was requested from the Rest API, or the first snapshot + /// received from a socket subscription + /// + /// The last update sequence number until which the snapshot is in sync + /// List of asks + /// List of bids + protected void SetInitialOrderBook(long orderBookSequenceNumber, ISymbolOrderBookEntry[] bidList, ISymbolOrderBookEntry[] askList) + { + _processQueue.Enqueue(new InitialOrderBookItem { StartUpdateId = orderBookSequenceNumber, EndUpdateId = orderBookSequenceNumber, Asks = askList, Bids = bidList }); + _queueEvent.Set(); + } + + /// + /// Add an update to the process queue. Updates the book by providing changed bids and asks, along with an update number which should be higher than the previous update numbers + /// + /// The sequence number + /// List of updated/new bids + /// List of updated/new asks + protected void UpdateOrderBook(long updateId, ISymbolOrderBookEntry[] bids, ISymbolOrderBookEntry[] asks) + { + _processQueue.Enqueue(new ProcessQueueItem { StartUpdateId = updateId, EndUpdateId = updateId, Asks = asks, Bids = bids }); + _queueEvent.Set(); + } + + /// + /// Add an update to the process queue. Updates the book by providing changed bids and asks, along with the first and last sequence number in the update + /// + /// The sequence number of the first update + /// The sequence number of the last update + /// List of updated/new bids + /// List of updated/new asks + protected void UpdateOrderBook(long firstUpdateId, long lastUpdateId, ISymbolOrderBookEntry[] bids, ISymbolOrderBookEntry[] asks) + { + _processQueue.Enqueue(new ProcessQueueItem { StartUpdateId = firstUpdateId, EndUpdateId = lastUpdateId, Asks = asks, Bids = bids }); + _queueEvent.Set(); + } + + /// + /// Add an update to the process queue. Updates the book by providing changed bids and asks, each with its own sequence number + /// + /// List of updated/new bids + /// List of updated/new asks + protected void UpdateOrderBook(ISymbolOrderSequencedBookEntry[] bids, ISymbolOrderSequencedBookEntry[] asks) + { + var highest = Math.Max(bids.Length != 0 ? bids.Max(b => b.Sequence) : 0, asks.Length != 0 ? asks.Max(a => a.Sequence) : 0); + var lowest = Math.Min(bids.Length != 0 ? bids.Min(b => b.Sequence) : long.MaxValue, asks.Length != 0 ? asks.Min(a => a.Sequence) : long.MaxValue); + + _processQueue.Enqueue(new ProcessQueueItem { StartUpdateId = lowest, EndUpdateId = highest, Asks = asks, Bids = bids }); + _queueEvent.Set(); + } + + /// + /// Add a checksum value to the process queue + /// + /// The checksum value + protected void AddChecksum(int checksum) + { + _processQueue.Enqueue(new ChecksumItem() { Checksum = checksum }); + _queueEvent.Set(); + } + + /// + /// Check and empty the process buffer; see what entries to update the book with + /// + protected void CheckProcessBuffer() + { + var pbList = _processBuffer.ToList(); + if (pbList.Count > 0) + _logger.OrderBookProcessingBufferedUpdates(Api, Symbol, pbList.Count); + + foreach (var bufferEntry in pbList) + { + ProcessRangeUpdates(bufferEntry.FirstUpdateId, bufferEntry.LastUpdateId, bufferEntry.Bids, bufferEntry.Asks); + _processBuffer.Remove(bufferEntry); + } + } + + /// + /// Update order book with an entry + /// + /// Sequence number of the update + /// Type of entry + /// The entry + protected virtual bool ProcessUpdate(long sequence, OrderBookEntryType type, ISymbolOrderBookEntry entry) + { + if (sequence <= LastSequenceNumber) + { + _logger.OrderBookSkippedMessage(Api, Symbol, sequence, LastSequenceNumber); + return false; + } + + if (_sequencesAreConsecutive && sequence > LastSequenceNumber + 1) + { + // Out of sync + _logger.OrderBookOutOfSync(Api, Symbol, LastSequenceNumber + 1, sequence); + _stopProcessing = true; + Resubscribe(); + return false; + } + + UpdateTime = DateTime.UtcNow; + var listToChange = type == OrderBookEntryType.Ask ? _asks : _bids; + if (entry.Quantity == 0) + { + if (!listToChange.ContainsKey(entry.Price)) + return true; + + listToChange.Remove(entry.Price); + if (type == OrderBookEntryType.Ask) AskCount--; + else BidCount--; + } + else + { + if (!listToChange.ContainsKey(entry.Price)) { - // Out of sync - _logger.OrderBookOutOfSync(Api, Symbol, LastSequenceNumber + 1, sequence); - _stopProcessing = true; - Resubscribe(); - return false; - } - - UpdateTime = DateTime.UtcNow; - var listToChange = type == OrderBookEntryType.Ask ? _asks : _bids; - if (entry.Quantity == 0) - { - if (!listToChange.ContainsKey(entry.Price)) - return true; - - listToChange.Remove(entry.Price); - if (type == OrderBookEntryType.Ask) AskCount--; - else BidCount--; + listToChange.Add(entry.Price, entry); + if (type == OrderBookEntryType.Ask) AskCount++; + else BidCount++; } else { - if (!listToChange.ContainsKey(entry.Price)) - { - listToChange.Add(entry.Price, entry); - if (type == OrderBookEntryType.Ask) AskCount++; - else BidCount++; - } - else - { - listToChange[entry.Price] = entry; - } + listToChange[entry.Price] = entry; } - - return true; } - /// - /// Wait until the order book snapshot has been set - /// - /// Max wait time - /// Cancellation token - /// - protected async Task> WaitForSetOrderBookAsync(TimeSpan timeout, CancellationToken ct) + return true; + } + + /// + /// Wait until the order book snapshot has been set + /// + /// Max wait time + /// Cancellation token + /// + protected async Task> WaitForSetOrderBookAsync(TimeSpan timeout, CancellationToken ct) + { + var startWait = DateTime.UtcNow; + while (!_bookSet && Status == OrderBookStatus.Syncing) { - var startWait = DateTime.UtcNow; - while (!_bookSet && Status == OrderBookStatus.Syncing) + if(ct.IsCancellationRequested) + return new CallResult(new CancellationRequestedError()); + + if (DateTime.UtcNow - startWait > timeout) + return new CallResult(new ServerError(new ErrorInfo(ErrorType.OrderBookTimeout, "Timeout while waiting for data"))); + + try { - if(ct.IsCancellationRequested) - return new CallResult(new CancellationRequestedError()); - - if (DateTime.UtcNow - startWait > timeout) - return new CallResult(new ServerError(new ErrorInfo(ErrorType.OrderBookTimeout, "Timeout while waiting for data"))); - - try - { - await Task.Delay(50, ct).ConfigureAwait(false); - } - catch (OperationCanceledException) - { } + await Task.Delay(50, ct).ConfigureAwait(false); } - - return new CallResult(true); + catch (OperationCanceledException) + { } } - /// - /// IDisposable implementation for the order book - /// - public void Dispose() + return new CallResult(true); + } + + /// + /// IDisposable implementation for the order book + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Dispose method + /// + /// + protected virtual void Dispose(bool disposing) + { + Status = OrderBookStatus.Disposing; + + _cts?.Cancel(); + _queueEvent.Set(); + + // Clear queue + while (_processQueue.TryDequeue(out _)) { } + + _processBuffer.Clear(); + _asks.Clear(); + _bids.Clear(); + AskCount = 0; + BidCount = 0; + + Status = OrderBookStatus.Disposed; + } + + /// + /// String representation of the top 3 entries + /// + /// + public override string ToString() + { + return ToString(3); + } + + /// + /// String representation of the top x entries + /// + /// + public string ToString(int rows) + { + var stringBuilder = new StringBuilder(); + var book = Book; + stringBuilder.AppendLine($" Ask quantity Ask price | Bid price Bid quantity"); + for(var i = 0; i < rows; i++) { - Dispose(true); - GC.SuppressFinalize(this); + var ask = book.asks.Length > i ? book.asks[i]: null; + var bid = book.bids.Length > i ? book.bids[i]: null; + stringBuilder.AppendLine($"[{ask?.Quantity.ToString(CultureInfo.InvariantCulture),14}] {ask?.Price.ToString(CultureInfo.InvariantCulture),14} | {bid?.Price.ToString(CultureInfo.InvariantCulture),-14} [{bid?.Quantity.ToString(CultureInfo.InvariantCulture),-14}]"); } - /// - /// Dispose method - /// - /// - protected virtual void Dispose(bool disposing) + return stringBuilder.ToString(); + } + + private void CheckBestOffersChanged(ISymbolOrderBookEntry prevBestBid, ISymbolOrderBookEntry prevBestAsk) + { + var (bestBid, bestAsk) = BestOffers; + if (bestBid.Price != prevBestBid.Price || bestBid.Quantity != prevBestBid.Quantity || + bestAsk.Price != prevBestAsk.Price || bestAsk.Quantity != prevBestAsk.Quantity) { - Status = OrderBookStatus.Disposing; + OnBestOffersChanged?.Invoke((bestBid, bestAsk)); + } + } - _cts?.Cancel(); - _queueEvent.Set(); + private void Reset() + { + _queueEvent.Set(); + // Clear queue + while (_processQueue.TryDequeue(out _)) { } - // Clear queue - while (_processQueue.TryDequeue(out _)) { } + _processBuffer.Clear(); + _bookSet = false; + DoReset(); + } - _processBuffer.Clear(); + private async Task ResyncAsync() + { + Status = OrderBookStatus.Syncing; + var success = false; + while (!success) + { + if (Status != OrderBookStatus.Syncing) + return; + + var resyncResult = await DoResyncAsync(_cts!.Token).ConfigureAwait(false); + success = resyncResult; + } + + _logger.OrderBookResynced(Api, Symbol); + Status = OrderBookStatus.Synced; + } + + private async Task ProcessQueue() + { + while (Status != OrderBookStatus.Disconnected && Status != OrderBookStatus.Disposed) + { + await _queueEvent.WaitAsync().ConfigureAwait(false); + + while (_processQueue.TryDequeue(out var item)) + { + if (Status == OrderBookStatus.Disconnected || Status == OrderBookStatus.Disposed) + break; + + if (_stopProcessing) + { + _logger.OrderBookMessageSkippedResubscribing(Api, Symbol); + continue; + } + + if (item is InitialOrderBookItem iobi) + ProcessInitialOrderBookItem(iobi); + if (item is ProcessQueueItem pqi) + ProcessQueueItem(pqi); + else if (item is ChecksumItem ci) + ProcessChecksum(ci); + } + } + } + + private void ProcessInitialOrderBookItem(InitialOrderBookItem item) + { + lock (_bookLock) + { + _bookSet = true; _asks.Clear(); + foreach (var ask in item.Asks) + _asks.Add(ask.Price, ask); _bids.Clear(); - AskCount = 0; - BidCount = 0; + foreach (var bid in item.Bids) + _bids.Add(bid.Price, bid); - Status = OrderBookStatus.Disposed; + LastSequenceNumber = item.EndUpdateId; + + AskCount = _asks.Count; + BidCount = _bids.Count; + + UpdateTime = DateTime.UtcNow; + _logger.OrderBookDataSet(Api, Symbol, BidCount, AskCount, item.EndUpdateId); + CheckProcessBuffer(); + OnOrderBookUpdate?.Invoke((item.Bids.ToArray(), item.Asks.ToArray())); + OnBestOffersChanged?.Invoke((BestBid, BestAsk)); } + } - /// - /// String representation of the top 3 entries - /// - /// - public override string ToString() + private void ProcessQueueItem(ProcessQueueItem item) + { + lock (_bookLock) { - return ToString(3); - } - - /// - /// String representation of the top x entries - /// - /// - public string ToString(int numberOfEntries) - { - var stringBuilder = new StringBuilder(); - var book = Book; - stringBuilder.AppendLine($" Ask quantity Ask price | Bid price Bid quantity"); - for(var i = 0; i < numberOfEntries; i++) + if (!_bookSet) { - var ask = book.asks.Count() > i ? book.asks.ElementAt(i): null; - var bid = book.bids.Count() > i ? book.bids.ElementAt(i): null; - stringBuilder.AppendLine($"[{ask?.Quantity.ToString(CultureInfo.InvariantCulture),14}] {ask?.Price.ToString(CultureInfo.InvariantCulture),14} | {bid?.Price.ToString(CultureInfo.InvariantCulture),-14} [{bid?.Quantity.ToString(CultureInfo.InvariantCulture),-14}]"); - } - - return stringBuilder.ToString(); - } - - private void CheckBestOffersChanged(ISymbolOrderBookEntry prevBestBid, ISymbolOrderBookEntry prevBestAsk) - { - var (bestBid, bestAsk) = BestOffers; - if (bestBid.Price != prevBestBid.Price || bestBid.Quantity != prevBestBid.Quantity || - bestAsk.Price != prevBestAsk.Price || bestAsk.Quantity != prevBestAsk.Quantity) - { - OnBestOffersChanged?.Invoke((bestBid, bestAsk)); - } - } - - private void Reset() - { - _queueEvent.Set(); - // Clear queue - while (_processQueue.TryDequeue(out _)) { } - - _processBuffer.Clear(); - _bookSet = false; - DoReset(); - } - - private async Task ResyncAsync() - { - Status = OrderBookStatus.Syncing; - var success = false; - while (!success) - { - if (Status != OrderBookStatus.Syncing) - return; - - var resyncResult = await DoResyncAsync(_cts!.Token).ConfigureAwait(false); - success = resyncResult; - } - - _logger.OrderBookResynced(Api, Symbol); - Status = OrderBookStatus.Synced; - } - - private async Task ProcessQueue() - { - while (Status != OrderBookStatus.Disconnected && Status != OrderBookStatus.Disposed) - { - await _queueEvent.WaitAsync().ConfigureAwait(false); - - while (_processQueue.TryDequeue(out var item)) + _processBuffer.Add(new ProcessBufferRangeSequenceEntry() { - if (Status == OrderBookStatus.Disconnected || Status == OrderBookStatus.Disposed) - break; + Asks = item.Asks, + Bids = item.Bids, + FirstUpdateId = item.StartUpdateId, + LastUpdateId = item.EndUpdateId, + }); - if (_stopProcessing) - { - _logger.OrderBookMessageSkippedResubscribing(Api, Symbol); - continue; - } - - if (item is InitialOrderBookItem iobi) - ProcessInitialOrderBookItem(iobi); - if (item is ProcessQueueItem pqi) - ProcessQueueItem(pqi); - else if (item is ChecksumItem ci) - ProcessChecksum(ci); - } + _logger.OrderBookUpdateBuffered(Api, Symbol, item.StartUpdateId, item.EndUpdateId, item.Asks.Length, item.Bids.Length); } - } - - private void ProcessInitialOrderBookItem(InitialOrderBookItem item) - { - lock (_bookLock) + else { - _bookSet = true; - _asks.Clear(); - foreach (var ask in item.Asks) - _asks.Add(ask.Price, ask); - _bids.Clear(); - foreach (var bid in item.Bids) - _bids.Add(bid.Price, bid); - - LastSequenceNumber = item.EndUpdateId; - - AskCount = _asks.Count; - BidCount = _bids.Count; - - UpdateTime = DateTime.UtcNow; - _logger.OrderBookDataSet(Api, Symbol, BidCount, AskCount, item.EndUpdateId); CheckProcessBuffer(); - OnOrderBookUpdate?.Invoke((item.Bids.ToArray(), item.Asks.ToArray())); - OnBestOffersChanged?.Invoke((BestBid, BestAsk)); - } - } + var (prevBestBid, prevBestAsk) = BestOffers; + ProcessRangeUpdates(item.StartUpdateId, item.EndUpdateId, item.Bids, item.Asks); - private void ProcessQueueItem(ProcessQueueItem item) - { - lock (_bookLock) - { - if (!_bookSet) - { - _processBuffer.Add(new ProcessBufferRangeSequenceEntry() - { - Asks = item.Asks, - Bids = item.Bids, - FirstUpdateId = item.StartUpdateId, - LastUpdateId = item.EndUpdateId, - }); - - _logger.OrderBookUpdateBuffered(Api, Symbol, item.StartUpdateId, item.EndUpdateId, item.Asks.Count(), item.Bids.Count()); - } - else - { - CheckProcessBuffer(); - var (prevBestBid, prevBestAsk) = BestOffers; - ProcessRangeUpdates(item.StartUpdateId, item.EndUpdateId, item.Bids, item.Asks); - - if (_asks.Count == 0 || _bids.Count == 0) - return; - - if (_asks.First().Key < _bids.First().Key) - { - _logger.OrderBookOutOfSyncDetected(Api, Symbol, _asks.First().Key, _bids.First().Key); - _stopProcessing = true; - Resubscribe(); - return; - } - - OnOrderBookUpdate?.Invoke((item.Bids.ToArray(), item.Asks.ToArray())); - CheckBestOffersChanged(prevBestBid, prevBestAsk); - } - } - } - - private void ProcessChecksum(ChecksumItem ci) - { - lock (_bookLock) - { - if (!_validateChecksum) + if (_asks.Count == 0 || _bids.Count == 0) return; - bool checksumResult = false; - try + if (_asks.First().Key < _bids.First().Key) { - checksumResult = DoChecksum(ci.Checksum); - } - catch (Exception) - { - // If the status is not synced it can be expected a checksum is failing - - if (Status == OrderBookStatus.Synced) - throw; - } - - if (!checksumResult) - { - _logger.OrderBookOutOfSyncChecksum(Api, Symbol); + _logger.OrderBookOutOfSyncDetected(Api, Symbol, _asks.First().Key, _bids.First().Key); _stopProcessing = true; Resubscribe(); - } - } - } - - private void Resubscribe() - { - Status = OrderBookStatus.Syncing; - _ = Task.Run(async () => - { - if(_subscription == null) - { - Status = OrderBookStatus.Disconnected; return; } - await _subscription!.UnsubscribeAsync().ConfigureAwait(false); - Reset(); - _stopProcessing = false; - if (!await _subscription!.ResubscribeAsync().ConfigureAwait(false)) - { - // Resubscribing failed, reconnect the socket - _logger.OrderBookResyncFailed(Api, Symbol); - Status = OrderBookStatus.Reconnecting; - _ = _subscription!.ReconnectAsync(); - } - else - { - await ResyncAsync().ConfigureAwait(false); - } - }); + OnOrderBookUpdate?.Invoke((item.Bids.ToArray(), item.Asks.ToArray())); + CheckBestOffersChanged(prevBestBid, prevBestAsk); + } } + } - private void ProcessRangeUpdates(long firstUpdateId, long lastUpdateId, IEnumerable bids, IEnumerable asks) + private void ProcessChecksum(ChecksumItem ci) + { + lock (_bookLock) { - if (lastUpdateId <= LastSequenceNumber) + if (!_validateChecksum) + return; + + bool checksumResult = false; + try { - _logger.OrderBookUpdateSkipped(Api, Symbol, lastUpdateId, LastSequenceNumber); + checksumResult = DoChecksum(ci.Checksum); + } + catch (Exception) + { + // If the status is not synced it can be expected a checksum is failing + + if (Status == OrderBookStatus.Synced) + throw; + } + + if (!checksumResult) + { + _logger.OrderBookOutOfSyncChecksum(Api, Symbol); + _stopProcessing = true; + Resubscribe(); + } + } + } + + private void Resubscribe() + { + Status = OrderBookStatus.Syncing; + _ = Task.Run(async () => + { + if(_subscription == null) + { + Status = OrderBookStatus.Disconnected; return; } - foreach (var entry in bids) - ProcessUpdate(LastSequenceNumber + 1, OrderBookEntryType.Bid, entry); - - foreach (var entry in asks) - ProcessUpdate(LastSequenceNumber + 1, OrderBookEntryType.Ask, entry); - - if (Levels.HasValue && _strictLevels) + await _subscription!.UnsubscribeAsync().ConfigureAwait(false); + Reset(); + _stopProcessing = false; + if (!await _subscription!.ResubscribeAsync().ConfigureAwait(false)) { - while (this._bids.Count > Levels.Value) - { - BidCount--; - this._bids.Remove(this._bids.Last().Key); - } + // Resubscribing failed, reconnect the socket + _logger.OrderBookResyncFailed(Api, Symbol); + Status = OrderBookStatus.Reconnecting; + _ = _subscription!.ReconnectAsync(); + } + else + { + await ResyncAsync().ConfigureAwait(false); + } + }); + } - while (this._asks.Count > Levels.Value) - { - AskCount--; - this._asks.Remove(this._asks.Last().Key); - } + private void ProcessRangeUpdates(long firstUpdateId, long lastUpdateId, IEnumerable bids, IEnumerable asks) + { + if (lastUpdateId <= LastSequenceNumber) + { + _logger.OrderBookUpdateSkipped(Api, Symbol, lastUpdateId, LastSequenceNumber); + return; + } + + foreach (var entry in bids) + ProcessUpdate(LastSequenceNumber + 1, OrderBookEntryType.Bid, entry); + + foreach (var entry in asks) + ProcessUpdate(LastSequenceNumber + 1, OrderBookEntryType.Ask, entry); + + if (Levels.HasValue && _strictLevels) + { + while (this._bids.Count > Levels.Value) + { + BidCount--; + this._bids.Remove(this._bids.Last().Key); } - LastSequenceNumber = lastUpdateId; - - _logger.OrderBookProcessedMessage(Api, Symbol, firstUpdateId, lastUpdateId); - } - } - - internal class DescComparer : IComparer - { - public int Compare(T? x, T? y) - { - return Comparer.Default.Compare(y!, x!); + while (this._asks.Count > Levels.Value) + { + AskCount--; + this._asks.Remove(this._asks.Last().Key); + } } + + LastSequenceNumber = lastUpdateId; + + _logger.OrderBookProcessedMessage(Api, Symbol, firstUpdateId, lastUpdateId); + } +} + +internal class DescComparer : IComparer +{ + public int Compare(T? x, T? y) + { + return Comparer.Default.Compare(y!, x!); } } diff --git a/CryptoExchange.Net/RateLimiting/Filters/AuthenticatedEndpointFilter.cs b/CryptoExchange.Net/RateLimiting/Filters/AuthenticatedEndpointFilter.cs index b40b36f..63af2fb 100644 --- a/CryptoExchange.Net/RateLimiting/Filters/AuthenticatedEndpointFilter.cs +++ b/CryptoExchange.Net/RateLimiting/Filters/AuthenticatedEndpointFilter.cs @@ -1,26 +1,25 @@ -using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects; using CryptoExchange.Net.RateLimiting.Interfaces; -namespace CryptoExchange.Net.RateLimiting.Filters +namespace CryptoExchange.Net.RateLimiting.Filters; + +/// +/// Filter requests based on whether they're authenticated or not +/// +public class AuthenticatedEndpointFilter : IGuardFilter { + private readonly bool _authenticated; + /// - /// Filter requests based on whether they're authenticated or not + /// ctor /// - public class AuthenticatedEndpointFilter : IGuardFilter + /// + public AuthenticatedEndpointFilter(bool authenticated) { - private readonly bool _authenticated; - - /// - /// ctor - /// - /// - public AuthenticatedEndpointFilter(bool authenticated) - { - _authenticated = authenticated; - } - - /// - public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey) - => definition.Authenticated == _authenticated; + _authenticated = authenticated; } + + /// + public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey) + => definition.Authenticated == _authenticated; } diff --git a/CryptoExchange.Net/RateLimiting/Filters/ExactPathFilter.cs b/CryptoExchange.Net/RateLimiting/Filters/ExactPathFilter.cs index bf8681e..9738201 100644 --- a/CryptoExchange.Net/RateLimiting/Filters/ExactPathFilter.cs +++ b/CryptoExchange.Net/RateLimiting/Filters/ExactPathFilter.cs @@ -1,27 +1,26 @@ -using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects; using CryptoExchange.Net.RateLimiting.Interfaces; using System; -namespace CryptoExchange.Net.RateLimiting.Filters +namespace CryptoExchange.Net.RateLimiting.Filters; + +/// +/// Filter requests based on whether the request path matches a specific path +/// +public class ExactPathFilter : IGuardFilter { + private readonly string _path; + /// - /// Filter requests based on whether the request path matches a specific path + /// ctor /// - public class ExactPathFilter : IGuardFilter + /// + public ExactPathFilter(string path) { - private readonly string _path; - - /// - /// ctor - /// - /// - public ExactPathFilter(string path) - { - _path = path; - } - - /// - public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey) - => string.Equals(definition.Path, _path, StringComparison.OrdinalIgnoreCase); + _path = path; } + + /// + public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey) + => string.Equals(definition.Path, _path, StringComparison.OrdinalIgnoreCase); } diff --git a/CryptoExchange.Net/RateLimiting/Filters/ExactPathsFilter.cs b/CryptoExchange.Net/RateLimiting/Filters/ExactPathsFilter.cs index 6f66377..65b6242 100644 --- a/CryptoExchange.Net/RateLimiting/Filters/ExactPathsFilter.cs +++ b/CryptoExchange.Net/RateLimiting/Filters/ExactPathsFilter.cs @@ -1,27 +1,26 @@ -using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects; using CryptoExchange.Net.RateLimiting.Interfaces; using System.Collections.Generic; -namespace CryptoExchange.Net.RateLimiting.Filters +namespace CryptoExchange.Net.RateLimiting.Filters; + +/// +/// Filter requests based on whether the request path matches any specific path in a list +/// +public class ExactPathsFilter : IGuardFilter { + private readonly HashSet _paths; + /// - /// Filter requests based on whether the request path matches any specific path in a list + /// ctor /// - public class ExactPathsFilter : IGuardFilter + /// + public ExactPathsFilter(IEnumerable paths) { - private readonly HashSet _paths; - - /// - /// ctor - /// - /// - public ExactPathsFilter(IEnumerable paths) - { - _paths = new HashSet(paths); - } - - /// - public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey) - => _paths.Contains(definition.Path); + _paths = new HashSet(paths); } + + /// + public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey) + => _paths.Contains(definition.Path); } diff --git a/CryptoExchange.Net/RateLimiting/Filters/HostFilter.cs b/CryptoExchange.Net/RateLimiting/Filters/HostFilter.cs index 65b47df..13e599f 100644 --- a/CryptoExchange.Net/RateLimiting/Filters/HostFilter.cs +++ b/CryptoExchange.Net/RateLimiting/Filters/HostFilter.cs @@ -1,27 +1,26 @@ -using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects; using CryptoExchange.Net.RateLimiting.Interfaces; -namespace CryptoExchange.Net.RateLimiting.Filters +namespace CryptoExchange.Net.RateLimiting.Filters; + +/// +/// Filter requests based on whether the host address matches a specific address +/// +public class HostFilter : IGuardFilter { + private readonly string _host; + /// - /// Filter requests based on whether the host address matches a specific address + /// ctor /// - public class HostFilter : IGuardFilter + /// + public HostFilter(string host) { - private readonly string _host; - - /// - /// ctor - /// - /// - public HostFilter(string host) - { - _host = host; - } - - /// - public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey) - => host == _host; - + _host = host; } + + /// + public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey) + => host == _host; + } diff --git a/CryptoExchange.Net/RateLimiting/Filters/LimitItemTypeFilter.cs b/CryptoExchange.Net/RateLimiting/Filters/LimitItemTypeFilter.cs index 93137a5..52175a4 100644 --- a/CryptoExchange.Net/RateLimiting/Filters/LimitItemTypeFilter.cs +++ b/CryptoExchange.Net/RateLimiting/Filters/LimitItemTypeFilter.cs @@ -1,26 +1,25 @@ -using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects; using CryptoExchange.Net.RateLimiting.Interfaces; -namespace CryptoExchange.Net.RateLimiting.Filters +namespace CryptoExchange.Net.RateLimiting.Filters; + +/// +/// Filter requests based on whether it's a connection or a request +/// +public class LimitItemTypeFilter : IGuardFilter { + private readonly RateLimitItemType _type; + /// - /// Filter requests based on whether it's a connection or a request + /// ctor /// - public class LimitItemTypeFilter : IGuardFilter + /// + public LimitItemTypeFilter(RateLimitItemType type) { - private readonly RateLimitItemType _type; - - /// - /// ctor - /// - /// - public LimitItemTypeFilter(RateLimitItemType type) - { - _type = type; - } - - /// - public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey) - => type == _type; + _type = type; } + + /// + public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey) + => type == _type; } diff --git a/CryptoExchange.Net/RateLimiting/Filters/PathStartFilter.cs b/CryptoExchange.Net/RateLimiting/Filters/PathStartFilter.cs index ba68b8b..7042d87 100644 --- a/CryptoExchange.Net/RateLimiting/Filters/PathStartFilter.cs +++ b/CryptoExchange.Net/RateLimiting/Filters/PathStartFilter.cs @@ -1,27 +1,26 @@ -using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects; using CryptoExchange.Net.RateLimiting.Interfaces; using System; -namespace CryptoExchange.Net.RateLimiting.Filters +namespace CryptoExchange.Net.RateLimiting.Filters; + +/// +/// Filter requests based on whether the path starts with a specific string +/// +public class PathStartFilter : IGuardFilter { + private readonly string _path; + /// - /// Filter requests based on whether the path starts with a specific string + /// ctor /// - public class PathStartFilter : IGuardFilter + /// + public PathStartFilter(string path) { - private readonly string _path; - - /// - /// ctor - /// - /// - public PathStartFilter(string path) - { - _path = path; - } - - /// - public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey) - => definition.Path.StartsWith(_path, StringComparison.OrdinalIgnoreCase); + _path = path; } + + /// + public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey) + => definition.Path.StartsWith(_path, StringComparison.OrdinalIgnoreCase); } diff --git a/CryptoExchange.Net/RateLimiting/Guards/RateLimitGuard.cs b/CryptoExchange.Net/RateLimiting/Guards/RateLimitGuard.cs index 6d5b971..14d9115 100644 --- a/CryptoExchange.Net/RateLimiting/Guards/RateLimitGuard.cs +++ b/CryptoExchange.Net/RateLimiting/Guards/RateLimitGuard.cs @@ -1,148 +1,147 @@ -using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects; using CryptoExchange.Net.RateLimiting.Interfaces; using CryptoExchange.Net.RateLimiting.Trackers; using System; using System.Collections.Generic; -namespace CryptoExchange.Net.RateLimiting.Guards +namespace CryptoExchange.Net.RateLimiting.Guards; + +/// +public class RateLimitGuard : IRateLimitGuard { + /// + /// Apply guard per host + /// + public static Func PerHost { get; } = new Func((def, host, key) => host); + /// + /// Apply guard per endpoint + /// + public static Func PerEndpoint { get; } = new Func((def, host, key) => def.Path + def.Method); + /// + /// Apply guard per connection + /// + public static Func PerConnection { get; } = new Func((def, host, key) => def.ConnectionId.ToString()!); + /// + /// Apply guard per API key + /// + public static Func PerApiKey { get; } = new Func((def, host, key) => key!); + /// + /// Apply guard per API key per endpoint + /// + public static Func PerApiKeyPerEndpoint { get; } = new Func((def, host, key) => key! + def.Path + def.Method); + + private readonly IEnumerable _filters; + private readonly Dictionary _trackers; + private readonly RateLimitWindowType _windowType; + private readonly double? _decayRate; + private readonly int? _connectionWeight; + private readonly Func _keySelector; + /// - public class RateLimitGuard : IRateLimitGuard + public string Name => "RateLimitGuard"; + + /// + public string Description => _windowType == RateLimitWindowType.Decay ? $"Limit of {Limit} with a decay rate of {_decayRate}" : $"Limit of {Limit} per {TimeSpan}"; + + /// + /// The limit per period + /// + public int Limit { get; } + /// + /// The time period for the limit + /// + public TimeSpan TimeSpan { get; } + + /// + /// ctor + /// + /// The rate limit key selector + /// Filter for rate limit items. Only when the rate limit item passes the filter the guard will apply + /// Limit per period + /// Timespan for the period + /// Type of rate limit window + /// The decay per timespan if windowType is DecayWindowTracker + /// The weight of a new connection + public RateLimitGuard(Func keySelector, IGuardFilter filter, int limit, TimeSpan timeSpan, RateLimitWindowType windowType, double? decayPerTimeSpan = null, int? connectionWeight = null) + : this(keySelector, new[] { filter }, limit, timeSpan, windowType, decayPerTimeSpan, connectionWeight) { - /// - /// Apply guard per host - /// - public static Func PerHost { get; } = new Func((def, host, key) => host); - /// - /// Apply guard per endpoint - /// - public static Func PerEndpoint { get; } = new Func((def, host, key) => def.Path + def.Method); - /// - /// Apply guard per connection - /// - public static Func PerConnection { get; } = new Func((def, host, key) => def.ConnectionId.ToString()!); - /// - /// Apply guard per API key - /// - public static Func PerApiKey { get; } = new Func((def, host, key) => key!); - /// - /// Apply guard per API key per endpoint - /// - public static Func PerApiKeyPerEndpoint { get; } = new Func((def, host, key) => key! + def.Path + def.Method); + } - private readonly IEnumerable _filters; - private readonly Dictionary _trackers; - private readonly RateLimitWindowType _windowType; - private readonly double? _decayRate; - private readonly int? _connectionWeight; - private readonly Func _keySelector; + /// + /// ctor + /// + /// The rate limit key selector + /// Filters for rate limit items. Only when the rate limit item passes all filters the guard will apply + /// Limit per period + /// Timespan for the period + /// Type of rate limit window + /// The decay per timespan if windowType is DecayWindowTracker + /// The weight of a new connection + public RateLimitGuard(Func keySelector, IEnumerable filters, int limit, TimeSpan timeSpan, RateLimitWindowType windowType, double? decayPerTimeSpan = null, int? connectionWeight = null) + { + _filters = filters; + _trackers = new Dictionary(); + _windowType = windowType; + Limit = limit; + TimeSpan = timeSpan; + _keySelector = keySelector; + _decayRate = decayPerTimeSpan; + _connectionWeight = connectionWeight; + } - /// - public string Name => "RateLimitGuard"; - - /// - public string Description => _windowType == RateLimitWindowType.Decay ? $"Limit of {Limit} with a decay rate of {_decayRate}" : $"Limit of {Limit} per {TimeSpan}"; - - /// - /// The limit per period - /// - public int Limit { get; } - /// - /// The time period for the limit - /// - public TimeSpan TimeSpan { get; } - - /// - /// ctor - /// - /// The rate limit key selector - /// Filter for rate limit items. Only when the rate limit item passes the filter the guard will apply - /// Limit per period - /// Timespan for the period - /// Type of rate limit window - /// The decay per timespan if windowType is DecayWindowTracker - /// The weight of a new connection - public RateLimitGuard(Func keySelector, IGuardFilter filter, int limit, TimeSpan timeSpan, RateLimitWindowType windowType, double? decayPerTimeSpan = null, int? connectionWeight = null) - : this(keySelector, new[] { filter }, limit, timeSpan, windowType, decayPerTimeSpan, connectionWeight) + /// + public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix) + { + foreach(var filter in _filters) { + if (!filter.Passes(type, definition, host, apiKey)) + return LimitCheck.NotApplicable; } - /// - /// ctor - /// - /// The rate limit key selector - /// Filters for rate limit items. Only when the rate limit item passes all filters the guard will apply - /// Limit per period - /// Timespan for the period - /// Type of rate limit window - /// The decay per timespan if windowType is DecayWindowTracker - /// The weight of a new connection - public RateLimitGuard(Func keySelector, IEnumerable filters, int limit, TimeSpan timeSpan, RateLimitWindowType windowType, double? decayPerTimeSpan = null, int? connectionWeight = null) + if (type == RateLimitItemType.Connection) + requestWeight = _connectionWeight ?? requestWeight; + + var key = _keySelector(definition, host, apiKey) + keySuffix; + if (!_trackers.TryGetValue(key, out var tracker)) { - _filters = filters; - _trackers = new Dictionary(); - _windowType = windowType; - Limit = limit; - TimeSpan = timeSpan; - _keySelector = keySelector; - _decayRate = decayPerTimeSpan; - _connectionWeight = connectionWeight; + tracker = CreateTracker(); + _trackers.Add(key, tracker); } - /// - public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix) + var delay = tracker.GetWaitTime(requestWeight); + if (delay == default) + return LimitCheck.NotNeeded(Limit, TimeSpan, tracker.Current); + + return LimitCheck.Needed(delay, Limit, TimeSpan, tracker.Current); + } + + /// + public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix) + { + foreach (var filter in _filters) { - foreach(var filter in _filters) - { - if (!filter.Passes(type, definition, host, apiKey)) - return LimitCheck.NotApplicable; - } - - if (type == RateLimitItemType.Connection) - requestWeight = _connectionWeight ?? requestWeight; - - var key = _keySelector(definition, host, apiKey) + keySuffix; - if (!_trackers.TryGetValue(key, out var tracker)) - { - tracker = CreateTracker(); - _trackers.Add(key, tracker); - } - - var delay = tracker.GetWaitTime(requestWeight); - if (delay == default) - return LimitCheck.NotNeeded(Limit, TimeSpan, tracker.Current); - - return LimitCheck.Needed(delay, Limit, TimeSpan, tracker.Current); + if (!filter.Passes(type, definition, host, apiKey)) + return RateLimitState.NotApplied; } - /// - public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix) - { - foreach (var filter in _filters) - { - if (!filter.Passes(type, definition, host, apiKey)) - return RateLimitState.NotApplied; - } + if (type == RateLimitItemType.Connection) + requestWeight = _connectionWeight ?? requestWeight; - if (type == RateLimitItemType.Connection) - requestWeight = _connectionWeight ?? requestWeight; + var key = _keySelector(definition, host, apiKey) + keySuffix; + var tracker = _trackers[key]; + tracker.ApplyWeight(requestWeight); + return RateLimitState.Applied(Limit, TimeSpan, tracker.Current); + } - var key = _keySelector(definition, host, apiKey) + keySuffix; - var tracker = _trackers[key]; - tracker.ApplyWeight(requestWeight); - return RateLimitState.Applied(Limit, TimeSpan, tracker.Current); - } - - /// - /// Create a new WindowTracker - /// - /// - protected IWindowTracker CreateTracker() - { - return _windowType == RateLimitWindowType.Sliding ? new SlidingWindowTracker(Limit, TimeSpan) - : _windowType == RateLimitWindowType.Fixed ? new FixedWindowTracker(Limit, TimeSpan) - : _windowType == RateLimitWindowType.FixedAfterFirst ? new FixedAfterStartWindowTracker(Limit, TimeSpan) : - new DecayWindowTracker(Limit, TimeSpan, _decayRate ?? throw new InvalidOperationException("Decay rate not provided")); - } + /// + /// Create a new WindowTracker + /// + /// + protected IWindowTracker CreateTracker() + { + return _windowType == RateLimitWindowType.Sliding ? new SlidingWindowTracker(Limit, TimeSpan) + : _windowType == RateLimitWindowType.Fixed ? new FixedWindowTracker(Limit, TimeSpan) + : _windowType == RateLimitWindowType.FixedAfterFirst ? new FixedAfterStartWindowTracker(Limit, TimeSpan) : + new DecayWindowTracker(Limit, TimeSpan, _decayRate ?? throw new InvalidOperationException("Decay rate not provided")); } } diff --git a/CryptoExchange.Net/RateLimiting/Guards/RetryAfterGuard.cs b/CryptoExchange.Net/RateLimiting/Guards/RetryAfterGuard.cs index c9c4bbe..2d00f25 100644 --- a/CryptoExchange.Net/RateLimiting/Guards/RetryAfterGuard.cs +++ b/CryptoExchange.Net/RateLimiting/Guards/RetryAfterGuard.cs @@ -1,69 +1,68 @@ -using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects; using CryptoExchange.Net.RateLimiting.Interfaces; using System; -namespace CryptoExchange.Net.RateLimiting.Guards +namespace CryptoExchange.Net.RateLimiting.Guards; + +/// +/// Retry after guard +/// +public class RetryAfterGuard : IRateLimitGuard { /// - /// Retry after guard + /// Additional wait time to apply to account for time offset between server and client /// - public class RetryAfterGuard : IRateLimitGuard + private static readonly TimeSpan _windowBuffer = TimeSpan.FromMilliseconds(1000); + + /// + public string Name => "RetryAfterGuard"; + + /// + public string Description => $"Pause {Type} until after {After}"; + + /// + /// The timestamp after which requests are allowed again + /// + public DateTime After { get; private set; } + + /// + /// The type of rate limit item this guard is for + /// + public RateLimitItemType Type { get; private set; } + + /// + /// ctor + /// + /// + /// + public RetryAfterGuard(DateTime after, RateLimitItemType type) { - /// - /// Additional wait time to apply to account for time offset between server and client - /// - private static readonly TimeSpan _windowBuffer = TimeSpan.FromMilliseconds(1000); - - /// - public string Name => "RetryAfterGuard"; - - /// - public string Description => $"Pause {Type} until after {After}"; - - /// - /// The timestamp after which requests are allowed again - /// - public DateTime After { get; private set; } - - /// - /// The type of rate limit item this guard is for - /// - public RateLimitItemType Type { get; private set; } - - /// - /// ctor - /// - /// - /// - public RetryAfterGuard(DateTime after, RateLimitItemType type) - { - After = after; - Type = type; - } - - /// - public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix) - { - if (type != Type) - return LimitCheck.NotApplicable; - - var dif = (After + _windowBuffer) - DateTime.UtcNow; - if (dif <= TimeSpan.Zero) - return LimitCheck.NotApplicable; - - return LimitCheck.Needed(dif, default, default, default); - } - - /// - public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix) - { - return RateLimitState.NotApplied; - } - - /// - /// Update the 'after' time - /// - /// - public void UpdateAfter(DateTime after) => After = after; + After = after; + Type = type; } + + /// + public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix) + { + if (type != Type) + return LimitCheck.NotApplicable; + + var dif = (After + _windowBuffer) - DateTime.UtcNow; + if (dif <= TimeSpan.Zero) + return LimitCheck.NotApplicable; + + return LimitCheck.Needed(dif, default, default, default); + } + + /// + public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix) + { + return RateLimitState.NotApplied; + } + + /// + /// Update the 'after' time + /// + /// + public void UpdateAfter(DateTime after) => After = after; } diff --git a/CryptoExchange.Net/RateLimiting/Guards/SingleLimitGuard.cs b/CryptoExchange.Net/RateLimiting/Guards/SingleLimitGuard.cs index f3e7998..9d05869 100644 --- a/CryptoExchange.Net/RateLimiting/Guards/SingleLimitGuard.cs +++ b/CryptoExchange.Net/RateLimiting/Guards/SingleLimitGuard.cs @@ -1,92 +1,91 @@ -using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects; using CryptoExchange.Net.RateLimiting.Interfaces; using CryptoExchange.Net.RateLimiting.Trackers; using System; using System.Collections.Generic; -namespace CryptoExchange.Net.RateLimiting.Guards +namespace CryptoExchange.Net.RateLimiting.Guards; + +/// +/// Rate limit guard for a per endpoint limit +/// +public class SingleLimitGuard : IRateLimitGuard { /// - /// Rate limit guard for a per endpoint limit + /// Default endpoint limit /// - public class SingleLimitGuard : IRateLimitGuard + public static Func Default { get; } = new Func((def, host, key) => def.Path + def.Method); + + /// + /// Endpoint limit per API key + /// + public static Func PerApiKey { get; } = new Func((def, host, key) => def.Path + def.Method + key); + + private readonly Dictionary _trackers; + private readonly RateLimitWindowType _windowType; + private readonly double? _decayRate; + private readonly int _limit; + private readonly TimeSpan _period; + private readonly Func _keySelector; + + /// + public string Name => "EndpointLimitGuard"; + + /// + public string Description => $"Limit requests to endpoint"; + + /// + /// ctor + /// + public SingleLimitGuard( + int limit, + TimeSpan period, + RateLimitWindowType windowType, + double? decayRate = null, + Func? keySelector = null) { - /// - /// Default endpoint limit - /// - public static Func Default { get; } = new Func((def, host, key) => def.Path + def.Method); + _limit = limit; + _period = period; + _windowType = windowType; + _decayRate = decayRate; + _keySelector = keySelector ?? Default; + _trackers = new Dictionary(); + } - /// - /// Endpoint limit per API key - /// - public static Func PerApiKey { get; } = new Func((def, host, key) => def.Path + def.Method + key); - - private readonly Dictionary _trackers; - private readonly RateLimitWindowType _windowType; - private readonly double? _decayRate; - private readonly int _limit; - private readonly TimeSpan _period; - private readonly Func _keySelector; - - /// - public string Name => "EndpointLimitGuard"; - - /// - public string Description => $"Limit requests to endpoint"; - - /// - /// ctor - /// - public SingleLimitGuard( - int limit, - TimeSpan period, - RateLimitWindowType windowType, - double? decayRate = null, - Func? keySelector = null) + /// + public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix) + { + var key = _keySelector(definition, host, apiKey) + keySuffix; + if (!_trackers.TryGetValue(key, out var tracker)) { - _limit = limit; - _period = period; - _windowType = windowType; - _decayRate = decayRate; - _keySelector = keySelector ?? Default; - _trackers = new Dictionary(); + tracker = CreateTracker(); + _trackers.Add(key, tracker); } - /// - public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix) - { - var key = _keySelector(definition, host, apiKey) + keySuffix; - if (!_trackers.TryGetValue(key, out var tracker)) - { - tracker = CreateTracker(); - _trackers.Add(key, tracker); - } + var delay = tracker.GetWaitTime(requestWeight); + if (delay == default) + return LimitCheck.NotNeeded(_limit, _period, tracker.Current); - var delay = tracker.GetWaitTime(requestWeight); - if (delay == default) - return LimitCheck.NotNeeded(_limit, _period, tracker.Current); + return LimitCheck.Needed(delay, _limit, _period, tracker.Current); + } - return LimitCheck.Needed(delay, _limit, _period, tracker.Current); - } + /// + public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix) + { + var key = _keySelector(definition, host, apiKey) + keySuffix; + var tracker = _trackers[key]; + tracker.ApplyWeight(requestWeight); + return RateLimitState.Applied(_limit, _period, tracker.Current); + } - /// - public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix) - { - var key = _keySelector(definition, host, apiKey) + keySuffix; - var tracker = _trackers[key]; - tracker.ApplyWeight(requestWeight); - return RateLimitState.Applied(_limit, _period, tracker.Current); - } - - /// - /// Create a new WindowTracker - /// - /// - protected IWindowTracker CreateTracker() - { - return _windowType == RateLimitWindowType.Sliding ? new SlidingWindowTracker(_limit, _period) - : _windowType == RateLimitWindowType.Fixed ? new FixedWindowTracker(_limit, _period) : - new DecayWindowTracker(_limit, _period, _decayRate ?? throw new InvalidOperationException("Decay rate not provided")); - } + /// + /// Create a new WindowTracker + /// + /// + protected IWindowTracker CreateTracker() + { + return _windowType == RateLimitWindowType.Sliding ? new SlidingWindowTracker(_limit, _period) + : _windowType == RateLimitWindowType.Fixed ? new FixedWindowTracker(_limit, _period) : + new DecayWindowTracker(_limit, _period, _decayRate ?? throw new InvalidOperationException("Decay rate not provided")); } } diff --git a/CryptoExchange.Net/RateLimiting/Interfaces/IGuardFilter.cs b/CryptoExchange.Net/RateLimiting/Interfaces/IGuardFilter.cs index 8a75999..fbd6e67 100644 --- a/CryptoExchange.Net/RateLimiting/Interfaces/IGuardFilter.cs +++ b/CryptoExchange.Net/RateLimiting/Interfaces/IGuardFilter.cs @@ -1,20 +1,19 @@ -using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects; -namespace CryptoExchange.Net.RateLimiting.Interfaces +namespace CryptoExchange.Net.RateLimiting.Interfaces; + +/// +/// Filter requests based on specific condition +/// +public interface IGuardFilter { /// - /// Filter requests based on specific condition + /// Whether a request or connection passes this filter /// - public interface IGuardFilter - { - /// - /// Whether a request or connection passes this filter - /// - /// The type of item - /// The request definition - /// The host address - /// The API key - /// True if passed - bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey); - } + /// The type of item + /// The request definition + /// The host address + /// The API key + /// True if passed + bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey); } diff --git a/CryptoExchange.Net/RateLimiting/Interfaces/IRateLimitGate.cs b/CryptoExchange.Net/RateLimiting/Interfaces/IRateLimitGate.cs index eb38e6a..af9df5b 100644 --- a/CryptoExchange.Net/RateLimiting/Interfaces/IRateLimitGate.cs +++ b/CryptoExchange.Net/RateLimiting/Interfaces/IRateLimitGate.cs @@ -1,78 +1,77 @@ -using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects; using Microsoft.Extensions.Logging; using System; using System.Threading; using System.Threading.Tasks; -namespace CryptoExchange.Net.RateLimiting.Interfaces +namespace CryptoExchange.Net.RateLimiting.Interfaces; + +/// +/// Rate limit gate +/// +public interface IRateLimitGate { /// - /// Rate limit gate + /// Event when the rate limit is triggered /// - public interface IRateLimitGate - { - /// - /// Event when the rate limit is triggered - /// - event Action RateLimitTriggered; + event Action RateLimitTriggered; - /// - /// Event when the rate limit is updated. Note that it's only updated when a request is send, so there are no specific updates when the current usage is decaying. - /// - event Action? RateLimitUpdated; + /// + /// Event when the rate limit is updated. Note that it's only updated when a request is send, so there are no specific updates when the current usage is decaying. + /// + event Action? RateLimitUpdated; - /// - /// Add a rate limit guard - /// - /// Guard to add - /// - IRateLimitGate AddGuard(IRateLimitGuard guard); + /// + /// Add a rate limit guard + /// + /// Guard to add + /// + IRateLimitGate AddGuard(IRateLimitGuard guard); - /// - /// Set a RetryAfter guard, can be used when a server rate limit is hit and a RetryAfter header is specified - /// - /// The time after which requests can be send again - /// RateLimitType - /// - Task SetRetryAfterGuardAsync(DateTime retryAfter, RateLimitItemType type = RateLimitItemType.Request); + /// + /// Set a RetryAfter guard, can be used when a server rate limit is hit and a RetryAfter header is specified + /// + /// The time after which requests can be send again + /// RateLimitType + /// + Task SetRetryAfterGuardAsync(DateTime retryAfter, RateLimitItemType type = RateLimitItemType.Request); - /// - /// Returns the 'retry after' timestamp if set - /// - /// - Task GetRetryAfterTime(); + /// + /// Returns the 'retry after' timestamp if set + /// + /// + Task GetRetryAfterTime(); - /// - /// Process a request. Enforces the configured rate limits. When a rate limit is hit will wait for the rate limit to pass if RateLimitingBehaviour is Wait, or return an error if it is set to Fail - /// - /// Logger - /// Id of the item to check - /// The rate limit item type - /// The request definition - /// The host address - /// The API key - /// Request weight - /// Behaviour when rate limit is hit - /// An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters. - /// Cancelation token - /// Error if RateLimitingBehaviour is Fail and rate limit is hit - Task ProcessAsync(ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string baseAddress, string? apiKey, int requestWeight, RateLimitingBehaviour behaviour, string? keySuffix, CancellationToken ct); + /// + /// Process a request. Enforces the configured rate limits. When a rate limit is hit will wait for the rate limit to pass if RateLimitingBehaviour is Wait, or return an error if it is set to Fail + /// + /// Logger + /// Id of the item to check + /// The rate limit item type + /// The request definition + /// The host address + /// The API key + /// Request weight + /// Behaviour when rate limit is hit + /// An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters. + /// Cancelation token + /// Error if RateLimitingBehaviour is Fail and rate limit is hit + Task ProcessAsync(ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string baseAddress, string? apiKey, int requestWeight, RateLimitingBehaviour behaviour, string? keySuffix, CancellationToken ct); - /// - /// Enforces the rate limit as defined in the request definition. When a rate limit is hit will wait for the rate limit to pass if RateLimitingBehaviour is Wait, or return an error if it is set to Fail - /// - /// Logger - /// Id of the item to check - /// The guard - /// The rate limit item type - /// The request definition - /// The host address - /// The API key - /// Behaviour when rate limit is hit - /// The weight to apply to the limit guard - /// An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters. - /// Cancelation token - /// Error if RateLimitingBehaviour is Fail and rate limit is hit - Task ProcessSingleAsync(ILogger logger, int itemId, IRateLimitGuard guard, RateLimitItemType type, RequestDefinition definition, string baseAddress, string? apiKey, int requestWeight, RateLimitingBehaviour behaviour, string? keySuffix, CancellationToken ct); - } + /// + /// Enforces the rate limit as defined in the request definition. When a rate limit is hit will wait for the rate limit to pass if RateLimitingBehaviour is Wait, or return an error if it is set to Fail + /// + /// Logger + /// Id of the item to check + /// The guard + /// The rate limit item type + /// The request definition + /// The host address + /// The API key + /// Behaviour when rate limit is hit + /// The weight to apply to the limit guard + /// An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters. + /// Cancelation token + /// Error if RateLimitingBehaviour is Fail and rate limit is hit + Task ProcessSingleAsync(ILogger logger, int itemId, IRateLimitGuard guard, RateLimitItemType type, RequestDefinition definition, string baseAddress, string? apiKey, int requestWeight, RateLimitingBehaviour behaviour, string? keySuffix, CancellationToken ct); } diff --git a/CryptoExchange.Net/RateLimiting/Interfaces/IRateLimitGuard.cs b/CryptoExchange.Net/RateLimiting/Interfaces/IRateLimitGuard.cs index 9a3f6a7..1e59c1a 100644 --- a/CryptoExchange.Net/RateLimiting/Interfaces/IRateLimitGuard.cs +++ b/CryptoExchange.Net/RateLimiting/Interfaces/IRateLimitGuard.cs @@ -1,44 +1,43 @@ -using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects; -namespace CryptoExchange.Net.RateLimiting.Interfaces +namespace CryptoExchange.Net.RateLimiting.Interfaces; + +/// +/// Rate limit guard +/// +public interface IRateLimitGuard { /// - /// Rate limit guard + /// Name /// - public interface IRateLimitGuard - { - /// - /// Name - /// - string Name { get; } + string Name { get; } - /// - /// Description - /// - string Description { get; } + /// + /// Description + /// + string Description { get; } - /// - /// Check whether a request can pass this rate limit guard - /// - /// The rate limit item type - /// The request definition - /// The host address - /// The API key - /// The request weight - /// An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters. - /// - LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix); + /// + /// Check whether a request can pass this rate limit guard + /// + /// The rate limit item type + /// The request definition + /// The host address + /// The API key + /// The request weight + /// An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters. + /// + LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix); - /// - /// Apply the request to this guard with the specified weight - /// - /// The rate limit item type - /// The request definition - /// The host address - /// The API key - /// The request weight - /// An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters. - /// - RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix); - } + /// + /// Apply the request to this guard with the specified weight + /// + /// The rate limit item type + /// The request definition + /// The host address + /// The API key + /// The request weight + /// An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters. + /// + RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix); } diff --git a/CryptoExchange.Net/RateLimiting/Interfaces/IWindowTracker.cs b/CryptoExchange.Net/RateLimiting/Interfaces/IWindowTracker.cs index ab768dd..7524237 100644 --- a/CryptoExchange.Net/RateLimiting/Interfaces/IWindowTracker.cs +++ b/CryptoExchange.Net/RateLimiting/Interfaces/IWindowTracker.cs @@ -1,34 +1,33 @@ -using System; +using System; -namespace CryptoExchange.Net.RateLimiting.Interfaces +namespace CryptoExchange.Net.RateLimiting.Interfaces; + +/// +/// Rate limit window tracker +/// +public interface IWindowTracker { /// - /// Rate limit window tracker + /// Time period the limit is for /// - public interface IWindowTracker - { - /// - /// Time period the limit is for - /// - TimeSpan TimePeriod { get; } - /// - /// The limit in the time period - /// - int Limit { get; } - /// - /// The current count within the time period - /// - int Current { get; } - /// - /// Get the time to wait to fit the weight - /// - /// - /// - TimeSpan GetWaitTime(int weight); - /// - /// Register the weight in this window - /// - /// Request weight - void ApplyWeight(int weight); - } + TimeSpan TimePeriod { get; } + /// + /// The limit in the time period + /// + int Limit { get; } + /// + /// The current count within the time period + /// + int Current { get; } + /// + /// Get the time to wait to fit the weight + /// + /// + /// + TimeSpan GetWaitTime(int weight); + /// + /// Register the weight in this window + /// + /// Request weight + void ApplyWeight(int weight); } diff --git a/CryptoExchange.Net/RateLimiting/LimitCheck.cs b/CryptoExchange.Net/RateLimiting/LimitCheck.cs index 678f963..8695135 100644 --- a/CryptoExchange.Net/RateLimiting/LimitCheck.cs +++ b/CryptoExchange.Net/RateLimiting/LimitCheck.cs @@ -1,60 +1,59 @@ -using System; +using System; -namespace CryptoExchange.Net.RateLimiting +namespace CryptoExchange.Net.RateLimiting; + +/// +/// Limit check +/// +public readonly struct LimitCheck { /// - /// Limit check + /// Is guard applicable /// - public readonly struct LimitCheck + public bool Applicable { get; } + /// + /// Delay needed + /// + public TimeSpan Delay { get; } + /// + /// Current counter + /// + public int Current { get; } + /// + /// Limit + /// + public int? Limit { get; } + /// + /// Time period + /// + public TimeSpan? Period { get; } + + private LimitCheck(bool applicable, TimeSpan delay, int limit, TimeSpan period, int current) { - /// - /// Is guard applicable - /// - public bool Applicable { get; } - /// - /// Delay needed - /// - public TimeSpan Delay { get; } - /// - /// Current counter - /// - public int Current { get; } - /// - /// Limit - /// - public int? Limit { get; } - /// - /// Time period - /// - public TimeSpan? Period { get; } - - private LimitCheck(bool applicable, TimeSpan delay, int limit, TimeSpan period, int current) - { - Applicable = applicable; - Delay = delay; - Limit = limit; - Period = period; - Current = current; - } - - /// - /// Not applicable - /// - public static LimitCheck NotApplicable { get; } = new LimitCheck(false, default, default, default, default); - - /// - /// No wait needed - /// - public static LimitCheck NotNeeded(int limit, TimeSpan period, int current) => new(true, default, limit, period, current); - - /// - /// Wait needed - /// - /// The delay needed - /// Limit per period - /// Period the limit is for - /// Current counter - /// - public static LimitCheck Needed(TimeSpan delay, int limit, TimeSpan period, int current) => new(true, delay, limit, period, current); + Applicable = applicable; + Delay = delay; + Limit = limit; + Period = period; + Current = current; } + + /// + /// Not applicable + /// + public static LimitCheck NotApplicable { get; } = new LimitCheck(false, default, default, default, default); + + /// + /// No wait needed + /// + public static LimitCheck NotNeeded(int limit, TimeSpan period, int current) => new(true, default, limit, period, current); + + /// + /// Wait needed + /// + /// The delay needed + /// Limit per period + /// Period the limit is for + /// Current counter + /// + public static LimitCheck Needed(TimeSpan delay, int limit, TimeSpan period, int current) => new(true, delay, limit, period, current); } diff --git a/CryptoExchange.Net/RateLimiting/LimitEntry.cs b/CryptoExchange.Net/RateLimiting/LimitEntry.cs index e27d2c5..964aa2e 100644 --- a/CryptoExchange.Net/RateLimiting/LimitEntry.cs +++ b/CryptoExchange.Net/RateLimiting/LimitEntry.cs @@ -1,30 +1,29 @@ -using System; +using System; -namespace CryptoExchange.Net.RateLimiting +namespace CryptoExchange.Net.RateLimiting; + +/// +/// A rate limit entry +/// +public struct LimitEntry { /// - /// A rate limit entry + /// Timestamp of the item /// - public struct LimitEntry - { - /// - /// Timestamp of the item - /// - public DateTime Timestamp { get; set; } - /// - /// Item weight - /// - public int Weight { get; set; } + public DateTime Timestamp { get; set; } + /// + /// Item weight + /// + public int Weight { get; set; } - /// - /// ctor - /// - /// - /// - public LimitEntry(DateTime timestamp, int weight) - { - Timestamp = timestamp; - Weight = weight; - } + /// + /// ctor + /// + /// + /// + public LimitEntry(DateTime timestamp, int weight) + { + Timestamp = timestamp; + Weight = weight; } } diff --git a/CryptoExchange.Net/RateLimiting/RateLimitEvent.cs b/CryptoExchange.Net/RateLimiting/RateLimitEvent.cs index c79e03e..9864a04 100644 --- a/CryptoExchange.Net/RateLimiting/RateLimitEvent.cs +++ b/CryptoExchange.Net/RateLimiting/RateLimitEvent.cs @@ -1,75 +1,74 @@ -using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects; using System; -namespace CryptoExchange.Net.RateLimiting +namespace CryptoExchange.Net.RateLimiting; + +/// +/// Rate limit triggered event +/// +public record RateLimitEvent { /// - /// Rate limit triggered event + /// Id of the item the limit was checked for /// - public record RateLimitEvent + public int ItemId { get; set; } + /// + /// Name of the API limit that is reached + /// + public string ApiLimit { get; set; } = string.Empty; + /// + /// Description of the limit that is reached + /// + public string LimitDescription { get; set; } = string.Empty; + /// + /// The request definition + /// + public RequestDefinition RequestDefinition { get; set; } + /// + /// The host the request is for + /// + public string Host { get; set; } = default!; + /// + /// The current counter value + /// + public int Current { get; set; } + /// + /// The weight of the limited request + /// + public int RequestWeight { get; set; } + /// + /// The limit per time period + /// + public int? Limit { get; set; } + /// + /// The time period the limit is for + /// + public TimeSpan? TimePeriod { get; set; } + /// + /// The time the request will be delayed for if the Behaviour is RateLimitingBehaviour.Wait + /// + public TimeSpan? DelayTime { get; set; } + /// + /// The handling behaviour for the request + /// + public RateLimitingBehaviour Behaviour { get; set; } + + /// + /// ctor + /// + public RateLimitEvent(int itemId, string apiLimit, string limitDescription, RequestDefinition definition, string host, int current, int requestWeight, int? limit, TimeSpan? timePeriod, TimeSpan? delayTime, RateLimitingBehaviour behaviour) { - /// - /// Id of the item the limit was checked for - /// - public int ItemId { get; set; } - /// - /// Name of the API limit that is reached - /// - public string ApiLimit { get; set; } = string.Empty; - /// - /// Description of the limit that is reached - /// - public string LimitDescription { get; set; } = string.Empty; - /// - /// The request definition - /// - public RequestDefinition RequestDefinition { get; set; } - /// - /// The host the request is for - /// - public string Host { get; set; } = default!; - /// - /// The current counter value - /// - public int Current { get; set; } - /// - /// The weight of the limited request - /// - public int RequestWeight { get; set; } - /// - /// The limit per time period - /// - public int? Limit { get; set; } - /// - /// The time period the limit is for - /// - public TimeSpan? TimePeriod { get; set; } - /// - /// The time the request will be delayed for if the Behaviour is RateLimitingBehaviour.Wait - /// - public TimeSpan? DelayTime { get; set; } - /// - /// The handling behaviour for the rquest - /// - public RateLimitingBehaviour Behaviour { get; set; } - - /// - /// ctor - /// - public RateLimitEvent(int itemId, string apiLimit, string limitDescription, RequestDefinition definition, string host, int current, int requestWeight, int? limit, TimeSpan? timePeriod, TimeSpan? delayTime, RateLimitingBehaviour behaviour) - { - ItemId = itemId; - ApiLimit = apiLimit; - LimitDescription = limitDescription; - RequestDefinition = definition; - Host = host; - Current = current; - RequestWeight = requestWeight; - Limit = limit; - TimePeriod = timePeriod; - DelayTime = delayTime; - Behaviour = behaviour; - } - + ItemId = itemId; + ApiLimit = apiLimit; + LimitDescription = limitDescription; + RequestDefinition = definition; + Host = host; + Current = current; + RequestWeight = requestWeight; + Limit = limit; + TimePeriod = timePeriod; + DelayTime = delayTime; + Behaviour = behaviour; } + } diff --git a/CryptoExchange.Net/RateLimiting/RateLimitGate.cs b/CryptoExchange.Net/RateLimiting/RateLimitGate.cs index c07c319..b7e737e 100644 --- a/CryptoExchange.Net/RateLimiting/RateLimitGate.cs +++ b/CryptoExchange.Net/RateLimiting/RateLimitGate.cs @@ -1,4 +1,4 @@ -using CryptoExchange.Net.Logging.Extensions; +using CryptoExchange.Net.Logging.Extensions; using CryptoExchange.Net.Objects; using CryptoExchange.Net.RateLimiting.Guards; using CryptoExchange.Net.RateLimiting.Interfaces; @@ -10,184 +10,185 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -namespace CryptoExchange.Net.RateLimiting +namespace CryptoExchange.Net.RateLimiting; + +/// +#pragma warning disable CA1001 // Types that own disposable fields should be disposable +public class RateLimitGate : IRateLimitGate +#pragma warning restore CA1001 // Types that own disposable fields should be disposable { + private readonly ConcurrentBag _guards; + private readonly SemaphoreSlim _semaphore; + private readonly string _name; + + private int _waitingCount; + /// - public class RateLimitGate : IRateLimitGate + public event Action? RateLimitTriggered; + /// + public event Action? RateLimitUpdated; + + /// + /// ctor + /// + public RateLimitGate(string name) { - private readonly ConcurrentBag _guards; - private readonly SemaphoreSlim _semaphore; - private readonly string _name; + _name = name; + _guards = new ConcurrentBag(); + _semaphore = new SemaphoreSlim(1, 1); + } - private int _waitingCount; - - /// - public event Action? RateLimitTriggered; - /// - public event Action? RateLimitUpdated; - - /// - /// ctor - /// - public RateLimitGate(string name) + /// + public async Task ProcessAsync(ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string baseAddress, string? apiKey, int requestWeight, RateLimitingBehaviour behaviour, string? keySuffix, CancellationToken ct) + { + await _semaphore.WaitAsync(ct).ConfigureAwait(false); + bool release = true; + _waitingCount++; + try { - _name = name; - _guards = new ConcurrentBag(); - _semaphore = new SemaphoreSlim(1, 1); + return await CheckGuardsAsync(_guards, logger, itemId, type, definition, baseAddress, apiKey, requestWeight, behaviour, keySuffix, ct).ConfigureAwait(false); } - - /// - public async Task ProcessAsync(ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, RateLimitingBehaviour rateLimitingBehaviour, string? keySuffix, CancellationToken ct) + catch (TaskCanceledException tce) { - await _semaphore.WaitAsync(ct).ConfigureAwait(false); - bool release = true; - _waitingCount++; - try - { - return await CheckGuardsAsync(_guards, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, keySuffix, ct).ConfigureAwait(false); - } - catch (TaskCanceledException tce) - { - // The semaphore has already been released if the task was cancelled - release = false; - return new CallResult(new CancellationRequestedError(tce)); - } - finally - { - _waitingCount--; - if (release) - _semaphore.Release(); - } + // The semaphore has already been released if the task was cancelled + release = false; + return new CallResult(new CancellationRequestedError(tce)); } - - /// - public async Task ProcessSingleAsync( - ILogger logger, - int itemId, - IRateLimitGuard guard, - RateLimitItemType type, - RequestDefinition definition, - string host, - string? apiKey, - int requestWeight, - RateLimitingBehaviour rateLimitingBehaviour, - string? keySuffix, - CancellationToken ct) + finally { - await _semaphore.WaitAsync(ct).ConfigureAwait(false); - bool release = true; - _waitingCount++; - try - { - return await CheckGuardsAsync(new IRateLimitGuard[] { guard }, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, keySuffix, ct).ConfigureAwait(false); - } - catch (TaskCanceledException tce) - { - // The semaphore has already been released if the task was cancelled - release = false; - return new CallResult(new CancellationRequestedError(tce)); - } - finally - { - _waitingCount--; - if (release) - _semaphore.Release(); - } + _waitingCount--; + if (release) + _semaphore.Release(); } + } - private async Task CheckGuardsAsync(IEnumerable guards, ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, RateLimitingBehaviour rateLimitingBehaviour, string? keySuffix, CancellationToken ct) + /// + public async Task ProcessSingleAsync( + ILogger logger, + int itemId, + IRateLimitGuard guard, + RateLimitItemType type, + RequestDefinition definition, + string baseAddress, + string? apiKey, + int requestWeight, + RateLimitingBehaviour behaviour, + string? keySuffix, + CancellationToken ct) + { + await _semaphore.WaitAsync(ct).ConfigureAwait(false); + bool release = true; + _waitingCount++; + try { - foreach (var guard in guards) - { - // Check if a wait is needed for this guard - var result = guard.Check(type, definition, host, apiKey, requestWeight, keySuffix); - if (result.Delay != TimeSpan.Zero && rateLimitingBehaviour == RateLimitingBehaviour.Fail) - { - // Delay is needed and limit behaviour is to fail the request - if (type == RateLimitItemType.Connection) - logger.RateLimitConnectionFailed(itemId, guard.Name, guard.Description); - else - logger.RateLimitRequestFailed(itemId, definition.Path, guard.Name, guard.Description); - - RateLimitTriggered?.Invoke(new RateLimitEvent(itemId, _name, guard.Description, definition, host, result.Current, requestWeight, result.Limit, result.Period, result.Delay, rateLimitingBehaviour)); - return new CallResult(new ClientRateLimitError($"Rate limit check failed on guard {guard.Name}; {guard.Description}")); - } - - if (result.Delay != TimeSpan.Zero) - { - // Delay is needed and limit behaviour is to wait for the request to be under the limit - _semaphore.Release(); - - var description = result.Limit == null ? guard.Description : $"{guard.Description}, Request weight: {requestWeight}, Current: {result.Current}, Limit: {result.Limit}, requests now being limited: {_waitingCount}"; - if (type == RateLimitItemType.Connection) - logger.RateLimitDelayingConnection(itemId, result.Delay, guard.Name, description); - else - logger.RateLimitDelayingRequest(itemId, definition.Path, result.Delay, guard.Name, description); - - RateLimitTriggered?.Invoke(new RateLimitEvent(itemId, _name, guard.Description, definition, host, result.Current, requestWeight, result.Limit, result.Period, result.Delay, rateLimitingBehaviour)); - await Task.Delay((int)result.Delay.TotalMilliseconds + 1, ct).ConfigureAwait(false); - await _semaphore.WaitAsync(ct).ConfigureAwait(false); - return await CheckGuardsAsync(guards, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, keySuffix, ct).ConfigureAwait(false); - } - } - - // Apply the weight on each guard - foreach (var guard in guards) - { - var result = guard.ApplyWeight(type, definition, host, apiKey, requestWeight, keySuffix); - if (result.IsApplied) - { - RateLimitUpdated?.Invoke(new RateLimitUpdateEvent(itemId, _name, guard.Description, result.Current, result.Limit, result.Period)); - - if (type == RateLimitItemType.Connection) - logger.RateLimitAppliedConnection(itemId, guard.Name, guard.Description, result.Current); - else - logger.RateLimitAppliedRequest(itemId, definition.Path, guard.Name, guard.Description, result.Current); - } - } - - return CallResult.SuccessResult; + return await CheckGuardsAsync(new IRateLimitGuard[] { guard }, logger, itemId, type, definition, baseAddress, apiKey, requestWeight, behaviour, keySuffix, ct).ConfigureAwait(false); } - - /// - public IRateLimitGate AddGuard(IRateLimitGuard guard) + catch (TaskCanceledException tce) { - _guards.Add(guard); - return this; + // The semaphore has already been released if the task was cancelled + release = false; + return new CallResult(new CancellationRequestedError(tce)); } - - /// - public async Task SetRetryAfterGuardAsync(DateTime retryAfter, RateLimitItemType type) + finally { - await _semaphore.WaitAsync().ConfigureAwait(false); + _waitingCount--; + if (release) + _semaphore.Release(); + } + } - try + private async Task CheckGuardsAsync(IEnumerable guards, ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, RateLimitingBehaviour rateLimitingBehaviour, string? keySuffix, CancellationToken ct) + { + foreach (var guard in guards) + { + // Check if a wait is needed for this guard + var result = guard.Check(type, definition, host, apiKey, requestWeight, keySuffix); + if (result.Delay != TimeSpan.Zero && rateLimitingBehaviour == RateLimitingBehaviour.Fail) { - var retryAfterGuard = _guards.OfType().SingleOrDefault(); - if (retryAfterGuard == null) - _guards.Add(new RetryAfterGuard(retryAfter, type)); + // Delay is needed and limit behaviour is to fail the request + if (type == RateLimitItemType.Connection) + logger.RateLimitConnectionFailed(itemId, guard.Name, guard.Description); else - retryAfterGuard.UpdateAfter(retryAfter); + logger.RateLimitRequestFailed(itemId, definition.Path, guard.Name, guard.Description); + + RateLimitTriggered?.Invoke(new RateLimitEvent(itemId, _name, guard.Description, definition, host, result.Current, requestWeight, result.Limit, result.Period, result.Delay, rateLimitingBehaviour)); + return new CallResult(new ClientRateLimitError($"Rate limit check failed on guard {guard.Name}; {guard.Description}")); } - finally + + if (result.Delay != TimeSpan.Zero) { + // Delay is needed and limit behaviour is to wait for the request to be under the limit _semaphore.Release(); + + var description = result.Limit == null ? guard.Description : $"{guard.Description}, Request weight: {requestWeight}, Current: {result.Current}, Limit: {result.Limit}, requests now being limited: {_waitingCount}"; + if (type == RateLimitItemType.Connection) + logger.RateLimitDelayingConnection(itemId, result.Delay, guard.Name, description); + else + logger.RateLimitDelayingRequest(itemId, definition.Path, result.Delay, guard.Name, description); + + RateLimitTriggered?.Invoke(new RateLimitEvent(itemId, _name, guard.Description, definition, host, result.Current, requestWeight, result.Limit, result.Period, result.Delay, rateLimitingBehaviour)); + await Task.Delay((int)result.Delay.TotalMilliseconds + 1, ct).ConfigureAwait(false); + await _semaphore.WaitAsync(ct).ConfigureAwait(false); + return await CheckGuardsAsync(guards, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, keySuffix, ct).ConfigureAwait(false); } } - /// - public async Task GetRetryAfterTime() + // Apply the weight on each guard + foreach (var guard in guards) { - await _semaphore.WaitAsync().ConfigureAwait(false); - try + var result = guard.ApplyWeight(type, definition, host, apiKey, requestWeight, keySuffix); + if (result.IsApplied) { - var retryAfterGuard = _guards.OfType().SingleOrDefault(); - return retryAfterGuard?.After; - } - finally - { - _semaphore.Release(); + RateLimitUpdated?.Invoke(new RateLimitUpdateEvent(itemId, _name, guard.Description, result.Current, result.Limit, result.Period)); + + if (type == RateLimitItemType.Connection) + logger.RateLimitAppliedConnection(itemId, guard.Name, guard.Description, result.Current); + else + logger.RateLimitAppliedRequest(itemId, definition.Path, guard.Name, guard.Description, result.Current); } } + + return CallResult.SuccessResult; + } + + /// + public IRateLimitGate AddGuard(IRateLimitGuard guard) + { + _guards.Add(guard); + return this; + } + + /// + public async Task SetRetryAfterGuardAsync(DateTime retryAfter, RateLimitItemType type) + { + await _semaphore.WaitAsync().ConfigureAwait(false); + + try + { + var retryAfterGuard = _guards.OfType().SingleOrDefault(); + if (retryAfterGuard == null) + _guards.Add(new RetryAfterGuard(retryAfter, type)); + else + retryAfterGuard.UpdateAfter(retryAfter); + } + finally + { + _semaphore.Release(); + } + } + + /// + public async Task GetRetryAfterTime() + { + await _semaphore.WaitAsync().ConfigureAwait(false); + try + { + var retryAfterGuard = _guards.OfType().SingleOrDefault(); + return retryAfterGuard?.After; + } + finally + { + _semaphore.Release(); + } } } diff --git a/CryptoExchange.Net/RateLimiting/RateLimitItemType.cs b/CryptoExchange.Net/RateLimiting/RateLimitItemType.cs index 4e18f16..c41b2c7 100644 --- a/CryptoExchange.Net/RateLimiting/RateLimitItemType.cs +++ b/CryptoExchange.Net/RateLimiting/RateLimitItemType.cs @@ -1,20 +1,19 @@ -using System; +using System; -namespace CryptoExchange.Net.RateLimiting +namespace CryptoExchange.Net.RateLimiting; + +/// +/// Rate limit item type +/// +[Flags] +public enum RateLimitItemType { /// - /// Rate limit item type + /// A connection attempt /// - [Flags] - public enum RateLimitItemType - { - /// - /// A connection attempt - /// - Connection = 1, - /// - /// A request - /// - Request = 2 - } + Connection = 1, + /// + /// A request + /// + Request = 2 } diff --git a/CryptoExchange.Net/RateLimiting/RateLimitState.cs b/CryptoExchange.Net/RateLimiting/RateLimitState.cs index 33811ce..7dae70d 100644 --- a/CryptoExchange.Net/RateLimiting/RateLimitState.cs +++ b/CryptoExchange.Net/RateLimiting/RateLimitState.cs @@ -1,55 +1,54 @@ -using System; +using System; -namespace CryptoExchange.Net.RateLimiting +namespace CryptoExchange.Net.RateLimiting; + +/// +/// Limit state +/// +public struct RateLimitState { /// - /// Limit state + /// Limit /// - public struct RateLimitState + public int Limit { get; } + /// + /// Period + /// + public TimeSpan Period { get; } + /// + /// Current count + /// + public int Current { get; } + /// + /// Whether the limit is applied + /// + public bool IsApplied { get; set; } + + /// + /// ctor + /// + /// + /// + /// + /// + public RateLimitState(bool applied, int limit, TimeSpan period, int current) { - /// - /// Limit - /// - public int Limit { get; } - /// - /// Period - /// - public TimeSpan Period { get; } - /// - /// Current count - /// - public int Current { get; } - /// - /// Whether the limit is applied - /// - public bool IsApplied { get; set; } - - /// - /// ctor - /// - /// - /// - /// - /// - public RateLimitState(bool applied, int limit, TimeSpan period, int current) - { - IsApplied = applied; - Limit = limit; - Period = period; - Current = current; - } - - /// - /// Not applied result - /// - public static RateLimitState NotApplied { get; } = new RateLimitState(false, default, default, default); - /// - /// Applied result - /// - /// - /// - /// - /// - public static RateLimitState Applied(int limit, TimeSpan period, int current) => new RateLimitState(true, limit, period, current); + IsApplied = applied; + Limit = limit; + Period = period; + Current = current; } + + /// + /// Not applied result + /// + public static RateLimitState NotApplied { get; } = new RateLimitState(false, default, default, default); + /// + /// Applied result + /// + /// + /// + /// + /// + public static RateLimitState Applied(int limit, TimeSpan period, int current) => new RateLimitState(true, limit, period, current); } diff --git a/CryptoExchange.Net/RateLimiting/RateLimitUpdateEvent.cs b/CryptoExchange.Net/RateLimiting/RateLimitUpdateEvent.cs index 341b822..37615f5 100644 --- a/CryptoExchange.Net/RateLimiting/RateLimitUpdateEvent.cs +++ b/CryptoExchange.Net/RateLimiting/RateLimitUpdateEvent.cs @@ -1,50 +1,48 @@ -using CryptoExchange.Net.Objects; using System; -namespace CryptoExchange.Net.RateLimiting +namespace CryptoExchange.Net.RateLimiting; + +/// +/// Rate limit update event +/// +public record RateLimitUpdateEvent { /// - /// Rate limit update event + /// Id of the item the limit was checked for /// - public record RateLimitUpdateEvent + public int ItemId { get; set; } + /// + /// Name of the API limit that is reached + /// + public string ApiLimit { get; set; } = string.Empty; + /// + /// Description of the limit that is reached + /// + public string LimitDescription { get; set; } = string.Empty; + /// + /// The current counter value + /// + public int Current { get; set; } + /// + /// The limit per time period + /// + public int? Limit { get; set; } + /// + /// The time period the limit is for + /// + public TimeSpan? TimePeriod { get; set; } + + /// + /// ctor + /// + public RateLimitUpdateEvent(int itemId, string apiLimit, string limitDescription, int current, int? limit, TimeSpan? timePeriod) { - /// - /// Id of the item the limit was checked for - /// - public int ItemId { get; set; } - /// - /// Name of the API limit that is reached - /// - public string ApiLimit { get; set; } = string.Empty; - /// - /// Description of the limit that is reached - /// - public string LimitDescription { get; set; } = string.Empty; - /// - /// The current counter value - /// - public int Current { get; set; } - /// - /// The limit per time period - /// - public int? Limit { get; set; } - /// - /// The time period the limit is for - /// - public TimeSpan? TimePeriod { get; set; } - - /// - /// ctor - /// - public RateLimitUpdateEvent(int itemId, string apiLimit, string limitDescription, int current, int? limit, TimeSpan? timePeriod) - { - ItemId = itemId; - ApiLimit = apiLimit; - LimitDescription = limitDescription; - Current = current; - Limit = limit; - TimePeriod = timePeriod; - } - + ItemId = itemId; + ApiLimit = apiLimit; + LimitDescription = limitDescription; + Current = current; + Limit = limit; + TimePeriod = timePeriod; } + } diff --git a/CryptoExchange.Net/RateLimiting/Trackers/DecayWindowTracker.cs b/CryptoExchange.Net/RateLimiting/Trackers/DecayWindowTracker.cs index 86b783b..0c4cca2 100644 --- a/CryptoExchange.Net/RateLimiting/Trackers/DecayWindowTracker.cs +++ b/CryptoExchange.Net/RateLimiting/Trackers/DecayWindowTracker.cs @@ -1,89 +1,88 @@ -using System; +using System; using CryptoExchange.Net.RateLimiting.Interfaces; -namespace CryptoExchange.Net.RateLimiting.Trackers +namespace CryptoExchange.Net.RateLimiting.Trackers; + +internal class DecayWindowTracker : IWindowTracker { - internal class DecayWindowTracker : IWindowTracker + /// + public TimeSpan TimePeriod { get; } + /// + /// Decrease rate per TimePeriod + /// + public double DecreaseRate { get; } + /// + public int Limit { get; } + /// + public int Current => _currentWeight; + + private int _currentWeight; + private DateTime _lastDecrease = DateTime.UtcNow; + + public DecayWindowTracker(int limit, TimeSpan period, double decayRate) { - /// - public TimeSpan TimePeriod { get; } - /// - /// Decrease rate per TimePeriod - /// - public double DecreaseRate { get; } - /// - public int Limit { get; } - /// - public int Current => _currentWeight; + Limit = limit; + TimePeriod = period; + DecreaseRate = decayRate; + } - private int _currentWeight = 0; - private DateTime _lastDecrease = DateTime.UtcNow; + /// + public TimeSpan GetWaitTime(int weight) + { + // Decrease the counter based on the last update time and decay rate + DecreaseCounter(DateTime.UtcNow); - public DecayWindowTracker(int limit, TimeSpan period, double decayRate) + if (Current + weight > Limit) { - Limit = limit; - TimePeriod = period; - DecreaseRate = decayRate; - } - - /// - public TimeSpan GetWaitTime(int weight) - { - // Decrease the counter based on the last update time and decay rate - DecreaseCounter(DateTime.UtcNow); - - if (Current + weight > Limit) + // The weight would cause the rate limit to be passed + if (Current == 0) { - // The weight would cause the rate limit to be passed - if (Current == 0) - { - throw new Exception("Request limit reached without any prior request. " + - $"This request can never execute with the current rate limiter. Request weight: {weight}, RateLimit: {Limit}"); - } - - // Determine the time to wait before this weight can be applied without going over the rate limit - return DetermineWaitTime(weight); + throw new Exception("Request limit reached without any prior request. " + + $"This request can never execute with the current rate limiter. Request weight: {weight}, RateLimit: {Limit}"); } - // Weight can fit without going over limit - return TimeSpan.Zero; + // Determine the time to wait before this weight can be applied without going over the rate limit + return DetermineWaitTime(weight); } - /// - public void ApplyWeight(int weight) - { - if (_currentWeight == 0) - _lastDecrease = DateTime.UtcNow; - _currentWeight += weight; - } + // Weight can fit without going over limit + return TimeSpan.Zero; + } - /// - /// Decrease the counter based on time passed since last update and the decay rate - /// - /// - protected void DecreaseCounter(DateTime time) - { - var dif = (time - _lastDecrease).TotalMilliseconds / TimePeriod.TotalMilliseconds * DecreaseRate; - var decrease = (int)Math.Floor(dif); - if (decrease >= 1) - { - _currentWeight = Math.Max(0, _currentWeight - (int)Math.Floor(dif)); - _lastDecrease = time; - } - } + /// + public void ApplyWeight(int weight) + { + if (_currentWeight == 0) + _lastDecrease = DateTime.UtcNow; + _currentWeight += weight; + } - /// - /// Determine the time to wait before the weight would fit - /// - /// - /// - private TimeSpan DetermineWaitTime(int requestWeight) + /// + /// Decrease the counter based on time passed since last update and the decay rate + /// + /// + protected void DecreaseCounter(DateTime time) + { + var dif = (time - _lastDecrease).TotalMilliseconds / TimePeriod.TotalMilliseconds * DecreaseRate; + var decrease = (int)Math.Floor(dif); + if (decrease >= 1) { - var weightToRemove = Math.Max(Current - (Limit - requestWeight), 0); - var result = TimeSpan.FromMilliseconds(Math.Ceiling(weightToRemove / DecreaseRate) * TimePeriod.TotalMilliseconds); - if (result < TimeSpan.Zero) - return TimeSpan.Zero; - return result; + _currentWeight = Math.Max(0, _currentWeight - (int)Math.Floor(dif)); + _lastDecrease = time; } } + + /// + /// Determine the time to wait before the weight would fit + /// + /// + /// + private TimeSpan DetermineWaitTime(int requestWeight) + { + var weightToRemove = Math.Max(Current - (Limit - requestWeight), 0); + var result = TimeSpan.FromMilliseconds(Math.Ceiling(weightToRemove / DecreaseRate) * TimePeriod.TotalMilliseconds); + if (result < TimeSpan.Zero) + return TimeSpan.Zero; + return result; + } } diff --git a/CryptoExchange.Net/RateLimiting/Trackers/FixedAfterStartWindowTracker.cs b/CryptoExchange.Net/RateLimiting/Trackers/FixedAfterStartWindowTracker.cs index be0d713..ee2a028 100644 --- a/CryptoExchange.Net/RateLimiting/Trackers/FixedAfterStartWindowTracker.cs +++ b/CryptoExchange.Net/RateLimiting/Trackers/FixedAfterStartWindowTracker.cs @@ -1,106 +1,105 @@ -using System; +using System; using System.Collections.Generic; using CryptoExchange.Net.RateLimiting.Interfaces; -namespace CryptoExchange.Net.RateLimiting.Trackers +namespace CryptoExchange.Net.RateLimiting.Trackers; + +internal class FixedAfterStartWindowTracker : IWindowTracker { - internal class FixedAfterStartWindowTracker : IWindowTracker + /// + public TimeSpan TimePeriod { get; } + /// + public int Limit { get; } + /// + public int Current => _currentWeight; + + private readonly Queue _entries; + private int _currentWeight; + private DateTime? _nextReset; + + /// + /// Additional wait time to apply to account for time offset between server and client + /// + private static TimeSpan _fixedWindowBuffer = TimeSpan.FromMilliseconds(1000); + + public FixedAfterStartWindowTracker(int limit, TimeSpan period) { - /// - public TimeSpan TimePeriod { get; } - /// - public int Limit { get; } - /// - public int Current => _currentWeight; + Limit = limit; + TimePeriod = period; + _entries = new Queue(); + } - private readonly Queue _entries; - private int _currentWeight = 0; - private DateTime? _nextReset; + public TimeSpan GetWaitTime(int weight) + { + // Remove requests no longer in time period from the history + var checkTime = DateTime.UtcNow; + if (_nextReset != null && checkTime > _nextReset) + RemoveBefore(_nextReset.Value); - /// - /// Additional wait time to apply to account for time offset between server and client - /// - private static TimeSpan _fixedWindowBuffer = TimeSpan.FromMilliseconds(1000); + if (Current == 0) + _nextReset = null; - public FixedAfterStartWindowTracker(int limit, TimeSpan period) + if (Current + weight > Limit) { - Limit = limit; - TimePeriod = period; - _entries = new Queue(); - } - - public TimeSpan GetWaitTime(int weight) - { - // Remove requests no longer in time period from the history - var checkTime = DateTime.UtcNow; - if (_nextReset != null && checkTime > _nextReset) - RemoveBefore(_nextReset.Value); - + // The weight would cause the rate limit to be passed if (Current == 0) - _nextReset = null; - - if (Current + weight > Limit) { - // The weight would cause the rate limit to be passed - if (Current == 0) - { - throw new Exception("Request limit reached without any prior request. " + - $"This request can never execute with the current rate limiter. Request weight: {weight}, RateLimit: {Limit}"); - } - - // Determine the time to wait before this weight can be applied without going over the rate limit - return DetermineWaitTime(); + throw new Exception("Request limit reached without any prior request. " + + $"This request can never execute with the current rate limiter. Request weight: {weight}, RateLimit: {Limit}"); } - // Weight can fit without going over limit - return TimeSpan.Zero; + // Determine the time to wait before this weight can be applied without going over the rate limit + return DetermineWaitTime(); } - /// - public void ApplyWeight(int weight) - { - if (_currentWeight == 0) - _nextReset = DateTime.UtcNow + TimePeriod; - _currentWeight += weight; - _entries.Enqueue(new LimitEntry(DateTime.UtcNow, weight)); - } + // Weight can fit without going over limit + return TimeSpan.Zero; + } - /// - /// Remove items before a certain time - /// - /// - protected void RemoveBefore(DateTime time) + /// + public void ApplyWeight(int weight) + { + if (_currentWeight == 0) + _nextReset = DateTime.UtcNow + TimePeriod; + _currentWeight += weight; + _entries.Enqueue(new LimitEntry(DateTime.UtcNow, weight)); + } + + /// + /// Remove items before a certain time + /// + /// + protected void RemoveBefore(DateTime time) + { + while (true) { - while (true) + if (_entries.Count == 0) + break; + + var firstItem = _entries.Peek(); + if (firstItem.Timestamp < time) { - if (_entries.Count == 0) - break; - - var firstItem = _entries.Peek(); - if (firstItem.Timestamp < time) - { - _entries.Dequeue(); - _currentWeight -= firstItem.Weight; - } - else - { - // Either no entries left, or the entry time is still within the window - break; - } + _entries.Dequeue(); + _currentWeight -= firstItem.Weight; + } + else + { + // Either no entries left, or the entry time is still within the window + break; } - } - - /// - /// Determine the time to wait before a new item would fit - /// - /// - private TimeSpan DetermineWaitTime() - { - var checkTime = DateTime.UtcNow; - var result = (_nextReset!.Value - checkTime) + _fixedWindowBuffer; - if (result < TimeSpan.Zero) - return TimeSpan.Zero; - return result; } } + + /// + /// Determine the time to wait before a new item would fit + /// + /// + private TimeSpan DetermineWaitTime() + { + var checkTime = DateTime.UtcNow; + var result = (_nextReset!.Value - checkTime) + _fixedWindowBuffer; + if (result < TimeSpan.Zero) + return TimeSpan.Zero; + return result; + } } diff --git a/CryptoExchange.Net/RateLimiting/Trackers/FixedWindowTracker.cs b/CryptoExchange.Net/RateLimiting/Trackers/FixedWindowTracker.cs index 1481894..2d094ef 100644 --- a/CryptoExchange.Net/RateLimiting/Trackers/FixedWindowTracker.cs +++ b/CryptoExchange.Net/RateLimiting/Trackers/FixedWindowTracker.cs @@ -1,102 +1,101 @@ -using System; +using System; using System.Collections.Generic; using CryptoExchange.Net.RateLimiting.Interfaces; -namespace CryptoExchange.Net.RateLimiting.Trackers +namespace CryptoExchange.Net.RateLimiting.Trackers; + +internal class FixedWindowTracker : IWindowTracker { - internal class FixedWindowTracker : IWindowTracker + /// + public TimeSpan TimePeriod { get; } + /// + public int Limit { get; } + /// + public int Current => _currentWeight; + + private readonly Queue _entries; + private int _currentWeight; + + /// + /// Additional wait time to apply to account for time offset between server and client + /// + private static readonly TimeSpan _fixedWindowBuffer = TimeSpan.FromMilliseconds(1000); + + public FixedWindowTracker(int limit, TimeSpan period) { - /// - public TimeSpan TimePeriod { get; } - /// - public int Limit { get; } - /// - public int Current => _currentWeight; + Limit = limit; + TimePeriod = period; + _entries = new Queue(); + } - private readonly Queue _entries; - private int _currentWeight = 0; + /// + public TimeSpan GetWaitTime(int weight) + { + // Remove requests no longer in time period from the history + var checkTime = DateTime.UtcNow; + RemoveBefore(checkTime.AddTicks(-(checkTime.Ticks % TimePeriod.Ticks))); - /// - /// Additional wait time to apply to account for time offset between server and client - /// - private static readonly TimeSpan _fixedWindowBuffer = TimeSpan.FromMilliseconds(1000); - - public FixedWindowTracker(int limit, TimeSpan period) + if (Current + weight > Limit) { - Limit = limit; - TimePeriod = period; - _entries = new Queue(); - } - - /// - public TimeSpan GetWaitTime(int weight) - { - // Remove requests no longer in time period from the history - var checkTime = DateTime.UtcNow; - RemoveBefore(checkTime.AddTicks(-(checkTime.Ticks % TimePeriod.Ticks))); - - if (Current + weight > Limit) + // The weight would cause the rate limit to be passed + if (Current == 0) { - // The weight would cause the rate limit to be passed - if (Current == 0) - { - throw new Exception("Request limit reached without any prior request. " + - $"This request can never execute with the current rate limiter. Request weight: {weight}, RateLimit: {Limit}"); - } - - // Determine the time to wait before this weight can be applied without going over the rate limit - return DetermineWaitTime(); + throw new Exception("Request limit reached without any prior request. " + + $"This request can never execute with the current rate limiter. Request weight: {weight}, RateLimit: {Limit}"); } - // Weight can fit without going over limit - return TimeSpan.Zero; + // Determine the time to wait before this weight can be applied without going over the rate limit + return DetermineWaitTime(); } - /// - public void ApplyWeight(int weight) - { - _currentWeight += weight; - _entries.Enqueue(new LimitEntry(DateTime.UtcNow, weight)); - } + // Weight can fit without going over limit + return TimeSpan.Zero; + } - /// - /// Remove items before a certain time - /// - /// - protected void RemoveBefore(DateTime time) + /// + public void ApplyWeight(int weight) + { + _currentWeight += weight; + _entries.Enqueue(new LimitEntry(DateTime.UtcNow, weight)); + } + + /// + /// Remove items before a certain time + /// + /// + protected void RemoveBefore(DateTime time) + { + while (true) { - while (true) + if (_entries.Count == 0) + break; + + var firstItem = _entries.Peek(); + if (firstItem.Timestamp < time) { - if (_entries.Count == 0) - break; - - var firstItem = _entries.Peek(); - if (firstItem.Timestamp < time) - { - _entries.Dequeue(); - _currentWeight -= firstItem.Weight; - } - else - { - // Either no entries left, or the entry time is still within the window - break; - } + _entries.Dequeue(); + _currentWeight -= firstItem.Weight; + } + else + { + // Either no entries left, or the entry time is still within the window + break; } - } - - /// - /// Determine the time to wait before a new item would fit - /// - /// - private TimeSpan DetermineWaitTime() - { - var checkTime = DateTime.UtcNow; - var startCurrentWindow = checkTime.AddTicks(-(checkTime.Ticks % TimePeriod.Ticks)); - var wait = startCurrentWindow.Add(TimePeriod) - checkTime; - var result = wait.Add(_fixedWindowBuffer); - if (result < TimeSpan.Zero) - return TimeSpan.Zero; - return result; } } + + /// + /// Determine the time to wait before a new item would fit + /// + /// + private TimeSpan DetermineWaitTime() + { + var checkTime = DateTime.UtcNow; + var startCurrentWindow = checkTime.AddTicks(-(checkTime.Ticks % TimePeriod.Ticks)); + var wait = startCurrentWindow.Add(TimePeriod) - checkTime; + var result = wait.Add(_fixedWindowBuffer); + if (result < TimeSpan.Zero) + return TimeSpan.Zero; + return result; + } } diff --git a/CryptoExchange.Net/RateLimiting/Trackers/SlidingWindowTracker.cs b/CryptoExchange.Net/RateLimiting/Trackers/SlidingWindowTracker.cs index 19f9abb..6d127f5 100644 --- a/CryptoExchange.Net/RateLimiting/Trackers/SlidingWindowTracker.cs +++ b/CryptoExchange.Net/RateLimiting/Trackers/SlidingWindowTracker.cs @@ -1,108 +1,107 @@ -using System; +using System; using System.Collections.Generic; using CryptoExchange.Net.RateLimiting.Interfaces; -namespace CryptoExchange.Net.RateLimiting.Trackers +namespace CryptoExchange.Net.RateLimiting.Trackers; + +internal class SlidingWindowTracker : IWindowTracker { - internal class SlidingWindowTracker : IWindowTracker + /// + public TimeSpan TimePeriod { get; } + /// + public int Limit { get; } + /// + public int Current => _currentWeight; + + private readonly List _entries; + private int _currentWeight; + + /// + /// Additional wait time to apply to account for fluctuating request times + /// + private static readonly TimeSpan _slidingWindowBuffer = TimeSpan.FromMilliseconds(1000); + + public SlidingWindowTracker(int limit, TimeSpan period) { - /// - public TimeSpan TimePeriod { get; } - /// - public int Limit { get; } - /// - public int Current => _currentWeight; + Limit = limit; + TimePeriod = period; + _entries = new List(); + } - private readonly List _entries; - private int _currentWeight = 0; + /// + public TimeSpan GetWaitTime(int weight) + { + // Remove requests no longer in time period from the history + RemoveBefore(DateTime.UtcNow - TimePeriod); - /// - /// Additional wait time to apply to account for fluctuating request times - /// - private static readonly TimeSpan _slidingWindowBuffer = TimeSpan.FromMilliseconds(1000); - - public SlidingWindowTracker(int limit, TimeSpan period) + if (Current + weight > Limit) { - Limit = limit; - TimePeriod = period; - _entries = new List(); - } - - /// - public TimeSpan GetWaitTime(int weight) - { - // Remove requests no longer in time period from the history - RemoveBefore(DateTime.UtcNow - TimePeriod); - - if (Current + weight > Limit) + // The weight would cause the rate limit to be passed + if (Current == 0) { - // The weight would cause the rate limit to be passed - if (Current == 0) - { - throw new Exception("Request limit reached without any prior request. " + - $"This request can never execute with the current rate limiter. Request weight: {weight}, RateLimit: {Limit}"); - } - - // Determine the time to wait before this weight can be applied without going over the rate limit - return DetermineWaitTime(weight); + throw new Exception("Request limit reached without any prior request. " + + $"This request can never execute with the current rate limiter. Request weight: {weight}, RateLimit: {Limit}"); } - // Weight can fit without going over limit - return TimeSpan.Zero; + // Determine the time to wait before this weight can be applied without going over the rate limit + return DetermineWaitTime(weight); } - /// - public void ApplyWeight(int weight) - { - _currentWeight += weight; - _entries.Add(new LimitEntry(DateTime.UtcNow, weight)); - } + // Weight can fit without going over limit + return TimeSpan.Zero; + } - /// - /// Remove items before a certain time - /// - /// - protected void RemoveBefore(DateTime time) - { - for (var i = 0; i < _entries.Count; i++) - { - if (_entries[i].Timestamp < time) - { - var entry = _entries[i]; - _entries.Remove(entry); - _currentWeight -= entry.Weight; - i--; - } - else - { - break; - } - } - } + /// + public void ApplyWeight(int weight) + { + _currentWeight += weight; + _entries.Add(new LimitEntry(DateTime.UtcNow, weight)); + } - /// - /// Determine the time to wait before the weight would fit - /// - /// - private TimeSpan DetermineWaitTime(int requestWeight) + /// + /// Remove items before a certain time + /// + /// + protected void RemoveBefore(DateTime time) + { + for (var i = 0; i < _entries.Count; i++) { - var weightToRemove = Math.Max(Current - (Limit - requestWeight), 0); - var removedWeight = 0; - for (var i = 0; i < _entries.Count; i++) + if (_entries[i].Timestamp < time) { var entry = _entries[i]; - removedWeight += entry.Weight; - if (removedWeight >= weightToRemove) - { - var result = entry.Timestamp + TimePeriod + _slidingWindowBuffer - DateTime.UtcNow; - if (result < TimeSpan.Zero) - return TimeSpan.Zero; - return result; - } + _entries.Remove(entry); + _currentWeight -= entry.Weight; + i--; + } + else + { + break; } - - throw new Exception("Request not possible to execute with current rate limit guard. " + - $" Request weight: {requestWeight}, RateLimit: {Limit}"); } } + + /// + /// Determine the time to wait before the weight would fit + /// + /// + private TimeSpan DetermineWaitTime(int requestWeight) + { + var weightToRemove = Math.Max(Current - (Limit - requestWeight), 0); + var removedWeight = 0; + for (var i = 0; i < _entries.Count; i++) + { + var entry = _entries[i]; + removedWeight += entry.Weight; + if (removedWeight >= weightToRemove) + { + var result = entry.Timestamp + TimePeriod + _slidingWindowBuffer - DateTime.UtcNow; + if (result < TimeSpan.Zero) + return TimeSpan.Zero; + return result; + } + } + + throw new Exception("Request not possible to execute with current rate limit guard. " + + $" Request weight: {requestWeight}, RateLimit: {Limit}"); + } } diff --git a/CryptoExchange.Net/Requests/Request.cs b/CryptoExchange.Net/Requests/Request.cs index 23f1f1c..3ca65b0 100644 --- a/CryptoExchange.Net/Requests/Request.cs +++ b/CryptoExchange.Net/Requests/Request.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; @@ -8,80 +8,79 @@ using System.Threading; using System.Threading.Tasks; using CryptoExchange.Net.Interfaces; -namespace CryptoExchange.Net.Requests +namespace CryptoExchange.Net.Requests; + +/// +/// Request object, wrapper for HttpRequestMessage +/// +public class Request : IRequest { + private readonly HttpRequestMessage _request; + private readonly HttpClient _httpClient; + /// - /// Request object, wrapper for HttpRequestMessage + /// Create request object for web request /// - public class Request : IRequest + /// + /// + /// + public Request(HttpRequestMessage request, HttpClient client, int requestId) { - private readonly HttpRequestMessage _request; - private readonly HttpClient _httpClient; + _httpClient = client; + _request = request; + RequestId = requestId; + } + + /// + public string? Content { get; private set; } - /// - /// Create request object for web request - /// - /// - /// - /// - public Request(HttpRequestMessage request, HttpClient client, int requestId) - { - _httpClient = client; - _request = request; - RequestId = requestId; - } - - /// - public string? Content { get; private set; } + /// + public string Accept + { + set => _request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(value)); + } - /// - public string Accept - { - set => _request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(value)); - } + /// + public HttpMethod Method + { + get => _request.Method; + set => _request.Method = value; + } - /// - public HttpMethod Method - { - get => _request.Method; - set => _request.Method = value; - } + /// + public Uri Uri => _request.RequestUri!; - /// - public Uri Uri => _request.RequestUri!; + /// + public int RequestId { get; } - /// - public int RequestId { get; } + /// + public void SetContent(string data, string contentType) + { + Content = data; + _request.Content = new StringContent(data, Encoding.UTF8, contentType); + } - /// - public void SetContent(string data, string contentType) - { - Content = data; - _request.Content = new StringContent(data, Encoding.UTF8, contentType); - } + /// + public void AddHeader(string key, string value) + { + _request.Headers.Add(key, value); + } - /// - public void AddHeader(string key, string value) - { - _request.Headers.Add(key, value); - } + /// + public KeyValuePair[] GetHeaders() + { + return _request.Headers.Select(h => new KeyValuePair(h.Key, h.Value.ToArray())).ToArray(); + } - /// - public KeyValuePair[] GetHeaders() - { - return _request.Headers.Select(h => new KeyValuePair(h.Key, h.Value.ToArray())).ToArray(); - } + /// + public void SetContent(byte[] data) + { + _request.Content = new ByteArrayContent(data); + } - /// - public void SetContent(byte[] data) - { - _request.Content = new ByteArrayContent(data); - } - - /// - public async Task GetResponseAsync(CancellationToken cancellationToken) - { - return new Response(await _httpClient.SendAsync(_request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false)); - } + /// + public async Task GetResponseAsync(CancellationToken cancellationToken) + { + return new Response(await _httpClient.SendAsync(_request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false)); } } diff --git a/CryptoExchange.Net/Requests/RequestFactory.cs b/CryptoExchange.Net/Requests/RequestFactory.cs index 83a5dc7..ca92c50 100644 --- a/CryptoExchange.Net/Requests/RequestFactory.cs +++ b/CryptoExchange.Net/Requests/RequestFactory.cs @@ -1,67 +1,66 @@ -using System; +using System; using System.Net; using System.Net.Http; using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Objects; -namespace CryptoExchange.Net.Requests +namespace CryptoExchange.Net.Requests; + +/// +/// Request factory +/// +public class RequestFactory : IRequestFactory { - /// - /// Request factory - /// - public class RequestFactory : IRequestFactory + private HttpClient? _httpClient; + + /// + public void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? httpClient = null) { - private HttpClient? _httpClient; + if (httpClient == null) + httpClient = CreateClient(proxy, requestTimeout); - /// - public void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? client = null) + _httpClient = httpClient; + } + + /// + public IRequest Create(HttpMethod method, Uri uri, int requestId) + { + if (_httpClient == null) + throw new InvalidOperationException("Cant create request before configuring http client"); + + return new Request(new HttpRequestMessage(method, uri), _httpClient, requestId); + } + + /// + public void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout) + { + _httpClient = CreateClient(proxy, requestTimeout); + } + + private static HttpClient CreateClient(ApiProxy? proxy, TimeSpan requestTimeout) + { + var handler = new HttpClientHandler(); + try { - if (client == null) - client = CreateClient(proxy, requestTimeout); - - _httpClient = client; + handler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; + handler.DefaultProxyCredentials = CredentialCache.DefaultCredentials; } + catch (PlatformNotSupportedException) { } + catch (NotImplementedException) { } // Mono runtime throws NotImplementedException - /// - public IRequest Create(HttpMethod method, Uri uri, int requestId) + if (proxy != null) { - if (_httpClient == null) - throw new InvalidOperationException("Cant create request before configuring http client"); - - return new Request(new HttpRequestMessage(method, uri), _httpClient, requestId); - } - - /// - public void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout) - { - _httpClient = CreateClient(proxy, requestTimeout); - } - - private static HttpClient CreateClient(ApiProxy? proxy, TimeSpan requestTimeout) - { - var handler = new HttpClientHandler(); - try + handler.Proxy = new WebProxy { - handler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; - handler.DefaultProxyCredentials = CredentialCache.DefaultCredentials; - } - catch (PlatformNotSupportedException) { } - catch (NotImplementedException) { } // Mono runtime throws NotImplementedException - - if (proxy != null) - { - handler.Proxy = new WebProxy - { - Address = new Uri($"{proxy.Host}:{proxy.Port}"), - Credentials = proxy.Password == null ? null : new NetworkCredential(proxy.Login, proxy.Password) - }; - } - - var client = new HttpClient(handler) - { - Timeout = requestTimeout + Address = new Uri($"{proxy.Host}:{proxy.Port}"), + Credentials = proxy.Password == null ? null : new NetworkCredential(proxy.Login, proxy.Password) }; - return client; } + + var client = new HttpClient(handler) + { + Timeout = requestTimeout + }; + return client; } } diff --git a/CryptoExchange.Net/Requests/Response.cs b/CryptoExchange.Net/Requests/Response.cs index 78505b1..721b1fe 100644 --- a/CryptoExchange.Net/Requests/Response.cs +++ b/CryptoExchange.Net/Requests/Response.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; @@ -6,46 +6,45 @@ using System.Net.Http; using System.Threading.Tasks; using CryptoExchange.Net.Interfaces; -namespace CryptoExchange.Net.Requests +namespace CryptoExchange.Net.Requests; + +/// +/// Response object, wrapper for HttpResponseMessage +/// +internal class Response : IResponse { + private readonly HttpResponseMessage _response; + + /// + public HttpStatusCode StatusCode => _response.StatusCode; + + /// + public bool IsSuccessStatusCode => _response.IsSuccessStatusCode; + + /// + public long? ContentLength => _response.Content.Headers.ContentLength; + + /// + public KeyValuePair[] ResponseHeaders => _response.Headers.Select(x => new KeyValuePair(x.Key, x.Value.ToArray())).ToArray(); + /// - /// Response object, wrapper for HttpResponseMessage + /// Create response for a http response message /// - internal class Response : IResponse + /// The actual response + public Response(HttpResponseMessage response) { - private readonly HttpResponseMessage _response; + this._response = response; + } - /// - public HttpStatusCode StatusCode => _response.StatusCode; + /// + public async Task GetResponseStreamAsync() + { + return await _response.Content.ReadAsStreamAsync().ConfigureAwait(false); + } - /// - public bool IsSuccessStatusCode => _response.IsSuccessStatusCode; - - /// - public long? ContentLength => _response.Content.Headers.ContentLength; - - /// - public KeyValuePair[] ResponseHeaders => _response.Headers.Select(x => new KeyValuePair(x.Key, x.Value.ToArray())).ToArray(); - - /// - /// Create response for a http response message - /// - /// The actual response - public Response(HttpResponseMessage response) - { - this._response = response; - } - - /// - public async Task GetResponseStreamAsync() - { - return await _response.Content.ReadAsStreamAsync().ConfigureAwait(false); - } - - /// - public void Close() - { - _response.Dispose(); - } + /// + public void Close() + { + _response.Dispose(); } } diff --git a/CryptoExchange.Net/SharedApis/Enums/SharedFeeAssetType.cs b/CryptoExchange.Net/SharedApis/Enums/SharedFeeAssetType.cs index f1e08a5..baab672 100644 --- a/CryptoExchange.Net/SharedApis/Enums/SharedFeeAssetType.cs +++ b/CryptoExchange.Net/SharedApis/Enums/SharedFeeAssetType.cs @@ -1,29 +1,28 @@ -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Fee asset selection type +/// +public enum SharedFeeAssetType { /// - /// Fee asset selection type + /// Fee is always in the base asset /// - public enum SharedFeeAssetType - { - /// - /// Fee is always in the base asset - /// - BaseAsset, - /// - /// Fee is always in the quote asset - /// - QuoteAsset, - /// - /// Fee is always in the input asset - /// - InputAsset, - /// - /// Fee is always in the output asset - /// - OutputAsset, - /// - /// Fee is variable - /// - Variable - } + BaseAsset, + /// + /// Fee is always in the quote asset + /// + QuoteAsset, + /// + /// Fee is always in the input asset + /// + InputAsset, + /// + /// Fee is always in the output asset + /// + OutputAsset, + /// + /// Fee is variable + /// + Variable } diff --git a/CryptoExchange.Net/SharedApis/Enums/SharedFeeDeductionType.cs b/CryptoExchange.Net/SharedApis/Enums/SharedFeeDeductionType.cs index 3da2b0e..112552f 100644 --- a/CryptoExchange.Net/SharedApis/Enums/SharedFeeDeductionType.cs +++ b/CryptoExchange.Net/SharedApis/Enums/SharedFeeDeductionType.cs @@ -1,17 +1,16 @@ -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Fee deduction type +/// +public enum SharedFeeDeductionType { /// - /// Fee deduction type + /// The fee is deducted from the output amount. For example buying 1 ETH at 1000 USDT with a 1% fee would cost 1000 USDT and output 0.99 ETH /// - public enum SharedFeeDeductionType - { - /// - /// The fee is deducted from the output amount. For example buying 1 ETH at 1000 USDT with a 1% fee would cost 1000 USDT and output 0.99 ETH - /// - DeductFromOutput, - /// - /// The fee is added to the order cost. For example buying 1 ETH at 1000 USDT with a 1% fee would cost 1010 USDT and output 1 ETH - /// - AddToCost - } + DeductFromOutput, + /// + /// The fee is added to the order cost. For example buying 1 ETH at 1000 USDT with a 1% fee would cost 1010 USDT and output 1 ETH + /// + AddToCost } diff --git a/CryptoExchange.Net/SharedApis/Enums/SharedKlineInterval.cs b/CryptoExchange.Net/SharedApis/Enums/SharedKlineInterval.cs index 4c2a4d1..5598bf5 100644 --- a/CryptoExchange.Net/SharedApis/Enums/SharedKlineInterval.cs +++ b/CryptoExchange.Net/SharedApis/Enums/SharedKlineInterval.cs @@ -1,65 +1,64 @@ -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Kline interval +/// +public enum SharedKlineInterval { /// - /// Kline interval + /// 1 min /// - public enum SharedKlineInterval - { - /// - /// 1 min - /// - OneMinute = 60, - /// - /// 3 min - /// - ThreeMinutes = 60 * 3, - /// - /// 5 min - /// - FiveMinutes = 60 * 5, - /// - /// 15 min - /// - FifteenMinutes = 60 * 15, - /// - /// Thirty minutes - /// - ThirtyMinutes = 60 * 30, - /// - /// 1 hour - /// - OneHour = 60 * 60, - /// - /// 2 hours - /// - TwoHours = 60 * 60 * 2, - /// - /// 4 hours - /// - FourHours = 60 * 60 * 4, - /// - /// 6 hours - /// - SixHours = 60 * 60 * 6, - /// - /// 8 hours - /// - EightHours = 60 * 60 * 8, - /// - /// 12 hours - /// - TwelveHours = 60 * 60 * 12, - /// - /// 1 day - /// - OneDay = 60 * 60 * 24, - /// - /// 1 week - /// - OneWeek = 60 * 60 * 24 * 7, - /// - /// 1 month - /// - OneMonth = 60 * 60 * 24 * 30 - } + OneMinute = 60, + /// + /// 3 min + /// + ThreeMinutes = 60 * 3, + /// + /// 5 min + /// + FiveMinutes = 60 * 5, + /// + /// 15 min + /// + FifteenMinutes = 60 * 15, + /// + /// Thirty minutes + /// + ThirtyMinutes = 60 * 30, + /// + /// 1 hour + /// + OneHour = 60 * 60, + /// + /// 2 hours + /// + TwoHours = 60 * 60 * 2, + /// + /// 4 hours + /// + FourHours = 60 * 60 * 4, + /// + /// 6 hours + /// + SixHours = 60 * 60 * 6, + /// + /// 8 hours + /// + EightHours = 60 * 60 * 8, + /// + /// 12 hours + /// + TwelveHours = 60 * 60 * 12, + /// + /// 1 day + /// + OneDay = 60 * 60 * 24, + /// + /// 1 week + /// + OneWeek = 60 * 60 * 24 * 7, + /// + /// 1 month + /// + OneMonth = 60 * 60 * 24 * 30 } diff --git a/CryptoExchange.Net/SharedApis/Enums/SharedLeverageSettingMode.cs b/CryptoExchange.Net/SharedApis/Enums/SharedLeverageSettingMode.cs index b069b6b..c026344 100644 --- a/CryptoExchange.Net/SharedApis/Enums/SharedLeverageSettingMode.cs +++ b/CryptoExchange.Net/SharedApis/Enums/SharedLeverageSettingMode.cs @@ -1,21 +1,20 @@ -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Leverage setting mode +/// +public enum SharedLeverageSettingMode { /// - /// Leverage setting mode + /// Leverage is configured per side (in hedge mode) /// - public enum SharedLeverageSettingMode - { - /// - /// Leverage is configured per side (in hedge mode) - /// - PerSide, - /// - /// Leverage is configured for the symbol - /// - PerSymbol, - /// - /// Leverage is configured for the entire account - /// - PerAccount - } + PerSide, + /// + /// Leverage is configured for the symbol + /// + PerSymbol, + /// + /// Leverage is configured for the entire account + /// + PerAccount } diff --git a/CryptoExchange.Net/SharedApis/Enums/SharedMarginMode.cs b/CryptoExchange.Net/SharedApis/Enums/SharedMarginMode.cs index 7f82b25..54b4f6a 100644 --- a/CryptoExchange.Net/SharedApis/Enums/SharedMarginMode.cs +++ b/CryptoExchange.Net/SharedApis/Enums/SharedMarginMode.cs @@ -1,17 +1,16 @@ -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Margin mode +/// +public enum SharedMarginMode { /// - /// Margin mode + /// Cross margin, margin is shared across symbols /// - public enum SharedMarginMode - { - /// - /// Cross margin, margin is shared across symbols - /// - Cross, - /// - /// Isolated margin, margin is isolated on a symbol - /// - Isolated - } + Cross, + /// + /// Isolated margin, margin is isolated on a symbol + /// + Isolated } diff --git a/CryptoExchange.Net/SharedApis/Enums/SharedOrderSide.cs b/CryptoExchange.Net/SharedApis/Enums/SharedOrderSide.cs index 7b87ab5..0fc5c7e 100644 --- a/CryptoExchange.Net/SharedApis/Enums/SharedOrderSide.cs +++ b/CryptoExchange.Net/SharedApis/Enums/SharedOrderSide.cs @@ -1,17 +1,16 @@ -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Side of an order +/// +public enum SharedOrderSide { /// - /// Side of an order + /// Buy order /// - public enum SharedOrderSide - { - /// - /// Buy order - /// - Buy, - /// - /// Sell order - /// - Sell - } + Buy, + /// + /// Sell order + /// + Sell } diff --git a/CryptoExchange.Net/SharedApis/Enums/SharedOrderStatus.cs b/CryptoExchange.Net/SharedApis/Enums/SharedOrderStatus.cs index a7aa928..c7909b0 100644 --- a/CryptoExchange.Net/SharedApis/Enums/SharedOrderStatus.cs +++ b/CryptoExchange.Net/SharedApis/Enums/SharedOrderStatus.cs @@ -1,21 +1,20 @@ -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Status of an order +/// +public enum SharedOrderStatus { /// - /// Status of an order + /// Order is open waiting to be filled /// - public enum SharedOrderStatus - { - /// - /// Order is open waiting to be filled - /// - Open, - /// - /// Order has been fully filled - /// - Filled, - /// - /// Order has been canceled - /// - Canceled - } + Open, + /// + /// Order has been fully filled + /// + Filled, + /// + /// Order has been canceled + /// + Canceled } diff --git a/CryptoExchange.Net/SharedApis/Enums/SharedOrderType.cs b/CryptoExchange.Net/SharedApis/Enums/SharedOrderType.cs index ad59118..6a42314 100644 --- a/CryptoExchange.Net/SharedApis/Enums/SharedOrderType.cs +++ b/CryptoExchange.Net/SharedApis/Enums/SharedOrderType.cs @@ -1,25 +1,24 @@ -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Type of an order +/// +public enum SharedOrderType { /// - /// Type of an order + /// Limit order, execute at a specific price /// - public enum SharedOrderType - { - /// - /// Limit order, execute at a specific price - /// - Limit, - /// - /// Limit maker order, a limit order with the condition that is will never be executed as a maker - /// - LimitMaker, - /// - /// Market order, execute at the best price currently available - /// - Market, - /// - /// Other order type, used for parsing unsupported order types - /// - Other - } + Limit, + /// + /// Limit maker order, a limit order with the condition that is will never be executed as a maker + /// + LimitMaker, + /// + /// Market order, execute at the best price currently available + /// + Market, + /// + /// Other order type, used for parsing unsupported order types + /// + Other } diff --git a/CryptoExchange.Net/SharedApis/Enums/SharedPaginationSupport.cs b/CryptoExchange.Net/SharedApis/Enums/SharedPaginationSupport.cs index 0d228d9..d3c78ba 100644 --- a/CryptoExchange.Net/SharedApis/Enums/SharedPaginationSupport.cs +++ b/CryptoExchange.Net/SharedApis/Enums/SharedPaginationSupport.cs @@ -1,21 +1,20 @@ -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Supported pagination type +/// +public enum SharedPaginationSupport { /// - /// Supported pagination type + /// Pagination is not supported for this exchange request /// - public enum SharedPaginationSupport - { - /// - /// Pagination is not supported for this exchange request - /// - NotSupported, - /// - /// Pagination is in ascending order - /// - Ascending, - /// - /// Pagination is in descending order - /// - Descending - } + NotSupported, + /// + /// Pagination is in ascending order + /// + Ascending, + /// + /// Pagination is in descending order + /// + Descending } diff --git a/CryptoExchange.Net/SharedApis/Enums/SharedPositionMode.cs b/CryptoExchange.Net/SharedApis/Enums/SharedPositionMode.cs index ac268f6..ac1a0ba 100644 --- a/CryptoExchange.Net/SharedApis/Enums/SharedPositionMode.cs +++ b/CryptoExchange.Net/SharedApis/Enums/SharedPositionMode.cs @@ -1,17 +1,16 @@ -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Position mode +/// +public enum SharedPositionMode { /// - /// Position mode + /// Hedge mode, a symbol can have both a long and a short position at the same time /// - public enum SharedPositionMode - { - /// - /// Hedge mode, a symbol can have both a long and a short position at the same time - /// - HedgeMode, - /// - /// One way mode, a symbol can only have one open position side at a time - /// - OneWay - } + HedgeMode, + /// + /// One way mode, a symbol can only have one open position side at a time + /// + OneWay } diff --git a/CryptoExchange.Net/SharedApis/Enums/SharedPositionModeSelection.cs b/CryptoExchange.Net/SharedApis/Enums/SharedPositionModeSelection.cs index bce8557..861f07b 100644 --- a/CryptoExchange.Net/SharedApis/Enums/SharedPositionModeSelection.cs +++ b/CryptoExchange.Net/SharedApis/Enums/SharedPositionModeSelection.cs @@ -1,17 +1,16 @@ -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Position mode selection type +/// +public enum SharedPositionModeSelection { /// - /// Position mode selection type + /// Position mode is configured per symbol /// - public enum SharedPositionModeSelection - { - /// - /// Position mode is configured per symbol - /// - PerSymbol, - /// - /// Position mode is configured for the entire account - /// - PerAccount - } + PerSymbol, + /// + /// Position mode is configured for the entire account + /// + PerAccount } diff --git a/CryptoExchange.Net/SharedApis/Enums/SharedPositionSide.cs b/CryptoExchange.Net/SharedApis/Enums/SharedPositionSide.cs index 98d2886..dd6b775 100644 --- a/CryptoExchange.Net/SharedApis/Enums/SharedPositionSide.cs +++ b/CryptoExchange.Net/SharedApis/Enums/SharedPositionSide.cs @@ -1,17 +1,16 @@ -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// The side of a position +/// +public enum SharedPositionSide { /// - /// The side of a position + /// Long position /// - public enum SharedPositionSide - { - /// - /// Long position - /// - Long, - /// - /// Short position - /// - Short - } + Long, + /// + /// Short position + /// + Short } diff --git a/CryptoExchange.Net/SharedApis/Enums/SharedQuantityType.cs b/CryptoExchange.Net/SharedApis/Enums/SharedQuantityType.cs index 2fb41e8..e3068a3 100644 --- a/CryptoExchange.Net/SharedApis/Enums/SharedQuantityType.cs +++ b/CryptoExchange.Net/SharedApis/Enums/SharedQuantityType.cs @@ -1,29 +1,28 @@ -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Quote asset order quantity support +/// +public enum SharedQuantityType { /// - /// Quote asset order quantity support + /// Quantity should be in the base asset /// - public enum SharedQuantityType - { - /// - /// Quantity should be in the base asset - /// - BaseAsset, - /// - /// Quantity should be in the quote asset - /// - QuoteAsset, - /// - /// Quantity is in the number of contracts - /// - Contracts, - /// - /// Quantity can be either base or quote quantity - /// - BaseAndQuoteAsset, - /// - /// Quantity can be either base or quote quantity, or in contracts - /// - BaseAndQuoteAssetAndContracts - } + BaseAsset, + /// + /// Quantity should be in the quote asset + /// + QuoteAsset, + /// + /// Quantity is in the number of contracts + /// + Contracts, + /// + /// Quantity can be either base or quote quantity + /// + BaseAndQuoteAsset, + /// + /// Quantity can be either base or quote quantity, or in contracts + /// + BaseAndQuoteAssetAndContracts } diff --git a/CryptoExchange.Net/SharedApis/Enums/SharedRole.cs b/CryptoExchange.Net/SharedApis/Enums/SharedRole.cs index ef93b8b..77102cc 100644 --- a/CryptoExchange.Net/SharedApis/Enums/SharedRole.cs +++ b/CryptoExchange.Net/SharedApis/Enums/SharedRole.cs @@ -1,17 +1,16 @@ -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// The role of a trade +/// +public enum SharedRole { /// - /// The role of a trade + /// Maker role, put an order on the order book which has been filled /// - public enum SharedRole - { - /// - /// Maker role, put an order on the order book which has been filled - /// - Maker, - /// - /// Taker role, took an order of the order book to fill - /// - Taker - } + Maker, + /// + /// Taker role, took an order of the order book to fill + /// + Taker } diff --git a/CryptoExchange.Net/SharedApis/Enums/SharedSymbolType.cs b/CryptoExchange.Net/SharedApis/Enums/SharedSymbolType.cs index fa3f2d9..9d42168 100644 --- a/CryptoExchange.Net/SharedApis/Enums/SharedSymbolType.cs +++ b/CryptoExchange.Net/SharedApis/Enums/SharedSymbolType.cs @@ -1,25 +1,24 @@ -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Type of a symbol +/// +public enum SharedSymbolType { /// - /// Type of a symbol + /// Perpetual linear, contract has no delivery date and is settled in stablecoin /// - public enum SharedSymbolType - { - /// - /// Perpetual linear, contract has no delivery date and is settled in stablecoin - /// - PerpetualLinear, - /// - /// Perpetual inverse, contract has no delivery date and is settled in crypto - /// - PerpetualInverse, - /// - /// Delivery linear, contract has a specific delivery date and is settled in stablecoin - /// - DeliveryLinear, - /// - /// Delivery inverse, contract has a specific delivery date and is settled in crypto - /// - DeliveryInverse - } + PerpetualLinear, + /// + /// Perpetual inverse, contract has no delivery date and is settled in crypto + /// + PerpetualInverse, + /// + /// Delivery linear, contract has a specific delivery date and is settled in stablecoin + /// + DeliveryLinear, + /// + /// Delivery inverse, contract has a specific delivery date and is settled in crypto + /// + DeliveryInverse } diff --git a/CryptoExchange.Net/SharedApis/Enums/SharedTimeInForce.cs b/CryptoExchange.Net/SharedApis/Enums/SharedTimeInForce.cs index 2f8bbe2..4dda984 100644 --- a/CryptoExchange.Net/SharedApis/Enums/SharedTimeInForce.cs +++ b/CryptoExchange.Net/SharedApis/Enums/SharedTimeInForce.cs @@ -1,21 +1,20 @@ -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Time in force for an order +/// +public enum SharedTimeInForce { /// - /// Time in force for an order + /// Order is good until canceled /// - public enum SharedTimeInForce - { - /// - /// Order is good until canceled - /// - GoodTillCanceled, - /// - /// Order should execute immediately, not executed part is canceled - /// - ImmediateOrCancel, - /// - /// Order should execute fully immediately or is fully canceled - /// - FillOrKill - } + GoodTillCanceled, + /// + /// Order should execute immediately, not executed part is canceled + /// + ImmediateOrCancel, + /// + /// Order should execute fully immediately or is fully canceled + /// + FillOrKill } diff --git a/CryptoExchange.Net/SharedApis/Enums/SharedTpSlSide.cs b/CryptoExchange.Net/SharedApis/Enums/SharedTpSlSide.cs index c8cd0ab..d1e59ea 100644 --- a/CryptoExchange.Net/SharedApis/Enums/SharedTpSlSide.cs +++ b/CryptoExchange.Net/SharedApis/Enums/SharedTpSlSide.cs @@ -1,21 +1,16 @@ -using System; -using System.Collections.Generic; -using System.Text; +namespace CryptoExchange.Net.SharedApis; -namespace CryptoExchange.Net.SharedApis +/// +/// Take Profit / Stop Loss side +/// +public enum SharedTpSlSide { /// - /// Take Profit / Stop Loss side + /// Take profit /// - public enum SharedTpSlSide - { - /// - /// Take profit - /// - TakeProfit, - /// - /// Stop loss - /// - StopLoss - } + TakeProfit, + /// + /// Stop loss + /// + StopLoss } diff --git a/CryptoExchange.Net/SharedApis/Enums/SharedTriggerOrderDirection.cs b/CryptoExchange.Net/SharedApis/Enums/SharedTriggerOrderDirection.cs index 1aefff9..aa5604c 100644 --- a/CryptoExchange.Net/SharedApis/Enums/SharedTriggerOrderDirection.cs +++ b/CryptoExchange.Net/SharedApis/Enums/SharedTriggerOrderDirection.cs @@ -1,21 +1,16 @@ -using System; -using System.Collections.Generic; -using System.Text; +namespace CryptoExchange.Net.SharedApis; -namespace CryptoExchange.Net.SharedApis +/// +/// The order direction when order trigger parameters are reached +/// +public enum SharedTriggerOrderDirection { /// - /// The order direction when order trigger parameters are reached + /// Enter, Buy for Spot and long futures positions, Sell for short futures positions /// - public enum SharedTriggerOrderDirection - { - /// - /// Enter, Buy for Spot and long futures positions, Sell for short futures positions - /// - Enter, - /// - /// Exit, Sell for Spot and long futures positions, Buy for short futures positions - /// - Exit - } + Enter, + /// + /// Exit, Sell for Spot and long futures positions, Buy for short futures positions + /// + Exit } diff --git a/CryptoExchange.Net/SharedApis/Enums/SharedTriggerOrderStatus.cs b/CryptoExchange.Net/SharedApis/Enums/SharedTriggerOrderStatus.cs index 60082a9..b64d664 100644 --- a/CryptoExchange.Net/SharedApis/Enums/SharedTriggerOrderStatus.cs +++ b/CryptoExchange.Net/SharedApis/Enums/SharedTriggerOrderStatus.cs @@ -1,29 +1,24 @@ -using System; -using System.Collections.Generic; -using System.Text; +namespace CryptoExchange.Net.SharedApis; -namespace CryptoExchange.Net.SharedApis +/// +/// Trigger order status +/// +public enum SharedTriggerOrderStatus { /// - /// Trigger order status + /// Order is active /// - public enum SharedTriggerOrderStatus - { - /// - /// Order is active - /// - Active, - /// - /// Order has been filled - /// - Filled, - /// - /// Trigger canceled, can be user cancelation or system cancelation due to an error - /// - CanceledOrRejected, - /// - /// Trigger order has been triggered. Resulting order might be filled or not. - /// - Triggered - } + Active, + /// + /// Order has been filled + /// + Filled, + /// + /// Trigger canceled, can be user cancelation or system cancelation due to an error + /// + CanceledOrRejected, + /// + /// Trigger order has been triggered. Resulting order might be filled or not. + /// + Triggered } diff --git a/CryptoExchange.Net/SharedApis/Enums/SharedTriggerPriceDirection.cs b/CryptoExchange.Net/SharedApis/Enums/SharedTriggerPriceDirection.cs index be513d2..9acf5c4 100644 --- a/CryptoExchange.Net/SharedApis/Enums/SharedTriggerPriceDirection.cs +++ b/CryptoExchange.Net/SharedApis/Enums/SharedTriggerPriceDirection.cs @@ -1,21 +1,16 @@ -using System; -using System.Collections.Generic; -using System.Text; +namespace CryptoExchange.Net.SharedApis; -namespace CryptoExchange.Net.SharedApis +/// +/// Price direction for trigger order +/// +public enum SharedTriggerPriceDirection { /// - /// Price direction for trigger order + /// Trigger when the price goes below the specified trigger price /// - public enum SharedTriggerPriceDirection - { - /// - /// Trigger when the price goes below the specified trigger price - /// - PriceBelow, - /// - /// Trigger when the price goes above the specified trigger price - /// - PriceAbove - } + PriceBelow, + /// + /// Trigger when the price goes above the specified trigger price + /// + PriceAbove } diff --git a/CryptoExchange.Net/SharedApis/Enums/SharedTriggerPriceType.cs b/CryptoExchange.Net/SharedApis/Enums/SharedTriggerPriceType.cs index 84a2a1c..7d33533 100644 --- a/CryptoExchange.Net/SharedApis/Enums/SharedTriggerPriceType.cs +++ b/CryptoExchange.Net/SharedApis/Enums/SharedTriggerPriceType.cs @@ -1,25 +1,20 @@ -using System; -using System.Collections.Generic; -using System.Text; +namespace CryptoExchange.Net.SharedApis; -namespace CryptoExchange.Net.SharedApis +/// +/// Price direction for trigger order +/// +public enum SharedTriggerPriceType { /// - /// Price direction for trigger order + /// Last traded price /// - public enum SharedTriggerPriceType - { - /// - /// Last traded price - /// - LastPrice, - /// - /// Mark price - /// - MarkPrice, - /// - /// Index price - /// - IndexPrice - } + LastPrice, + /// + /// Mark price + /// + MarkPrice, + /// + /// Index price + /// + IndexPrice } diff --git a/CryptoExchange.Net/SharedApis/Enums/TradingMode.cs b/CryptoExchange.Net/SharedApis/Enums/TradingMode.cs index 04344aa..6e91c08 100644 --- a/CryptoExchange.Net/SharedApis/Enums/TradingMode.cs +++ b/CryptoExchange.Net/SharedApis/Enums/TradingMode.cs @@ -1,29 +1,28 @@ -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Trading mode +/// +public enum TradingMode { /// - /// Trading mode + /// Spot trading /// - public enum TradingMode - { - /// - /// Spot trading - /// - Spot, - /// - /// Perpetual linear futures - /// - PerpetualLinear, - /// - /// Delivery linear futures - /// - DeliveryLinear, - /// - /// Perpetual inverse futures - /// - PerpetualInverse, - /// - /// Delivery inverse futures - /// - DeliveryInverse - } + Spot, + /// + /// Perpetual linear futures + /// + PerpetualLinear, + /// + /// Delivery linear futures + /// + DeliveryLinear, + /// + /// Perpetual inverse futures + /// + PerpetualInverse, + /// + /// Delivery inverse futures + /// + DeliveryInverse } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/INextPageToken.cs b/CryptoExchange.Net/SharedApis/Interfaces/INextPageToken.cs index ceb1e73..76f745a 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/INextPageToken.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/INextPageToken.cs @@ -1,111 +1,110 @@ -using System; +using System; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// A token which a request can use to retrieve the next page if there are more pages in the result set +/// +public interface INextPageToken +{ +} + +/// +/// A datetime offset token +/// +public record DateTimeToken: INextPageToken { /// - /// A token which a request can use to retrieve the next page if there are more pages in the result set + /// Last result time /// - public interface INextPageToken - { - } + public DateTime LastTime { get; set; } /// - /// A datetime offset token + /// ctor /// - public record DateTimeToken: INextPageToken + public DateTimeToken(DateTime timestamp) { - /// - /// Last result time - /// - public DateTime LastTime { get; set; } - - /// - /// ctor - /// - public DateTimeToken(DateTime timestamp) - { - LastTime = timestamp; - } - } - - /// - /// A current page index token - /// - public record PageToken: INextPageToken - { - /// - /// The next page index - /// - public int Page { get; set; } - /// - /// Page size - /// - public int PageSize { get; set; } - - /// - /// ctor - /// - public PageToken(int page, int pageSize) - { - Page = page; - PageSize = pageSize; - } - } - - /// - /// A id offset token - /// - public record FromIdToken : INextPageToken - { - /// - /// The last id from previous result - /// - public string FromToken { get; set; } - - /// - /// ctor - /// - public FromIdToken(string fromToken) - { - FromToken = fromToken; - } - } - - /// - /// A cursor token - /// - public record CursorToken : INextPageToken - { - /// - /// The next page cursor - /// - public string Cursor { get; set; } - - /// - /// ctor - /// - public CursorToken(string cursor) - { - Cursor = cursor; - } - } - - /// - /// A result offset token - /// - public record OffsetToken : INextPageToken - { - /// - /// Offset in the result set - /// - public int Offset { get; set; } - - /// - /// ctor - /// - public OffsetToken(int offset) - { - Offset = offset; - } + LastTime = timestamp; + } +} + +/// +/// A current page index token +/// +public record PageToken: INextPageToken +{ + /// + /// The next page index + /// + public int Page { get; set; } + /// + /// Page size + /// + public int PageSize { get; set; } + + /// + /// ctor + /// + public PageToken(int page, int pageSize) + { + Page = page; + PageSize = pageSize; + } +} + +/// +/// A id offset token +/// +public record FromIdToken : INextPageToken +{ + /// + /// The last id from previous result + /// + public string FromToken { get; set; } + + /// + /// ctor + /// + public FromIdToken(string fromToken) + { + FromToken = fromToken; + } +} + +/// +/// A cursor token +/// +public record CursorToken : INextPageToken +{ + /// + /// The next page cursor + /// + public string Cursor { get; set; } + + /// + /// ctor + /// + public CursorToken(string cursor) + { + Cursor = cursor; + } +} + +/// +/// A result offset token +/// +public record OffsetToken : INextPageToken +{ + /// + /// Offset in the result set + /// + public int Offset { get; set; } + + /// + /// ctor + /// + public OffsetToken(int offset) + { + Offset = offset; } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/ISharedClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/ISharedClient.cs index eca53e9..f08fda4 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/ISharedClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/ISharedClient.cs @@ -1,48 +1,46 @@ -using CryptoExchange.Net.Objects; using System; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// A shared/common client interface +/// +public interface ISharedClient { /// - /// A shared/common client interface + /// Name of the exchange /// - public interface ISharedClient - { - /// - /// Name of the exchange - /// - string Exchange { get; } + string Exchange { get; } - /// - /// Which trading modes this client supports - /// - TradingMode[] SupportedTradingModes { get; } + /// + /// Which trading modes this client supports + /// + TradingMode[] SupportedTradingModes { get; } - /// - /// Whether or not API credentials have been configured for this client. Does not check the credentials are actually valid. - /// - bool Authenticated { get; } + /// + /// Whether or not API credentials have been configured for this client. Does not check the credentials are actually valid. + /// + bool Authenticated { get; } - /// - /// Format a base and quote asset to an exchange accepted symbol - /// - /// The base asset - /// The quote asset - /// The trading mode - /// The deliver date for a delivery futures symbol - /// - string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverDate = null); + /// + /// Format a base and quote asset to an exchange accepted symbol + /// + /// The base asset + /// The quote asset + /// The trading mode + /// The deliver date for a delivery futures symbol + /// + string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverDate = null); - /// - /// Set a default exchange parameter. This can be used instead of passing in an ExchangeParameters object which each request. - /// - /// Parameter name - /// Parameter value - void SetDefaultExchangeParameter(string name, object value); + /// + /// Set a default exchange parameter. This can be used instead of passing in an ExchangeParameters object which each request. + /// + /// Parameter name + /// Parameter value + void SetDefaultExchangeParameter(string name, object value); - /// - /// Reset the default exchange parameters, resets parameters for all exchanges - /// - void ResetDefaultExchangeParameters(); - } + /// + /// Reset the default exchange parameters, resets parameters for all exchanges + /// + void ResetDefaultExchangeParameters(); } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFundingRateRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFundingRateRestClient.cs index 0553db7..40d5816 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFundingRateRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFundingRateRestClient.cs @@ -1,24 +1,22 @@ -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Client for request funding rate records +/// +public interface IFundingRateRestClient : ISharedClient { /// - /// Client for request funding rate records + /// Funding rate request options /// - public interface IFundingRateRestClient : ISharedClient - { - /// - /// Funding rate request options - /// - GetFundingRateHistoryOptions GetFundingRateHistoryOptions { get; } - /// - /// Get funding rate records - /// - /// Request info - /// The pagination token from the previous request to continue pagination - /// Cancellation token - Task> GetFundingRateHistoryAsync(GetFundingRateHistoryRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default); - } + GetFundingRateHistoryOptions GetFundingRateHistoryOptions { get; } + /// + /// Get funding rate records + /// + /// Request info + /// The pagination token from the previous request to continue pagination + /// Cancellation token + Task> GetFundingRateHistoryAsync(GetFundingRateHistoryRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default); } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesOrderClientIdRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesOrderClientIdRestClient.cs index f937403..66676a6 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesOrderClientIdRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesOrderClientIdRestClient.cs @@ -1,37 +1,33 @@ -using System; -using System.Collections.Generic; -using System.Text; using System.Threading.Tasks; using System.Threading; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Client for managing futures orders using a client order id +/// +public interface IFuturesOrderClientIdRestClient : ISharedClient { /// - /// Client for managing futures orders using a client order id + /// Futures get order by client order id request options /// - public interface IFuturesOrderClientIdRestClient : ISharedClient - { - /// - /// Futures get order by client order id request options - /// - EndpointOptions GetFuturesOrderByClientOrderIdOptions { get; } + EndpointOptions GetFuturesOrderByClientOrderIdOptions { get; } - /// - /// Get info on a specific futures order using a client order id - /// - /// Request info - /// Cancellation token - Task> GetFuturesOrderByClientOrderIdAsync(GetOrderRequest request, CancellationToken ct = default); + /// + /// Get info on a specific futures order using a client order id + /// + /// Request info + /// Cancellation token + Task> GetFuturesOrderByClientOrderIdAsync(GetOrderRequest request, CancellationToken ct = default); - /// - /// Futures cancel order by client order id request options - /// - EndpointOptions CancelFuturesOrderByClientOrderIdOptions { get; } - /// - /// Cancel a futures order using client order id - /// - /// Request info - /// Cancellation token - Task> CancelFuturesOrderByClientOrderIdAsync(CancelOrderRequest request, CancellationToken ct = default); - } + /// + /// Futures cancel order by client order id request options + /// + EndpointOptions CancelFuturesOrderByClientOrderIdOptions { get; } + /// + /// Cancel a futures order using client order id + /// + /// Request info + /// Cancellation token + Task> CancelFuturesOrderByClientOrderIdAsync(CancelOrderRequest request, CancellationToken ct = default); } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesOrderRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesOrderRestClient.cs index 42b2ef1..4a3b36c 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesOrderRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesOrderRestClient.cs @@ -1,143 +1,141 @@ -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Client for placing and managing futures orders +/// +public interface IFuturesOrderRestClient : ISharedClient { /// - /// Client for placing and managing futures orders + /// How the trading fee is deducted /// - public interface IFuturesOrderRestClient : ISharedClient - { - /// - /// How the trading fee is deducted - /// - SharedFeeDeductionType FuturesFeeDeductionType { get; } - /// - /// How the asset is determined in which the trading fee is paid - /// - SharedFeeAssetType FuturesFeeAssetType { get; } + SharedFeeDeductionType FuturesFeeDeductionType { get; } + /// + /// How the asset is determined in which the trading fee is paid + /// + SharedFeeAssetType FuturesFeeAssetType { get; } - /// - /// Supported order types - /// - SharedOrderType[] FuturesSupportedOrderTypes { get; } - /// - /// Supported time in force - /// - SharedTimeInForce[] FuturesSupportedTimeInForce { get; } - /// - /// Quantity types support - /// - SharedQuantitySupport FuturesSupportedOrderQuantity { get; } + /// + /// Supported order types + /// + SharedOrderType[] FuturesSupportedOrderTypes { get; } + /// + /// Supported time in force + /// + SharedTimeInForce[] FuturesSupportedTimeInForce { get; } + /// + /// Quantity types support + /// + SharedQuantitySupport FuturesSupportedOrderQuantity { get; } - /// - /// Generate a new random client order id - /// - /// - string GenerateClientOrderId(); + /// + /// Generate a new random client order id + /// + /// + string GenerateClientOrderId(); - /// - /// Futures place order request options - /// - PlaceFuturesOrderOptions PlaceFuturesOrderOptions { get; } + /// + /// Futures place order request options + /// + PlaceFuturesOrderOptions PlaceFuturesOrderOptions { get; } - /// - /// Place a new futures order - /// - /// Request info - /// Cancellation token - Task> PlaceFuturesOrderAsync(PlaceFuturesOrderRequest request, CancellationToken ct = default); + /// + /// Place a new futures order + /// + /// Request info + /// Cancellation token + Task> PlaceFuturesOrderAsync(PlaceFuturesOrderRequest request, CancellationToken ct = default); - /// - /// Futures get order request options - /// - EndpointOptions GetFuturesOrderOptions { get; } - /// - /// Get info on a specific futures order - /// - /// Request info - /// Cancellation token - Task> GetFuturesOrderAsync(GetOrderRequest request, CancellationToken ct = default); + /// + /// Futures get order request options + /// + EndpointOptions GetFuturesOrderOptions { get; } + /// + /// Get info on a specific futures order + /// + /// Request info + /// Cancellation token + Task> GetFuturesOrderAsync(GetOrderRequest request, CancellationToken ct = default); - /// - /// Futures get open orders request options - /// - EndpointOptions GetOpenFuturesOrdersOptions { get; } - /// - /// Get info on a open futures orders - /// - /// Request info - /// Cancellation token - Task> GetOpenFuturesOrdersAsync(GetOpenOrdersRequest request, CancellationToken ct = default); + /// + /// Futures get open orders request options + /// + EndpointOptions GetOpenFuturesOrdersOptions { get; } + /// + /// Get info on a open futures orders + /// + /// Request info + /// Cancellation token + Task> GetOpenFuturesOrdersAsync(GetOpenOrdersRequest request, CancellationToken ct = default); - /// - /// Spot get closed orders request options - /// - PaginatedEndpointOptions GetClosedFuturesOrdersOptions { get; } - /// - /// Get info on closed futures orders - /// - /// Request info - /// The pagination token from the previous request to continue pagination - /// Cancellation token - Task> GetClosedFuturesOrdersAsync(GetClosedOrdersRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default); + /// + /// Spot get closed orders request options + /// + PaginatedEndpointOptions GetClosedFuturesOrdersOptions { get; } + /// + /// Get info on closed futures orders + /// + /// Request info + /// The pagination token from the previous request to continue pagination + /// Cancellation token + Task> GetClosedFuturesOrdersAsync(GetClosedOrdersRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default); - /// - /// Futures get order trades request options - /// - EndpointOptions GetFuturesOrderTradesOptions { get; } - /// - /// Get trades for a specific futures order - /// - /// Request info - /// Cancellation token - Task> GetFuturesOrderTradesAsync(GetOrderTradesRequest request, CancellationToken ct = default); + /// + /// Futures get order trades request options + /// + EndpointOptions GetFuturesOrderTradesOptions { get; } + /// + /// Get trades for a specific futures order + /// + /// Request info + /// Cancellation token + Task> GetFuturesOrderTradesAsync(GetOrderTradesRequest request, CancellationToken ct = default); - /// - /// Futures user trades request options - /// - PaginatedEndpointOptions GetFuturesUserTradesOptions { get; } - /// - /// Get futures user trade records - /// - /// Request info - /// The pagination token from the previous request to continue pagination - /// Cancellation token - Task> GetFuturesUserTradesAsync(GetUserTradesRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default); + /// + /// Futures user trades request options + /// + PaginatedEndpointOptions GetFuturesUserTradesOptions { get; } + /// + /// Get futures user trade records + /// + /// Request info + /// The pagination token from the previous request to continue pagination + /// Cancellation token + Task> GetFuturesUserTradesAsync(GetUserTradesRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default); - /// - /// Futures cancel order request options - /// - EndpointOptions CancelFuturesOrderOptions { get; } - /// - /// Cancel a futures order - /// - /// Request info - /// Cancellation token - Task> CancelFuturesOrderAsync(CancelOrderRequest request, CancellationToken ct = default); + /// + /// Futures cancel order request options + /// + EndpointOptions CancelFuturesOrderOptions { get; } + /// + /// Cancel a futures order + /// + /// Request info + /// Cancellation token + Task> CancelFuturesOrderAsync(CancelOrderRequest request, CancellationToken ct = default); - /// - /// Positions request options - /// - EndpointOptions GetPositionsOptions { get; } - /// - /// Get open position info - /// - /// Request info - /// Cancellation token - Task> GetPositionsAsync(GetPositionsRequest request, CancellationToken ct = default); + /// + /// Positions request options + /// + EndpointOptions GetPositionsOptions { get; } + /// + /// Get open position info + /// + /// Request info + /// Cancellation token + Task> GetPositionsAsync(GetPositionsRequest request, CancellationToken ct = default); - /// - /// Close position order request options - /// - EndpointOptions ClosePositionOptions { get; } - /// - /// Close a currently open position - /// - /// Request info - /// Cancellation token - /// - Task> ClosePositionAsync(ClosePositionRequest request, CancellationToken ct = default); - } + /// + /// Close position order request options + /// + EndpointOptions ClosePositionOptions { get; } + /// + /// Close a currently open position + /// + /// Request info + /// Cancellation token + /// + Task> ClosePositionAsync(ClosePositionRequest request, CancellationToken ct = default); } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesSymbolRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesSymbolRestClient.cs index 672ef51..8c3c828 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesSymbolRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesSymbolRestClient.cs @@ -1,23 +1,21 @@ -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Client for request futures symbol info +/// +public interface IFuturesSymbolRestClient : ISharedClient { /// - /// Client for request futures symbol info + /// Futures symbol request options /// - public interface IFuturesSymbolRestClient : ISharedClient - { - /// - /// Futures symbol request options - /// - EndpointOptions GetFuturesSymbolsOptions { get; } - /// - /// Get info on all futures symbols supported on the exchange - /// - /// Request info - /// Cancellation token - Task> GetFuturesSymbolsAsync(GetSymbolsRequest request, CancellationToken ct = default); - } + EndpointOptions GetFuturesSymbolsOptions { get; } + /// + /// Get info on all futures symbols supported on the exchange + /// + /// Request info + /// Cancellation token + Task> GetFuturesSymbolsAsync(GetSymbolsRequest request, CancellationToken ct = default); } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesTickerRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesTickerRestClient.cs index b11c01c..ee2595a 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesTickerRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesTickerRestClient.cs @@ -1,34 +1,32 @@ -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Client for requesting ticker info for futures symbols +/// +public interface IFuturesTickerRestClient : ISharedClient { /// - /// Client for requesting ticker info for futures symbols + /// Futures get ticker request options /// - public interface IFuturesTickerRestClient : ISharedClient - { - /// - /// Futures get ticker request options - /// - EndpointOptions GetFuturesTickerOptions { get; } - /// - /// Get ticker info for a specific futures symbol - /// - /// Request info - /// Cancellation token - Task> GetFuturesTickerAsync(GetTickerRequest request, CancellationToken ct = default); + EndpointOptions GetFuturesTickerOptions { get; } + /// + /// Get ticker info for a specific futures symbol + /// + /// Request info + /// Cancellation token + Task> GetFuturesTickerAsync(GetTickerRequest request, CancellationToken ct = default); - /// - /// Futures get tickers request options - /// - EndpointOptions GetFuturesTickersOptions { get; } - /// - /// Get ticker info for all futures symbols - /// - /// Request info - /// Cancellation token - Task> GetFuturesTickersAsync(GetTickersRequest request, CancellationToken ct = default); - } + /// + /// Futures get tickers request options + /// + EndpointOptions GetFuturesTickersOptions { get; } + /// + /// Get ticker info for all futures symbols + /// + /// Request info + /// Cancellation token + Task> GetFuturesTickersAsync(GetTickersRequest request, CancellationToken ct = default); } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesTpSlRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesTpSlRestClient.cs index adc9f94..6b3e287 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesTpSlRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesTpSlRestClient.cs @@ -1,38 +1,34 @@ -using System; -using System.Collections.Generic; -using System.Text; using System.Threading.Tasks; using System.Threading; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Take profit / Stop loss client +/// +public interface IFuturesTpSlRestClient : ISharedClient { /// - /// Take profit / Stop loss client + /// Set take profit and/or stop loss options /// - public interface IFuturesTpSlRestClient : ISharedClient - { - /// - /// Set take profit and/or stop loss options - /// - EndpointOptions SetFuturesTpSlOptions { get; } - /// - /// Set a take profit and/or stop loss for an open position - /// - /// Request info - /// Cancellation token - /// - Task> SetFuturesTpSlAsync(SetTpSlRequest request, CancellationToken ct = default); + EndpointOptions SetFuturesTpSlOptions { get; } + /// + /// Set a take profit and/or stop loss for an open position + /// + /// Request info + /// Cancellation token + /// + Task> SetFuturesTpSlAsync(SetTpSlRequest request, CancellationToken ct = default); - /// - /// Cancel a take profit and/or stop loss options - /// - EndpointOptions CancelFuturesTpSlOptions { get; } - /// - /// Cancel an active take profit and/or stop loss for an open position - /// - /// Request info - /// Cancellation token - /// - Task> CancelFuturesTpSlAsync(CancelTpSlRequest request, CancellationToken ct = default); - } + /// + /// Cancel a take profit and/or stop loss options + /// + EndpointOptions CancelFuturesTpSlOptions { get; } + /// + /// Cancel an active take profit and/or stop loss for an open position + /// + /// Request info + /// Cancellation token + /// + Task> CancelFuturesTpSlAsync(CancelTpSlRequest request, CancellationToken ct = default); } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesTriggerOrderRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesTriggerOrderRestClient.cs index 176f2ef..cf47dc1 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesTriggerOrderRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesTriggerOrderRestClient.cs @@ -1,50 +1,46 @@ -using System; -using System.Collections.Generic; -using System.Text; using System.Threading; using System.Threading.Tasks; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Client for placing trigger orders +/// +public interface IFuturesTriggerOrderRestClient : ISharedClient { /// - /// Client for placing trigger orders + /// Place spot trigger order options /// - public interface IFuturesTriggerOrderRestClient : ISharedClient - { - /// - /// Place spot trigger order options - /// - PlaceFuturesTriggerOrderOptions PlaceFuturesTriggerOrderOptions { get; } + PlaceFuturesTriggerOrderOptions PlaceFuturesTriggerOrderOptions { get; } - /// - /// Place a new trigger order - /// - /// Request info - /// Cancellation token - /// - Task> PlaceFuturesTriggerOrderAsync(PlaceFuturesTriggerOrderRequest request, CancellationToken ct = default); + /// + /// Place a new trigger order + /// + /// Request info + /// Cancellation token + /// + Task> PlaceFuturesTriggerOrderAsync(PlaceFuturesTriggerOrderRequest request, CancellationToken ct = default); - /// - /// Get trigger order request options - /// - EndpointOptions GetFuturesTriggerOrderOptions { get; } - /// - /// Get info on a specific trigger order - /// - /// Request info - /// Cancellation token - Task> GetFuturesTriggerOrderAsync(GetOrderRequest request, CancellationToken ct = default); + /// + /// Get trigger order request options + /// + EndpointOptions GetFuturesTriggerOrderOptions { get; } + /// + /// Get info on a specific trigger order + /// + /// Request info + /// Cancellation token + Task> GetFuturesTriggerOrderAsync(GetOrderRequest request, CancellationToken ct = default); - /// - /// Cancel trigger order request options - /// - EndpointOptions CancelFuturesTriggerOrderOptions { get; } - /// - /// Cancel a trigger order - /// - /// Request info - /// Cancellation token - Task> CancelFuturesTriggerOrderAsync(CancelOrderRequest request, CancellationToken ct = default); - } + /// + /// Cancel trigger order request options + /// + EndpointOptions CancelFuturesTriggerOrderOptions { get; } + /// + /// Cancel a trigger order + /// + /// Request info + /// Cancellation token + Task> CancelFuturesTriggerOrderAsync(CancelOrderRequest request, CancellationToken ct = default); } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IIndexPriceKlineRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IIndexPriceKlineRestClient.cs index 103055d..b449d51 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IIndexPriceKlineRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IIndexPriceKlineRestClient.cs @@ -1,24 +1,22 @@ -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Client for getting the index price klines for a symbol +/// +public interface IIndexPriceKlineRestClient : ISharedClient { /// - /// Client for getting the index price klines for a symbol + /// Index price klines request options /// - public interface IIndexPriceKlineRestClient : ISharedClient - { - /// - /// Index price klines request options - /// - GetKlinesOptions GetIndexPriceKlinesOptions { get; } - /// - /// Get index price kline/candlestick data - /// - /// Request info - /// The pagination token from the previous request to continue pagination - /// Cancellation token - Task> GetIndexPriceKlinesAsync(GetKlinesRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default); - } + GetKlinesOptions GetIndexPriceKlinesOptions { get; } + /// + /// Get index price kline/candlestick data + /// + /// Request info + /// The pagination token from the previous request to continue pagination + /// Cancellation token + Task> GetIndexPriceKlinesAsync(GetKlinesRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default); } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/ILeverageRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/ILeverageRestClient.cs index 0262f69..ad6af6d 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/ILeverageRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/ILeverageRestClient.cs @@ -1,39 +1,38 @@ -using System.Threading; +using System.Threading; using System.Threading.Tasks; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Client for managing the leverage of a symbol +/// +public interface ILeverageRestClient : ISharedClient { /// - /// Client for managing the leverage of a symbol + /// How the leverage setting is configured on the exchange /// - public interface ILeverageRestClient : ISharedClient - { - /// - /// How the leverage setting is configured on the exchange - /// - SharedLeverageSettingMode LeverageSettingType { get; } + SharedLeverageSettingMode LeverageSettingType { get; } - /// - /// Leverage request options - /// - EndpointOptions GetLeverageOptions { get; } - /// - /// Get the current leverage setting for a symbol - /// - /// Request info - /// Cancellation token - Task> GetLeverageAsync(GetLeverageRequest request, CancellationToken ct = default); + /// + /// Leverage request options + /// + EndpointOptions GetLeverageOptions { get; } + /// + /// Get the current leverage setting for a symbol + /// + /// Request info + /// Cancellation token + Task> GetLeverageAsync(GetLeverageRequest request, CancellationToken ct = default); - /// - /// Leverage set request options - /// - SetLeverageOptions SetLeverageOptions { get; } - /// - /// Set the leverage for a symbol - /// - /// Request info - /// Cancellation token - Task> SetLeverageAsync(SetLeverageRequest request, CancellationToken ct = default); + /// + /// Leverage set request options + /// + SetLeverageOptions SetLeverageOptions { get; } + /// + /// Set the leverage for a symbol + /// + /// Request info + /// Cancellation token + Task> SetLeverageAsync(SetLeverageRequest request, CancellationToken ct = default); - } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IMarkPriceKlineRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IMarkPriceKlineRestClient.cs index 69f2f5c..5989cf3 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IMarkPriceKlineRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IMarkPriceKlineRestClient.cs @@ -1,24 +1,22 @@ -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Client for getting the mark price klines for a symbol +/// +public interface IMarkPriceKlineRestClient : ISharedClient { /// - /// Client for getting the mark price klines for a symbol + /// Mark price klines request options /// - public interface IMarkPriceKlineRestClient : ISharedClient - { - /// - /// Mark price klines request options - /// - GetKlinesOptions GetMarkPriceKlinesOptions { get; } - /// - /// Get mark price kline/candlestick data - /// - /// Request info - /// The pagination token from the previous request to continue pagination - /// Cancellation token - Task> GetMarkPriceKlinesAsync(GetKlinesRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default); - } + GetKlinesOptions GetMarkPriceKlinesOptions { get; } + /// + /// Get mark price kline/candlestick data + /// + /// Request info + /// The pagination token from the previous request to continue pagination + /// Cancellation token + Task> GetMarkPriceKlinesAsync(GetKlinesRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default); } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IOpenInterestRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IOpenInterestRestClient.cs index 1dac2c8..37b3674 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IOpenInterestRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IOpenInterestRestClient.cs @@ -1,22 +1,21 @@ -using System.Threading; +using System.Threading; using System.Threading.Tasks; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Client for getting the open interest for a symbol +/// +public interface IOpenInterestRestClient : ISharedClient { /// - /// Client for getting the open interest for a symbol + /// Open interest request options /// - public interface IOpenInterestRestClient : ISharedClient - { - /// - /// Open interest request options - /// - EndpointOptions GetOpenInterestOptions { get; } - /// - /// Get the open interest for a symbol - /// - /// Request info - /// Cancellation token - Task> GetOpenInterestAsync(GetOpenInterestRequest request, CancellationToken ct = default); - } + EndpointOptions GetOpenInterestOptions { get; } + /// + /// Get the open interest for a symbol + /// + /// Request info + /// Cancellation token + Task> GetOpenInterestAsync(GetOpenInterestRequest request, CancellationToken ct = default); } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IPositionHistoryRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IPositionHistoryRestClient.cs index 8097fff..dd8e8a7 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IPositionHistoryRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IPositionHistoryRestClient.cs @@ -1,24 +1,22 @@ -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Client for getting position history +/// +public interface IPositionHistoryRestClient : ISharedClient { /// - /// Client for getting position history + /// Position history request options /// - public interface IPositionHistoryRestClient : ISharedClient - { - /// - /// Position history request options - /// - GetPositionHistoryOptions GetPositionHistoryOptions { get; } - /// - /// Get position history - /// - /// Request info - /// The pagination token from the previous request to continue pagination - /// Cancellation token - Task> GetPositionHistoryAsync(GetPositionHistoryRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default); - } + GetPositionHistoryOptions GetPositionHistoryOptions { get; } + /// + /// Get position history + /// + /// Request info + /// The pagination token from the previous request to continue pagination + /// Cancellation token + Task> GetPositionHistoryAsync(GetPositionHistoryRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default); } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IPositionModeRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IPositionModeRestClient.cs index 733bf8b..66fa581 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IPositionModeRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IPositionModeRestClient.cs @@ -1,38 +1,37 @@ -using System.Threading; +using System.Threading; using System.Threading.Tasks; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Client for managing the position mode setting +/// +public interface IPositionModeRestClient : ISharedClient { /// - /// Client for managing the position mode setting + /// How the exchange handles setting the position mode /// - public interface IPositionModeRestClient : ISharedClient - { - /// - /// How the exchange handles setting the position mode - /// - SharedPositionModeSelection PositionModeSettingType { get; } + SharedPositionModeSelection PositionModeSettingType { get; } - /// - /// Position mode request options - /// - GetPositionModeOptions GetPositionModeOptions { get; } - /// - /// Get the current position mode setting - /// - /// Request info - /// Cancellation token - Task> GetPositionModeAsync(GetPositionModeRequest request, CancellationToken ct = default); + /// + /// Position mode request options + /// + GetPositionModeOptions GetPositionModeOptions { get; } + /// + /// Get the current position mode setting + /// + /// Request info + /// Cancellation token + Task> GetPositionModeAsync(GetPositionModeRequest request, CancellationToken ct = default); - /// - /// Position mode set request options - /// - SetPositionModeOptions SetPositionModeOptions { get; } - /// - /// Set the position mode to a new value - /// - /// Request info - /// Cancellation token - Task> SetPositionModeAsync(SetPositionModeRequest request, CancellationToken ct = default); - } + /// + /// Position mode set request options + /// + SetPositionModeOptions SetPositionModeOptions { get; } + /// + /// Set the position mode to a new value + /// + /// Request info + /// Cancellation token + Task> SetPositionModeAsync(SetPositionModeRequest request, CancellationToken ct = default); } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IAssetsRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IAssetsRestClient.cs index a688386..e8ffd18 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IAssetsRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IAssetsRestClient.cs @@ -1,36 +1,34 @@ -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Client for requesting asset info +/// +public interface IAssetsRestClient : ISharedClient { /// - /// Client for requesting asset info + /// Asset request options /// - public interface IAssetsRestClient : ISharedClient - { - /// - /// Asset request options - /// - EndpointOptions GetAssetOptions { get; } + EndpointOptions GetAssetOptions { get; } - /// - /// Get info on a specific asset - /// - /// Request info - /// Cancellation token - Task> GetAssetAsync(GetAssetRequest request, CancellationToken ct = default); + /// + /// Get info on a specific asset + /// + /// Request info + /// Cancellation token + Task> GetAssetAsync(GetAssetRequest request, CancellationToken ct = default); - /// - /// Assets request options - /// - EndpointOptions GetAssetsOptions { get; } + /// + /// Assets request options + /// + EndpointOptions GetAssetsOptions { get; } - /// - /// Get info on all assets the exchange supports - /// - /// Request info - /// Cancellation token - Task> GetAssetsAsync(GetAssetsRequest request, CancellationToken ct = default); - } + /// + /// Get info on all assets the exchange supports + /// + /// Request info + /// Cancellation token + Task> GetAssetsAsync(GetAssetsRequest request, CancellationToken ct = default); } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IBalanceRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IBalanceRestClient.cs index 8983c61..71f884b 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IBalanceRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IBalanceRestClient.cs @@ -1,25 +1,23 @@ -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Client for requesting user balance info +/// +public interface IBalanceRestClient : ISharedClient { /// - /// Client for requesting user balance info + /// Balances request options /// - public interface IBalanceRestClient : ISharedClient - { - /// - /// Balances request options - /// - EndpointOptions GetBalancesOptions { get; } + EndpointOptions GetBalancesOptions { get; } - /// - /// Get balances for the user - /// - /// Request info - /// Cancellation token - /// - Task> GetBalancesAsync(GetBalancesRequest request, CancellationToken ct = default); - } + /// + /// Get balances for the user + /// + /// Request info + /// Cancellation token + /// + Task> GetBalancesAsync(GetBalancesRequest request, CancellationToken ct = default); } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IBookTickerRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IBookTickerRestClient.cs index a0ca57d..5d46bcd 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IBookTickerRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IBookTickerRestClient.cs @@ -1,24 +1,23 @@ -using System.Threading; +using System.Threading; using System.Threading.Tasks; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Client for retrieving the current best bid/ask price +/// +public interface IBookTickerRestClient : ISharedClient { /// - /// Client for retrieving the current best bid/ask price + /// Book ticker request options /// - public interface IBookTickerRestClient : ISharedClient - { - /// - /// Book ticker request options - /// - EndpointOptions GetBookTickerOptions { get; } + EndpointOptions GetBookTickerOptions { get; } - /// - /// Get the best ask/bid info for a symbol - /// - /// Request info - /// Cancellation token - /// - Task> GetBookTickerAsync(GetBookTickerRequest request, CancellationToken ct = default); - } + /// + /// Get the best ask/bid info for a symbol + /// + /// Request info + /// Cancellation token + /// + Task> GetBookTickerAsync(GetBookTickerRequest request, CancellationToken ct = default); } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IDepositRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IDepositRestClient.cs index 0229d4a..a05cafd 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IDepositRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IDepositRestClient.cs @@ -1,39 +1,37 @@ -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Client for requesting deposit addresses and deposit records +/// +public interface IDepositRestClient : ISharedClient { /// - /// Client for requesting deposit addresses and deposit records + /// Deposit addresses request options /// - public interface IDepositRestClient : ISharedClient - { - /// - /// Deposit addresses request options - /// - EndpointOptions GetDepositAddressesOptions { get; } + EndpointOptions GetDepositAddressesOptions { get; } - /// - /// Get deposit addresses for an asset - /// - /// Request info - /// Cancellation token - /// - Task> GetDepositAddressesAsync(GetDepositAddressesRequest request, CancellationToken ct = default); + /// + /// Get deposit addresses for an asset + /// + /// Request info + /// Cancellation token + /// + Task> GetDepositAddressesAsync(GetDepositAddressesRequest request, CancellationToken ct = default); - /// - /// Deposits request options - /// - GetDepositsOptions GetDepositsOptions { get; } + /// + /// Deposits request options + /// + GetDepositsOptions GetDepositsOptions { get; } - /// - /// Get deposit records - /// - /// Request info - /// The pagination token from the previous request to continue pagination - /// Cancellation token - /// - Task> GetDepositsAsync(GetDepositsRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default); - } + /// + /// Get deposit records + /// + /// Request info + /// The pagination token from the previous request to continue pagination + /// Cancellation token + /// + Task> GetDepositsAsync(GetDepositsRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default); } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IFeeRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IFeeRestClient.cs index 3a012cb..aa571c3 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IFeeRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IFeeRestClient.cs @@ -1,26 +1,22 @@ -using System; -using System.Collections.Generic; -using System.Text; using System.Threading.Tasks; using System.Threading; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Client for requesting user trading fees +/// +public interface IFeeRestClient : ISharedClient { /// - /// Client for requesting user trading fees + /// Fee request options /// - public interface IFeeRestClient : ISharedClient - { - /// - /// Fee request options - /// - EndpointOptions GetFeeOptions { get; } + EndpointOptions GetFeeOptions { get; } - /// - /// Get trading fees for a symbol - /// - /// Request info - /// Cancellation token - Task> GetFeesAsync(GetFeeRequest request, CancellationToken ct = default); - } + /// + /// Get trading fees for a symbol + /// + /// Request info + /// Cancellation token + Task> GetFeesAsync(GetFeeRequest request, CancellationToken ct = default); } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IKlineRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IKlineRestClient.cs index 06cee87..bae78c6 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IKlineRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IKlineRestClient.cs @@ -1,26 +1,24 @@ -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Client for requesting kline/candlestick data +/// +public interface IKlineRestClient : ISharedClient { /// - /// Client for requesting kline/candlestick data + /// Kline request options /// - public interface IKlineRestClient : ISharedClient - { - /// - /// Kline request options - /// - GetKlinesOptions GetKlinesOptions { get; } + GetKlinesOptions GetKlinesOptions { get; } - /// - /// Get kline/candlestick data - /// - /// Request info - /// The pagination token from the previous request to continue pagination - /// Cancellation token - /// - Task> GetKlinesAsync(GetKlinesRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default); - } + /// + /// Get kline/candlestick data + /// + /// Request info + /// The pagination token from the previous request to continue pagination + /// Cancellation token + /// + Task> GetKlinesAsync(GetKlinesRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default); } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IListenKeyRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IListenKeyRestClient.cs index 5ffd6cd..9afaf13 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IListenKeyRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IListenKeyRestClient.cs @@ -1,45 +1,44 @@ -using System.Threading; +using System.Threading; using System.Threading.Tasks; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Client for managing the listen key for user stream updates +/// +public interface IListenKeyRestClient : ISharedClient { /// - /// Client for managing the listen key for user stream updates + /// Start listen key request options /// - public interface IListenKeyRestClient : ISharedClient - { - /// - /// Start listen key request options - /// - EndpointOptions StartOptions { get; } - /// - /// Get the listen key which can be used for user data updates on the socket client - /// - /// Request info - /// Cancellation token - /// - Task> StartListenKeyAsync(StartListenKeyRequest request, CancellationToken ct = default); - /// - /// Keep-alive listen key request options - /// - EndpointOptions KeepAliveOptions { get; } - /// - /// Keep-alive the listen key, needs to be called at a regular interval (typically every 30 minutes) - /// - /// Request info - /// Cancellation token - /// - Task> KeepAliveListenKeyAsync(KeepAliveListenKeyRequest request, CancellationToken ct = default); - /// - /// Stop listen key request options - /// - EndpointOptions StopOptions { get; } - /// - /// Stop the listen key, updates will no longer be send to the user data stream for this listen key - /// - /// Request info - /// Cancellation token - /// - Task> StopListenKeyAsync(StopListenKeyRequest request, CancellationToken ct = default); - } + EndpointOptions StartOptions { get; } + /// + /// Get the listen key which can be used for user data updates on the socket client + /// + /// Request info + /// Cancellation token + /// + Task> StartListenKeyAsync(StartListenKeyRequest request, CancellationToken ct = default); + /// + /// Keep-alive listen key request options + /// + EndpointOptions KeepAliveOptions { get; } + /// + /// Keep-alive the listen key, needs to be called at a regular interval (typically every 30 minutes) + /// + /// Request info + /// Cancellation token + /// + Task> KeepAliveListenKeyAsync(KeepAliveListenKeyRequest request, CancellationToken ct = default); + /// + /// Stop listen key request options + /// + EndpointOptions StopOptions { get; } + /// + /// Stop the listen key, updates will no longer be send to the user data stream for this listen key + /// + /// Request info + /// Cancellation token + /// + Task> StopListenKeyAsync(StopListenKeyRequest request, CancellationToken ct = default); } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IOrderBookRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IOrderBookRestClient.cs index 2994d58..366c1e3 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IOrderBookRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IOrderBookRestClient.cs @@ -1,24 +1,23 @@ -using System.Threading; +using System.Threading; using System.Threading.Tasks; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Client for retrieving the order book for a symbol +/// +public interface IOrderBookRestClient : ISharedClient { /// - /// Client for retrieving the order book for a symbol + /// Order book request options /// - public interface IOrderBookRestClient : ISharedClient - { - /// - /// Order book request options - /// - GetOrderBookOptions GetOrderBookOptions { get; } + GetOrderBookOptions GetOrderBookOptions { get; } - /// - /// Get the order book for a symbol - /// - /// Request info - /// Cancellation token - /// - Task> GetOrderBookAsync(GetOrderBookRequest request, CancellationToken ct = default); - } + /// + /// Get the order book for a symbol + /// + /// Request info + /// Cancellation token + /// + Task> GetOrderBookAsync(GetOrderBookRequest request, CancellationToken ct = default); } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IRecentTradeRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IRecentTradeRestClient.cs index 714b7d5..ccb2263 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IRecentTradeRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IRecentTradeRestClient.cs @@ -1,25 +1,23 @@ -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Client for retrieving the most recent public trades +/// +public interface IRecentTradeRestClient : ISharedClient { /// - /// Client for retrieving the most recent public trades + /// Recent trades request options /// - public interface IRecentTradeRestClient : ISharedClient - { - /// - /// Recent trades request options - /// - GetRecentTradesOptions GetRecentTradesOptions { get; } + GetRecentTradesOptions GetRecentTradesOptions { get; } - /// - /// Get the most recent public trades - /// - /// Request info - /// Cancellation token - /// - Task> GetRecentTradesAsync(GetRecentTradesRequest request, CancellationToken ct = default); - } + /// + /// Get the most recent public trades + /// + /// Request info + /// Cancellation token + /// + Task> GetRecentTradesAsync(GetRecentTradesRequest request, CancellationToken ct = default); } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/ITradeHistoryRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/ITradeHistoryRestClient.cs index e14c64d..1ba0e58 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/ITradeHistoryRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/ITradeHistoryRestClient.cs @@ -1,26 +1,24 @@ -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Client for retrieving trading history +/// +public interface ITradeHistoryRestClient : ISharedClient { /// - /// Client for retrieving trading history + /// Trade history request options /// - public interface ITradeHistoryRestClient : ISharedClient - { - /// - /// Trade history request options - /// - GetTradeHistoryOptions GetTradeHistoryOptions { get; } + GetTradeHistoryOptions GetTradeHistoryOptions { get; } - /// - /// Get public trade history - /// - /// Request info - /// The pagination token from the previous request to continue pagination - /// Cancellation token - /// - Task> GetTradeHistoryAsync(GetTradeHistoryRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default); - } + /// + /// Get public trade history + /// + /// Request info + /// The pagination token from the previous request to continue pagination + /// Cancellation token + /// + Task> GetTradeHistoryAsync(GetTradeHistoryRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default); } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IWithdrawRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IWithdrawRestClient.cs index 4ad2ac0..4769cc7 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IWithdrawRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IWithdrawRestClient.cs @@ -1,24 +1,23 @@ -using System.Threading; +using System.Threading; using System.Threading.Tasks; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Client for requesting to withdraw funds from the exchange +/// +public interface IWithdrawRestClient : ISharedClient { /// - /// Client for requesting to withdraw funds from the exchange + /// Withdraw request options /// - public interface IWithdrawRestClient : ISharedClient - { - /// - /// Withdraw request options - /// - WithdrawOptions WithdrawOptions { get; } + WithdrawOptions WithdrawOptions { get; } - /// - /// Request a withdrawal - /// - /// Request info - /// Cancellation token - /// - Task> WithdrawAsync(WithdrawRequest request, CancellationToken ct = default); - } + /// + /// Request a withdrawal + /// + /// Request info + /// Cancellation token + /// + Task> WithdrawAsync(WithdrawRequest request, CancellationToken ct = default); } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IWithdrawalRestClient .cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IWithdrawalRestClient .cs index 3316888..adafa23 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IWithdrawalRestClient .cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IWithdrawalRestClient .cs @@ -1,26 +1,24 @@ -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Client for retrieving withdrawal records +/// +public interface IWithdrawalRestClient : ISharedClient { /// - /// Client for retrieving withdrawal records + /// Withdrawal record request options /// - public interface IWithdrawalRestClient : ISharedClient - { - /// - /// Withdrawal record request options - /// - GetWithdrawalsOptions GetWithdrawalsOptions { get; } + GetWithdrawalsOptions GetWithdrawalsOptions { get; } - /// - /// Get withdrawal records - /// - /// Request info - /// The pagination token from the previous request to continue pagination - /// Cancellation token - /// - Task> GetWithdrawalsAsync(GetWithdrawalsRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default); - } + /// + /// Get withdrawal records + /// + /// Request info + /// The pagination token from the previous request to continue pagination + /// Cancellation token + /// + Task> GetWithdrawalsAsync(GetWithdrawalsRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default); } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotOrderClientIdRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotOrderClientIdRestClient.cs index c77a76c..38d8a9d 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotOrderClientIdRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotOrderClientIdRestClient.cs @@ -1,37 +1,33 @@ -using System; -using System.Collections.Generic; -using System.Text; using System.Threading.Tasks; using System.Threading; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Client for managing spot orders using a client order id +/// +public interface ISpotOrderClientIdRestClient : ISharedClient { /// - /// Client for managing spot orders using a client order id + /// Spot get order by client order id request options /// - public interface ISpotOrderClientIdRestClient : ISharedClient - { - /// - /// Spot get order by client order id request options - /// - EndpointOptions GetSpotOrderByClientOrderIdOptions { get; } + EndpointOptions GetSpotOrderByClientOrderIdOptions { get; } - /// - /// Get info on a specific spot order using a client order id - /// - /// Request info - /// Cancellation token - Task> GetSpotOrderByClientOrderIdAsync(GetOrderRequest request, CancellationToken ct = default); + /// + /// Get info on a specific spot order using a client order id + /// + /// Request info + /// Cancellation token + Task> GetSpotOrderByClientOrderIdAsync(GetOrderRequest request, CancellationToken ct = default); - /// - /// Spot cancel order by client order id request options - /// - EndpointOptions CancelSpotOrderByClientOrderIdOptions { get; } - /// - /// Cancel a spot order using client order id - /// - /// Request info - /// Cancellation token - Task> CancelSpotOrderByClientOrderIdAsync(CancelOrderRequest request, CancellationToken ct = default); - } + /// + /// Spot cancel order by client order id request options + /// + EndpointOptions CancelSpotOrderByClientOrderIdOptions { get; } + /// + /// Cancel a spot order using client order id + /// + /// Request info + /// Cancellation token + Task> CancelSpotOrderByClientOrderIdAsync(CancelOrderRequest request, CancellationToken ct = default); } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotOrderRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotOrderRestClient.cs index cb5e816..3d67d8b 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotOrderRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotOrderRestClient.cs @@ -1,120 +1,118 @@ -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Client for placing and managing spot orders +/// +public interface ISpotOrderRestClient : ISharedClient { /// - /// Client for placing and managing spot orders + /// How the trading fee is deducted /// - public interface ISpotOrderRestClient : ISharedClient - { - /// - /// How the trading fee is deducted - /// - SharedFeeDeductionType SpotFeeDeductionType { get; } - /// - /// How the asset is determined in which the trading fee is paid - /// - SharedFeeAssetType SpotFeeAssetType { get; } + SharedFeeDeductionType SpotFeeDeductionType { get; } + /// + /// How the asset is determined in which the trading fee is paid + /// + SharedFeeAssetType SpotFeeAssetType { get; } - /// - /// Supported order types - /// - SharedOrderType[] SpotSupportedOrderTypes { get; } - /// - /// Supported time in force - /// - SharedTimeInForce[] SpotSupportedTimeInForce { get; } - /// - /// Quantity types support - /// - SharedQuantitySupport SpotSupportedOrderQuantity { get; } + /// + /// Supported order types + /// + SharedOrderType[] SpotSupportedOrderTypes { get; } + /// + /// Supported time in force + /// + SharedTimeInForce[] SpotSupportedTimeInForce { get; } + /// + /// Quantity types support + /// + SharedQuantitySupport SpotSupportedOrderQuantity { get; } - /// - /// Generate a new random client order id - /// - /// - string GenerateClientOrderId(); + /// + /// Generate a new random client order id + /// + /// + string GenerateClientOrderId(); - /// - /// Spot place order request options - /// - PlaceSpotOrderOptions PlaceSpotOrderOptions { get; } - /// - /// Place a new spot order - /// - /// Request info - /// Cancellation token - Task> PlaceSpotOrderAsync(PlaceSpotOrderRequest request, CancellationToken ct = default); + /// + /// Spot place order request options + /// + PlaceSpotOrderOptions PlaceSpotOrderOptions { get; } + /// + /// Place a new spot order + /// + /// Request info + /// Cancellation token + Task> PlaceSpotOrderAsync(PlaceSpotOrderRequest request, CancellationToken ct = default); - /// - /// Spot get order request options - /// - EndpointOptions GetSpotOrderOptions { get; } - /// - /// Get info on a specific spot order - /// - /// Request info - /// Cancellation token - Task> GetSpotOrderAsync(GetOrderRequest request, CancellationToken ct = default); + /// + /// Spot get order request options + /// + EndpointOptions GetSpotOrderOptions { get; } + /// + /// Get info on a specific spot order + /// + /// Request info + /// Cancellation token + Task> GetSpotOrderAsync(GetOrderRequest request, CancellationToken ct = default); - /// - /// Spot get open orders request options - /// - EndpointOptions GetOpenSpotOrdersOptions { get; } - /// - /// Get info on a open spot orders - /// - /// Request info - /// Cancellation token - Task> GetOpenSpotOrdersAsync(GetOpenOrdersRequest request, CancellationToken ct = default); + /// + /// Spot get open orders request options + /// + EndpointOptions GetOpenSpotOrdersOptions { get; } + /// + /// Get info on a open spot orders + /// + /// Request info + /// Cancellation token + Task> GetOpenSpotOrdersAsync(GetOpenOrdersRequest request, CancellationToken ct = default); - /// - /// Spot get closed orders request options - /// - PaginatedEndpointOptions GetClosedSpotOrdersOptions { get; } - /// - /// Get info on closed spot orders - /// - /// Request info - /// The pagination token from the previous request to continue pagination - /// Cancellation token - Task> GetClosedSpotOrdersAsync(GetClosedOrdersRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default); + /// + /// Spot get closed orders request options + /// + PaginatedEndpointOptions GetClosedSpotOrdersOptions { get; } + /// + /// Get info on closed spot orders + /// + /// Request info + /// The pagination token from the previous request to continue pagination + /// Cancellation token + Task> GetClosedSpotOrdersAsync(GetClosedOrdersRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default); - /// - /// Spot get order trades request options - /// - EndpointOptions GetSpotOrderTradesOptions { get; } - /// - /// Get trades for a specific spot order - /// - /// Request info - /// Cancellation token - Task> GetSpotOrderTradesAsync(GetOrderTradesRequest request, CancellationToken ct = default); + /// + /// Spot get order trades request options + /// + EndpointOptions GetSpotOrderTradesOptions { get; } + /// + /// Get trades for a specific spot order + /// + /// Request info + /// Cancellation token + Task> GetSpotOrderTradesAsync(GetOrderTradesRequest request, CancellationToken ct = default); - /// - /// Spot user trades request options - /// - PaginatedEndpointOptions GetSpotUserTradesOptions { get; } - /// - /// Get spot user trade records - /// - /// Request info - /// The pagination token from the previous request to continue pagination - /// Cancellation token - Task> GetSpotUserTradesAsync(GetUserTradesRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default); + /// + /// Spot user trades request options + /// + PaginatedEndpointOptions GetSpotUserTradesOptions { get; } + /// + /// Get spot user trade records + /// + /// Request info + /// The pagination token from the previous request to continue pagination + /// Cancellation token + Task> GetSpotUserTradesAsync(GetUserTradesRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default); - /// - /// Spot cancel order request options - /// - EndpointOptions CancelSpotOrderOptions { get; } - /// - /// Cancel a spot order - /// - /// Request info - /// Cancellation token - Task> CancelSpotOrderAsync(CancelOrderRequest request, CancellationToken ct = default); + /// + /// Spot cancel order request options + /// + EndpointOptions CancelSpotOrderOptions { get; } + /// + /// Cancel a spot order + /// + /// Request info + /// Cancellation token + Task> CancelSpotOrderAsync(CancelOrderRequest request, CancellationToken ct = default); - } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotSymbolRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotSymbolRestClient.cs index 520f07c..a54cffe 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotSymbolRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotSymbolRestClient.cs @@ -1,24 +1,22 @@ -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Client for requesting spot symbols +/// +public interface ISpotSymbolRestClient : ISharedClient { /// - /// Client for requesting spot symbols + /// Spot symbols request options /// - public interface ISpotSymbolRestClient : ISharedClient - { - /// - /// Spot symbols request options - /// - EndpointOptions GetSpotSymbolsOptions { get; } + EndpointOptions GetSpotSymbolsOptions { get; } - /// - /// Get info on all available spot symbols on the exchange - /// - /// Request info - /// Cancellation token - Task> GetSpotSymbolsAsync(GetSymbolsRequest request, CancellationToken ct = default); - } + /// + /// Get info on all available spot symbols on the exchange + /// + /// Request info + /// Cancellation token + Task> GetSpotSymbolsAsync(GetSymbolsRequest request, CancellationToken ct = default); } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotTickerRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotTickerRestClient.cs index 84a9108..52b8dae 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotTickerRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotTickerRestClient.cs @@ -1,33 +1,31 @@ -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Client for requesting spot tickers +/// +public interface ISpotTickerRestClient : ISharedClient { /// - /// Client for requesting spot tickers + /// Spot ticker request options /// - public interface ISpotTickerRestClient : ISharedClient - { - /// - /// Spot ticker request options - /// - EndpointOptions GetSpotTickerOptions { get; } - /// - /// Get ticker for a specific spot symbol - /// - /// Request info - /// Cancellation token - Task> GetSpotTickerAsync(GetTickerRequest request, CancellationToken ct = default); - /// - /// Spot tickers request options - /// - EndpointOptions GetSpotTickersOptions { get; } - /// - /// Get tickers for all spot symbols - /// - /// Request info - /// Cancellation token - Task> GetSpotTickersAsync(GetTickersRequest request, CancellationToken ct = default); - } + EndpointOptions GetSpotTickerOptions { get; } + /// + /// Get ticker for a specific spot symbol + /// + /// Request info + /// Cancellation token + Task> GetSpotTickerAsync(GetTickerRequest request, CancellationToken ct = default); + /// + /// Spot tickers request options + /// + EndpointOptions GetSpotTickersOptions { get; } + /// + /// Get tickers for all spot symbols + /// + /// Request info + /// Cancellation token + Task> GetSpotTickersAsync(GetTickersRequest request, CancellationToken ct = default); } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotTriggerOrderRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotTriggerOrderRestClient.cs index a3009c7..bc76c75 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotTriggerOrderRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotTriggerOrderRestClient.cs @@ -1,49 +1,45 @@ -using System; -using System.Collections.Generic; -using System.Text; using System.Threading; using System.Threading.Tasks; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Client for placing trigger orders +/// +public interface ISpotTriggerOrderRestClient : ISharedClient { /// - /// Client for placing trigger orders + /// Place spot trigger order options /// - public interface ISpotTriggerOrderRestClient : ISharedClient - { - /// - /// Place spot trigger order options - /// - PlaceSpotTriggerOrderOptions PlaceSpotTriggerOrderOptions { get; } + PlaceSpotTriggerOrderOptions PlaceSpotTriggerOrderOptions { get; } - /// - /// Place a new trigger order - /// - /// Request info - /// Cancellation token - /// - Task> PlaceSpotTriggerOrderAsync(PlaceSpotTriggerOrderRequest request, CancellationToken ct = default); + /// + /// Place a new trigger order + /// + /// Request info + /// Cancellation token + /// + Task> PlaceSpotTriggerOrderAsync(PlaceSpotTriggerOrderRequest request, CancellationToken ct = default); - /// - /// Get trigger order request options - /// - EndpointOptions GetSpotTriggerOrderOptions { get; } - /// - /// Get info on a specific trigger order - /// - /// Request info - /// Cancellation token - Task> GetSpotTriggerOrderAsync(GetOrderRequest request, CancellationToken ct = default); + /// + /// Get trigger order request options + /// + EndpointOptions GetSpotTriggerOrderOptions { get; } + /// + /// Get info on a specific trigger order + /// + /// Request info + /// Cancellation token + Task> GetSpotTriggerOrderAsync(GetOrderRequest request, CancellationToken ct = default); - /// - /// Cancel trigger order request options - /// - EndpointOptions CancelSpotTriggerOrderOptions { get; } - /// - /// Cancel a trigger order - /// - /// Request info - /// Cancellation token - Task> CancelSpotTriggerOrderAsync(CancelOrderRequest request, CancellationToken ct = default); - } + /// + /// Cancel trigger order request options + /// + EndpointOptions CancelSpotTriggerOrderOptions { get; } + /// + /// Cancel a trigger order + /// + /// Request info + /// Cancellation token + Task> CancelSpotTriggerOrderAsync(CancelOrderRequest request, CancellationToken ct = default); } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Socket/Futures/IFuturesOrderSocketClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Socket/Futures/IFuturesOrderSocketClient.cs index db22ab6..af9572c 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Socket/Futures/IFuturesOrderSocketClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Socket/Futures/IFuturesOrderSocketClient.cs @@ -1,28 +1,26 @@ -using CryptoExchange.Net.Objects.Sockets; +using CryptoExchange.Net.Objects.Sockets; using System; -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Client for subscribing to user futures order updates +/// +public interface IFuturesOrderSocketClient : ISharedClient { /// - /// Client for subscribing to user futures order updates + /// Futures orders subscription options /// - public interface IFuturesOrderSocketClient : ISharedClient - { - /// - /// Futures orders subscription options - /// - EndpointOptions SubscribeFuturesOrderOptions { get; } + EndpointOptions SubscribeFuturesOrderOptions { get; } - /// - /// Subscribe to user futures order updates - /// - /// Request info - /// Update handler - /// Cancellation token, can be used to stop the updates - /// - Task> SubscribeToFuturesOrderUpdatesAsync(SubscribeFuturesOrderRequest request, Action> handler, CancellationToken ct = default); - } + /// + /// Subscribe to user futures order updates + /// + /// Request info + /// Update handler + /// Cancellation token, can be used to stop the updates + /// + Task> SubscribeToFuturesOrderUpdatesAsync(SubscribeFuturesOrderRequest request, Action> handler, CancellationToken ct = default); } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Socket/Futures/IPositionSocketClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Socket/Futures/IPositionSocketClient.cs index 03cba56..a149172 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Socket/Futures/IPositionSocketClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Socket/Futures/IPositionSocketClient.cs @@ -1,28 +1,26 @@ -using System; -using System.Collections.Generic; +using System; using System.Threading.Tasks; using System.Threading; using CryptoExchange.Net.Objects.Sockets; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Client for subscribing to position updates +/// +public interface IPositionSocketClient : ISharedClient { /// - /// Client for subscribing to position updates + /// Position subscription options /// - public interface IPositionSocketClient : ISharedClient - { - /// - /// Position subscription options - /// - EndpointOptions SubscribePositionOptions { get; } + EndpointOptions SubscribePositionOptions { get; } - /// - /// Subscribe to user position updates - /// - /// Request info - /// Update handler - /// Cancellation token, can be used to stop the updates - /// - Task> SubscribeToPositionUpdatesAsync(SubscribePositionRequest request, Action> handler, CancellationToken ct = default); - } + /// + /// Subscribe to user position updates + /// + /// Request info + /// Update handler + /// Cancellation token, can be used to stop the updates + /// + Task> SubscribeToPositionUpdatesAsync(SubscribePositionRequest request, Action> handler, CancellationToken ct = default); } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Socket/IBalanceSocketClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Socket/IBalanceSocketClient.cs index 24c66a6..361587b 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Socket/IBalanceSocketClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Socket/IBalanceSocketClient.cs @@ -1,28 +1,26 @@ -using CryptoExchange.Net.Objects.Sockets; +using CryptoExchange.Net.Objects.Sockets; using System; -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Client for subscribing to user balance updates +/// +public interface IBalanceSocketClient : ISharedClient { /// - /// Client for subscribing to user balance updates + /// Balance subscription options /// - public interface IBalanceSocketClient : ISharedClient - { - /// - /// Balance subscription options - /// - EndpointOptions SubscribeBalanceOptions { get; } + EndpointOptions SubscribeBalanceOptions { get; } - /// - /// Subscribe to user balance updates - /// - /// Request info - /// Update handler - /// Cancellation token, can be used to stop the updates - /// - Task> SubscribeToBalanceUpdatesAsync(SubscribeBalancesRequest request, Action> handler, CancellationToken ct = default); - } + /// + /// Subscribe to user balance updates + /// + /// Request info + /// Update handler + /// Cancellation token, can be used to stop the updates + /// + Task> SubscribeToBalanceUpdatesAsync(SubscribeBalancesRequest request, Action> handler, CancellationToken ct = default); } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Socket/IBookTickerSocketClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Socket/IBookTickerSocketClient.cs index ad2c575..0818bf4 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Socket/IBookTickerSocketClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Socket/IBookTickerSocketClient.cs @@ -1,27 +1,26 @@ -using CryptoExchange.Net.Objects.Sockets; +using CryptoExchange.Net.Objects.Sockets; using System; using System.Threading; using System.Threading.Tasks; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Client for subscribing to book ticker updates for a symbol +/// +public interface IBookTickerSocketClient : ISharedClient { /// - /// Client for subscribing to book ticker updates for a symbol + /// Book ticker subscription options /// - public interface IBookTickerSocketClient : ISharedClient - { - /// - /// Book ticker subscription options - /// - EndpointOptions SubscribeBookTickerOptions { get; } + EndpointOptions SubscribeBookTickerOptions { get; } - /// - /// Subscribe to book ticker (best ask/bid) updates for a symbol - /// - /// Request info - /// Update handler - /// Cancellation token, can be used to stop the updates - /// - Task> SubscribeToBookTickerUpdatesAsync(SubscribeBookTickerRequest request, Action> handler, CancellationToken ct = default); - } + /// + /// Subscribe to book ticker (best ask/bid) updates for a symbol + /// + /// Request info + /// Update handler + /// Cancellation token, can be used to stop the updates + /// + Task> SubscribeToBookTickerUpdatesAsync(SubscribeBookTickerRequest request, Action> handler, CancellationToken ct = default); } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Socket/IKlineSocketClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Socket/IKlineSocketClient.cs index 9a8c520..fae6330 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Socket/IKlineSocketClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Socket/IKlineSocketClient.cs @@ -1,27 +1,26 @@ -using System; +using System; using System.Threading.Tasks; using System.Threading; using CryptoExchange.Net.Objects.Sockets; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Client for subscribing to kline/candlestick updates for a symbol +/// +public interface IKlineSocketClient : ISharedClient { /// - /// Client for subscribing to kline/candlestick updates for a symbol + /// Kline subscription options /// - public interface IKlineSocketClient : ISharedClient - { - /// - /// Kline subscription options - /// - SubscribeKlineOptions SubscribeKlineOptions { get; } + SubscribeKlineOptions SubscribeKlineOptions { get; } - /// - /// Subscribe to kline/candlestick updates for a symbol - /// - /// Request info - /// Update handler - /// Cancellation token, can be used to stop the updates - /// - Task> SubscribeToKlineUpdatesAsync(SubscribeKlineRequest request, Action> handler, CancellationToken ct = default); - } + /// + /// Subscribe to kline/candlestick updates for a symbol + /// + /// Request info + /// Update handler + /// Cancellation token, can be used to stop the updates + /// + Task> SubscribeToKlineUpdatesAsync(SubscribeKlineRequest request, Action> handler, CancellationToken ct = default); } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Socket/IOrderBookSocketClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Socket/IOrderBookSocketClient.cs index 4cab64f..831e416 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Socket/IOrderBookSocketClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Socket/IOrderBookSocketClient.cs @@ -1,27 +1,26 @@ -using CryptoExchange.Net.Objects.Sockets; +using CryptoExchange.Net.Objects.Sockets; using System; using System.Threading; using System.Threading.Tasks; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Client for subscribing to order book snapshot updates for a symbol +/// +public interface IOrderBookSocketClient : ISharedClient { /// - /// Client for subscribing to order book snapshot updates for a symbol + /// Order book subscription options /// - public interface IOrderBookSocketClient : ISharedClient - { - /// - /// Order book subscription options - /// - SubscribeOrderBookOptions SubscribeOrderBookOptions { get; } + SubscribeOrderBookOptions SubscribeOrderBookOptions { get; } - /// - /// Subscribe to order book snapshot updates for a symbol - /// - /// Request info - /// Update handler - /// Cancellation token, can be used to stop the updates - /// - Task> SubscribeToOrderBookUpdatesAsync(SubscribeOrderBookRequest request, Action> handler, CancellationToken ct = default); - } + /// + /// Subscribe to order book snapshot updates for a symbol + /// + /// Request info + /// Update handler + /// Cancellation token, can be used to stop the updates + /// + Task> SubscribeToOrderBookUpdatesAsync(SubscribeOrderBookRequest request, Action> handler, CancellationToken ct = default); } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Socket/ITickerSocketClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Socket/ITickerSocketClient.cs index 329d1d3..1e151a2 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Socket/ITickerSocketClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Socket/ITickerSocketClient.cs @@ -1,27 +1,26 @@ -using CryptoExchange.Net.Objects.Sockets; +using CryptoExchange.Net.Objects.Sockets; using System; using System.Threading; using System.Threading.Tasks; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Client for subscribing to ticker updates for a symbol +/// +public interface ITickerSocketClient : ISharedClient { /// - /// Client for subscribing to ticker updates for a symbol + /// Ticker subscription options /// - public interface ITickerSocketClient : ISharedClient - { - /// - /// Ticker subscription options - /// - EndpointOptions SubscribeTickerOptions { get; } + EndpointOptions SubscribeTickerOptions { get; } - /// - /// Subscribe to ticker updates for a symbol - /// - /// Request info - /// Update handler - /// Cancellation token, can be used to stop the updates - /// - Task> SubscribeToTickerUpdatesAsync(SubscribeTickerRequest request, Action> handler, CancellationToken ct = default); - } + /// + /// Subscribe to ticker updates for a symbol + /// + /// Request info + /// Update handler + /// Cancellation token, can be used to stop the updates + /// + Task> SubscribeToTickerUpdatesAsync(SubscribeTickerRequest request, Action> handler, CancellationToken ct = default); } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Socket/ITickersSocketClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Socket/ITickersSocketClient.cs index f3652c3..bb0525c 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Socket/ITickersSocketClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Socket/ITickersSocketClient.cs @@ -1,28 +1,26 @@ -using CryptoExchange.Net.Objects.Sockets; +using CryptoExchange.Net.Objects.Sockets; using System; -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Client for subscribing to ticker updates for all symbols +/// +public interface ITickersSocketClient : ISharedClient { /// - /// Client for subscribing to ticker updates for all symbols + /// Tickers subscription options /// - public interface ITickersSocketClient : ISharedClient - { - /// - /// Tickers subscription options - /// - EndpointOptions SubscribeAllTickersOptions { get; } + EndpointOptions SubscribeAllTickersOptions { get; } - /// - /// Subscribe to tickers updates for all symbols - /// - /// Request info - /// Update handler - /// Cancellation token, can be used to stop the updates - /// - Task> SubscribeToAllTickersUpdatesAsync(SubscribeAllTickersRequest request, Action> handler, CancellationToken ct = default); - } + /// + /// Subscribe to tickers updates for all symbols + /// + /// Request info + /// Update handler + /// Cancellation token, can be used to stop the updates + /// + Task> SubscribeToAllTickersUpdatesAsync(SubscribeAllTickersRequest request, Action> handler, CancellationToken ct = default); } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Socket/ITradeSocketClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Socket/ITradeSocketClient.cs index e6a8f21..227b387 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Socket/ITradeSocketClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Socket/ITradeSocketClient.cs @@ -1,28 +1,26 @@ -using CryptoExchange.Net.Objects.Sockets; +using CryptoExchange.Net.Objects.Sockets; using System; -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Client for subscribing to public trade updates for a symbol +/// +public interface ITradeSocketClient : ISharedClient { /// - /// Client for subscribing to public trade updates for a symbol + /// Trade subscription options /// - public interface ITradeSocketClient : ISharedClient - { - /// - /// Trade subscription options - /// - EndpointOptions SubscribeTradeOptions { get; } + EndpointOptions SubscribeTradeOptions { get; } - /// - /// Subscribe to public trade updates for a symbol - /// - /// Request info - /// Update handler - /// Cancellation token, can be used to stop the updates - /// - Task> SubscribeToTradeUpdatesAsync(SubscribeTradeRequest request, Action> handler, CancellationToken ct = default); - } + /// + /// Subscribe to public trade updates for a symbol + /// + /// Request info + /// Update handler + /// Cancellation token, can be used to stop the updates + /// + Task> SubscribeToTradeUpdatesAsync(SubscribeTradeRequest request, Action> handler, CancellationToken ct = default); } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Socket/IUserTradeSocketClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Socket/IUserTradeSocketClient.cs index 031e9f8..57c18f1 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Socket/IUserTradeSocketClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Socket/IUserTradeSocketClient.cs @@ -1,28 +1,26 @@ -using CryptoExchange.Net.Objects.Sockets; +using CryptoExchange.Net.Objects.Sockets; using System; -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Client for subscribing to user trade updates +/// +public interface IUserTradeSocketClient : ISharedClient { /// - /// Client for subscribing to user trade updates + /// User trade subscription options /// - public interface IUserTradeSocketClient : ISharedClient - { - /// - /// User trade subscription options - /// - EndpointOptions SubscribeUserTradeOptions { get; } + EndpointOptions SubscribeUserTradeOptions { get; } - /// - /// Subscribe to user trade updates - /// - /// Request info - /// Update handler - /// Cancellation token, can be used to stop the updates - /// - Task> SubscribeToUserTradeUpdatesAsync(SubscribeUserTradeRequest request, Action> handler, CancellationToken ct = default); - } + /// + /// Subscribe to user trade updates + /// + /// Request info + /// Update handler + /// Cancellation token, can be used to stop the updates + /// + Task> SubscribeToUserTradeUpdatesAsync(SubscribeUserTradeRequest request, Action> handler, CancellationToken ct = default); } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Socket/Spot/ISpotOrderSocketClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Socket/Spot/ISpotOrderSocketClient.cs index 12d24b2..fe1897d 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Socket/Spot/ISpotOrderSocketClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Socket/Spot/ISpotOrderSocketClient.cs @@ -1,28 +1,26 @@ -using CryptoExchange.Net.Objects.Sockets; +using CryptoExchange.Net.Objects.Sockets; using System; -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Client for subscribing to user spot order updates +/// +public interface ISpotOrderSocketClient : ISharedClient { /// - /// Client for subscribing to user spot order updates + /// Spot orders subscription options /// - public interface ISpotOrderSocketClient : ISharedClient - { - /// - /// Spot orders subscription options - /// - EndpointOptions SubscribeSpotOrderOptions { get; } + EndpointOptions SubscribeSpotOrderOptions { get; } - /// - /// Subscribe to user spot order updates - /// - /// Request info - /// Update handler - /// Cancellation token, can be used to stop the updates - /// - Task> SubscribeToSpotOrderUpdatesAsync(SubscribeSpotOrderRequest request, Action> handler, CancellationToken ct = default); - } + /// + /// Subscribe to user spot order updates + /// + /// Request info + /// Update handler + /// Cancellation token, can be used to stop the updates + /// + Task> SubscribeToSpotOrderUpdatesAsync(SubscribeSpotOrderRequest request, Action> handler, CancellationToken ct = default); } diff --git a/CryptoExchange.Net/SharedApis/Models/ExchangeEvent.cs b/CryptoExchange.Net/SharedApis/Models/ExchangeEvent.cs index 6e1df01..7bde82b 100644 --- a/CryptoExchange.Net/SharedApis/Models/ExchangeEvent.cs +++ b/CryptoExchange.Net/SharedApis/Models/ExchangeEvent.cs @@ -1,34 +1,33 @@ -using CryptoExchange.Net.Objects.Sockets; +using CryptoExchange.Net.Objects.Sockets; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// An update event for a specific exchange +/// +/// Type of the data +public class ExchangeEvent : DataEvent { /// - /// An update event for a specific exchange + /// The exchange /// - /// Type of the data - public class ExchangeEvent : DataEvent + public string Exchange { get; } + + /// + /// ctor + /// + public ExchangeEvent(string exchange, DataEvent evnt) : + base(evnt.Data, + evnt.StreamId, + evnt.Symbol, + evnt.OriginalData, + evnt.ReceiveTime, + evnt.UpdateType) { - /// - /// The exchange - /// - public string Exchange { get; } - - /// - /// ctor - /// - public ExchangeEvent(string exchange, DataEvent evnt) : - base(evnt.Data, - evnt.StreamId, - evnt.Symbol, - evnt.OriginalData, - evnt.ReceiveTime, - evnt.UpdateType) - { - DataTime = evnt.DataTime; - Exchange = exchange; - } - - /// - public override string ToString() => $"{Exchange} - " + base.ToString(); + DataTime = evnt.DataTime; + Exchange = exchange; } + + /// + public override string ToString() => $"{Exchange} - " + base.ToString(); } diff --git a/CryptoExchange.Net/SharedApis/Models/ExchangeParameter.cs b/CryptoExchange.Net/SharedApis/Models/ExchangeParameter.cs index c317ac8..acb56b2 100644 --- a/CryptoExchange.Net/SharedApis/Models/ExchangeParameter.cs +++ b/CryptoExchange.Net/SharedApis/Models/ExchangeParameter.cs @@ -1,34 +1,33 @@ -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Exchange parameter +/// +public class ExchangeParameter { /// - /// Exchange parameter + /// Exchange name /// - public class ExchangeParameter - { - /// - /// Exchange name - /// - public string Exchange { get; set; } - /// - /// Parameter name - /// - public string Name { get; set; } - /// - /// Parameter value - /// - public object Value { get; set; } + public string Exchange { get; set; } + /// + /// Parameter name + /// + public string Name { get; set; } + /// + /// Parameter value + /// + public object Value { get; set; } - /// - /// Create a new exchange parameter - /// - /// Exchange name - /// Parameter name - /// Parameter value - public ExchangeParameter(string exchange, string name, object value) - { - Exchange = exchange; - Name = name; - Value = value; - } + /// + /// Create a new exchange parameter + /// + /// Exchange name + /// Parameter name + /// Parameter value + public ExchangeParameter(string exchange, string name, object value) + { + Exchange = exchange; + Name = name; + Value = value; } } diff --git a/CryptoExchange.Net/SharedApis/Models/ExchangeParameters.cs b/CryptoExchange.Net/SharedApis/Models/ExchangeParameters.cs index 85f838b..d688b71 100644 --- a/CryptoExchange.Net/SharedApis/Models/ExchangeParameters.cs +++ b/CryptoExchange.Net/SharedApis/Models/ExchangeParameters.cs @@ -1,176 +1,175 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Exchange parameters +/// +public class ExchangeParameters { + private readonly List _parameters; + private readonly static List _staticParameters = new List(); + /// - /// Exchange parameters + /// ctor /// - public class ExchangeParameters + /// The parameters to add + public ExchangeParameters(params ExchangeParameter[] parameters) { - private readonly List _parameters; - private readonly static List _staticParameters = new List(); + _parameters = parameters.ToList(); + } - /// - /// ctor - /// - /// The parameters to add - public ExchangeParameters(params ExchangeParameter[] parameters) + /// + /// Add a new parameter value + /// + /// + public void AddValue(ExchangeParameter exchangeParameter) + { + _parameters.Add(exchangeParameter); + } + + /// + /// Check whether a specific parameter is provided in this specific instance + /// + /// The exchange name + /// Parameter name + /// Type of the parameter value + /// + public bool HasValue(string exchange, string name, Type type) + { + var val = _parameters.SingleOrDefault(x => x.Exchange == exchange && x.Name == name); + val ??= _staticParameters.SingleOrDefault(x => x.Exchange == exchange && x.Name == name); + + if (val == null) + return false; + + try { - _parameters = parameters.ToList(); + Type t = Nullable.GetUnderlyingType(type) ?? type; + Convert.ChangeType(val.Value, t); + return true; } - - /// - /// Add a new parameter value - /// - /// - public void AddValue(ExchangeParameter exchangeParameter) + catch { - _parameters.Add(exchangeParameter); + return false; } + } - /// - /// Check whether a specific parameter is provided in this specific instance - /// - /// The exchange name - /// Parameter name - /// Type of the parameter value - /// - public bool HasValue(string exchange, string name, Type type) + /// + /// Check whether a specific parameter is provided in the default parameters or the provided instance + /// + /// The provided exchange parameter in the request + /// The exchange name + /// Parameter name + /// Type of the parameter value + /// + public static bool HasValue(ExchangeParameters? exchangeParameters, string exchange, string name, Type type) + { + var provided = exchangeParameters?.HasValue(exchange, name, type); + if (provided == true) + return true; + + var val = _staticParameters.SingleOrDefault(x => x.Exchange == exchange && x.Name == name); + if (val == null) + return false; + + try { - var val = _parameters.SingleOrDefault(x => x.Exchange == exchange && x.Name == name); - val ??= _staticParameters.SingleOrDefault(x => x.Exchange == exchange && x.Name == name); - - if (val == null) - return false; - - try - { - Type t = Nullable.GetUnderlyingType(type) ?? type; - Convert.ChangeType(val.Value, t); - return true; - } - catch - { - return false; - } + Type t = Nullable.GetUnderlyingType(type) ?? type; + Convert.ChangeType(val.Value, t); + return true; } - - /// - /// Check whether a specific parameter is provided in the default parameters or the provided instance - /// - /// The provided exchange parameter in the request - /// The exchange name - /// Parameter name - /// Type of the parameter value - /// - public static bool HasValue(ExchangeParameters? exchangeParameters, string exchange, string name, Type type) + catch { - var provided = exchangeParameters?.HasValue(exchange, name, type); - if (provided == true) - return true; - - var val = _staticParameters.SingleOrDefault(x => x.Exchange == exchange && x.Name == name); - if (val == null) - return false; - - try - { - Type t = Nullable.GetUnderlyingType(type) ?? type; - Convert.ChangeType(val.Value, t); - return true; - } - catch - { - return false; - } + return false; } + } - /// - /// Get the value of a parameter from this instance - /// - /// Type of the parameter value - /// Exchange name - /// Parameter name - public T? GetValue(string exchange, string name) + /// + /// Get the value of a parameter from this instance + /// + /// Type of the parameter value + /// Exchange name + /// Parameter name + public T? GetValue(string exchange, string name) + { + var val = _parameters.SingleOrDefault(x => x.Exchange == exchange && x.Name == name); + if (val == null) + return default; + + try { - var val = _parameters.SingleOrDefault(x => x.Exchange == exchange && x.Name == name); - if (val == null) + Type t = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T); + return (T)Convert.ChangeType(val.Value, t); + } + catch + { + throw new ArgumentException("Incorrect type for parameter, expected " + typeof(T).Name, name); + } + } + + /// + /// Get the value of a parameter from this instance or the default values + /// + /// Type of the parameter value + /// The request parameters + /// Exchange name + /// Parameter name + public static T? GetValue(ExchangeParameters? exchangeParameters, string exchange, string name) + { + T? value; + if (exchangeParameters == null) + { + var parameter = _staticParameters.SingleOrDefault(x => x.Exchange == exchange && x.Name == name); + if (parameter == null) return default; + if (parameter.Value is T val) + return val; + try { Type t = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T); - return (T)Convert.ChangeType(val.Value, t); + return (T)Convert.ChangeType(parameter.Value, t); } catch { throw new ArgumentException("Incorrect type for parameter, expected " + typeof(T).Name, name); } } - - /// - /// Get the value of a parameter from this instance or the default values - /// - /// Type of the parameter value - /// The request parameters - /// Exchange name - /// Parameter name - public static T? GetValue(ExchangeParameters? exchangeParameters, string exchange, string name) + else { - T? value; - if (exchangeParameters == null) - { - var parameter = _staticParameters.SingleOrDefault(x => x.Exchange == exchange && x.Name == name); - if (parameter == null) - return default; - - if (parameter.Value is T val) - return val; - - try - { - Type t = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T); - return (T)Convert.ChangeType(parameter.Value, t); - } - catch - { - throw new ArgumentException("Incorrect type for parameter, expected " + typeof(T).Name, name); - } - } - else - { - value = exchangeParameters.GetValue(exchange, name); - } - - return value; + value = exchangeParameters.GetValue(exchange, name); } - /// - /// Set static parameters - /// - /// Exchange name - /// Parameter name - /// Parameter value - public static void SetStaticParameter(string exchange, string key, object value) - { - var existing = _staticParameters.SingleOrDefault(x => x.Exchange == exchange && x.Name == key); - if (existing != null) - { - existing.Value = value; - return; - } + return value; + } - _staticParameters.Add(new ExchangeParameter(exchange, key, value)); - } - - /// - /// Reset the static parameters, clears all parameters for all exchanges - /// - public static void ResetStaticParameters() + /// + /// Set static parameters + /// + /// Exchange name + /// Parameter name + /// Parameter value + public static void SetStaticParameter(string exchange, string key, object value) + { + var existing = _staticParameters.SingleOrDefault(x => x.Exchange == exchange && x.Name == key); + if (existing != null) { - _staticParameters.Clear(); - } + existing.Value = value; + return; + } + + _staticParameters.Add(new ExchangeParameter(exchange, key, value)); + } + + /// + /// Reset the static parameters, clears all parameters for all exchanges + /// + public static void ResetStaticParameters() + { + _staticParameters.Clear(); } } diff --git a/CryptoExchange.Net/SharedApis/Models/ExchangeResult.cs b/CryptoExchange.Net/SharedApis/Models/ExchangeResult.cs index be09d54..1765a9d 100644 --- a/CryptoExchange.Net/SharedApis/Models/ExchangeResult.cs +++ b/CryptoExchange.Net/SharedApis/Models/ExchangeResult.cs @@ -1,44 +1,43 @@ -using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// A CallResult from an exchange +/// +/// +public class ExchangeResult : CallResult { /// - /// A CallResult from an exchange + /// The exchange /// - /// - public class ExchangeResult : CallResult + public string Exchange { get; } + + /// + /// ctor + /// + public ExchangeResult( + string exchange, + Error error) : + base(error) { - /// - /// The exchange - /// - public string Exchange { get; } - - /// - /// ctor - /// - public ExchangeResult( - string exchange, - Error error) : - base(error) - { - Exchange = exchange; - } - - /// - /// ctor - /// - public ExchangeResult( - string exchange, - CallResult result) : - base( - result.Data, - result.OriginalData, - result.Error) - { - Exchange = exchange; - } - - /// - public override string ToString() => $"{Exchange} - " + base.ToString(); + Exchange = exchange; } + + /// + /// ctor + /// + public ExchangeResult( + string exchange, + CallResult result) : + base( + result.Data, + result.OriginalData, + result.Error) + { + Exchange = exchange; + } + + /// + public override string ToString() => $"{Exchange} - " + base.ToString(); } diff --git a/CryptoExchange.Net/SharedApis/Models/ExchangeWebResult.cs b/CryptoExchange.Net/SharedApis/Models/ExchangeWebResult.cs index b00ad20..2e104df 100644 --- a/CryptoExchange.Net/SharedApis/Models/ExchangeWebResult.cs +++ b/CryptoExchange.Net/SharedApis/Models/ExchangeWebResult.cs @@ -1,149 +1,148 @@ -using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects; using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Net.Http; using System.Net; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// A WebCallResult from an exchange +/// +/// The result type +public class ExchangeWebResult : WebCallResult { /// - /// A WebCallResult from an exchange + /// The exchange /// - /// The result type - public class ExchangeWebResult : WebCallResult + public string Exchange { get; } + + /// + /// The trade modes for which the result data is + /// + public TradingMode[]? DataTradeMode { get; } + + /// + /// Token to retrieve the next page with + /// + public INextPageToken? NextPageToken { get; } + + /// + /// ctor + /// + public ExchangeWebResult( + string exchange, + Error error) : + base(error) { - /// - /// The exchange - /// - public string Exchange { get; } - - /// - /// The trade modes for which the result data is - /// - public TradingMode[]? DataTradeMode { get; } - - /// - /// Token to retrieve the next page with - /// - public INextPageToken? NextPageToken { get; } - - /// - /// ctor - /// - public ExchangeWebResult( - string exchange, - Error error) : - base(error) - { - Exchange = exchange; - } - - /// - /// ctor - /// - public ExchangeWebResult( - string exchange, - TradingMode dataTradeMode, - WebCallResult result, - INextPageToken? nextPageToken = null) : - base(result.ResponseStatusCode, - result.ResponseHeaders, - result.ResponseTime, - result.ResponseLength, - result.OriginalData, - result.RequestId, - result.RequestUrl, - result.RequestBody, - result.RequestMethod, - result.RequestHeaders, - result.DataSource, - result.Data, - result.Error) - { - DataTradeMode = new[] { dataTradeMode }; - Exchange = exchange; - NextPageToken = nextPageToken; - } - - /// - /// ctor - /// - public ExchangeWebResult( - string exchange, - TradingMode[]? dataTradeModes, - WebCallResult result, - INextPageToken? nextPageToken = null) : - base(result.ResponseStatusCode, - result.ResponseHeaders, - result.ResponseTime, - result.ResponseLength, - result.OriginalData, - result.RequestId, - result.RequestUrl, - result.RequestBody, - result.RequestMethod, - result.RequestHeaders, - result.DataSource, - result.Data, - result.Error) - { - DataTradeMode = dataTradeModes; - Exchange = exchange; - NextPageToken = nextPageToken; - } - - /// - /// Create a new result - /// - public ExchangeWebResult( - string exchange, - TradingMode[]? dataTradeModes, - HttpStatusCode? code, - KeyValuePair[]? responseHeaders, - TimeSpan? responseTime, - long? responseLength, - string? originalData, - int? requestId, - string? requestUrl, - string? requestBody, - HttpMethod? requestMethod, - KeyValuePair[]? requestHeaders, - ResultDataSource dataSource, - [AllowNull] T data, - Error? error, - INextPageToken? nextPageToken = null) : base( - code, - responseHeaders, - responseTime, - responseLength, - originalData, - requestId, - requestUrl, - requestBody, - requestMethod, - requestHeaders, - dataSource, - data, - error) - { - DataTradeMode = dataTradeModes; - Exchange = exchange; - NextPageToken = nextPageToken; - } - - /// - /// Copy the ExchangeWebResult to a new data type - /// - /// The new type - /// The data of the new type - /// - public new ExchangeWebResult As([AllowNull] K data) - { - return new ExchangeWebResult(Exchange, DataTradeMode, ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, data, Error, NextPageToken); - } - - /// - public override string ToString() => $"{Exchange} - " + base.ToString(); + Exchange = exchange; } + + /// + /// ctor + /// + public ExchangeWebResult( + string exchange, + TradingMode dataTradeMode, + WebCallResult result, + INextPageToken? nextPageToken = null) : + base(result.ResponseStatusCode, + result.ResponseHeaders, + result.ResponseTime, + result.ResponseLength, + result.OriginalData, + result.RequestId, + result.RequestUrl, + result.RequestBody, + result.RequestMethod, + result.RequestHeaders, + result.DataSource, + result.Data, + result.Error) + { + DataTradeMode = new[] { dataTradeMode }; + Exchange = exchange; + NextPageToken = nextPageToken; + } + + /// + /// ctor + /// + public ExchangeWebResult( + string exchange, + TradingMode[]? dataTradeModes, + WebCallResult result, + INextPageToken? nextPageToken = null) : + base(result.ResponseStatusCode, + result.ResponseHeaders, + result.ResponseTime, + result.ResponseLength, + result.OriginalData, + result.RequestId, + result.RequestUrl, + result.RequestBody, + result.RequestMethod, + result.RequestHeaders, + result.DataSource, + result.Data, + result.Error) + { + DataTradeMode = dataTradeModes; + Exchange = exchange; + NextPageToken = nextPageToken; + } + + /// + /// Create a new result + /// + public ExchangeWebResult( + string exchange, + TradingMode[]? dataTradeModes, + HttpStatusCode? code, + KeyValuePair[]? responseHeaders, + TimeSpan? responseTime, + long? responseLength, + string? originalData, + int? requestId, + string? requestUrl, + string? requestBody, + HttpMethod? requestMethod, + KeyValuePair[]? requestHeaders, + ResultDataSource dataSource, + [AllowNull] T data, + Error? error, + INextPageToken? nextPageToken = null) : base( + code, + responseHeaders, + responseTime, + responseLength, + originalData, + requestId, + requestUrl, + requestBody, + requestMethod, + requestHeaders, + dataSource, + data, + error) + { + DataTradeMode = dataTradeModes; + Exchange = exchange; + NextPageToken = nextPageToken; + } + + /// + /// Copy the ExchangeWebResult to a new data type + /// + /// The new type + /// The data of the new type + /// + public new ExchangeWebResult As([AllowNull] TNew data) + { + return new ExchangeWebResult(Exchange, DataTradeMode, ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, data, Error, NextPageToken); + } + + /// + public override string ToString() => $"{Exchange} - " + base.ToString(); } diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/EndpointOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/EndpointOptions.cs index 8d4d552..7e54ba3 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/EndpointOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/EndpointOptions.cs @@ -1,190 +1,189 @@ -using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects; using System; using System.Collections.Generic; -using System.Diagnostics; +#if NET5_0_OR_GREATER using System.Diagnostics.CodeAnalysis; +#endif using System.Linq; using System.Text; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Options for an exchange endpoint +/// +public class EndpointOptions { /// - /// Options for an exchange endpoint + /// Required exchange-specific parameters /// - public class EndpointOptions + public List RequiredExchangeParameters { get; set; } = new List(); + /// + /// Optional exchange-specific parameters + /// + public List OptionalExchangeParameters { get; set; } = new List(); + /// + /// Endpoint name + /// + public string EndpointName { get; set; } + /// + /// Information on the specific exchange request + /// + public string? RequestNotes { get; set; } + /// + /// Whether the call requires authentication + /// + public bool NeedsAuthentication { get; set; } + /// + /// Whether the call is supported by the exchange + /// + public bool Supported { get; set; } = true; + + /// + /// ctor + /// + public EndpointOptions(string endpointName, bool needAuthentication) { - /// - /// Required exchange-specific parameters - /// - public List RequiredExchangeParameters { get; set; } = new List(); - /// - /// Optional exchange-specific parameters - /// - public List OptionalExchangeParameters { get; set; } = new List(); - /// - /// Endpoint name - /// - public string EndpointName { get; set; } - /// - /// Information on the specific exchange request - /// - public string? RequestNotes { get; set; } - /// - /// Whether the call requires authentication - /// - public bool NeedsAuthentication { get; set; } - /// - /// Whether the call is supported by the exchange - /// - public bool Supported { get; set; } = true; - - /// - /// ctor - /// - public EndpointOptions(string endpointName, bool needAuthentication) - { - EndpointName = endpointName; - NeedsAuthentication = needAuthentication; - } - - /// - /// Validate a request - /// - /// Exchange name - /// Provided exchange parameters - /// Request trading mode - /// Supported trading modes - /// - public virtual Error? ValidateRequest(string exchange, ExchangeParameters? exchangeParameters, TradingMode? tradingMode, TradingMode[] supportedTradingModes) - { - if (tradingMode != null && !supportedTradingModes.Contains(tradingMode.Value)) - return ArgumentError.Invalid("TradingMode", $"TradingMode.{tradingMode} is not supported, supported types: {string.Join(", ", supportedTradingModes)}"); - - foreach (var param in RequiredExchangeParameters) - { - if (!string.IsNullOrEmpty(param.Name)) - { - if (ExchangeParameters.HasValue(exchangeParameters, exchange, param.Name!, param.ValueType) != true) - return ArgumentError.Invalid(param.Name!, $"Required exchange parameter `{param.Name}` for exchange `{exchange}` is missing or has incorrect type. Expected type is {param.ValueType.Name}. Example: {param.ExampleValue}"); - } - else - { - if (param.Names!.All(x => ExchangeParameters.HasValue(exchangeParameters, exchange, x, param.ValueType) != true)) - return ArgumentError.Invalid(string.Join("/", param.Names!), $"One of exchange parameters `{string.Join(", ", param.Names!)}` for exchange `{exchange}` should be provided. Example: {param.ExampleValue}"); - } - } - - return null; - } - - /// - public virtual string ToString(string exchange) - { - if (!Supported) - return $"{exchange} {EndpointName} NOT SUPPORTED"; - - var sb = new StringBuilder(); - sb.AppendLine($"{exchange} {EndpointName}"); - if (!string.IsNullOrEmpty(RequestNotes)) - sb.AppendLine(RequestNotes); - sb.AppendLine($"Needs authentication: {NeedsAuthentication}"); - sb.AppendLine($"Required exchange specific parameters: {string.Join(", ", RequiredExchangeParameters.Select(x => x.ToString()))}"); - sb.AppendLine($"Optional exchange specific parameters: {string.Join(", ", OptionalExchangeParameters.Select(x => x.ToString()))}"); - return sb.ToString(); - } + EndpointName = endpointName; + NeedsAuthentication = needAuthentication; } /// - /// Options for an exchange endpoint + /// Validate a request /// - /// Type of data -#if NET5_0_OR_GREATER - public class EndpointOptions<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T> : EndpointOptions where T : SharedRequest -#else - public class EndpointOptions : EndpointOptions where T : SharedRequest -#endif + /// Exchange name + /// Provided exchange parameters + /// Request trading mode + /// Supported trading modes + /// + public virtual Error? ValidateRequest(string exchange, ExchangeParameters? exchangeParameters, TradingMode? tradingMode, TradingMode[] supportedTradingModes) { - /// - /// Required optional parameters in the request - /// - public List RequiredOptionalParameters { get; set; } = new List(); + if (tradingMode != null && !supportedTradingModes.Contains(tradingMode.Value)) + return ArgumentError.Invalid("TradingMode", $"TradingMode.{tradingMode} is not supported, supported types: {string.Join(", ", supportedTradingModes)}"); - /// - /// Whether this accepts multiple symbols (Only applicable to request requiring symbol parameters) - /// - public bool SupportsMultipleSymbols { get; set; } = false; - /// - /// The max number of symbols which can be passed in a call (Only applicable to request requiring symbol parameters) - /// - public int? MaxSymbolCount { get; set; } - - /// - /// ctor - /// - public EndpointOptions(bool needsAuthentication) : base(typeof(T).Name, needsAuthentication) + foreach (var param in RequiredExchangeParameters) { - } - - /// - /// Validate a request - /// - /// Exchange name - /// The request - /// Request trading mode - /// Supported trading modes - /// - public virtual Error? ValidateRequest(string exchange, T request, TradingMode? tradingMode, TradingMode[] supportedTradingModes) - { - foreach (var param in RequiredOptionalParameters) + if (!string.IsNullOrEmpty(param.Name)) { - if (!string.IsNullOrEmpty(param.Name)) - { - if (typeof(T).GetProperty(param.Name)!.GetValue(request, null) == null) - return ArgumentError.Invalid(param.Name!, $"Required optional parameter `{param.Name}` for exchange `{exchange}` is missing. Example: {param.ExampleValue}"); - } - else - { - if (param.Names!.All(x => typeof(T).GetProperty(param.Name!)!.GetValue(request, null) == null)) - return ArgumentError.Invalid(string.Join("/", param.Names!), $"One of optional parameters `{string.Join(", ", param.Names!)}` for exchange `{exchange}` should be provided. Example: {param.ExampleValue}"); - } - + if (ExchangeParameters.HasValue(exchangeParameters, exchange, param.Name!, param.ValueType) != true) + return ArgumentError.Invalid(param.Name!, $"Required exchange parameter `{param.Name}` for exchange `{exchange}` is missing or has incorrect type. Expected type is {param.ValueType.Name}. Example: {param.ExampleValue}"); } - - if (request is SharedSymbolRequest symbolsRequest) + else { - if (symbolsRequest.Symbols != null) - { - if (!SupportsMultipleSymbols) - return ArgumentError.Invalid(nameof(SharedSymbolRequest.Symbols), $"Only a single symbol parameter is allowed, multiple symbols are not supported"); - - if (symbolsRequest.Symbols.Length > MaxSymbolCount) - return ArgumentError.Invalid(nameof(SharedSymbolRequest.Symbols), $"Max number of symbols is {MaxSymbolCount} but {symbolsRequest.Symbols.Length} were passed"); - } - + if (param.Names!.All(x => ExchangeParameters.HasValue(exchangeParameters, exchange, x, param.ValueType) != true)) + return ArgumentError.Invalid(string.Join("/", param.Names!), $"One of exchange parameters `{string.Join(", ", param.Names!)}` for exchange `{exchange}` should be provided. Example: {param.ExampleValue}"); } - - return ValidateRequest(exchange, request.ExchangeParameters, tradingMode, supportedTradingModes); } - /// - public override string ToString(string exchange) - { - if (!Supported) - return $"{exchange} {EndpointName} NOT SUPPORTED"; - - var sb = new StringBuilder(); - sb.AppendLine($"{exchange} {typeof(T).Name}"); - sb.AppendLine($"Needs authentication: {NeedsAuthentication}"); - if (!string.IsNullOrEmpty(RequestNotes)) - sb.AppendLine(RequestNotes); - if (RequiredOptionalParameters.Any()) - sb.AppendLine($"Required optional parameters: {string.Join(", ", RequiredOptionalParameters.Select(x => x.ToString()))}"); - if (RequiredExchangeParameters.Any()) - sb.AppendLine($"Required exchange specific parameters: {string.Join(", ", RequiredExchangeParameters.Select(x => x.ToString()))}"); - if (OptionalExchangeParameters.Any()) - sb.AppendLine($"Optional exchange specific parameters: {string.Join(", ", RequiredExchangeParameters.Select(x => x.ToString()))}"); - return sb.ToString(); - } + return null; } + /// + public virtual string ToString(string exchange) + { + if (!Supported) + return $"{exchange} {EndpointName} NOT SUPPORTED"; + + var sb = new StringBuilder(); + sb.AppendLine($"{exchange} {EndpointName}"); + if (!string.IsNullOrEmpty(RequestNotes)) + sb.AppendLine(RequestNotes); + sb.AppendLine($"Needs authentication: {NeedsAuthentication}"); + sb.AppendLine($"Required exchange specific parameters: {string.Join(", ", RequiredExchangeParameters.Select(x => x.ToString()))}"); + sb.AppendLine($"Optional exchange specific parameters: {string.Join(", ", OptionalExchangeParameters.Select(x => x.ToString()))}"); + return sb.ToString(); + } +} + +/// +/// Options for an exchange endpoint +/// +/// Type of data +#if NET5_0_OR_GREATER +public class EndpointOptions<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T> : EndpointOptions where T : SharedRequest +#else +public class EndpointOptions : EndpointOptions where T : SharedRequest +#endif +{ + /// + /// Required optional parameters in the request + /// + public List RequiredOptionalParameters { get; set; } = new List(); + + /// + /// Whether this accepts multiple symbols (Only applicable to request requiring symbol parameters) + /// + public bool SupportsMultipleSymbols { get; set; } + /// + /// The max number of symbols which can be passed in a call (Only applicable to request requiring symbol parameters) + /// + public int? MaxSymbolCount { get; set; } + + /// + /// ctor + /// + public EndpointOptions(bool needsAuthentication) : base(typeof(T).Name, needsAuthentication) + { + } + + /// + /// Validate a request + /// + /// Exchange name + /// The request + /// Request trading mode + /// Supported trading modes + /// + public virtual Error? ValidateRequest(string exchange, T request, TradingMode? tradingMode, TradingMode[] supportedTradingModes) + { + foreach (var param in RequiredOptionalParameters) + { + if (!string.IsNullOrEmpty(param.Name)) + { + if (typeof(T).GetProperty(param.Name)!.GetValue(request, null) == null) + return ArgumentError.Invalid(param.Name!, $"Required optional parameter `{param.Name}` for exchange `{exchange}` is missing. Example: {param.ExampleValue}"); + } + else + { + if (param.Names!.All(x => typeof(T).GetProperty(param.Name!)!.GetValue(request, null) == null)) + return ArgumentError.Invalid(string.Join("/", param.Names!), $"One of optional parameters `{string.Join(", ", param.Names!)}` for exchange `{exchange}` should be provided. Example: {param.ExampleValue}"); + } + + } + + if (request is SharedSymbolRequest symbolsRequest) + { + if (symbolsRequest.Symbols != null) + { + if (!SupportsMultipleSymbols) + return ArgumentError.Invalid(nameof(SharedSymbolRequest.Symbols), $"Only a single symbol parameter is allowed, multiple symbols are not supported"); + + if (symbolsRequest.Symbols.Length > MaxSymbolCount) + return ArgumentError.Invalid(nameof(SharedSymbolRequest.Symbols), $"Max number of symbols is {MaxSymbolCount} but {symbolsRequest.Symbols.Length} were passed"); + } + + } + + return ValidateRequest(exchange, request.ExchangeParameters, tradingMode, supportedTradingModes); + } + + /// + public override string ToString(string exchange) + { + if (!Supported) + return $"{exchange} {EndpointName} NOT SUPPORTED"; + + var sb = new StringBuilder(); + sb.AppendLine($"{exchange} {typeof(T).Name}"); + sb.AppendLine($"Needs authentication: {NeedsAuthentication}"); + if (!string.IsNullOrEmpty(RequestNotes)) + sb.AppendLine(RequestNotes); + if (RequiredOptionalParameters.Count != 0) + sb.AppendLine($"Required optional parameters: {string.Join(", ", RequiredOptionalParameters.Select(x => x.ToString()))}"); + if (RequiredExchangeParameters.Count != 0) + sb.AppendLine($"Required exchange specific parameters: {string.Join(", ", RequiredExchangeParameters.Select(x => x.ToString()))}"); + if (OptionalExchangeParameters.Count != 0) + sb.AppendLine($"Optional exchange specific parameters: {string.Join(", ", RequiredExchangeParameters.Select(x => x.ToString()))}"); + return sb.ToString(); + } } diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetClosedOrdersOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetClosedOrdersOptions.cs index d273f1a..3a8cc82 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetClosedOrdersOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetClosedOrdersOptions.cs @@ -1,41 +1,40 @@ -using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects; using System.Text; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Options for requesting closed orders +/// +public class GetClosedOrdersOptions : PaginatedEndpointOptions { /// - /// Options for requesting closed orders + /// Whether the start/end time filter is supported /// - public class GetClosedOrdersOptions : PaginatedEndpointOptions + public bool TimeFilterSupported { get; set; } + + /// + /// ctor + /// + public GetClosedOrdersOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit) : base(paginationType, timeFilterSupported, maxLimit, true) { - /// - /// Whether the start/end time filter is supported - /// - public bool TimeFilterSupported { get; set; } + TimeFilterSupported = timeFilterSupported; + } - /// - /// ctor - /// - public GetClosedOrdersOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit) : base(paginationType, timeFilterSupported, maxLimit, true) - { - TimeFilterSupported = timeFilterSupported; - } + /// + public override Error? ValidateRequest(string exchange, GetClosedOrdersRequest request, TradingMode? tradingMode, TradingMode[] supportedTradingModes) + { + if (TimeFilterSupported && request.StartTime != null) + return ArgumentError.Invalid(nameof(GetClosedOrdersRequest.StartTime), $"Time filter is not supported"); - /// - public override Error? ValidateRequest(string exchange, GetClosedOrdersRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes) - { - if (TimeFilterSupported && request.StartTime != null) - return ArgumentError.Invalid(nameof(GetClosedOrdersRequest.StartTime), $"Time filter is not supported"); + return base.ValidateRequest(exchange, request, tradingMode, supportedTradingModes); + } - return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes); - } - - /// - public override string ToString(string exchange) - { - var sb = new StringBuilder(base.ToString(exchange)); - sb.AppendLine($"Time filter supported: {TimeFilterSupported}"); - return sb.ToString(); - } + /// + public override string ToString(string exchange) + { + var sb = new StringBuilder(base.ToString(exchange)); + sb.AppendLine($"Time filter supported: {TimeFilterSupported}"); + return sb.ToString(); } } diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetDepositsOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetDepositsOptions.cs index 873f144..94375dd 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetDepositsOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetDepositsOptions.cs @@ -1,41 +1,40 @@ -using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects; using System.Text; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Options for requesting deposits +/// +public class GetDepositsOptions : PaginatedEndpointOptions { /// - /// Options for requesting deposits + /// Whether the start/end time filter is supported /// - public class GetDepositsOptions : PaginatedEndpointOptions + public bool TimeFilterSupported { get; set; } + + /// + /// ctor + /// + public GetDepositsOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit) : base(paginationType, timeFilterSupported, maxLimit, true) { - /// - /// Whether the start/end time filter is supported - /// - public bool TimeFilterSupported { get; set; } + TimeFilterSupported = timeFilterSupported; + } - /// - /// ctor - /// - public GetDepositsOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit) : base(paginationType, timeFilterSupported, maxLimit, true) - { - TimeFilterSupported = timeFilterSupported; - } + /// + public override Error? ValidateRequest(string exchange, GetDepositsRequest request, TradingMode? tradingMode, TradingMode[] supportedTradingModes) + { + if (TimeFilterSupported && request.StartTime != null) + return ArgumentError.Invalid(nameof(GetDepositsRequest.StartTime), $"Time filter is not supported"); - /// - public override Error? ValidateRequest(string exchange, GetDepositsRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes) - { - if (TimeFilterSupported && request.StartTime != null) - return ArgumentError.Invalid(nameof(GetDepositsRequest.StartTime), $"Time filter is not supported"); + return base.ValidateRequest(exchange, request, tradingMode, supportedTradingModes); + } - return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes); - } - - /// - public override string ToString(string exchange) - { - var sb = new StringBuilder(base.ToString(exchange)); - sb.AppendLine($"Time filter supported: {TimeFilterSupported}"); - return sb.ToString(); - } + /// + public override string ToString(string exchange) + { + var sb = new StringBuilder(base.ToString(exchange)); + sb.AppendLine($"Time filter supported: {TimeFilterSupported}"); + return sb.ToString(); } } diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetFundingRateHistoryOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetFundingRateHistoryOptions.cs index be857c2..911f10b 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetFundingRateHistoryOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetFundingRateHistoryOptions.cs @@ -1,15 +1,14 @@ -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Options for requesting funding rate history +/// +public class GetFundingRateHistoryOptions : PaginatedEndpointOptions { /// - /// Options for requesting funding rate history + /// ctor /// - public class GetFundingRateHistoryOptions : PaginatedEndpointOptions + public GetFundingRateHistoryOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit, bool needsAuthentication) : base(paginationType, timeFilterSupported, maxLimit, needsAuthentication) { - /// - /// ctor - /// - public GetFundingRateHistoryOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit, bool needsAuthentication) : base(paginationType, timeFilterSupported, maxLimit, needsAuthentication) - { - } } } diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetKlinesOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetKlinesOptions.cs index a5e496f..8ef7cc1 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetKlinesOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetKlinesOptions.cs @@ -1,105 +1,103 @@ -using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects; using System; -using System.Collections.Generic; using System.Linq; using System.Text; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Options for requesting kline/candlestick data +/// +public class GetKlinesOptions : PaginatedEndpointOptions { /// - /// Options for requesting kline/candlestick data + /// The supported kline intervals /// - public class GetKlinesOptions : PaginatedEndpointOptions + public SharedKlineInterval[] SupportIntervals { get; } + /// + /// Max number of data points which can be requested + /// + public int? MaxTotalDataPoints { get; set; } + /// + /// The max age of the data that can be requested + /// + public TimeSpan? MaxAge { get; set; } + + /// + /// ctor + /// + public GetKlinesOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit, bool needsAuthentication) : base(paginationType, timeFilterSupported, maxLimit, needsAuthentication) { - /// - /// The supported kline intervals - /// - public SharedKlineInterval[] SupportIntervals { get; } - /// - /// Max number of data points which can be requested - /// - public int? MaxTotalDataPoints { get; set; } - /// - /// The max age of the data that can be requested - /// - public TimeSpan? MaxAge { get; set; } - - /// - /// ctor - /// - public GetKlinesOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit, bool needsAuthentication) : base(paginationType, timeFilterSupported, maxLimit, needsAuthentication) + SupportIntervals = new[] { - SupportIntervals = new[] + SharedKlineInterval.OneMinute, + SharedKlineInterval.ThreeMinutes, + SharedKlineInterval.FiveMinutes, + SharedKlineInterval.FifteenMinutes, + SharedKlineInterval.ThirtyMinutes, + SharedKlineInterval.OneHour, + SharedKlineInterval.TwoHours, + SharedKlineInterval.FourHours, + SharedKlineInterval.SixHours, + SharedKlineInterval.EightHours, + SharedKlineInterval.TwelveHours, + SharedKlineInterval.OneDay, + SharedKlineInterval.OneWeek, + SharedKlineInterval.OneMonth + }; + } + + /// + /// ctor + /// + public GetKlinesOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit, bool needsAuthentication, params SharedKlineInterval[] intervals) : base(paginationType, timeFilterSupported, maxLimit, needsAuthentication) + { + SupportIntervals = intervals; + } + + /// + /// Check whether a specific interval is supported + /// + /// + /// + public bool IsSupported(SharedKlineInterval interval) => SupportIntervals.Contains(interval); + + /// + public override Error? ValidateRequest(string exchange, GetKlinesRequest request, TradingMode? tradingMode, TradingMode[] supportedTradingModes) + { + if (!IsSupported(request.Interval)) + return ArgumentError.Invalid(nameof(GetKlinesRequest.Interval), "Interval not supported"); + + if (MaxAge.HasValue && request.StartTime < DateTime.UtcNow.Add(-MaxAge.Value)) + return ArgumentError.Invalid(nameof(GetKlinesRequest.StartTime), $"Only the most recent {MaxAge} klines are available"); + + if (request.Limit > MaxLimit) + return ArgumentError.Invalid(nameof(GetKlinesRequest.Limit), $"Only {MaxLimit} klines can be retrieved per request"); + + if (MaxTotalDataPoints.HasValue) + { + if (request.Limit > MaxTotalDataPoints.Value) + return ArgumentError.Invalid(nameof(GetKlinesRequest.Limit), $"Only the most recent {MaxTotalDataPoints} klines are available"); + + if (request.StartTime.HasValue == true) { - SharedKlineInterval.OneMinute, - SharedKlineInterval.ThreeMinutes, - SharedKlineInterval.FiveMinutes, - SharedKlineInterval.FifteenMinutes, - SharedKlineInterval.ThirtyMinutes, - SharedKlineInterval.OneHour, - SharedKlineInterval.TwoHours, - SharedKlineInterval.FourHours, - SharedKlineInterval.SixHours, - SharedKlineInterval.EightHours, - SharedKlineInterval.TwelveHours, - SharedKlineInterval.OneDay, - SharedKlineInterval.OneWeek, - SharedKlineInterval.OneMonth - }; - } - - /// - /// ctor - /// - public GetKlinesOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit, bool needsAuthentication, params SharedKlineInterval[] intervals) : base(paginationType, timeFilterSupported, maxLimit, needsAuthentication) - { - SupportIntervals = intervals; - } - - /// - /// Check whether a specific interval is supported - /// - /// - /// - public bool IsSupported(SharedKlineInterval interval) => SupportIntervals.Contains(interval); - - /// - public override Error? ValidateRequest(string exchange, GetKlinesRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes) - { - if (!IsSupported(request.Interval)) - return ArgumentError.Invalid(nameof(GetKlinesRequest.Interval), "Interval not supported"); - - if (MaxAge.HasValue && request.StartTime < DateTime.UtcNow.Add(-MaxAge.Value)) - return ArgumentError.Invalid(nameof(GetKlinesRequest.StartTime), $"Only the most recent {MaxAge} klines are available"); - - if (request.Limit > MaxLimit) - return ArgumentError.Invalid(nameof(GetKlinesRequest.Limit), $"Only {MaxLimit} klines can be retrieved per request"); - - if (MaxTotalDataPoints.HasValue) - { - if (request.Limit > MaxTotalDataPoints.Value) - return ArgumentError.Invalid(nameof(GetKlinesRequest.Limit), $"Only the most recent {MaxTotalDataPoints} klines are available"); - - if (request.StartTime.HasValue == true) - { - if (((request.EndTime ?? DateTime.UtcNow) - request.StartTime.Value).TotalSeconds / (int)request.Interval > MaxTotalDataPoints.Value) - return ArgumentError.Invalid(nameof(GetKlinesRequest.StartTime), $"Only the most recent {MaxTotalDataPoints} klines are available, time filter failed"); - } + if (((request.EndTime ?? DateTime.UtcNow) - request.StartTime.Value).TotalSeconds / (int)request.Interval > MaxTotalDataPoints.Value) + return ArgumentError.Invalid(nameof(GetKlinesRequest.StartTime), $"Only the most recent {MaxTotalDataPoints} klines are available, time filter failed"); } - - return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes); } - /// - public override string ToString(string exchange) - { - var sb = new StringBuilder(base.ToString(exchange)); - sb.AppendLine($"Supported SharedKlineInterval values: {string.Join(", ", SupportIntervals)}"); - if (MaxAge != null) - sb.AppendLine($"Max age of data: {MaxAge}"); - if (MaxTotalDataPoints != null) - sb.AppendLine($"Max total data points available: {MaxTotalDataPoints}"); - return sb.ToString(); - } + return base.ValidateRequest(exchange, request, tradingMode, supportedTradingModes); + } + + /// + public override string ToString(string exchange) + { + var sb = new StringBuilder(base.ToString(exchange)); + sb.AppendLine($"Supported SharedKlineInterval values: {string.Join(", ", SupportIntervals)}"); + if (MaxAge != null) + sb.AppendLine($"Max age of data: {MaxAge}"); + if (MaxTotalDataPoints != null) + sb.AppendLine($"Max total data points available: {MaxTotalDataPoints}"); + return sb.ToString(); } } diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetOrderBookOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetOrderBookOptions.cs index a7ff8e9..ce05d8f 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetOrderBookOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetOrderBookOptions.cs @@ -1,71 +1,69 @@ -using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects; using System; -using System.Collections.Generic; using System.Linq; using System.Text; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Options for requesting order book +/// +public class GetOrderBookOptions : EndpointOptions { /// - /// Options for requesting order book + /// Supported order book depths /// - public class GetOrderBookOptions : EndpointOptions + public int[]? SupportedLimits { get; set; } + + /// + /// The min order book depth + /// + public int? MinLimit { get; set; } + /// + /// The max order book depth + /// + public int? MaxLimit { get; set; } + + /// + /// ctor + /// + public GetOrderBookOptions(int minLimit, int maxLimit, bool authenticated) : base(authenticated) { - /// - /// Supported order book depths - /// - public int[]? SupportedLimits { get; set; } + MinLimit = minLimit; + MaxLimit = maxLimit; + } - /// - /// The min order book depth - /// - public int? MinLimit { get; set; } - /// - /// The max order book depth - /// - public int? MaxLimit { get; set; } + /// + /// ctor + /// + public GetOrderBookOptions(int[] supportedLimits, bool authenticated) : base(authenticated) + { + SupportedLimits = supportedLimits; + } - /// - /// ctor - /// - public GetOrderBookOptions(int minLimit, int maxLimit, bool authenticated) : base(authenticated) - { - MinLimit = minLimit; - MaxLimit = maxLimit; - } + /// + public override Error? ValidateRequest(string exchange, GetOrderBookRequest request, TradingMode? tradingMode, TradingMode[] supportedTradingModes) + { + if (request.Limit == null) + return null; - /// - /// ctor - /// - public GetOrderBookOptions(int[] supportedLimits, bool authenticated) : base(authenticated) - { - SupportedLimits = supportedLimits; - } + if (MaxLimit.HasValue && request.Limit.Value > MaxLimit) + return ArgumentError.Invalid(nameof(GetOrderBookRequest.Limit), $"Max limit is {MaxLimit}"); - /// - public override Error? ValidateRequest(string exchange, GetOrderBookRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes) - { - if (request.Limit == null) - return null; + if (MinLimit.HasValue && request.Limit.Value < MinLimit) + return ArgumentError.Invalid(nameof(GetOrderBookRequest.Limit), $"Min limit is {MaxLimit}"); - if (MaxLimit.HasValue && request.Limit.Value > MaxLimit) - return ArgumentError.Invalid(nameof(GetOrderBookRequest.Limit), $"Max limit is {MaxLimit}"); + if (SupportedLimits != null && !SupportedLimits.Contains(request.Limit.Value)) + return ArgumentError.Invalid(nameof(GetOrderBookRequest.Limit), $"Limit should be one of " + string.Join(", ", SupportedLimits)); - if (MinLimit.HasValue && request.Limit.Value < MinLimit) - return ArgumentError.Invalid(nameof(GetOrderBookRequest.Limit), $"Min limit is {MaxLimit}"); + return base.ValidateRequest(exchange, request, tradingMode, supportedTradingModes); + } - if (SupportedLimits != null && !SupportedLimits.Contains(request.Limit.Value)) - return ArgumentError.Invalid(nameof(GetOrderBookRequest.Limit), $"Limit should be one of " + string.Join(", ", SupportedLimits)); - - return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes); - } - - /// - public override string ToString(string exchange) - { - var sb = new StringBuilder(base.ToString(exchange)); - sb.AppendLine($"Supported limit values: [{(SupportedLimits != null ? string.Join(", ", SupportedLimits) : $"{MinLimit}..{MaxLimit}")}]"); - return sb.ToString(); - } + /// + public override string ToString(string exchange) + { + var sb = new StringBuilder(base.ToString(exchange)); + sb.AppendLine($"Supported limit values: [{(SupportedLimits != null ? string.Join(", ", SupportedLimits) : $"{MinLimit}..{MaxLimit}")}]"); + return sb.ToString(); } } diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetPositionHistoryOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetPositionHistoryOptions.cs index ffb0158..f33d47f 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetPositionHistoryOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetPositionHistoryOptions.cs @@ -1,15 +1,14 @@ -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Options for requesting position history +/// +public class GetPositionHistoryOptions : PaginatedEndpointOptions { /// - /// Options for requesting position history + /// ctor /// - public class GetPositionHistoryOptions : PaginatedEndpointOptions + public GetPositionHistoryOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit) : base(paginationType, timeFilterSupported, maxLimit, true) { - /// - /// ctor - /// - public GetPositionHistoryOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit) : base(paginationType, timeFilterSupported, maxLimit, true) - { - } } } diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetPositionModeOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetPositionModeOptions.cs index 7857d2b..96c4456 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetPositionModeOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetPositionModeOptions.cs @@ -1,15 +1,14 @@ -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Options for requesting current position mode +/// +public class GetPositionModeOptions : EndpointOptions { /// - /// Options for requesting current position mode + /// ctor /// - public class GetPositionModeOptions : EndpointOptions + public GetPositionModeOptions() : base(true) { - /// - /// ctor - /// - public GetPositionModeOptions() : base(true) - { - } } } diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetRecentTradesOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetRecentTradesOptions.cs index 815aa29..63e38d0 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetRecentTradesOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetRecentTradesOptions.cs @@ -1,41 +1,40 @@ -using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects; using System.Text; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Options for requesting recent trades +/// +public class GetRecentTradesOptions : EndpointOptions { /// - /// Options for requesting recent trades + /// The max number of trades that can be requested /// - public class GetRecentTradesOptions : EndpointOptions + public int MaxLimit { get; set; } + + /// + /// ctor + /// + public GetRecentTradesOptions(int limit, bool authenticated) : base(authenticated) { - /// - /// The max number of trades that can be requested - /// - public int MaxLimit { get; set; } + MaxLimit = limit; + } - /// - /// ctor - /// - public GetRecentTradesOptions(int limit, bool authenticated) : base(authenticated) - { - MaxLimit = limit; - } + /// + public Error? Validate(GetRecentTradesRequest request) + { + if (request.Limit > MaxLimit) + return ArgumentError.Invalid(nameof(GetRecentTradesRequest.Limit), $"Only the most recent {MaxLimit} trades are available"); - /// - public Error? Validate(GetRecentTradesRequest request) - { - if (request.Limit > MaxLimit) - return ArgumentError.Invalid(nameof(GetRecentTradesRequest.Limit), $"Only the most recent {MaxLimit} trades are available"); + return null; + } - return null; - } - - /// - public override string ToString(string exchange) - { - var sb = new StringBuilder(base.ToString(exchange)); - sb.AppendLine($"Max data points: {MaxLimit}"); - return sb.ToString(); - } + /// + public override string ToString(string exchange) + { + var sb = new StringBuilder(base.ToString(exchange)); + sb.AppendLine($"Max data points: {MaxLimit}"); + return sb.ToString(); } } diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetTradeHistoryOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetTradeHistoryOptions.cs index d07972f..2bed100 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetTradeHistoryOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetTradeHistoryOptions.cs @@ -1,42 +1,41 @@ -using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects; using System; using System.Text; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Options for requesting trade history +/// +public class GetTradeHistoryOptions : PaginatedEndpointOptions { /// - /// Options for requesting trade history + /// The max age of data that can be requested /// - public class GetTradeHistoryOptions : PaginatedEndpointOptions + public TimeSpan? MaxAge { get; set; } + + /// + /// ctor + /// + public GetTradeHistoryOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit, bool needsAuthentication) : base(paginationType, timeFilterSupported, maxLimit, needsAuthentication) { - /// - /// The max age of data that can be requested - /// - public TimeSpan? MaxAge { get; set; } + } - /// - /// ctor - /// - public GetTradeHistoryOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit, bool needsAuthentication) : base(paginationType, timeFilterSupported, maxLimit, needsAuthentication) - { - } + /// + public override Error? ValidateRequest(string exchange, GetTradeHistoryRequest request, TradingMode? tradingMode, TradingMode[] supportedTradingModes) + { + if (MaxAge.HasValue && request.StartTime < DateTime.UtcNow.Add(-MaxAge.Value)) + return ArgumentError.Invalid(nameof(GetTradeHistoryRequest.StartTime), $"Only the most recent {MaxAge} trades are available"); - /// - public override Error? ValidateRequest(string exchange, GetTradeHistoryRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes) - { - if (MaxAge.HasValue && request.StartTime < DateTime.UtcNow.Add(-MaxAge.Value)) - return ArgumentError.Invalid(nameof(GetTradeHistoryRequest.StartTime), $"Only the most recent {MaxAge} trades are available"); + return base.ValidateRequest(exchange, request, tradingMode, supportedTradingModes); + } - return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes); - } - - /// - public override string ToString(string exchange) - { - var sb = new StringBuilder(base.ToString(exchange)); - if (MaxAge != null) - sb.AppendLine($"Max age of data: {MaxAge}"); - return sb.ToString(); - } + /// + public override string ToString(string exchange) + { + var sb = new StringBuilder(base.ToString(exchange)); + if (MaxAge != null) + sb.AppendLine($"Max age of data: {MaxAge}"); + return sb.ToString(); } } diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetWithdrawalsOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetWithdrawalsOptions.cs index f6f3c7c..1dd1a01 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetWithdrawalsOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetWithdrawalsOptions.cs @@ -1,41 +1,40 @@ -using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects; using System.Text; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Options for requesting withdrawals +/// +public class GetWithdrawalsOptions : PaginatedEndpointOptions { /// - /// Options for requesting withdrawals + /// Whether the start/end time filter is supported /// - public class GetWithdrawalsOptions : PaginatedEndpointOptions + public bool TimeFilterSupported { get; set; } + + /// + /// ctor + /// + public GetWithdrawalsOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit) : base(paginationType, timeFilterSupported, maxLimit, true) { - /// - /// Whether the start/end time filter is supported - /// - public bool TimeFilterSupported { get; set; } + TimeFilterSupported = timeFilterSupported; + } - /// - /// ctor - /// - public GetWithdrawalsOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit) : base(paginationType, timeFilterSupported, maxLimit, true) - { - TimeFilterSupported = timeFilterSupported; - } + /// + public override Error? ValidateRequest(string exchange, GetWithdrawalsRequest request, TradingMode? tradingMode, TradingMode[] supportedTradingModes) + { + if (TimeFilterSupported && request.StartTime != null) + return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.StartTime), $"Time filter is not supported"); - /// - public override Error? ValidateRequest(string exchange, GetWithdrawalsRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes) - { - if (TimeFilterSupported && request.StartTime != null) - return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.StartTime), $"Time filter is not supported"); + return base.ValidateRequest(exchange, request, tradingMode, supportedTradingModes); + } - return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes); - } - - /// - public override string ToString(string exchange) - { - var sb = new StringBuilder(base.ToString(exchange)); - sb.AppendLine($"Time filter supported: {TimeFilterSupported}"); - return sb.ToString(); - } + /// + public override string ToString(string exchange) + { + var sb = new StringBuilder(base.ToString(exchange)); + sb.AppendLine($"Time filter supported: {TimeFilterSupported}"); + return sb.ToString(); } } diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PaginatedEndpointOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PaginatedEndpointOptions.cs index 10e98bb..f48d55b 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PaginatedEndpointOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PaginatedEndpointOptions.cs @@ -1,52 +1,52 @@ -using CryptoExchange.Net.Objects; -using System.Diagnostics.CodeAnalysis; using System.Text; +#if NET5_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Options for paginated endpoints +/// +/// +#if NET5_0_OR_GREATER +public class PaginatedEndpointOptions<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T> : EndpointOptions where T : SharedRequest +#else +public class PaginatedEndpointOptions : EndpointOptions where T : SharedRequest +#endif { /// - /// Options for paginated endpoints + /// Type of pagination supported /// - /// -#if NET5_0_OR_GREATER - public class PaginatedEndpointOptions<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T> : EndpointOptions where T : SharedRequest -#else - public class PaginatedEndpointOptions : EndpointOptions where T : SharedRequest -#endif + public SharedPaginationSupport PaginationSupport { get; } + + /// + /// Whether filtering based on start/end time is supported + /// + public bool TimePeriodFilterSupport { get; } + + /// + /// Max amount of results that can be requested + /// + public int MaxLimit { get; set; } + + /// + /// ctor + /// + public PaginatedEndpointOptions(SharedPaginationSupport paginationType, bool timePeriodSupport, int maxLimit, bool needsAuthentication) : base(needsAuthentication) { - /// - /// Type of pagination supported - /// - public SharedPaginationSupport PaginationSupport { get; } + PaginationSupport = paginationType; + TimePeriodFilterSupport = timePeriodSupport; + MaxLimit = maxLimit; + } - /// - /// Whether filtering based on start/end time is supported - /// - public bool TimePeriodFilterSupport { get; } - - /// - /// Max amount of results that can be requested - /// - public int MaxLimit { get; set; } - - /// - /// ctor - /// - public PaginatedEndpointOptions(SharedPaginationSupport paginationType, bool timePeriodSupport, int maxLimit, bool needsAuthentication) : base(needsAuthentication) - { - PaginationSupport = paginationType; - TimePeriodFilterSupport = timePeriodSupport; - MaxLimit = maxLimit; - } - - /// - public override string ToString(string exchange) - { - var sb = new StringBuilder(base.ToString(exchange)); - sb.AppendLine($"Pagination type: {PaginationSupport}"); - sb.AppendLine($"Time period filter support: {TimePeriodFilterSupport}"); - sb.AppendLine($"Max limit: {MaxLimit}"); - return sb.ToString(); - } + /// + public override string ToString(string exchange) + { + var sb = new StringBuilder(base.ToString(exchange)); + sb.AppendLine($"Pagination type: {PaginationSupport}"); + sb.AppendLine($"Time period filter support: {TimePeriodFilterSupport}"); + sb.AppendLine($"Max limit: {MaxLimit}"); + return sb.ToString(); } } diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceFuturesOrderOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceFuturesOrderOptions.cs index 8a7c9b1..9e5b0f6 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceFuturesOrderOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceFuturesOrderOptions.cs @@ -1,58 +1,58 @@ -using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects; using System; -using System.Collections.Generic; using System.Linq; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Options for placing a new futures order +/// +public class PlaceFuturesOrderOptions : EndpointOptions { /// - /// Options for placing a new futures order + /// Whether or not the API supports setting take profit / stop loss with the order /// - public class PlaceFuturesOrderOptions : EndpointOptions + public bool SupportsTpSl { get; set; } + + /// + /// ctor + /// + public PlaceFuturesOrderOptions(bool supportsTpSl) : base(true) { - /// - /// Whether or not the API supports setting take profit / stop loss with the order - /// - public bool SupportsTpSl { get; set; } - - /// - /// ctor - /// - public PlaceFuturesOrderOptions(bool supportsTpSl) : base(true) - { - SupportsTpSl = supportsTpSl; - } - - /// - /// Validate a request - /// - public Error? ValidateRequest( - string exchange, - PlaceFuturesOrderRequest request, - TradingMode? tradingMode, - TradingMode[] supportedApiTypes, - SharedOrderType[] supportedOrderTypes, - SharedTimeInForce[] supportedTimeInForce, - SharedQuantitySupport quantitySupport) - { - if (!SupportsTpSl && (request.StopLossPrice != null || request.TakeProfitPrice != null)) - return ArgumentError.Invalid(nameof(PlaceFuturesOrderRequest.StopLossPrice) + " / " + nameof(PlaceFuturesOrderRequest.TakeProfitPrice), "Tp/Sl parameters not supported"); - - if (request.OrderType == SharedOrderType.Other) - throw new ArgumentException("OrderType can't be `Other`", nameof(request.OrderType)); - - if (!supportedOrderTypes.Contains(request.OrderType)) - return ArgumentError.Invalid(nameof(PlaceFuturesOrderRequest.OrderType), "Order type not supported"); - - if (request.TimeInForce != null && !supportedTimeInForce.Contains(request.TimeInForce.Value)) - return ArgumentError.Invalid(nameof(PlaceFuturesOrderRequest.TimeInForce), "Order time in force not supported"); - - var quantityError = quantitySupport.Validate(request.Side, request.OrderType, request.Quantity); - if (quantityError != null) - return quantityError; - - return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes); - } - + SupportsTpSl = supportsTpSl; } + + /// + /// Validate a request + /// + public Error? ValidateRequest( + string exchange, + PlaceFuturesOrderRequest request, + TradingMode? tradingMode, + TradingMode[] supportedApiTypes, + SharedOrderType[] supportedOrderTypes, + SharedTimeInForce[] supportedTimeInForce, + SharedQuantitySupport quantitySupport) + { + if (!SupportsTpSl && (request.StopLossPrice != null || request.TakeProfitPrice != null)) + return ArgumentError.Invalid(nameof(PlaceFuturesOrderRequest.StopLossPrice) + " / " + nameof(PlaceFuturesOrderRequest.TakeProfitPrice), "Tp/Sl parameters not supported"); + + if (request.OrderType == SharedOrderType.Other) +#pragma warning disable CA2208 // Instantiate argument exceptions correctly + throw new ArgumentException("OrderType can't be `Other`", nameof(request.OrderType)); +#pragma warning restore CA2208 // Instantiate argument exceptions correctly + + if (!supportedOrderTypes.Contains(request.OrderType)) + return ArgumentError.Invalid(nameof(PlaceFuturesOrderRequest.OrderType), "Order type not supported"); + + if (request.TimeInForce != null && !supportedTimeInForce.Contains(request.TimeInForce.Value)) + return ArgumentError.Invalid(nameof(PlaceFuturesOrderRequest.TimeInForce), "Order time in force not supported"); + + var quantityError = quantitySupport.Validate(request.Side, request.OrderType, request.Quantity); + if (quantityError != null) + return quantityError; + + return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes); + } + } diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceFuturesTriggerOrderOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceFuturesTriggerOrderOptions.cs index a6e43db..518ca9f 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceFuturesTriggerOrderOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceFuturesTriggerOrderOptions.cs @@ -1,44 +1,40 @@ -using CryptoExchange.Net.Objects; -using System; -using System.Collections.Generic; -using System.Linq; +using CryptoExchange.Net.Objects; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Options for placing a new spot trigger order +/// +public class PlaceFuturesTriggerOrderOptions : EndpointOptions { /// - /// Options for placing a new spot trigger order + /// When true the API holds the funds until the order is triggered or canceled. When true the funds will only be required when the order is triggered and will fail if the funds are not available at that time. /// - public class PlaceFuturesTriggerOrderOptions : EndpointOptions + public bool HoldsFunds { get; set; } + + /// + /// ctor + /// + public PlaceFuturesTriggerOrderOptions(bool holdsFunds) : base(true) { - /// - /// When true the API holds the funds until the order is triggered or canceled. When true the funds will only be required when the order is triggered and will fail if the funds are not available at that time. - /// - public bool HoldsFunds { get; set; } + HoldsFunds = holdsFunds; + } - /// - /// ctor - /// - public PlaceFuturesTriggerOrderOptions(bool holdsFunds) : base(true) - { - HoldsFunds = holdsFunds; - } + /// + /// Validate a request + /// + public Error? ValidateRequest( + string exchange, + PlaceFuturesTriggerOrderRequest request, + TradingMode? tradingMode, + TradingMode[] supportedApiTypes, + SharedOrderSide side, + SharedQuantitySupport quantitySupport) + { + var quantityError = quantitySupport.Validate(side, request.OrderPrice == null ? SharedOrderType.Market : SharedOrderType.Limit, request.Quantity); + if (quantityError != null) + return quantityError; - /// - /// Validate a request - /// - public Error? ValidateRequest( - string exchange, - PlaceFuturesTriggerOrderRequest request, - TradingMode? tradingMode, - TradingMode[] supportedApiTypes, - SharedOrderSide side, - SharedQuantitySupport quantitySupport) - { - var quantityError = quantitySupport.Validate(side, request.OrderPrice == null ? SharedOrderType.Market : SharedOrderType.Limit, request.Quantity); - if (quantityError != null) - return quantityError; - - return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes); - } + return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes); } } diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceSpotOrderOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceSpotOrderOptions.cs index 7e3c2e3..4cbee32 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceSpotOrderOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceSpotOrderOptions.cs @@ -1,49 +1,49 @@ -using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects; using System; -using System.Collections.Generic; using System.Linq; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Options for placing a new spot order +/// +public class PlaceSpotOrderOptions : EndpointOptions { + /// - /// Options for placing a new spot order + /// ctor /// - public class PlaceSpotOrderOptions : EndpointOptions + public PlaceSpotOrderOptions() : base(true) { + } - /// - /// ctor - /// - public PlaceSpotOrderOptions() : base(true) - { - } + /// + /// Validate a request + /// + public Error? ValidateRequest( + string exchange, + PlaceSpotOrderRequest request, + TradingMode? tradingMode, + TradingMode[] supportedApiTypes, + SharedOrderType[] supportedOrderTypes, + SharedTimeInForce[] supportedTimeInForce, + SharedQuantitySupport quantitySupport) + { + if (request.OrderType == SharedOrderType.Other) +#pragma warning disable CA2208 // Instantiate argument exceptions correctly + throw new ArgumentException("OrderType can't be `Other`", nameof(request.OrderType)); +#pragma warning restore CA2208 // Instantiate argument exceptions correctly - /// - /// Validate a request - /// - public Error? ValidateRequest( - string exchange, - PlaceSpotOrderRequest request, - TradingMode? tradingMode, - TradingMode[] supportedApiTypes, - SharedOrderType[] supportedOrderTypes, - SharedTimeInForce[] supportedTimeInForce, - SharedQuantitySupport quantitySupport) - { - if (request.OrderType == SharedOrderType.Other) - throw new ArgumentException("OrderType can't be `Other`", nameof(request.OrderType)); + if (!supportedOrderTypes.Contains(request.OrderType)) + return ArgumentError.Invalid(nameof(PlaceSpotOrderRequest.OrderType), "Order type not supported"); - if (!supportedOrderTypes.Contains(request.OrderType)) - return ArgumentError.Invalid(nameof(PlaceSpotOrderRequest.OrderType), "Order type not supported"); + if (request.TimeInForce != null && !supportedTimeInForce.Contains(request.TimeInForce.Value)) + return ArgumentError.Invalid(nameof(PlaceSpotOrderRequest.TimeInForce), "Order time in force not supported"); - if (request.TimeInForce != null && !supportedTimeInForce.Contains(request.TimeInForce.Value)) - return ArgumentError.Invalid(nameof(PlaceSpotOrderRequest.TimeInForce), "Order time in force not supported"); + var quantityError = quantitySupport.Validate(request.Side, request.OrderType, request.Quantity); + if (quantityError != null) + return quantityError; - var quantityError = quantitySupport.Validate(request.Side, request.OrderType, request.Quantity); - if (quantityError != null) - return quantityError; - - return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes); - } + return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes); } } diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceSpotTriggerOrderOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceSpotTriggerOrderOptions.cs index ce7fd30..d600c82 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceSpotTriggerOrderOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceSpotTriggerOrderOptions.cs @@ -1,43 +1,39 @@ -using CryptoExchange.Net.Objects; -using System; -using System.Collections.Generic; -using System.Linq; +using CryptoExchange.Net.Objects; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Options for placing a new spot trigger order +/// +public class PlaceSpotTriggerOrderOptions : EndpointOptions { /// - /// Options for placing a new spot trigger order + /// When true the API holds the funds until the order is triggered or canceled. When true the funds will only be required when the order is triggered and will fail if the funds are not available at that time. /// - public class PlaceSpotTriggerOrderOptions : EndpointOptions + public bool HoldsFunds { get; set; } + + /// + /// ctor + /// + public PlaceSpotTriggerOrderOptions(bool holdsFunds) : base(true) { - /// - /// When true the API holds the funds until the order is triggered or canceled. When true the funds will only be required when the order is triggered and will fail if the funds are not available at that time. - /// - public bool HoldsFunds { get; set; } + HoldsFunds = holdsFunds; + } - /// - /// ctor - /// - public PlaceSpotTriggerOrderOptions(bool holdsFunds) : base(true) - { - HoldsFunds = holdsFunds; - } + /// + /// Validate a request + /// + public Error? ValidateRequest( + string exchange, + PlaceSpotTriggerOrderRequest request, + TradingMode? tradingMode, + TradingMode[] supportedApiTypes, + SharedQuantitySupport quantitySupport) + { + var quantityError = quantitySupport.Validate(request.OrderSide, request.OrderPrice == null ? SharedOrderType.Market : SharedOrderType.Limit, request.Quantity); + if (quantityError != null) + return quantityError; - /// - /// Validate a request - /// - public Error? ValidateRequest( - string exchange, - PlaceSpotTriggerOrderRequest request, - TradingMode? tradingMode, - TradingMode[] supportedApiTypes, - SharedQuantitySupport quantitySupport) - { - var quantityError = quantitySupport.Validate(request.OrderSide, request.OrderPrice == null ? SharedOrderType.Market : SharedOrderType.Limit, request.Quantity); - if (quantityError != null) - return quantityError; - - return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes); - } + return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes); } } diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/SetLeverageOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/SetLeverageOptions.cs index e776588..c6185b0 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/SetLeverageOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/SetLeverageOptions.cs @@ -1,15 +1,14 @@ -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Options for setting leverage +/// +public class SetLeverageOptions : EndpointOptions { /// - /// Options for setting leverage + /// ctor /// - public class SetLeverageOptions : EndpointOptions + public SetLeverageOptions() : base(true) { - /// - /// ctor - /// - public SetLeverageOptions() : base(true) - { - } } } diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/SetPositionModeOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/SetPositionModeOptions.cs index e74913c..129e9b5 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/SetPositionModeOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/SetPositionModeOptions.cs @@ -1,15 +1,14 @@ -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Options for setting position mode +/// +public class SetPositionModeOptions : EndpointOptions { /// - /// Options for setting position mode + /// ctor /// - public class SetPositionModeOptions : EndpointOptions + public SetPositionModeOptions() : base(true) { - /// - /// ctor - /// - public SetPositionModeOptions() : base(true) - { - } } } diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/WithdrawOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/WithdrawOptions.cs index 17dfda8..723e775 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/WithdrawOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/WithdrawOptions.cs @@ -1,15 +1,14 @@ -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Options for requesting a withdrawal +/// +public class WithdrawOptions : EndpointOptions { /// - /// Options for requesting a withdrawal + /// ctor /// - public class WithdrawOptions : EndpointOptions + public WithdrawOptions() : base(true) { - /// - /// ctor - /// - public WithdrawOptions() : base(true) - { - } } } diff --git a/CryptoExchange.Net/SharedApis/Models/Options/ParameterDescription.cs b/CryptoExchange.Net/SharedApis/Models/Options/ParameterDescription.cs index 8a5dc57..1b1a722 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/ParameterDescription.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/ParameterDescription.cs @@ -1,61 +1,60 @@ -using System; +using System; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Parameter description +/// +public class ParameterDescription { + /// + /// Name of the parameter + /// + public string? Name { get; set; } + /// + /// Names of the parameters + /// + public string[]? Names { get; set; } + /// + /// Type of the value + /// + public Type ValueType { get; set; } /// /// Parameter description /// - public class ParameterDescription + public string Description { get; set; } + /// + /// Example value + /// + public object ExampleValue { get; set; } + + /// + /// ctor + /// + public ParameterDescription(string parameterName, Type valueType, string description, object exampleValue) { - /// - /// Name of the parameter - /// - public string? Name { get; set; } - /// - /// Names of the parameters - /// - public string[]? Names { get; set; } - /// - /// Type of the value - /// - public Type ValueType { get; set; } - /// - /// Parameter description - /// - public string Description { get; set; } - /// - /// Example value - /// - public object ExampleValue { get; set; } + Name = parameterName; + ValueType = valueType; + Description = description; + ExampleValue = exampleValue; + } - /// - /// ctor - /// - public ParameterDescription(string parameterName, Type valueType, string description, object exampleValue) - { - Name = parameterName; - ValueType = valueType; - Description = description; - ExampleValue = exampleValue; - } + /// + /// ctor + /// + public ParameterDescription(string[] parameterNames, Type valueType, string description, object exampleValue) + { + Names = parameterNames; + ValueType = valueType; + Description = description; + ExampleValue = exampleValue; + } - /// - /// ctor - /// - public ParameterDescription(string[] parameterNames, Type valueType, string description, object exampleValue) - { - Names = parameterNames; - ValueType = valueType; - Description = description; - ExampleValue = exampleValue; - } - - /// - public override string ToString() - { - if (Name != null) - return $"[{ValueType.Name}] {Name}: {Description} | example: {ExampleValue}"; - return $"[{ValueType.Name}] {string.Join(" / ", Names!)}: {Description} | example: {ExampleValue}"; - } + /// + public override string ToString() + { + if (Name != null) + return $"[{ValueType.Name}] {Name}: {Description} | example: {ExampleValue}"; + return $"[{ValueType.Name}] {string.Join(" / ", Names!)}: {Description} | example: {ExampleValue}"; } } diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeKlineOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeKlineOptions.cs index 826c964..84ebd06 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeKlineOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeKlineOptions.cs @@ -1,68 +1,66 @@ -using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects; using System; -using System.Collections.Generic; using System.Linq; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Options for subscribing to kline/candlestick updates +/// +public class SubscribeKlineOptions : EndpointOptions { /// - /// Options for subscribing to kline/candlestick updates + /// Kline intervals supported for updates /// - public class SubscribeKlineOptions : EndpointOptions + public SharedKlineInterval[] SupportIntervals { get; } + + /// + /// ctor + /// + public SubscribeKlineOptions(bool needsAuthentication) : base(needsAuthentication) { - /// - /// Kline intervals supported for updates - /// - public SharedKlineInterval[] SupportIntervals { get; } - - /// - /// ctor - /// - public SubscribeKlineOptions(bool needsAuthentication) : base(needsAuthentication) + SupportIntervals = new[] { - SupportIntervals = new[] - { - SharedKlineInterval.OneMinute, - SharedKlineInterval.ThreeMinutes, - SharedKlineInterval.FiveMinutes, - SharedKlineInterval.FifteenMinutes, - SharedKlineInterval.ThirtyMinutes, - SharedKlineInterval.OneHour, - SharedKlineInterval.TwoHours, - SharedKlineInterval.FourHours, - SharedKlineInterval.SixHours, - SharedKlineInterval.EightHours, - SharedKlineInterval.TwelveHours, - SharedKlineInterval.OneDay, - SharedKlineInterval.OneWeek, - SharedKlineInterval.OneMonth - }; - } + SharedKlineInterval.OneMinute, + SharedKlineInterval.ThreeMinutes, + SharedKlineInterval.FiveMinutes, + SharedKlineInterval.FifteenMinutes, + SharedKlineInterval.ThirtyMinutes, + SharedKlineInterval.OneHour, + SharedKlineInterval.TwoHours, + SharedKlineInterval.FourHours, + SharedKlineInterval.SixHours, + SharedKlineInterval.EightHours, + SharedKlineInterval.TwelveHours, + SharedKlineInterval.OneDay, + SharedKlineInterval.OneWeek, + SharedKlineInterval.OneMonth + }; + } - /// - /// ctor - /// - public SubscribeKlineOptions(bool needsAuthentication, params SharedKlineInterval[] intervals) : base(needsAuthentication) - { - SupportIntervals = intervals; - } + /// + /// ctor + /// + public SubscribeKlineOptions(bool needsAuthentication, params SharedKlineInterval[] intervals) : base(needsAuthentication) + { + SupportIntervals = intervals; + } - /// - /// Check whether a specific interval is supported - /// - /// Interval - /// - public bool IsSupported(SharedKlineInterval interval) => SupportIntervals.Contains(interval); + /// + /// Check whether a specific interval is supported + /// + /// Interval + /// + public bool IsSupported(SharedKlineInterval interval) => SupportIntervals.Contains(interval); - /// - /// Validate a request - /// - public override Error? ValidateRequest(string exchange, SubscribeKlineRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes) - { - if (!IsSupported(request.Interval)) - return ArgumentError.Invalid(nameof(SubscribeKlineRequest.Interval), "Interval not supported"); + /// + /// Validate a request + /// + public override Error? ValidateRequest(string exchange, SubscribeKlineRequest request, TradingMode? tradingMode, TradingMode[] supportedTradingModes) + { + if (!IsSupported(request.Interval)) + return ArgumentError.Invalid(nameof(SubscribeKlineRequest.Interval), "Interval not supported"); - return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes); - } + return base.ValidateRequest(exchange, request, tradingMode, supportedTradingModes); } } diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeOrderBookOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeOrderBookOptions.cs index c2baed2..3c57461 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeOrderBookOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeOrderBookOptions.cs @@ -1,37 +1,35 @@ -using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects; using System; -using System.Collections.Generic; using System.Linq; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Options for subscribing to order book snapshot updates +/// +public class SubscribeOrderBookOptions : EndpointOptions { /// - /// Options for subscribing to order book snapshot updates + /// Order book depths supported for updates /// - public class SubscribeOrderBookOptions : EndpointOptions + public int[] SupportedLimits { get; } + + /// + /// ctor + /// + public SubscribeOrderBookOptions(bool needsAuthentication, int[] limits) : base(needsAuthentication) { - /// - /// Order book depths supported for updates - /// - public int[] SupportedLimits { get; } + SupportedLimits = limits; + } - /// - /// ctor - /// - public SubscribeOrderBookOptions(bool needsAuthentication, int[] limits) : base(needsAuthentication) - { - SupportedLimits = limits; - } + /// + /// Validate a request + /// + public override Error? ValidateRequest(string exchange, SubscribeOrderBookRequest request, TradingMode? tradingMode, TradingMode[] supportedTradingModes) + { + if (request.Limit != null && !SupportedLimits.Contains(request.Limit.Value)) + return ArgumentError.Invalid(nameof(SubscribeOrderBookRequest.Limit), "Limit not supported"); - /// - /// Validate a request - /// - public override Error? ValidateRequest(string exchange, SubscribeOrderBookRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes) - { - if (request.Limit != null && !SupportedLimits.Contains(request.Limit.Value)) - return ArgumentError.Invalid(nameof(SubscribeOrderBookRequest.Limit), "Limit not supported"); - - return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes); - } + return base.ValidateRequest(exchange, request, tradingMode, supportedTradingModes); } } diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/CancelOrderRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/CancelOrderRequest.cs index 07f3ca4..b22b1f5 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/CancelOrderRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/CancelOrderRequest.cs @@ -1,24 +1,23 @@ -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Request to cancel a currently open order +/// +public record CancelOrderRequest : SharedSymbolRequest { /// - /// Request to cancel a currently open order + /// Id of order to cancel /// - public record CancelOrderRequest : SharedSymbolRequest - { - /// - /// Id of order to cancel - /// - public string OrderId { get; set; } + public string OrderId { get; set; } - /// - /// ctor - /// - /// Symbol the order is on - /// Id of the order to close - /// Exchange specific parameters - public CancelOrderRequest(SharedSymbol symbol, string orderId, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) - { - OrderId = orderId; - } + /// + /// ctor + /// + /// Symbol the order is on + /// Id of the order to close + /// Exchange specific parameters + public CancelOrderRequest(SharedSymbol symbol, string orderId, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) + { + OrderId = orderId; } } diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/CancelTpSlRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/CancelTpSlRequest.cs index ae25693..3804ec6 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/CancelTpSlRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/CancelTpSlRequest.cs @@ -1,56 +1,55 @@ -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Request to cancel a take profit / stop loss +/// +public record CancelTpSlRequest : SharedSymbolRequest { /// - /// Request to cancel a take profit / stop loss + /// Id of order to cancel /// - public record CancelTpSlRequest : SharedSymbolRequest + public string? OrderId { get; set; } + + /// + /// Position mode + /// + public SharedPositionMode? PositionMode { get; set; } + /// + /// Position side + /// + public SharedPositionSide? PositionSide { get; set; } + /// + /// Take profit / Stop loss side + /// + public SharedTpSlSide? TpSlSide { get; set; } + /// + /// Margin mode + /// + public SharedMarginMode? MarginMode { get; set; } + + /// + /// ctor for canceling by order id + /// + /// Symbol the order is on + /// Id of the order to close + /// Exchange specific parameters + public CancelTpSlRequest(SharedSymbol symbol, string orderId, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) { - /// - /// Id of order to cancel - /// - public string? OrderId { get; set; } + OrderId = orderId; + } - /// - /// Position mode - /// - public SharedPositionMode? PositionMode { get; set; } - /// - /// Position side - /// - public SharedPositionSide? PositionSide { get; set; } - /// - /// Take profit / Stop loss side - /// - public SharedTpSlSide? TpSlSide { get; set; } - /// - /// Margin mode - /// - public SharedMarginMode? MarginMode { get; set; } - - /// - /// ctor for canceling by order id - /// - /// Symbol the order is on - /// Id of the order to close - /// Exchange specific parameters - public CancelTpSlRequest(SharedSymbol symbol, string orderId, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) - { - OrderId = orderId; - } - - /// - /// ctor for canceling without order id - /// - /// Symbol the order is on - /// The position mode of the account - /// The side of the position - /// The side to cancel - /// Exchange specific parameters - public CancelTpSlRequest(SharedSymbol symbol, SharedPositionMode mode, SharedPositionSide positionSide, SharedTpSlSide tpSlSide, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) - { - PositionMode = mode; - PositionSide = positionSide; - TpSlSide = tpSlSide; - } + /// + /// ctor for canceling without order id + /// + /// Symbol the order is on + /// The position mode of the account + /// The side of the position + /// The side to cancel + /// Exchange specific parameters + public CancelTpSlRequest(SharedSymbol symbol, SharedPositionMode mode, SharedPositionSide positionSide, SharedTpSlSide tpSlSide, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) + { + PositionMode = mode; + PositionSide = positionSide; + TpSlSide = tpSlSide; } } diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/ClosePositionRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/ClosePositionRequest.cs index 73fe265..0ac2e62 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/ClosePositionRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/ClosePositionRequest.cs @@ -1,48 +1,47 @@ -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Request to close a currently open position +/// +public record ClosePositionRequest : SharedSymbolRequest { /// - /// Request to close a currently open position + /// The current position mode of the account for the symbol /// - public record ClosePositionRequest : SharedSymbolRequest - { - /// - /// The current position mode of the account for the symbol - /// - public SharedPositionMode PositionMode { get; set; } - /// - /// The position side to close. Required when in hedge mode - /// - public SharedPositionSide? PositionSide { get; set; } - /// - /// Margin mode - /// - public SharedMarginMode? MarginMode { get; set; } - /// - /// Quantity of the position to close. Note that the quantity is needed for some exchanges, but will not be respected on other exchanges; don't use it as partial close quantity - /// - public decimal? Quantity { get; set; } + public SharedPositionMode PositionMode { get; set; } + /// + /// The position side to close. Required when in hedge mode + /// + public SharedPositionSide? PositionSide { get; set; } + /// + /// Margin mode + /// + public SharedMarginMode? MarginMode { get; set; } + /// + /// Quantity of the position to close. Note that the quantity is needed for some exchanges, but will not be respected on other exchanges; don't use it as partial close quantity + /// + public decimal? Quantity { get; set; } - /// - /// ctor - /// - /// Symbol to close the position on - /// The current position mode of the account for the symbol - /// The position side to close. Required when in hedge mode - /// Margin mode - /// Quantity of the position to close. Note that the quantity is needed for some exchanges, but will not be respected on all exchanges - /// Exchange specific parameters - public ClosePositionRequest( - SharedSymbol symbol, - SharedPositionMode mode, - SharedPositionSide? positionSide = null, - SharedMarginMode? marginMode = null, - decimal? quantity = null, - ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) - { - PositionMode = mode; - PositionSide = positionSide; - MarginMode = marginMode; - Quantity = quantity; - } + /// + /// ctor + /// + /// Symbol to close the position on + /// The current position mode of the account for the symbol + /// The position side to close. Required when in hedge mode + /// Margin mode + /// Quantity of the position to close. Note that the quantity is needed for some exchanges, but will not be respected on all exchanges + /// Exchange specific parameters + public ClosePositionRequest( + SharedSymbol symbol, + SharedPositionMode mode, + SharedPositionSide? positionSide = null, + SharedMarginMode? marginMode = null, + decimal? quantity = null, + ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) + { + PositionMode = mode; + PositionSide = positionSide; + MarginMode = marginMode; + Quantity = quantity; } } diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/GetAssetRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/GetAssetRequest.cs index c8a0a64..87b411d 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/GetAssetRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/GetAssetRequest.cs @@ -1,23 +1,22 @@ -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Request to retrieve info on a specific asset +/// +public record GetAssetRequest : SharedRequest { /// - /// Request to retrieve info on a specific asset + /// Asset name /// - public record GetAssetRequest : SharedRequest - { - /// - /// Asset name - /// - public string Asset { get; set; } + public string Asset { get; set; } - /// - /// ctor - /// - /// Asset to retrieve info on - /// Exchange specific parameters - public GetAssetRequest(string asset, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) - { - Asset = asset; - } + /// + /// ctor + /// + /// Asset to retrieve info on + /// Exchange specific parameters + public GetAssetRequest(string asset, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) + { + Asset = asset; } } diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/GetAssetsRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/GetAssetsRequest.cs index 7ef35a0..3b3a1eb 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/GetAssetsRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/GetAssetsRequest.cs @@ -1,18 +1,15 @@ -using CryptoExchange.Net.Objects; +namespace CryptoExchange.Net.SharedApis; -namespace CryptoExchange.Net.SharedApis +/// +/// Request to retrieve a list of supported assets +/// +public record GetAssetsRequest : SharedRequest { /// - /// Request to retrieve a list of supported assets + /// ctor /// - public record GetAssetsRequest : SharedRequest + /// Exchange specific parameters + public GetAssetsRequest(ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) { - /// - /// ctor - /// - /// Exchange specific parameters - public GetAssetsRequest(ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) - { - } } } diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/GetBalancesRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/GetBalancesRequest.cs index 2cab40b..92bd276 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/GetBalancesRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/GetBalancesRequest.cs @@ -1,25 +1,22 @@ -using CryptoExchange.Net.Objects; +namespace CryptoExchange.Net.SharedApis; -namespace CryptoExchange.Net.SharedApis +/// +/// Request to retrieve balance info for the user +/// +public record GetBalancesRequest : SharedRequest { /// - /// Request to retrieve balance info for the user + /// Trading mode /// - public record GetBalancesRequest : SharedRequest - { - /// - /// Trading mode - /// - public TradingMode? TradingMode { get; set; } + public TradingMode? TradingMode { get; set; } - /// - /// ctor - /// - /// Trading mode - /// Exchange specific parameters - public GetBalancesRequest(TradingMode? tradingMode = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) - { - TradingMode = tradingMode; - } + /// + /// ctor + /// + /// Trading mode + /// Exchange specific parameters + public GetBalancesRequest(TradingMode? tradingMode = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) + { + TradingMode = tradingMode; } } diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/GetBookTickerRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/GetBookTickerRequest.cs index 51f374d..36961cc 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/GetBookTickerRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/GetBookTickerRequest.cs @@ -1,17 +1,16 @@ -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Request to retrieve best bid/ask info for a symbol +/// +public record GetBookTickerRequest : SharedSymbolRequest { /// - /// Request to retrieve best bid/ask info for a symbol + /// ctor /// - public record GetBookTickerRequest : SharedSymbolRequest + /// Symbol to retrieve book ticker for + /// Exchange specific parameters + public GetBookTickerRequest(SharedSymbol symbol, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) { - /// - /// ctor - /// - /// Symbol to retrieve book ticker for - /// Exchange specific parameters - public GetBookTickerRequest(SharedSymbol symbol, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) - { - } } } diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/GetClosedOrdersRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/GetClosedOrdersRequest.cs index 4920d8e..45b6b9c 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/GetClosedOrdersRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/GetClosedOrdersRequest.cs @@ -1,38 +1,37 @@ -using System; +using System; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Request to retrieve closed orders for a symbol +/// +public record GetClosedOrdersRequest : SharedSymbolRequest { /// - /// Request to retrieve closed orders for a symbol + /// Filter by start time /// - public record GetClosedOrdersRequest : SharedSymbolRequest - { - /// - /// Filter by start time - /// - public DateTime? StartTime { get; } - /// - /// Filter by end time - /// - public DateTime? EndTime { get; } - /// - /// Max number of results - /// - public int? Limit { get; } + public DateTime? StartTime { get; } + /// + /// Filter by end time + /// + public DateTime? EndTime { get; } + /// + /// Max number of results + /// + public int? Limit { get; } - /// - /// ctor - /// - /// Symbol to get closed orders for - /// Filter by start time - /// Filter by end time - /// Max number of results - /// Exchange specific parameters - public GetClosedOrdersRequest(SharedSymbol symbol, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) - { - StartTime = startTime; - EndTime = endTime; - Limit = limit; - } + /// + /// ctor + /// + /// Symbol to get closed orders for + /// Filter by start time + /// Filter by end time + /// Max number of results + /// Exchange specific parameters + public GetClosedOrdersRequest(SharedSymbol symbol, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) + { + StartTime = startTime; + EndTime = endTime; + Limit = limit; } } diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/GetDepositAddressesRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/GetDepositAddressesRequest.cs index 53c060c..505f576 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/GetDepositAddressesRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/GetDepositAddressesRequest.cs @@ -1,29 +1,28 @@ -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Request to retrieve the deposit addresses for an asset +/// +public record GetDepositAddressesRequest : SharedRequest { /// - /// Request to retrieve the deposit addresses for an asset + /// Asset to get address for /// - public record GetDepositAddressesRequest : SharedRequest - { - /// - /// Asset to get address for - /// - public string Asset { get; set; } - /// - /// Network name - /// - public string? Network { get; set; } + public string Asset { get; set; } + /// + /// Network name + /// + public string? Network { get; set; } - /// - /// ctor - /// - /// Asset name to get address for - /// Network name - /// Exchange specific parameters - public GetDepositAddressesRequest(string asset, string? network = null, ExchangeParameters? exchangeParameters = null): base(exchangeParameters) - { - Asset = asset; - Network = network; - } + /// + /// ctor + /// + /// Asset name to get address for + /// Network name + /// Exchange specific parameters + public GetDepositAddressesRequest(string asset, string? network = null, ExchangeParameters? exchangeParameters = null): base(exchangeParameters) + { + Asset = asset; + Network = network; } } diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/GetDepositsRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/GetDepositsRequest.cs index 5f53be2..f518baf 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/GetDepositsRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/GetDepositsRequest.cs @@ -1,43 +1,42 @@ -using System; +using System; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Request to retrieve deposit history records +/// +public record GetDepositsRequest : SharedRequest { /// - /// Request to retrieve deposit history records + /// Filter by asset /// - public record GetDepositsRequest : SharedRequest - { - /// - /// Filter by asset - /// - public string? Asset { get; set; } - /// - /// Filter by start time - /// - public DateTime? StartTime { get; } - /// - /// Filter by end time - /// - public DateTime? EndTime { get; } - /// - /// Max number of results - /// - public int? Limit { get; } + public string? Asset { get; set; } + /// + /// Filter by start time + /// + public DateTime? StartTime { get; } + /// + /// Filter by end time + /// + public DateTime? EndTime { get; } + /// + /// Max number of results + /// + public int? Limit { get; } - /// - /// ctor - /// - /// Filter by asset - /// Filter by start time - /// Filter by end time - /// Max number of results - /// Exchange specific parameters - public GetDepositsRequest(string? asset = null, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) - { - Asset = asset; - StartTime = startTime; - EndTime = endTime; - Limit = limit; - } + /// + /// ctor + /// + /// Filter by asset + /// Filter by start time + /// Filter by end time + /// Max number of results + /// Exchange specific parameters + public GetDepositsRequest(string? asset = null, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) + { + Asset = asset; + StartTime = startTime; + EndTime = endTime; + Limit = limit; } } diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/GetFeeRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/GetFeeRequest.cs index 429e258..d044665 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/GetFeeRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/GetFeeRequest.cs @@ -1,21 +1,16 @@ -using System; -using System.Collections.Generic; -using System.Text; +namespace CryptoExchange.Net.SharedApis; -namespace CryptoExchange.Net.SharedApis +/// +/// Request to retrieve trading fees +/// +public record GetFeeRequest : SharedSymbolRequest { /// - /// Request to retrieve trading fees + /// ctor /// - public record GetFeeRequest : SharedSymbolRequest + /// Symbol to retrieve fees for + /// Exchange specific parameters + public GetFeeRequest(SharedSymbol symbol, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) { - /// - /// ctor - /// - /// Symbol to retrieve fees for - /// Exchange specific parameters - public GetFeeRequest(SharedSymbol symbol, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) - { - } } } diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/GetFundingRateHistoryRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/GetFundingRateHistoryRequest.cs index 213657f..8d470dd 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/GetFundingRateHistoryRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/GetFundingRateHistoryRequest.cs @@ -1,38 +1,37 @@ -using System; +using System; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Request to retrieve funding rate history data +/// +public record GetFundingRateHistoryRequest : SharedSymbolRequest { /// - /// Request to retrieve funding rate history data + /// Filter by start time /// - public record GetFundingRateHistoryRequest : SharedSymbolRequest - { - /// - /// Filter by start time - /// - public DateTime? StartTime { get; set; } - /// - /// Filter by end time - /// - public DateTime? EndTime { get; set; } - /// - /// Max number of results - /// - public int? Limit { get; set; } + public DateTime? StartTime { get; set; } + /// + /// Filter by end time + /// + public DateTime? EndTime { get; set; } + /// + /// Max number of results + /// + public int? Limit { get; set; } - /// - /// ctor - /// - /// Symbol to request klines for - /// Filter by start time - /// Filter by end time - /// Max number of results - /// Exchange specific parameters - public GetFundingRateHistoryRequest(SharedSymbol symbol, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) - { - StartTime = startTime; - EndTime = endTime; - Limit = limit; - } + /// + /// ctor + /// + /// Symbol to request klines for + /// Filter by start time + /// Filter by end time + /// Max number of results + /// Exchange specific parameters + public GetFundingRateHistoryRequest(SharedSymbol symbol, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) + { + StartTime = startTime; + EndTime = endTime; + Limit = limit; } } diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/GetKlinesRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/GetKlinesRequest.cs index 6eef5a2..ed5e09a 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/GetKlinesRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/GetKlinesRequest.cs @@ -1,44 +1,43 @@ -using System; +using System; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Request to retrieve kline/candlestick data +/// +public record GetKlinesRequest : SharedSymbolRequest { /// - /// Request to retrieve kline/candlestick data + /// The kline interval /// - public record GetKlinesRequest : SharedSymbolRequest - { - /// - /// The kline interval - /// - public SharedKlineInterval Interval { get; set; } - /// - /// Filter by start time - /// - public DateTime? StartTime { get; set; } - /// - /// Filter by end time - /// - public DateTime? EndTime { get; set; } - /// - /// Max number of results - /// - public int? Limit { get; set; } + public SharedKlineInterval Interval { get; set; } + /// + /// Filter by start time + /// + public DateTime? StartTime { get; set; } + /// + /// Filter by end time + /// + public DateTime? EndTime { get; set; } + /// + /// Max number of results + /// + public int? Limit { get; set; } - /// - /// ctor - /// - /// Symbol to request klines for - /// Interval of the klines - /// Filter by start time - /// Filter by end time - /// Max number of results - /// Exchange specific parameters - public GetKlinesRequest(SharedSymbol symbol, SharedKlineInterval interval, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) - { - Interval = interval; - StartTime = startTime; - EndTime = endTime; - Limit = limit; - } + /// + /// ctor + /// + /// Symbol to request klines for + /// Interval of the klines + /// Filter by start time + /// Filter by end time + /// Max number of results + /// Exchange specific parameters + public GetKlinesRequest(SharedSymbol symbol, SharedKlineInterval interval, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) + { + Interval = interval; + StartTime = startTime; + EndTime = endTime; + Limit = limit; } } diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/GetLeverageRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/GetLeverageRequest.cs index 05aa6e8..18c5377 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/GetLeverageRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/GetLeverageRequest.cs @@ -1,30 +1,29 @@ -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Request to retrieve the leverage setting for a symbol +/// +public record GetLeverageRequest : SharedSymbolRequest { /// - /// Request to retrieve the leverage setting for a symbol + /// Position side, required when in hedge mode /// - public record GetLeverageRequest : SharedSymbolRequest - { - /// - /// Position side, required when in hedge mode - /// - public SharedPositionSide? PositionSide { get; set; } - /// - /// Margin mode - /// - public SharedMarginMode? MarginMode { get; set; } + public SharedPositionSide? PositionSide { get; set; } + /// + /// Margin mode + /// + public SharedMarginMode? MarginMode { get; set; } - /// - /// ctor - /// - /// Symbol to request leverage for - /// Position side to get leverage for when in hedge mode - /// Margin mode - /// Exchange specific parameters - public GetLeverageRequest(SharedSymbol symbol, SharedPositionSide? positionSide = null, SharedMarginMode? marginMode = null, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) - { - PositionSide = positionSide; - MarginMode = marginMode; - } + /// + /// ctor + /// + /// Symbol to request leverage for + /// Position side to get leverage for when in hedge mode + /// Margin mode + /// Exchange specific parameters + public GetLeverageRequest(SharedSymbol symbol, SharedPositionSide? positionSide = null, SharedMarginMode? marginMode = null, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) + { + PositionSide = positionSide; + MarginMode = marginMode; } } diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/GetOpenInterestRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/GetOpenInterestRequest.cs index f6a58dd..82dcc39 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/GetOpenInterestRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/GetOpenInterestRequest.cs @@ -1,17 +1,16 @@ -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Request to retrieve the current open interest for a symbol +/// +public record GetOpenInterestRequest : SharedSymbolRequest { /// - /// Request to retrieve the current open interest for a symbol + /// ctor /// - public record GetOpenInterestRequest : SharedSymbolRequest + /// Symbol to retrieve open orders for + /// Exchange specific parameters + public GetOpenInterestRequest(SharedSymbol symbol, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) { - /// - /// ctor - /// - /// Symbol to retrieve open orders for - /// Exchange specific parameters - public GetOpenInterestRequest(SharedSymbol symbol, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) - { - } } } diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/GetOpenOrdersRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/GetOpenOrdersRequest.cs index 9f35a10..510f558 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/GetOpenOrdersRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/GetOpenOrdersRequest.cs @@ -1,39 +1,36 @@ -using CryptoExchange.Net.Objects; +namespace CryptoExchange.Net.SharedApis; -namespace CryptoExchange.Net.SharedApis +/// +/// Request to retrieve the current open orders +/// +public record GetOpenOrdersRequest : SharedRequest { /// - /// Request to retrieve the current open orders + /// Trading mode /// - public record GetOpenOrdersRequest : SharedRequest + public TradingMode? TradingMode { get; set; } + /// + /// Symbol filter + /// + public SharedSymbol? Symbol { get; set; } + + /// + /// ctor + /// + /// Trading mode + /// Exchange specific parameters + public GetOpenOrdersRequest(TradingMode? tradingMode = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) { - /// - /// Trading mode - /// - public TradingMode? TradingMode { get; set; } - /// - /// Symbol filter - /// - public SharedSymbol? Symbol { get; set; } + TradingMode = tradingMode; + } - /// - /// ctor - /// - /// Trading mode - /// Exchange specific parameters - public GetOpenOrdersRequest(TradingMode? tradingMode = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) - { - TradingMode = tradingMode; - } - - /// - /// ctor - /// - /// Symbol to retrieve open orders for - /// Exchange specific parameters - public GetOpenOrdersRequest(SharedSymbol symbol, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) - { - Symbol = symbol; - } + /// + /// ctor + /// + /// Symbol to retrieve open orders for + /// Exchange specific parameters + public GetOpenOrdersRequest(SharedSymbol symbol, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) + { + Symbol = symbol; } } diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/GetOrderBookRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/GetOrderBookRequest.cs index 2f9d025..46f923a 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/GetOrderBookRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/GetOrderBookRequest.cs @@ -1,24 +1,23 @@ -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Request to retrieve the current order book +/// +public record GetOrderBookRequest: SharedSymbolRequest { /// - /// Request to retrieve the current order book + /// Depth of the order book /// - public record GetOrderBookRequest: SharedSymbolRequest - { - /// - /// Depth of the order book - /// - public int? Limit { get; set; } + public int? Limit { get; set; } - /// - /// ctor - /// - /// The symbol the order is on - /// Depth of the order book - /// Exchange specific parameters - public GetOrderBookRequest(SharedSymbol symbol, int? limit = null, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) - { - Limit = limit; - } + /// + /// ctor + /// + /// The symbol the order is on + /// Depth of the order book + /// Exchange specific parameters + public GetOrderBookRequest(SharedSymbol symbol, int? limit = null, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) + { + Limit = limit; } } diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/GetOrderRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/GetOrderRequest.cs index 944edb2..fbd837b 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/GetOrderRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/GetOrderRequest.cs @@ -1,24 +1,23 @@ -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Request to retrieve info on a specific order +/// +public record GetOrderRequest : SharedSymbolRequest { /// - /// Request to retrieve info on a specific order + /// Id of the order /// - public record GetOrderRequest : SharedSymbolRequest - { - /// - /// Id of the order - /// - public string OrderId { get; set; } + public string OrderId { get; set; } - /// - /// ctor - /// - /// The symbol the order is on - /// The id of the order - /// Exchange specific parameters - public GetOrderRequest(SharedSymbol symbol, string orderId, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) - { - OrderId = orderId; - } + /// + /// ctor + /// + /// The symbol the order is on + /// The id of the order + /// Exchange specific parameters + public GetOrderRequest(SharedSymbol symbol, string orderId, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) + { + OrderId = orderId; } } diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/GetOrderTradesRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/GetOrderTradesRequest.cs index f9ad141..ec1d056 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/GetOrderTradesRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/GetOrderTradesRequest.cs @@ -1,24 +1,23 @@ -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Request to retrieve the trades for a specific order +/// +public record GetOrderTradesRequest : SharedSymbolRequest { /// - /// Request to retrieve the trades for a specific order + /// The id of the order to retrieve trades for /// - public record GetOrderTradesRequest : SharedSymbolRequest - { - /// - /// The id of the order to retrieve trades for - /// - public string OrderId { get; set; } + public string OrderId { get; set; } - /// - /// ctor - /// - /// The symbol the order is on - /// The id of the order - /// Exchange specific parameters - public GetOrderTradesRequest(SharedSymbol symbol, string orderId, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) - { - OrderId = orderId; - } + /// + /// ctor + /// + /// The symbol the order is on + /// The id of the order + /// Exchange specific parameters + public GetOrderTradesRequest(SharedSymbol symbol, string orderId, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) + { + OrderId = orderId; } } diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/GetPositionHistoryRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/GetPositionHistoryRequest.cs index cc2ec6e..b2652e0 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/GetPositionHistoryRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/GetPositionHistoryRequest.cs @@ -1,64 +1,62 @@ -using CryptoExchange.Net.Objects; using System; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Request to retrieve the position close history +/// +public record GetPositionHistoryRequest : SharedRequest { /// - /// Request to retrieve the position close history + /// Trading mode /// - public record GetPositionHistoryRequest : SharedRequest + public TradingMode? TradingMode { get; set; } + /// + /// Symbol + /// + public SharedSymbol? Symbol { get; set; } + /// + /// Filter by start time + /// + public DateTime? StartTime { get; set; } + /// + /// Filter by end time + /// + public DateTime? EndTime { get; set; } + /// + /// Max number of results + /// + public int? Limit { get; set; } + + /// + /// ctor + /// + /// Symbol filter + /// Filter by start time + /// Filter by end time + /// Max number of results + /// Exchange specific parameters + public GetPositionHistoryRequest(SharedSymbol symbol, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) { - /// - /// Trading mode - /// - public TradingMode? TradingMode { get; set; } - /// - /// Symbol - /// - public SharedSymbol? Symbol { get; set; } - /// - /// Filter by start time - /// - public DateTime? StartTime { get; set; } - /// - /// Filter by end time - /// - public DateTime? EndTime { get; set; } - /// - /// Max number of results - /// - public int? Limit { get; set; } + Symbol = symbol; + StartTime = startTime; + EndTime = endTime; + Limit = limit; + } - /// - /// ctor - /// - /// Symbol filter - /// Filter by start time - /// Filter by end time - /// Max number of results - /// Exchange specific parameters - public GetPositionHistoryRequest(SharedSymbol symbol, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) - { - Symbol = symbol; - StartTime = startTime; - EndTime = endTime; - Limit = limit; - } - - /// - /// ctor - /// - /// Trade mode - /// Filter by start time - /// Filter by end time - /// Max number of results - /// Exchange specific parameters - public GetPositionHistoryRequest(TradingMode? tradeMode = null, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) - { - TradingMode = tradeMode; - StartTime = startTime; - EndTime = endTime; - Limit = limit; - } + /// + /// ctor + /// + /// Trade mode + /// Filter by start time + /// Filter by end time + /// Max number of results + /// Exchange specific parameters + public GetPositionHistoryRequest(TradingMode? tradeMode = null, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) + { + TradingMode = tradeMode; + StartTime = startTime; + EndTime = endTime; + Limit = limit; } } diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/GetPositionModeRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/GetPositionModeRequest.cs index 7ff9b11..1d62815 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/GetPositionModeRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/GetPositionModeRequest.cs @@ -1,39 +1,36 @@ -using CryptoExchange.Net.Objects; +namespace CryptoExchange.Net.SharedApis; -namespace CryptoExchange.Net.SharedApis +/// +/// Request to retrieve the current position mode +/// +public record GetPositionModeRequest : SharedRequest { /// - /// Request to retrieve the current position mode + /// Trading mode /// - public record GetPositionModeRequest : SharedRequest + public TradingMode? TradingMode { get; set; } + /// + /// Symbol. Some exchanges set position mode per symbol + /// + public SharedSymbol? Symbol { get; set; } + + /// + /// ctor + /// + /// Symbol to retrieve position mode for + /// Exchange specific parameters + public GetPositionModeRequest(SharedSymbol symbol, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) { - /// - /// Trading mode - /// - public TradingMode? TradingMode { get; set; } - /// - /// Symbol. Some exchanges set position mode per symbol - /// - public SharedSymbol? Symbol { get; set; } + Symbol = symbol; + } - /// - /// ctor - /// - /// Symbol to retrieve position mode for - /// Exchange specific parameters - public GetPositionModeRequest(SharedSymbol symbol, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) - { - Symbol = symbol; - } - - /// - /// ctor - /// - /// Trading mode - /// Exchange specific parameters - public GetPositionModeRequest(TradingMode? tradingMode = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) - { - TradingMode = tradingMode; - } + /// + /// ctor + /// + /// Trading mode + /// Exchange specific parameters + public GetPositionModeRequest(TradingMode? tradingMode = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) + { + TradingMode = tradingMode; } } diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/GetPositionsRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/GetPositionsRequest.cs index bed5794..678e4e5 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/GetPositionsRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/GetPositionsRequest.cs @@ -1,39 +1,36 @@ -using CryptoExchange.Net.Objects; +namespace CryptoExchange.Net.SharedApis; -namespace CryptoExchange.Net.SharedApis +/// +/// Request to retrieve open positions +/// +public record GetPositionsRequest : SharedRequest { /// - /// Request to retrieve open positions + /// Trading mode /// - public record GetPositionsRequest : SharedRequest + public TradingMode? TradingMode { get; set; } + /// + /// Symbol filter, required for some exchanges + /// + public SharedSymbol? Symbol { get; set; } + + /// + /// ctor + /// + /// Trading mode + /// Exchange specific parameters + public GetPositionsRequest(TradingMode? tradingMode = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) { - /// - /// Trading mode - /// - public TradingMode? TradingMode { get; set; } - /// - /// Symbol filter, required for some exchanges - /// - public SharedSymbol? Symbol { get; set; } + TradingMode = tradingMode; + } - /// - /// ctor - /// - /// Trading mode - /// Exchange specific parameters - public GetPositionsRequest(TradingMode? tradingMode = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) - { - TradingMode = tradingMode; - } - - /// - /// ctor - /// - /// Symbol to retriecve positions for - /// Exchange specific parameters - public GetPositionsRequest(SharedSymbol symbol, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) - { - Symbol = symbol; - } + /// + /// ctor + /// + /// Symbol to retrieve positions for + /// Exchange specific parameters + public GetPositionsRequest(SharedSymbol symbol, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) + { + Symbol = symbol; } } diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/GetRecentTradesRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/GetRecentTradesRequest.cs index 37f5ea7..ddd414c 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/GetRecentTradesRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/GetRecentTradesRequest.cs @@ -1,24 +1,23 @@ -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Request to retrieve the most recent trades of a symbol +/// +public record GetRecentTradesRequest : SharedSymbolRequest { /// - /// Request to retrieve the most recent trades of a symbol + /// Max number of results /// - public record GetRecentTradesRequest : SharedSymbolRequest - { - /// - /// Max number of results - /// - public int? Limit { get; set; } + public int? Limit { get; set; } - /// - /// ctor - /// - /// Symbol to retrieve trades for - /// Max number of results - /// Exchange specific parameters - public GetRecentTradesRequest(SharedSymbol symbol, int? limit = null, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) - { - Limit = limit; - } + /// + /// ctor + /// + /// Symbol to retrieve trades for + /// Max number of results + /// Exchange specific parameters + public GetRecentTradesRequest(SharedSymbol symbol, int? limit = null, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) + { + Limit = limit; } } diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/GetSymbolsRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/GetSymbolsRequest.cs index d90a10e..b97ff19 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/GetSymbolsRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/GetSymbolsRequest.cs @@ -1,25 +1,22 @@ -using CryptoExchange.Net.Objects; +namespace CryptoExchange.Net.SharedApis; -namespace CryptoExchange.Net.SharedApis +/// +/// Request to retrieve symbol info +/// +public record GetSymbolsRequest : SharedRequest { /// - /// Request to retrieve symbol info + /// Filter by trading mode /// - public record GetSymbolsRequest : SharedRequest - { - /// - /// Filter by trading mode - /// - public TradingMode? TradingMode { get; set; } + public TradingMode? TradingMode { get; set; } - /// - /// ctor - /// - /// Trading mode filter - /// Exchange specific parameters - public GetSymbolsRequest(TradingMode? tradingMode = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) - { - TradingMode = tradingMode; - } + /// + /// ctor + /// + /// Trading mode filter + /// Exchange specific parameters + public GetSymbolsRequest(TradingMode? tradingMode = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) + { + TradingMode = tradingMode; } } diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/GetTickerRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/GetTickerRequest.cs index e5fb40a..29e4c78 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/GetTickerRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/GetTickerRequest.cs @@ -1,17 +1,16 @@ -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Request to retrieve ticker info for a symbol +/// +public record GetTickerRequest : SharedSymbolRequest { /// - /// Request to retrieve ticker info for a symbol + /// ctor /// - public record GetTickerRequest : SharedSymbolRequest + /// Symbol to retrieve ticker for + /// Exchange specific parameters + public GetTickerRequest(SharedSymbol symbol, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) { - /// - /// ctor - /// - /// Symbol to retrieve ticker for - /// Exchange specific parameters - public GetTickerRequest(SharedSymbol symbol, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) - { - } } } diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/GetTickersRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/GetTickersRequest.cs index c3267f4..cf2f383 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/GetTickersRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/GetTickersRequest.cs @@ -1,26 +1,22 @@ - -using CryptoExchange.Net.Objects; +namespace CryptoExchange.Net.SharedApis; -namespace CryptoExchange.Net.SharedApis +/// +/// Request to retrieve all symbol tickers +/// +public record GetTickersRequest : SharedRequest { /// - /// Request to retrieve all symbol tickers + /// Trading mode /// - public record GetTickersRequest : SharedRequest - { - /// - /// Trading mode - /// - public TradingMode? TradingMode { get; set; } + public TradingMode? TradingMode { get; set; } - /// - /// ctor - /// - /// Trading mode - /// Exchange specific parameters - public GetTickersRequest(TradingMode? tradingMode = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) - { - TradingMode = tradingMode; - } + /// + /// ctor + /// + /// Trading mode + /// Exchange specific parameters + public GetTickersRequest(TradingMode? tradingMode = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) + { + TradingMode = tradingMode; } } diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/GetTradeHistoryRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/GetTradeHistoryRequest.cs index 8664f74..4a54d99 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/GetTradeHistoryRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/GetTradeHistoryRequest.cs @@ -1,38 +1,37 @@ -using System; +using System; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Request to retrieve the public trade history +/// +public record GetTradeHistoryRequest : SharedSymbolRequest { /// - /// Request to retrieve the public trade history + /// Filter by start time /// - public record GetTradeHistoryRequest : SharedSymbolRequest - { - /// - /// Filter by start time - /// - public DateTime StartTime { get; } - /// - /// Filter by end time - /// - public DateTime EndTime { get; } - /// - /// Max number of results - /// - public int? Limit { get; } + public DateTime StartTime { get; } + /// + /// Filter by end time + /// + public DateTime EndTime { get; } + /// + /// Max number of results + /// + public int? Limit { get; } - /// - /// ctor - /// - /// Symbol to retrieve trades for - /// Filter by start time - /// Filter by end time - /// Max number of results - /// Exchange specific parameters - public GetTradeHistoryRequest(SharedSymbol symbol, DateTime startTime, DateTime endTime, int? limit = null, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) - { - StartTime = startTime; - EndTime = endTime; - Limit = limit; - } + /// + /// ctor + /// + /// Symbol to retrieve trades for + /// Filter by start time + /// Filter by end time + /// Max number of results + /// Exchange specific parameters + public GetTradeHistoryRequest(SharedSymbol symbol, DateTime startTime, DateTime endTime, int? limit = null, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) + { + StartTime = startTime; + EndTime = endTime; + Limit = limit; } } diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/GetUserTradesRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/GetUserTradesRequest.cs index 8fd8407..1787518 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/GetUserTradesRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/GetUserTradesRequest.cs @@ -1,38 +1,37 @@ -using System; +using System; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Request to retrieve the trades of the user +/// +public record GetUserTradesRequest : SharedSymbolRequest { /// - /// Request to retrieve the trades of the user + /// Filter by start time /// - public record GetUserTradesRequest : SharedSymbolRequest - { - /// - /// Filter by start time - /// - public DateTime? StartTime { get; } - /// - /// Filter by end time - /// - public DateTime? EndTime { get; } - /// - /// Max number of results - /// - public int? Limit { get; } + public DateTime? StartTime { get; } + /// + /// Filter by end time + /// + public DateTime? EndTime { get; } + /// + /// Max number of results + /// + public int? Limit { get; } - /// - /// ctor - /// - /// Symbol to retrieve trades for - /// Filter by start time - /// Filter by end time - /// Max number of results - /// Exchange specific parameters - public GetUserTradesRequest(SharedSymbol symbol, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) - { - StartTime = startTime; - EndTime = endTime; - Limit = limit; - } + /// + /// ctor + /// + /// Symbol to retrieve trades for + /// Filter by start time + /// Filter by end time + /// Max number of results + /// Exchange specific parameters + public GetUserTradesRequest(SharedSymbol symbol, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) + { + StartTime = startTime; + EndTime = endTime; + Limit = limit; } } diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/GetWithdrawalsRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/GetWithdrawalsRequest.cs index 5130079..e8b5f4c 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/GetWithdrawalsRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/GetWithdrawalsRequest.cs @@ -1,43 +1,42 @@ -using System; +using System; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Request to retrieve the withdrawal history +/// +public record GetWithdrawalsRequest : SharedRequest { /// - /// Request to retrieve the withdrawal history + /// Filter by asset /// - public record GetWithdrawalsRequest : SharedRequest - { - /// - /// Filter by asset - /// - public string? Asset { get; set; } - /// - /// Filter by start time - /// - public DateTime? StartTime { get; } - /// - /// Filter by end time - /// - public DateTime? EndTime { get; } - /// - /// Max number of results - /// - public int? Limit { get; } + public string? Asset { get; set; } + /// + /// Filter by start time + /// + public DateTime? StartTime { get; } + /// + /// Filter by end time + /// + public DateTime? EndTime { get; } + /// + /// Max number of results + /// + public int? Limit { get; } - /// - /// ctor - /// - /// Filter by asset - /// Filter by start time - /// Filter by end time - /// Max number of results - /// Exchange specific parameters - public GetWithdrawalsRequest(string? asset = null, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) - { - Asset = asset; - StartTime = startTime; - EndTime = endTime; - Limit = limit; - } + /// + /// ctor + /// + /// Filter by asset + /// Filter by start time + /// Filter by end time + /// Max number of results + /// Exchange specific parameters + public GetWithdrawalsRequest(string? asset = null, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) + { + Asset = asset; + StartTime = startTime; + EndTime = endTime; + Limit = limit; } } diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/KeepAliveListenKeyRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/KeepAliveListenKeyRequest.cs index 410f257..3f79956 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/KeepAliveListenKeyRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/KeepAliveListenKeyRequest.cs @@ -1,31 +1,28 @@ -using CryptoExchange.Net.Objects; +namespace CryptoExchange.Net.SharedApis; -namespace CryptoExchange.Net.SharedApis +/// +/// Request to keep-alive the update stream for the specified listen key +/// +public record KeepAliveListenKeyRequest : SharedRequest { /// - /// Request to keep-alive the update stream for the specified listen key + /// The key to stop updates for /// - public record KeepAliveListenKeyRequest : SharedRequest - { - /// - /// The key to stop updates for - /// - public string ListenKey { get; set; } - /// - /// Trading mode - /// - public TradingMode? TradingMode { get; set; } + public string ListenKey { get; set; } + /// + /// Trading mode + /// + public TradingMode? TradingMode { get; set; } - /// - /// ctor - /// - /// The key to keep alive - /// Trading mode - /// Exchange specific parameters - public KeepAliveListenKeyRequest(string listenKey, TradingMode? tradingMode = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) - { - ListenKey = listenKey; - TradingMode = tradingMode; - } + /// + /// ctor + /// + /// The key to keep alive + /// Trading mode + /// Exchange specific parameters + public KeepAliveListenKeyRequest(string listenKey, TradingMode? tradingMode = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) + { + ListenKey = listenKey; + TradingMode = tradingMode; } } diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/PlaceFuturesOrderRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/PlaceFuturesOrderRequest.cs index 42989f7..c237519 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/PlaceFuturesOrderRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/PlaceFuturesOrderRequest.cs @@ -1,100 +1,99 @@ -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Request to place a new futures order +/// +public record PlaceFuturesOrderRequest : SharedSymbolRequest { /// - /// Request to place a new futures order + /// Side of the order /// - public record PlaceFuturesOrderRequest : SharedSymbolRequest + public SharedOrderSide Side { get; set; } + /// + /// Type of the order + /// + public SharedOrderType OrderType { get; set; } + /// + /// Time in force of the order + /// + public SharedTimeInForce? TimeInForce { get; set; } + /// + /// Quantity of the order + /// + public SharedQuantity? Quantity { get; set; } + /// + /// Price of the order + /// + public decimal? Price { get; set; } + /// + /// Client order id + /// + public string? ClientOrderId { get; set; } + /// + /// Position side of the order. Required when in hedge mode, ignored in one-way mode + /// + public SharedPositionSide? PositionSide { get; set; } + /// + /// Margin mode + /// + public SharedMarginMode? MarginMode { get; set; } + /// + /// Reduce only order + /// + public bool? ReduceOnly { get; set; } + /// + /// Leverage for the position. Note that leverage might not be applied during order placement but instead needs to be set before opening the position depending on the exchange. In this case use the SetLeverageAsync method. + /// + public decimal? Leverage { get; set; } + + /// + /// Take profit price + /// + public decimal? TakeProfitPrice { get; set; } + /// + /// Stop loss price + /// + public decimal? StopLossPrice { get; set; } + + /// + /// ctor + /// + /// Symbol to place the order on + + /// Side of the order + /// Type of the order + /// Quantity of the order + /// Price of the order + /// Time in force + /// Client order id + /// Reduce only + /// Leverage for the position. Note that leverage might not be applied during order placement but instead needs to be set before opening the position depending on the exchange. In this case use the SetLeverageAsync method. + /// Position side of the order. Required when in hedge mode, ignored in one-way mode + /// Margin mode + /// Exchange specific parameters + public PlaceFuturesOrderRequest( + SharedSymbol symbol, + SharedOrderSide side, + SharedOrderType orderType, + SharedQuantity? quantity = null, + decimal? price = null, + bool? reduceOnly = null, + decimal? leverage = null, + SharedTimeInForce? timeInForce = null, + SharedPositionSide? positionSide = null, + SharedMarginMode? marginMode = null, + string? clientOrderId = null, + ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) { - /// - /// Side of the order - /// - public SharedOrderSide Side { get; set; } - /// - /// Type of the order - /// - public SharedOrderType OrderType { get; set; } - /// - /// Time in force of the order - /// - public SharedTimeInForce? TimeInForce { get; set; } - /// - /// Quantity of the order - /// - public SharedQuantity? Quantity { get; set; } - /// - /// Price of the order - /// - public decimal? Price { get; set; } - /// - /// Client order id - /// - public string? ClientOrderId { get; set; } - /// - /// Position side of the order. Required when in hedge mode, ignored in one-way mode - /// - public SharedPositionSide? PositionSide { get; set; } - /// - /// Margin mode - /// - public SharedMarginMode? MarginMode { get; set; } - /// - /// Reduce only order - /// - public bool? ReduceOnly { get; set; } - /// - /// Leverage for the position. Note that leverage might not be applied during order placement but instead needs to be set before opening the position depending on the exchange. In this case use the SetLeverageAsync method. - /// - public decimal? Leverage { get; set; } - - /// - /// Take profit price - /// - public decimal? TakeProfitPrice { get; set; } - /// - /// Stop loss price - /// - public decimal? StopLossPrice { get; set; } - - /// - /// ctor - /// - /// Symbol to place the order on - - /// Side of the order - /// Type of the order - /// Quantity of the order - /// Price of the order - /// Time in force - /// Client order id - /// Reduce only - /// Leverage for the position. Note that leverage might not be applied during order placement but instead needs to be set before opening the position depending on the exchange. In this case use the SetLeverageAsync method. - /// Position side of the order. Required when in hedge mode, ignored in one-way mode - /// Margin mode - /// Exchange specific parameters - public PlaceFuturesOrderRequest( - SharedSymbol symbol, - SharedOrderSide side, - SharedOrderType orderType, - SharedQuantity? quantity = null, - decimal? price = null, - bool? reduceOnly = null, - decimal? leverage = null, - SharedTimeInForce? timeInForce = null, - SharedPositionSide? positionSide = null, - SharedMarginMode? marginMode = null, - string? clientOrderId = null, - ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) - { - Side = side; - OrderType = orderType; - Quantity = quantity; - Price = price; - MarginMode = marginMode; - ClientOrderId = clientOrderId; - ReduceOnly = reduceOnly; - Leverage = leverage; - TimeInForce = timeInForce; - PositionSide = positionSide; - } + Side = side; + OrderType = orderType; + Quantity = quantity; + Price = price; + MarginMode = marginMode; + ClientOrderId = clientOrderId; + ReduceOnly = reduceOnly; + Leverage = leverage; + TimeInForce = timeInForce; + PositionSide = positionSide; } } diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/PlaceFuturesTriggerOrderRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/PlaceFuturesTriggerOrderRequest.cs index 83559b1..8dd99c0 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/PlaceFuturesTriggerOrderRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/PlaceFuturesTriggerOrderRequest.cs @@ -1,85 +1,84 @@ -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Request to place a new trigger order +/// +public record PlaceFuturesTriggerOrderRequest : SharedSymbolRequest { /// - /// Request to place a new trigger order + /// Client order id /// - public record PlaceFuturesTriggerOrderRequest : SharedSymbolRequest - { - /// - /// Client order id - /// - public string? ClientOrderId { get; set; } - /// - /// Direction of the trigger order - /// - public SharedTriggerOrderDirection OrderDirection { get; set; } - /// - /// Price trigger direction - /// - public SharedTriggerPriceDirection PriceDirection { get; set; } - /// - /// Quantity of the order - /// - public SharedQuantity Quantity { get; set; } - /// - /// Price of the order - /// - public decimal? OrderPrice { get; set; } - /// - /// Trigger price - /// - public decimal TriggerPrice { get; set; } - /// - /// Time in force - /// - public SharedTimeInForce? TimeInForce { get; set; } - /// - /// Position mode - /// - public SharedPositionMode? PositionMode { get; set; } - /// - /// Position side - /// - public SharedPositionSide PositionSide { get; set; } - /// - /// Margin mode - /// - public SharedMarginMode? MarginMode { get; set; } - /// - /// Leverage - /// - public decimal? Leverage { get; set; } - /// - /// Trigger price type - /// - public SharedTriggerPriceType? TriggerPriceType { get; set; } + public string? ClientOrderId { get; set; } + /// + /// Direction of the trigger order + /// + public SharedTriggerOrderDirection OrderDirection { get; set; } + /// + /// Price trigger direction + /// + public SharedTriggerPriceDirection PriceDirection { get; set; } + /// + /// Quantity of the order + /// + public SharedQuantity Quantity { get; set; } + /// + /// Price of the order + /// + public decimal? OrderPrice { get; set; } + /// + /// Trigger price + /// + public decimal TriggerPrice { get; set; } + /// + /// Time in force + /// + public SharedTimeInForce? TimeInForce { get; set; } + /// + /// Position mode + /// + public SharedPositionMode? PositionMode { get; set; } + /// + /// Position side + /// + public SharedPositionSide PositionSide { get; set; } + /// + /// Margin mode + /// + public SharedMarginMode? MarginMode { get; set; } + /// + /// Leverage + /// + public decimal? Leverage { get; set; } + /// + /// Trigger price type + /// + public SharedTriggerPriceType? TriggerPriceType { get; set; } - /// - /// ctor - /// - /// Symbol the order is on - /// Direction of the order when triggered - /// Price direction - /// Quantity of the order - /// Position side - /// Price at which the order should activate - /// Limit price for the order - /// Exchange specific parameters - public PlaceFuturesTriggerOrderRequest(SharedSymbol symbol, - SharedTriggerPriceDirection priceDirection, - decimal triggerPrice, - SharedTriggerOrderDirection orderDirection, - SharedPositionSide positionSide, - SharedQuantity quantity, - decimal? orderPrice = null, - ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) - { - PriceDirection = priceDirection; - PositionSide = positionSide; - Quantity = quantity; - OrderPrice = orderPrice; - TriggerPrice = triggerPrice; - OrderDirection = orderDirection; - } + /// + /// ctor + /// + /// Symbol the order is on + /// Direction of the order when triggered + /// Price direction + /// Quantity of the order + /// Position side + /// Price at which the order should activate + /// Limit price for the order + /// Exchange specific parameters + public PlaceFuturesTriggerOrderRequest(SharedSymbol symbol, + SharedTriggerPriceDirection priceDirection, + decimal triggerPrice, + SharedTriggerOrderDirection orderDirection, + SharedPositionSide positionSide, + SharedQuantity quantity, + decimal? orderPrice = null, + ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) + { + PriceDirection = priceDirection; + PositionSide = positionSide; + Quantity = quantity; + OrderPrice = orderPrice; + TriggerPrice = triggerPrice; + OrderDirection = orderDirection; } } diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/PlaceSpotOrderRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/PlaceSpotOrderRequest.cs index 379c6ec..874d61a 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/PlaceSpotOrderRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/PlaceSpotOrderRequest.cs @@ -1,62 +1,61 @@ -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Request to place a new spot order +/// +public record PlaceSpotOrderRequest : SharedSymbolRequest { /// - /// Request to place a new spot order + /// Type of the order /// - public record PlaceSpotOrderRequest : SharedSymbolRequest - { - /// - /// Type of the order - /// - public SharedOrderType OrderType { get; set; } - /// - /// Side of the order - /// - public SharedOrderSide Side { get; set; } - /// - /// Time in force of the order - /// - public SharedTimeInForce? TimeInForce { get; set; } - /// - /// Quantity of the order - /// - public SharedQuantity? Quantity { get; set; } - /// - /// Price of the order - /// - public decimal? Price { get; set; } - /// - /// Client order id - /// - public string? ClientOrderId { get; set; } + public SharedOrderType OrderType { get; set; } + /// + /// Side of the order + /// + public SharedOrderSide Side { get; set; } + /// + /// Time in force of the order + /// + public SharedTimeInForce? TimeInForce { get; set; } + /// + /// Quantity of the order + /// + public SharedQuantity? Quantity { get; set; } + /// + /// Price of the order + /// + public decimal? Price { get; set; } + /// + /// Client order id + /// + public string? ClientOrderId { get; set; } - /// - /// ctor - /// - /// Symbol to place the order on - /// Side of the order - /// Type of the order - /// Quantity of the order - /// Price of the order - /// Time in force - /// Client order id - /// Exchange specific parameters - public PlaceSpotOrderRequest( - SharedSymbol symbol, - SharedOrderSide side, - SharedOrderType orderType, - SharedQuantity? quantity = null, - decimal? price = null, - SharedTimeInForce? timeInForce = null, - string? clientOrderId = null, - ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) - { - OrderType = orderType; - Side = side; - Quantity = quantity; - Price = price; - TimeInForce = timeInForce; - ClientOrderId = clientOrderId; - } + /// + /// ctor + /// + /// Symbol to place the order on + /// Side of the order + /// Type of the order + /// Quantity of the order + /// Price of the order + /// Time in force + /// Client order id + /// Exchange specific parameters + public PlaceSpotOrderRequest( + SharedSymbol symbol, + SharedOrderSide side, + SharedOrderType orderType, + SharedQuantity? quantity = null, + decimal? price = null, + SharedTimeInForce? timeInForce = null, + string? clientOrderId = null, + ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) + { + OrderType = orderType; + Side = side; + Quantity = quantity; + Price = price; + TimeInForce = timeInForce; + ClientOrderId = clientOrderId; } } diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/PlaceSpotTriggerOrderRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/PlaceSpotTriggerOrderRequest.cs index 5f4e686..77f1c27 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/PlaceSpotTriggerOrderRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/PlaceSpotTriggerOrderRequest.cs @@ -1,62 +1,61 @@ -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Request to place a new trigger order +/// +public record PlaceSpotTriggerOrderRequest : SharedSymbolRequest { /// - /// Request to place a new trigger order + /// Client order id /// - public record PlaceSpotTriggerOrderRequest : SharedSymbolRequest - { - /// - /// Client order id - /// - public string? ClientOrderId { get; set; } - /// - /// Direction of the trigger order - /// - public SharedOrderSide OrderSide { get; set; } - /// - /// Price trigger direction - /// - public SharedTriggerPriceDirection PriceDirection { get; set; } - /// - /// Time in force - /// - public SharedTimeInForce? TimeInForce { get; set; } - /// - /// Quantity of the order - /// - public SharedQuantity Quantity { get; set; } - /// - /// Price of the order - /// - public decimal? OrderPrice { get; set; } - /// - /// Trigger price - /// - public decimal TriggerPrice { get; set; } + public string? ClientOrderId { get; set; } + /// + /// Direction of the trigger order + /// + public SharedOrderSide OrderSide { get; set; } + /// + /// Price trigger direction + /// + public SharedTriggerPriceDirection PriceDirection { get; set; } + /// + /// Time in force + /// + public SharedTimeInForce? TimeInForce { get; set; } + /// + /// Quantity of the order + /// + public SharedQuantity Quantity { get; set; } + /// + /// Price of the order + /// + public decimal? OrderPrice { get; set; } + /// + /// Trigger price + /// + public decimal TriggerPrice { get; set; } - /// - /// ctor - /// - /// Symbol the order is on - /// Order side - /// Price direction - /// Quantity of the order - /// Price at which the order should activate - /// Limit price for the order - /// Exchange specific parameters - public PlaceSpotTriggerOrderRequest(SharedSymbol symbol, - SharedTriggerPriceDirection priceDirection, - decimal triggerPrice, - SharedOrderSide orderSide, - SharedQuantity quantity, - decimal? orderPrice = null, - ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) - { - PriceDirection = priceDirection; - Quantity = quantity; - OrderPrice = orderPrice; - TriggerPrice = triggerPrice; - OrderSide = orderSide; - } + /// + /// ctor + /// + /// Symbol the order is on + /// Order side + /// Price direction + /// Quantity of the order + /// Price at which the order should activate + /// Limit price for the order + /// Exchange specific parameters + public PlaceSpotTriggerOrderRequest(SharedSymbol symbol, + SharedTriggerPriceDirection priceDirection, + decimal triggerPrice, + SharedOrderSide orderSide, + SharedQuantity quantity, + decimal? orderPrice = null, + ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) + { + PriceDirection = priceDirection; + Quantity = quantity; + OrderPrice = orderPrice; + TriggerPrice = triggerPrice; + OrderSide = orderSide; } } diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/SetLeverageRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/SetLeverageRequest.cs index 2ae8cc2..12a6850 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/SetLeverageRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/SetLeverageRequest.cs @@ -1,36 +1,35 @@ -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Request to change the current leverage +/// +public record SetLeverageRequest : SharedSymbolRequest { /// - /// Request to change the current leverage + /// Leverage to change to /// - public record SetLeverageRequest : SharedSymbolRequest - { - /// - /// Leverage to change to - /// - public decimal Leverage { get; set; } - /// - /// Position side to change leverage for. Some exchanges set leverage per side - /// - public SharedPositionSide? Side { get; set; } - /// - /// Margin mode of the position leverage to change - /// - public SharedMarginMode? MarginMode { get; set; } + public decimal Leverage { get; set; } + /// + /// Position side to change leverage for. Some exchanges set leverage per side + /// + public SharedPositionSide? Side { get; set; } + /// + /// Margin mode of the position leverage to change + /// + public SharedMarginMode? MarginMode { get; set; } - /// - /// ctor - /// - /// Symbol to change the leverage for - /// Leverage to change to - /// Position side to change leverage for. Some exchanges set leverage per side - /// Margin mode - /// Exchange specific parameters - public SetLeverageRequest(SharedSymbol symbol, decimal leverage, SharedPositionSide? positionSide = null, SharedMarginMode? marginMode = null, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) - { - Leverage = leverage; - Side = positionSide; - MarginMode = marginMode; - } + /// + /// ctor + /// + /// Symbol to change the leverage for + /// Leverage to change to + /// Position side to change leverage for. Some exchanges set leverage per side + /// Margin mode + /// Exchange specific parameters + public SetLeverageRequest(SharedSymbol symbol, decimal leverage, SharedPositionSide? positionSide = null, SharedMarginMode? marginMode = null, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) + { + Leverage = leverage; + Side = positionSide; + MarginMode = marginMode; } } diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/SetPositionModeRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/SetPositionModeRequest.cs index 0d65f0c..37ac024 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/SetPositionModeRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/SetPositionModeRequest.cs @@ -1,47 +1,44 @@ -using CryptoExchange.Net.Objects; +namespace CryptoExchange.Net.SharedApis; -namespace CryptoExchange.Net.SharedApis +/// +/// Request to change the current position mode +/// +public record SetPositionModeRequest : SharedRequest { /// - /// Request to change the current position mode + /// Symbol to change the mode for. Depending on the exchange position mode is set for the whole account or per symbol /// - public record SetPositionModeRequest : SharedRequest + public SharedSymbol? Symbol { get; set; } + /// + /// Trading mode + /// + public TradingMode? TradingMode { get; set; } + /// + /// Position mode to change to + /// + public SharedPositionMode PositionMode { get; set; } + + /// + /// ctor + /// + /// Position mode to change to + /// Trading mode + /// Exchange specific parameters + public SetPositionModeRequest(SharedPositionMode positionMode, TradingMode? tradingMode = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) { - /// - /// Symbol to change the mode for. Depending on the exchange position mode is set for the whole account or per symbol - /// - public SharedSymbol? Symbol { get; set; } - /// - /// Trading mode - /// - public TradingMode? TradingMode { get; set; } - /// - /// Position mode to change to - /// - public SharedPositionMode PositionMode { get; set; } + TradingMode = tradingMode; + PositionMode = positionMode; + } - /// - /// ctor - /// - /// Position mode to change to - /// Trading mode - /// Exchange specific parameters - public SetPositionModeRequest(SharedPositionMode positionMode, TradingMode? tradingMode = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) - { - TradingMode = tradingMode; - PositionMode = positionMode; - } - - /// - /// ctor - /// - /// Symbol to change to position mode for - /// Position mode to change to - /// Exchange specific parameters - public SetPositionModeRequest(SharedSymbol symbol, SharedPositionMode positionMode, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) - { - PositionMode = positionMode; - Symbol = symbol; - } + /// + /// ctor + /// + /// Symbol to change to position mode for + /// Position mode to change to + /// Exchange specific parameters + public SetPositionModeRequest(SharedSymbol symbol, SharedPositionMode positionMode, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) + { + PositionMode = positionMode; + Symbol = symbol; } } diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/SetTpSlRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/SetTpSlRequest.cs index eef810a..4f1da7e 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/SetTpSlRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/SetTpSlRequest.cs @@ -1,50 +1,49 @@ -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Set a take profit and/or stop loss for an open position +/// +public record SetTpSlRequest : SharedSymbolRequest { /// - /// Set a take profit and/or stop loss for an open position + /// Position mode /// - public record SetTpSlRequest : SharedSymbolRequest - { - /// - /// Position mode - /// - public SharedPositionMode? PositionMode { get; set; } - /// - /// Position side - /// - public SharedPositionSide PositionSide { get; set; } - /// - /// Margin mode - /// - public SharedMarginMode? MarginMode { get; set; } - /// - /// Take profit / Stop loss side - /// - public SharedTpSlSide TpSlSide { get; set; } - /// - /// Quantity to close. Only used for some API's which require a quantity in the order. Most API's will close the full position - /// - public decimal? Quantity { get; set; } - /// - /// Trigger price - /// - public decimal TriggerPrice { get; set; } + public SharedPositionMode? PositionMode { get; set; } + /// + /// Position side + /// + public SharedPositionSide PositionSide { get; set; } + /// + /// Margin mode + /// + public SharedMarginMode? MarginMode { get; set; } + /// + /// Take profit / Stop loss side + /// + public SharedTpSlSide TpSlSide { get; set; } + /// + /// Quantity to close. Only used for some API's which require a quantity in the order. Most API's will close the full position + /// + public decimal? Quantity { get; set; } + /// + /// Trigger price + /// + public decimal TriggerPrice { get; set; } - /// - /// ctor - /// - /// Symbol of the order - /// Position side - /// Take Profit / Stop Loss side - /// Trigger price - /// Exchange specific parameters - public SetTpSlRequest(SharedSymbol symbol, SharedPositionSide positionSide, SharedTpSlSide tpSlSide, decimal triggerPrice, ExchangeParameters? exchangeParameters = null) - : base(symbol, exchangeParameters) - { - PositionSide = positionSide; - TpSlSide = tpSlSide; - Symbol = symbol; - TriggerPrice = triggerPrice; - } + /// + /// ctor + /// + /// Symbol of the order + /// Position side + /// Take Profit / Stop Loss side + /// Trigger price + /// Exchange specific parameters + public SetTpSlRequest(SharedSymbol symbol, SharedPositionSide positionSide, SharedTpSlSide tpSlSide, decimal triggerPrice, ExchangeParameters? exchangeParameters = null) + : base(symbol, exchangeParameters) + { + PositionSide = positionSide; + TpSlSide = tpSlSide; + Symbol = symbol; + TriggerPrice = triggerPrice; } } diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/StartListenKeyRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/StartListenKeyRequest.cs index b6dcb45..ed76608 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/StartListenKeyRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/StartListenKeyRequest.cs @@ -1,25 +1,22 @@ -using CryptoExchange.Net.Objects; +namespace CryptoExchange.Net.SharedApis; -namespace CryptoExchange.Net.SharedApis +/// +/// Request to start the update stream for the current user +/// +public record StartListenKeyRequest : SharedRequest { /// - /// Request to start the update stream for the current user + /// Trading mode /// - public record StartListenKeyRequest : SharedRequest - { - /// - /// Trading mode - /// - public TradingMode? TradingMode { get; set; } + public TradingMode? TradingMode { get; set; } - /// - /// ctor - /// - /// Trading mode - /// Exchange specific parameters - public StartListenKeyRequest(TradingMode? tradingMode = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) - { - TradingMode = tradingMode; - } + /// + /// ctor + /// + /// Trading mode + /// Exchange specific parameters + public StartListenKeyRequest(TradingMode? tradingMode = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) + { + TradingMode = tradingMode; } } diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/StopListenKeyRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/StopListenKeyRequest.cs index 4a59a8a..6e61698 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/StopListenKeyRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/StopListenKeyRequest.cs @@ -1,31 +1,28 @@ -using CryptoExchange.Net.Objects; +namespace CryptoExchange.Net.SharedApis; -namespace CryptoExchange.Net.SharedApis +/// +/// Request to stop the update stream for the specific listen key +/// +public record StopListenKeyRequest : SharedRequest { /// - /// Request to stop the update stream for the specific listen key + /// The key to stop updates for /// - public record StopListenKeyRequest : SharedRequest - { - /// - /// The key to stop updates for - /// - public string ListenKey { get; set; } - /// - /// Trading mode - /// - public TradingMode? TradingMode { get; set; } + public string ListenKey { get; set; } + /// + /// Trading mode + /// + public TradingMode? TradingMode { get; set; } - /// - /// ctor - /// - /// The key to stop updates for - /// Trading mode - /// Exchange specific parameters - public StopListenKeyRequest(string listenKey, TradingMode? tradingMode = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) - { - ListenKey = listenKey; - TradingMode = tradingMode; - } + /// + /// ctor + /// + /// The key to stop updates for + /// Trading mode + /// Exchange specific parameters + public StopListenKeyRequest(string listenKey, TradingMode? tradingMode = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) + { + ListenKey = listenKey; + TradingMode = tradingMode; } } diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/WithdrawRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/WithdrawRequest.cs index d2c8bca..c40c6c2 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/WithdrawRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/WithdrawRequest.cs @@ -1,47 +1,46 @@ -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Request to withdraw funds from the exchange +/// +public record WithdrawRequest : SharedRequest { /// - /// Request to withdraw funds from the exchange + /// Asset to withdraw /// - public record WithdrawRequest : SharedRequest - { - /// - /// Asset to withdraw - /// - public string Asset { get; set; } - /// - /// Address to withdraw to - /// - public string Address { get; set; } - /// - /// Quantity to withdraw - /// - public decimal Quantity { get; set; } - /// - /// Address tag - /// - public string? AddressTag { get; set; } - /// - /// Network to use - /// - public string? Network { get; set; } + public string Asset { get; set; } + /// + /// Address to withdraw to + /// + public string Address { get; set; } + /// + /// Quantity to withdraw + /// + public decimal Quantity { get; set; } + /// + /// Address tag + /// + public string? AddressTag { get; set; } + /// + /// Network to use + /// + public string? Network { get; set; } - /// - /// ctor - /// - /// Asset to withdraw - /// Quantity to withdraw - /// Address to withdraw to - /// Network to use - /// Address tag - /// Exchange specific parameters - public WithdrawRequest(string asset, decimal quantity, string address, string? network = null, string? addressTag = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) - { - Asset = asset; - Address = address; - Quantity = quantity; - Network = network; - AddressTag = addressTag; - } + /// + /// ctor + /// + /// Asset to withdraw + /// Quantity to withdraw + /// Address to withdraw to + /// Network to use + /// Address tag + /// Exchange specific parameters + public WithdrawRequest(string asset, decimal quantity, string address, string? network = null, string? addressTag = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) + { + Asset = asset; + Address = address; + Quantity = quantity; + Network = network; + AddressTag = addressTag; } } diff --git a/CryptoExchange.Net/SharedApis/Models/SharedQuantitySupport.cs b/CryptoExchange.Net/SharedApis/Models/SharedQuantitySupport.cs index 709f441..de91888 100644 --- a/CryptoExchange.Net/SharedApis/Models/SharedQuantitySupport.cs +++ b/CryptoExchange.Net/SharedApis/Models/SharedQuantitySupport.cs @@ -1,107 +1,106 @@ -using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects; using System; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Support for different quantity notations +/// +public record SharedQuantitySupport { /// - /// Support for different quantity notations + /// Supported quantity notations for buy limit orders /// - public record SharedQuantitySupport + public SharedQuantityType BuyLimit { get; set; } + /// + /// Supported quantity notations for sell limit orders + /// + public SharedQuantityType SellLimit { get; set; } + /// + /// Supported quantity notations for buy market orders + /// + public SharedQuantityType BuyMarket { get; set; } + /// + /// Supported quantity notations for sell market orders + /// + public SharedQuantityType SellMarket { get; set; } + + /// + /// ctor + /// + public SharedQuantitySupport(SharedQuantityType buyLimit, SharedQuantityType sellLimit, SharedQuantityType buyMarket, SharedQuantityType sellMarket) { - /// - /// Supported quantity notations for buy limit orders - /// - public SharedQuantityType BuyLimit { get; set; } - /// - /// Supported quantity notations for sell limit orders - /// - public SharedQuantityType SellLimit { get; set; } - /// - /// Supported quantity notations for buy market orders - /// - public SharedQuantityType BuyMarket { get; set; } - /// - /// Supported quantity notations for sell market orders - /// - public SharedQuantityType SellMarket { get; set; } + BuyLimit = buyLimit; + SellLimit = sellLimit; + BuyMarket = buyMarket; + SellMarket = sellMarket; + } - /// - /// ctor - /// - public SharedQuantitySupport(SharedQuantityType buyLimit, SharedQuantityType sellLimit, SharedQuantityType buyMarket, SharedQuantityType sellMarket) - { - BuyLimit = buyLimit; - SellLimit = sellLimit; - BuyMarket = buyMarket; - SellMarket = sellMarket; - } + /// + /// Get the supported quantity type for a specific order configuration + /// + /// Side of the order + /// Type of the order + /// The supported quantity type + public SharedQuantityType GetSupportedQuantityType(SharedOrderSide side, SharedOrderType orderType) + { + if (side == SharedOrderSide.Buy && (orderType == SharedOrderType.Limit || orderType == SharedOrderType.LimitMaker)) return BuyLimit; + if (side == SharedOrderSide.Buy && orderType == SharedOrderType.Market) return BuyMarket; + if (side == SharedOrderSide.Sell && (orderType == SharedOrderType.Limit || orderType == SharedOrderType.LimitMaker)) return SellLimit; + if (side == SharedOrderSide.Sell && orderType == SharedOrderType.Market) return SellMarket; - /// - /// Get the supported quantity type for a specific order configuration - /// - /// Side of the order - /// Type of the order - /// The supported quantity type - public SharedQuantityType GetSupportedQuantityType(SharedOrderSide side, SharedOrderType orderType) - { - if (side == SharedOrderSide.Buy && (orderType == SharedOrderType.Limit || orderType == SharedOrderType.LimitMaker)) return BuyLimit; - if (side == SharedOrderSide.Buy && orderType == SharedOrderType.Market) return BuyMarket; - if (side == SharedOrderSide.Sell && (orderType == SharedOrderType.Limit || orderType == SharedOrderType.LimitMaker)) return SellLimit; - if (side == SharedOrderSide.Sell && orderType == SharedOrderType.Market) return SellMarket; + throw new ArgumentException("Unknown side/type combination"); + } - throw new ArgumentException("Unknown side/type combination"); - } + /// + /// Get whether the API supports a specific quantity type for an order configuration + /// + /// Side of the order + /// Type of the order + /// Type of quantity + /// True if supported, false if not + public bool IsSupported(SharedOrderSide side, SharedOrderType orderType, SharedQuantityType quantityType) + { + var supportedType = GetSupportedQuantityType(side, orderType); + if (supportedType == quantityType) + return true; - /// - /// Get whether the API supports a specific quantity type for an order configuration - /// - /// Side of the order - /// Type of the order - /// Type of quantity - /// True if supported, false if not - public bool IsSupported(SharedOrderSide side, SharedOrderType orderType, SharedQuantityType quantityType) - { - var supportedType = GetSupportedQuantityType(side, orderType); - if (supportedType == quantityType) - return true; + if (supportedType == SharedQuantityType.BaseAndQuoteAssetAndContracts) + return true; - if (supportedType == SharedQuantityType.BaseAndQuoteAssetAndContracts) - return true; + if (supportedType == SharedQuantityType.BaseAndQuoteAsset && (quantityType == SharedQuantityType.BaseAsset || quantityType == SharedQuantityType.QuoteAsset)) + return true; - if (supportedType == SharedQuantityType.BaseAndQuoteAsset && (quantityType == SharedQuantityType.BaseAsset || quantityType == SharedQuantityType.QuoteAsset)) - return true; - - return false; - } - - /// - /// Validate a request - /// - public Error? Validate(SharedOrderSide side, SharedOrderType type, SharedQuantity? quantity) - { - var supportedType = GetSupportedQuantityType(side, type); - if (supportedType == SharedQuantityType.BaseAndQuoteAsset || supportedType == SharedQuantityType.BaseAndQuoteAssetAndContracts) - return null; - - if (supportedType == SharedQuantityType.BaseAndQuoteAsset && quantity != null && quantity.QuantityInBaseAsset == null && quantity.QuantityInQuoteAsset == null) - return ArgumentError.Invalid("Quantity", $"Quantity for {side}.{type} required in base or quote asset"); - - if (supportedType == SharedQuantityType.QuoteAsset && quantity != null && quantity.QuantityInQuoteAsset == null) - return ArgumentError.Invalid("Quantity", $"Quantity for {side}.{type} required in quote asset"); - - if (supportedType == SharedQuantityType.BaseAsset && quantity != null && quantity.QuantityInBaseAsset == null && quantity.QuantityInContracts == null) - return ArgumentError.Invalid("Quantity", $"Quantity for {side}.{type} required in base asset"); - - if (supportedType == SharedQuantityType.Contracts && quantity != null && quantity.QuantityInContracts == null) - return ArgumentError.Invalid("Quantity", $"Quantity for {side}.{type} required in contracts"); + return false; + } + /// + /// Validate a request + /// + public Error? Validate(SharedOrderSide side, SharedOrderType type, SharedQuantity? quantity) + { + var supportedType = GetSupportedQuantityType(side, type); + if (supportedType == SharedQuantityType.BaseAndQuoteAsset || supportedType == SharedQuantityType.BaseAndQuoteAssetAndContracts) return null; - } - /// - public override string ToString() - { - return $"Limit buy: {BuyLimit}, limit sell: {SellLimit}, market buy: {BuyMarket}, market sell: {SellMarket}"; - } + if (supportedType == SharedQuantityType.BaseAndQuoteAsset && quantity != null && quantity.QuantityInBaseAsset == null && quantity.QuantityInQuoteAsset == null) + return ArgumentError.Invalid("Quantity", $"Quantity for {side}.{type} required in base or quote asset"); + + if (supportedType == SharedQuantityType.QuoteAsset && quantity != null && quantity.QuantityInQuoteAsset == null) + return ArgumentError.Invalid("Quantity", $"Quantity for {side}.{type} required in quote asset"); + + if (supportedType == SharedQuantityType.BaseAsset && quantity != null && quantity.QuantityInBaseAsset == null && quantity.QuantityInContracts == null) + return ArgumentError.Invalid("Quantity", $"Quantity for {side}.{type} required in base asset"); + + if (supportedType == SharedQuantityType.Contracts && quantity != null && quantity.QuantityInContracts == null) + return ArgumentError.Invalid("Quantity", $"Quantity for {side}.{type} required in contracts"); + + return null; + } + + /// + public override string ToString() + { + return $"Limit buy: {BuyLimit}, limit sell: {SellLimit}, market buy: {BuyMarket}, market sell: {SellMarket}"; } } diff --git a/CryptoExchange.Net/SharedApis/Models/SharedRequest.cs b/CryptoExchange.Net/SharedApis/Models/SharedRequest.cs index 2ee2aae..d745882 100644 --- a/CryptoExchange.Net/SharedApis/Models/SharedRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/SharedRequest.cs @@ -1,21 +1,20 @@ -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Request +/// +public record SharedRequest { /// - /// Request + /// Exchange parameters. Some calls may require exchange specific parameters to execute the request. /// - public record SharedRequest - { - /// - /// Exchange parameters. Some calls may require exchange specific parameters to execute the request. - /// - public ExchangeParameters? ExchangeParameters { get; set; } + public ExchangeParameters? ExchangeParameters { get; set; } - /// - /// ctor - /// - public SharedRequest(ExchangeParameters? exchangeParameters = null) - { - ExchangeParameters = exchangeParameters; - } + /// + /// ctor + /// + public SharedRequest(ExchangeParameters? exchangeParameters = null) + { + ExchangeParameters = exchangeParameters; } } diff --git a/CryptoExchange.Net/SharedApis/Models/SharedSymbolRequest.cs b/CryptoExchange.Net/SharedApis/Models/SharedSymbolRequest.cs index a50355c..67a7e75 100644 --- a/CryptoExchange.Net/SharedApis/Models/SharedSymbolRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/SharedSymbolRequest.cs @@ -1,50 +1,48 @@ -using System; -using System.Collections; +using System; using System.Collections.Generic; using System.Linq; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Symbol request +/// +public record SharedSymbolRequest : SharedRequest { /// - /// Symbol request + /// Trading mode /// - public record SharedSymbolRequest : SharedRequest + public TradingMode TradingMode { get; } + /// + /// The symbol + /// + public SharedSymbol? Symbol { get; set; } + /// + /// Symbols + /// + public SharedSymbol[]? Symbols { get; set; } + + /// + /// ctor + /// + public SharedSymbolRequest(SharedSymbol symbol, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) { - /// - /// Trading mode - /// - public TradingMode TradingMode { get; } - /// - /// The symbol - /// - public SharedSymbol? Symbol { get; set; } - /// - /// Symbols - /// - public SharedSymbol[]? Symbols { get; set; } + Symbol = symbol; + TradingMode = symbol.TradingMode; + } - /// - /// ctor - /// - public SharedSymbolRequest(SharedSymbol symbol, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) - { - Symbol = symbol; - TradingMode = symbol.TradingMode; - } + /// + /// ctor + /// + public SharedSymbolRequest(IEnumerable symbols, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) + { + if (!symbols.Any()) + throw new ArgumentException("Empty symbol list"); - /// - /// ctor - /// - public SharedSymbolRequest(IEnumerable symbols, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) - { - if (!symbols.Any()) - throw new ArgumentException("Empty symbol list"); + if (symbols.GroupBy(x => x.TradingMode).Count() > 1) + throw new ArgumentException("All symbols in the symbol list should have the same trading mode"); - if (symbols.GroupBy(x => x.TradingMode).Count() > 1) - throw new ArgumentException("All symbols in the symbol list should have the same trading mode"); - - Symbols = symbols.ToArray(); - TradingMode = Symbols.First().TradingMode; - } + Symbols = symbols.ToArray(); + TradingMode = Symbols.First().TradingMode; } } diff --git a/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeAllTickersRequest.cs b/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeAllTickersRequest.cs index c2e38cb..e83b710 100644 --- a/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeAllTickersRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeAllTickersRequest.cs @@ -1,25 +1,22 @@ -using CryptoExchange.Net.Objects; +namespace CryptoExchange.Net.SharedApis; -namespace CryptoExchange.Net.SharedApis +/// +/// Request to subscribe to ticker updates for all symbols +/// +public record SubscribeAllTickersRequest : SharedRequest { /// - /// Request to subscribe to ticker updates for all symbols + /// Trading mode /// - public record SubscribeAllTickersRequest : SharedRequest - { - /// - /// Trading mode - /// - public TradingMode? TradingMode { get; set; } + public TradingMode? TradingMode { get; set; } - /// - /// ctor - /// - /// Trading mode - /// Exchange specific parameters - public SubscribeAllTickersRequest(TradingMode? tradingMode = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) - { - TradingMode = tradingMode; - } + /// + /// ctor + /// + /// Trading mode + /// Exchange specific parameters + public SubscribeAllTickersRequest(TradingMode? tradingMode = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) + { + TradingMode = tradingMode; } } diff --git a/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeBalancesRequest.cs b/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeBalancesRequest.cs index 0179434..f9101bd 100644 --- a/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeBalancesRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeBalancesRequest.cs @@ -1,31 +1,28 @@ -using CryptoExchange.Net.Objects; +namespace CryptoExchange.Net.SharedApis; -namespace CryptoExchange.Net.SharedApis +/// +/// Request to subscribe to balance updates +/// +public record SubscribeBalancesRequest: SharedRequest { /// - /// Request to subscribe to balance updates + /// Trading mode /// - public record SubscribeBalancesRequest: SharedRequest - { - /// - /// Trading mode - /// - public TradingMode? TradingMode { get; set; } - /// - /// The listen key, needed for some exchanges. Can be obtained by the StartListenKeyAsync on the shared rest client - /// - public string? ListenKey { get; set; } + public TradingMode? TradingMode { get; set; } + /// + /// The listen key, needed for some exchanges. Can be obtained by the StartListenKeyAsync on the shared rest client + /// + public string? ListenKey { get; set; } - /// - /// ctor - /// - /// Listen key, needed for some exchanges. Can be obtained by the StartListenKeyAsync on the shared rest client - /// Trading mode - /// Exchange specific parameters - public SubscribeBalancesRequest(string? listenKey = null, TradingMode? tradingMode = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) - { - TradingMode = tradingMode; - ListenKey = listenKey; - } + /// + /// ctor + /// + /// Listen key, needed for some exchanges. Can be obtained by the StartListenKeyAsync on the shared rest client + /// Trading mode + /// Exchange specific parameters + public SubscribeBalancesRequest(string? listenKey = null, TradingMode? tradingMode = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) + { + TradingMode = tradingMode; + ListenKey = listenKey; } } diff --git a/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeBookTickerRequest.cs b/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeBookTickerRequest.cs index 6cabc03..ca7b4eb 100644 --- a/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeBookTickerRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeBookTickerRequest.cs @@ -1,36 +1,35 @@ -using System.Collections.Generic; +using System.Collections.Generic; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Request to subscribe to book ticker updates +/// +public record SubscribeBookTickerRequest : SharedSymbolRequest { /// - /// Request to subscribe to book ticker updates + /// ctor /// - public record SubscribeBookTickerRequest : SharedSymbolRequest + /// The symbol to subscribe to + /// Exchange specific parameters + public SubscribeBookTickerRequest(SharedSymbol symbol, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) { - /// - /// ctor - /// - /// The symbol to subscribe to - /// Exchange specific parameters - public SubscribeBookTickerRequest(SharedSymbol symbol, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) - { - } + } - /// - /// ctor - /// - /// The symbols to subscribe to - /// Exchange specific parameters - public SubscribeBookTickerRequest(IEnumerable symbols, ExchangeParameters? exchangeParameters = null) : base(symbols, exchangeParameters) - { - } + /// + /// ctor + /// + /// The symbols to subscribe to + /// Exchange specific parameters + public SubscribeBookTickerRequest(IEnumerable symbols, ExchangeParameters? exchangeParameters = null) : base(symbols, exchangeParameters) + { + } - /// - /// ctor - /// - /// The symbols to subscribe to - public SubscribeBookTickerRequest(params SharedSymbol[] symbols) : base(symbols, null) - { - } + /// + /// ctor + /// + /// The symbols to subscribe to + public SubscribeBookTickerRequest(params SharedSymbol[] symbols) : base(symbols, null) + { } } diff --git a/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeFuturesOrderRequest.cs b/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeFuturesOrderRequest.cs index a27ba51..3304b0c 100644 --- a/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeFuturesOrderRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeFuturesOrderRequest.cs @@ -1,31 +1,28 @@ -using CryptoExchange.Net.Objects; +namespace CryptoExchange.Net.SharedApis; -namespace CryptoExchange.Net.SharedApis +/// +/// Request to subscribe to futures order updates +/// +public record SubscribeFuturesOrderRequest : SharedRequest { /// - /// Request to subscribe to futures order updates + /// Trading mode /// - public record SubscribeFuturesOrderRequest : SharedRequest - { - /// - /// Trading mode - /// - public TradingMode? TradingMode { get; set; } - /// - /// The listen key, needed for some exchanges. Can be obtained by the StartListenKeyAsync on the shared rest client - /// - public string? ListenKey { get; set; } + public TradingMode? TradingMode { get; set; } + /// + /// The listen key, needed for some exchanges. Can be obtained by the StartListenKeyAsync on the shared rest client + /// + public string? ListenKey { get; set; } - /// - /// ctor - /// - /// Listen key, needed for some exchanges. Can be obtained by the StartListenKeyAsync on the shared rest client - /// Trading mode - /// Exchange specific parameters - public SubscribeFuturesOrderRequest(string? listenKey = null, TradingMode? tradingMode = null, ExchangeParameters? exchangeParameters = null): base(exchangeParameters) - { - TradingMode = tradingMode; - ListenKey = listenKey; - } + /// + /// ctor + /// + /// Listen key, needed for some exchanges. Can be obtained by the StartListenKeyAsync on the shared rest client + /// Trading mode + /// Exchange specific parameters + public SubscribeFuturesOrderRequest(string? listenKey = null, TradingMode? tradingMode = null, ExchangeParameters? exchangeParameters = null): base(exchangeParameters) + { + TradingMode = tradingMode; + ListenKey = listenKey; } } diff --git a/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeKlineRequest.cs b/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeKlineRequest.cs index 04a6dbb..72f7498 100644 --- a/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeKlineRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeKlineRequest.cs @@ -1,47 +1,46 @@ -using System.Collections.Generic; +using System.Collections.Generic; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Request to subscribe to kline/candlestick updates +/// +public record SubscribeKlineRequest : SharedSymbolRequest { /// - /// Request to subscribe to kline/candlestick updates + /// The kline interval /// - public record SubscribeKlineRequest : SharedSymbolRequest + public SharedKlineInterval Interval { get; set; } + + /// + /// ctor + /// + /// The symbol to subscribe to + /// Kline interval + /// Exchange specific parameters + public SubscribeKlineRequest(SharedSymbol symbol, SharedKlineInterval interval, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) { - /// - /// The kline interval - /// - public SharedKlineInterval Interval { get; set; } + Interval = interval; + } - /// - /// ctor - /// - /// The symbol to subscribe to - /// Kline interval - /// Exchange specific parameters - public SubscribeKlineRequest(SharedSymbol symbol, SharedKlineInterval interval, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) - { - Interval = interval; - } + /// + /// ctor + /// + /// The symbols to subscribe to + /// Kline interval + /// Exchange specific parameters + public SubscribeKlineRequest(IEnumerable symbols, SharedKlineInterval interval, ExchangeParameters? exchangeParameters = null) : base(symbols, exchangeParameters) + { + Interval = interval; + } - /// - /// ctor - /// - /// The symbols to subscribe to - /// Kline interval - /// Exchange specific parameters - public SubscribeKlineRequest(IEnumerable symbols, SharedKlineInterval interval, ExchangeParameters? exchangeParameters = null) : base(symbols, exchangeParameters) - { - Interval = interval; - } - - /// - /// ctor - /// - /// Kline interval - /// The symbols to subscribe to - public SubscribeKlineRequest(SharedKlineInterval interval, params SharedSymbol[] symbols) : base(symbols, null) - { - Interval = interval; - } + /// + /// ctor + /// + /// Kline interval + /// The symbols to subscribe to + public SubscribeKlineRequest(SharedKlineInterval interval, params SharedSymbol[] symbols) : base(symbols, null) + { + Interval = interval; } } diff --git a/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeOrderBookRequest.cs b/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeOrderBookRequest.cs index 65f744d..e51be33 100644 --- a/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeOrderBookRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeOrderBookRequest.cs @@ -1,43 +1,42 @@ -using System.Collections.Generic; +using System.Collections.Generic; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Request to subscribe to order book snapshot updates +/// +public record SubscribeOrderBookRequest : SharedSymbolRequest { /// - /// Request to subscribe to order book snapshot updates + /// The order book depth /// - public record SubscribeOrderBookRequest : SharedSymbolRequest + public int? Limit { get; set; } + + /// + /// ctor + /// + /// The symbol to subscribe to + /// Order book depth + /// Exchange specific parameters + public SubscribeOrderBookRequest(SharedSymbol symbol, int? limit = null, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) { - /// - /// The order book depth - /// - public int? Limit { get; set; } + Limit = limit; + } - /// - /// ctor - /// - /// The symbol to subscribe to - /// Order book depth - /// Exchange specific parameters - public SubscribeOrderBookRequest(SharedSymbol symbol, int? limit = null, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) - { - Limit = limit; - } + /// + /// ctor + /// + /// The symbols to subscribe to + /// Exchange specific parameters + public SubscribeOrderBookRequest(IEnumerable symbols, ExchangeParameters? exchangeParameters = null) : base(symbols, exchangeParameters) + { + } - /// - /// ctor - /// - /// The symbols to subscribe to - /// Exchange specific parameters - public SubscribeOrderBookRequest(IEnumerable symbols, ExchangeParameters? exchangeParameters = null) : base(symbols, exchangeParameters) - { - } - - /// - /// ctor - /// - /// The symbols to subscribe to - public SubscribeOrderBookRequest(params SharedSymbol[] symbols) : base(symbols, null) - { - } + /// + /// ctor + /// + /// The symbols to subscribe to + public SubscribeOrderBookRequest(params SharedSymbol[] symbols) : base(symbols, null) + { } } diff --git a/CryptoExchange.Net/SharedApis/Models/Socket/SubscribePositionRequest.cs b/CryptoExchange.Net/SharedApis/Models/Socket/SubscribePositionRequest.cs index 6ead352..334d2fc 100644 --- a/CryptoExchange.Net/SharedApis/Models/Socket/SubscribePositionRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Socket/SubscribePositionRequest.cs @@ -1,31 +1,28 @@ -using CryptoExchange.Net.Objects; +namespace CryptoExchange.Net.SharedApis; -namespace CryptoExchange.Net.SharedApis +/// +/// Request to subscribe to position updates +/// +public record SubscribePositionRequest: SharedRequest { /// - /// Request to subscribe to position updates + /// The listen key, needed for some exchanges. Can be obtained by the StartListenKeyAsync on the shared rest client /// - public record SubscribePositionRequest: SharedRequest - { - /// - /// The listen key, needed for some exchanges. Can be obtained by the StartListenKeyAsync on the shared rest client - /// - public string? ListenKey { get; set; } - /// - /// Trading mode - /// - public TradingMode? TradingMode { get; set; } + public string? ListenKey { get; set; } + /// + /// Trading mode + /// + public TradingMode? TradingMode { get; set; } - /// - /// ctor - /// - /// Listen key, needed for some exchanges. Can be obtained by the StartListenKeyAsync on the shared rest client - /// Trading mode - /// Exchange specific parameters - public SubscribePositionRequest(string? listenKey = null, TradingMode? tradingMode = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) - { - TradingMode = tradingMode; - ListenKey = listenKey; - } + /// + /// ctor + /// + /// Listen key, needed for some exchanges. Can be obtained by the StartListenKeyAsync on the shared rest client + /// Trading mode + /// Exchange specific parameters + public SubscribePositionRequest(string? listenKey = null, TradingMode? tradingMode = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) + { + TradingMode = tradingMode; + ListenKey = listenKey; } } diff --git a/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeSpotOrderRequest.cs b/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeSpotOrderRequest.cs index 3cb88a3..b7c354b 100644 --- a/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeSpotOrderRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeSpotOrderRequest.cs @@ -1,23 +1,22 @@ -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Request to subscribe to spot order updates +/// +public record SubscribeSpotOrderRequest : SharedRequest { /// - /// Request to subscribe to spot order updates + /// The listen key, needed for some exchanges. Can be obtained by the StartListenKeyAsync on the shared rest client /// - public record SubscribeSpotOrderRequest : SharedRequest - { - /// - /// The listen key, needed for some exchanges. Can be obtained by the StartListenKeyAsync on the shared rest client - /// - public string? ListenKey { get; set; } + public string? ListenKey { get; set; } - /// - /// ctor - /// - /// Listen key, needed for some exchanges. Can be obtained by the StartListenKeyAsync on the shared rest client - /// Exchange specific parameters - public SubscribeSpotOrderRequest(string? listenKey = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) - { - ListenKey = listenKey; - } + /// + /// ctor + /// + /// Listen key, needed for some exchanges. Can be obtained by the StartListenKeyAsync on the shared rest client + /// Exchange specific parameters + public SubscribeSpotOrderRequest(string? listenKey = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) + { + ListenKey = listenKey; } } diff --git a/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeTickerRequest.cs b/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeTickerRequest.cs index 46f3636..f1945ac 100644 --- a/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeTickerRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeTickerRequest.cs @@ -1,37 +1,35 @@ -using System.Collections; using System.Collections.Generic; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Request to subscribe to ticker updates +/// +public record SubscribeTickerRequest : SharedSymbolRequest { /// - /// Request to subscribe to ticker updates + /// ctor /// - public record SubscribeTickerRequest : SharedSymbolRequest + /// The symbol to subscribe to + /// Exchange specific parameters + public SubscribeTickerRequest(SharedSymbol symbol, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) { - /// - /// ctor - /// - /// The symbol to subscribe to - /// Exchange specific parameters - public SubscribeTickerRequest(SharedSymbol symbol, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) - { - } + } - /// - /// ctor - /// - /// The symbols to subscribe to - /// Exchange specific parameters - public SubscribeTickerRequest(IEnumerable symbols, ExchangeParameters? exchangeParameters = null) : base(symbols, exchangeParameters) - { - } + /// + /// ctor + /// + /// The symbols to subscribe to + /// Exchange specific parameters + public SubscribeTickerRequest(IEnumerable symbols, ExchangeParameters? exchangeParameters = null) : base(symbols, exchangeParameters) + { + } - /// - /// ctor - /// - /// The symbols to subscribe to - public SubscribeTickerRequest(params SharedSymbol[] symbols) : base(symbols, null) - { - } + /// + /// ctor + /// + /// The symbols to subscribe to + public SubscribeTickerRequest(params SharedSymbol[] symbols) : base(symbols, null) + { } } diff --git a/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeTradeRequest.cs b/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeTradeRequest.cs index 115cc1f..253a726 100644 --- a/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeTradeRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeTradeRequest.cs @@ -1,36 +1,35 @@ -using System.Collections.Generic; +using System.Collections.Generic; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Request to subscribe to trade updates +/// +public record SubscribeTradeRequest : SharedSymbolRequest { /// - /// Request to subscribe to trade updates + /// ctor /// - public record SubscribeTradeRequest : SharedSymbolRequest + /// The symbol to subscribe to + /// Exchange specific parameters + public SubscribeTradeRequest(SharedSymbol symbol, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) { - /// - /// ctor - /// - /// The symbol to subscribe to - /// Exchange specific parameters - public SubscribeTradeRequest(SharedSymbol symbol, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) - { - } + } - /// - /// ctor - /// - /// The symbols to subscribe to - /// Exchange specific parameters - public SubscribeTradeRequest(IEnumerable symbols, ExchangeParameters? exchangeParameters = null) : base(symbols, exchangeParameters) - { - } + /// + /// ctor + /// + /// The symbols to subscribe to + /// Exchange specific parameters + public SubscribeTradeRequest(IEnumerable symbols, ExchangeParameters? exchangeParameters = null) : base(symbols, exchangeParameters) + { + } - /// - /// ctor - /// - /// The symbols to subscribe to - public SubscribeTradeRequest(params SharedSymbol[] symbols) : base(symbols, null) - { - } + /// + /// ctor + /// + /// The symbols to subscribe to + public SubscribeTradeRequest(params SharedSymbol[] symbols) : base(symbols, null) + { } } diff --git a/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeUserTradeRequest.cs b/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeUserTradeRequest.cs index d4df63c..9d74cb3 100644 --- a/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeUserTradeRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Socket/SubscribeUserTradeRequest.cs @@ -1,31 +1,28 @@ -using CryptoExchange.Net.Objects; +namespace CryptoExchange.Net.SharedApis; -namespace CryptoExchange.Net.SharedApis +/// +/// Request to subscribe to user trade updates +/// +public record SubscribeUserTradeRequest : SharedRequest { /// - /// Request to subscribe to user trade updates + /// Trading mode /// - public record SubscribeUserTradeRequest : SharedRequest - { - /// - /// Trading mode - /// - public TradingMode? TradingMode { get; set; } - /// - /// The listen key, needed for some exchanges. Can be obtained by the StartListenKeyAsync on the shared rest client - /// - public string? ListenKey { get; set; } + public TradingMode? TradingMode { get; set; } + /// + /// The listen key, needed for some exchanges. Can be obtained by the StartListenKeyAsync on the shared rest client + /// + public string? ListenKey { get; set; } - /// - /// ctor - /// - /// Listen key, needed for some exchanges. Can be obtained by the StartListenKeyAsync on the shared rest client - /// Trading mode - /// Exchange specific parameters - public SubscribeUserTradeRequest(string? listenKey = null, TradingMode? tradingMode = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) - { - TradingMode = tradingMode; - ListenKey = listenKey; - } + /// + /// ctor + /// + /// Listen key, needed for some exchanges. Can be obtained by the StartListenKeyAsync on the shared rest client + /// Trading mode + /// Exchange specific parameters + public SubscribeUserTradeRequest(string? listenKey = null, TradingMode? tradingMode = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) + { + TradingMode = tradingMode; + ListenKey = listenKey; } } diff --git a/CryptoExchange.Net/SharedApis/ResponseModels/SharedAsset.cs b/CryptoExchange.Net/SharedApis/ResponseModels/SharedAsset.cs index 3fe5aff..66ad407 100644 --- a/CryptoExchange.Net/SharedApis/ResponseModels/SharedAsset.cs +++ b/CryptoExchange.Net/SharedApis/ResponseModels/SharedAsset.cs @@ -1,79 +1,77 @@ -using System; -using System.Collections.Generic; +using System; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Asset info +/// +public record SharedAsset { /// - /// Asset info + /// Name of the asset /// - public record SharedAsset - { - /// - /// Name of the asset - /// - public string Name { get; set; } - /// - /// Full or alternative name of the asset - /// - public string? FullName { get; set; } - /// - /// Asset networks info - /// - public SharedAssetNetwork[]? Networks { get; set; } = Array.Empty(); - - /// - /// ctor - /// - public SharedAsset(string name) - { - Name = name; - } - } + public string Name { get; set; } + /// + /// Full or alternative name of the asset + /// + public string? FullName { get; set; } + /// + /// Asset networks info + /// + public SharedAssetNetwork[]? Networks { get; set; } = Array.Empty(); /// - /// Asset network info + /// ctor /// - public record SharedAssetNetwork + public SharedAsset(string name) { - /// - /// Network name - /// - public string Name { get; set; } - /// - /// Network full/alternative name - /// - public string? FullName { get; set; } - /// - /// Withdrawal fee - /// - public decimal? WithdrawFee { get; set; } - /// - /// Minimal withdrawal quantity - /// - public decimal? MinWithdrawQuantity { get; set; } - /// - /// Max withdrawal quantity - /// - public decimal? MaxWithdrawQuantity { get; set; } - /// - /// Withdrawals are enabled - /// - public bool? WithdrawEnabled { get; set; } - /// - /// Deposits are enabled - /// - public bool? DepositEnabled { get; set; } - /// - /// Min number of confirmations - /// - public int? MinConfirmations { get; set; } - - /// - /// ctor - /// - public SharedAssetNetwork(string name) - { - Name = name; - } + Name = name; + } +} + +/// +/// Asset network info +/// +public record SharedAssetNetwork +{ + /// + /// Network name + /// + public string Name { get; set; } + /// + /// Network full/alternative name + /// + public string? FullName { get; set; } + /// + /// Withdrawal fee + /// + public decimal? WithdrawFee { get; set; } + /// + /// Minimal withdrawal quantity + /// + public decimal? MinWithdrawQuantity { get; set; } + /// + /// Max withdrawal quantity + /// + public decimal? MaxWithdrawQuantity { get; set; } + /// + /// Withdrawals are enabled + /// + public bool? WithdrawEnabled { get; set; } + /// + /// Deposits are enabled + /// + public bool? DepositEnabled { get; set; } + /// + /// Min number of confirmations + /// + public int? MinConfirmations { get; set; } + + /// + /// ctor + /// + public SharedAssetNetwork(string name) + { + Name = name; } } diff --git a/CryptoExchange.Net/SharedApis/ResponseModels/SharedBalance.cs b/CryptoExchange.Net/SharedApis/ResponseModels/SharedBalance.cs index ee622d1..76442a2 100644 --- a/CryptoExchange.Net/SharedApis/ResponseModels/SharedBalance.cs +++ b/CryptoExchange.Net/SharedApis/ResponseModels/SharedBalance.cs @@ -1,35 +1,34 @@ -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Balance info +/// +public record SharedBalance { /// - /// Balance info + /// Asset name /// - public record SharedBalance - { - /// - /// Asset name - /// - public string Asset { get; set; } - /// - /// Available quantity - /// - public decimal Available { get; set; } - /// - /// Total quantity - /// - public decimal Total { get; set; } - /// - /// Isolated margin symbol, only applicable for isolated margin futures - /// - public string? IsolatedMarginSymbol { get; set; } + public string Asset { get; set; } + /// + /// Available quantity + /// + public decimal Available { get; set; } + /// + /// Total quantity + /// + public decimal Total { get; set; } + /// + /// Isolated margin symbol, only applicable for isolated margin futures + /// + public string? IsolatedMarginSymbol { get; set; } - /// - /// ctor - /// - public SharedBalance(string asset, decimal available, decimal total) - { - Asset = asset; - Available = available; - Total = total; - } + /// + /// ctor + /// + public SharedBalance(string asset, decimal available, decimal total) + { + Asset = asset; + Available = available; + Total = total; } } diff --git a/CryptoExchange.Net/SharedApis/ResponseModels/SharedBookTicker.cs b/CryptoExchange.Net/SharedApis/ResponseModels/SharedBookTicker.cs index 7a67a10..6fbed9f 100644 --- a/CryptoExchange.Net/SharedApis/ResponseModels/SharedBookTicker.cs +++ b/CryptoExchange.Net/SharedApis/ResponseModels/SharedBookTicker.cs @@ -1,37 +1,36 @@ -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Book ticker +/// +public record SharedBookTicker : SharedSymbolModel { /// - /// Book ticker + /// Price of the best ask /// - public record SharedBookTicker : SharedSymbolModel - { - /// - /// Price of the best ask - /// - public decimal BestAskPrice { get; set; } - /// - /// Quantity of the best ask - /// - public decimal BestAskQuantity { get; set; } - /// - /// Price of the best bid - /// - public decimal BestBidPrice { get; set; } - /// - /// Quantity of the best bid - /// - public decimal BestBidQuantity { get; set; } + public decimal BestAskPrice { get; set; } + /// + /// Quantity of the best ask + /// + public decimal BestAskQuantity { get; set; } + /// + /// Price of the best bid + /// + public decimal BestBidPrice { get; set; } + /// + /// Quantity of the best bid + /// + public decimal BestBidQuantity { get; set; } - /// - /// ctor - /// - public SharedBookTicker(SharedSymbol? sharedSymbol, string symbol, decimal bestAskPrice, decimal bestAskQuantity, decimal bestBidPrice, decimal bestBidQuantity) - : base(sharedSymbol, symbol) - { - BestAskPrice = bestAskPrice; - BestAskQuantity = bestAskQuantity; - BestBidPrice = bestBidPrice; - BestBidQuantity = bestBidQuantity; - } + /// + /// ctor + /// + public SharedBookTicker(SharedSymbol? sharedSymbol, string symbol, decimal bestAskPrice, decimal bestAskQuantity, decimal bestBidPrice, decimal bestBidQuantity) + : base(sharedSymbol, symbol) + { + BestAskPrice = bestAskPrice; + BestAskQuantity = bestAskQuantity; + BestBidPrice = bestBidPrice; + BestBidQuantity = bestBidQuantity; } } diff --git a/CryptoExchange.Net/SharedApis/ResponseModels/SharedDeposit.cs b/CryptoExchange.Net/SharedApis/ResponseModels/SharedDeposit.cs index a59a141..9a3b1f9 100644 --- a/CryptoExchange.Net/SharedApis/ResponseModels/SharedDeposit.cs +++ b/CryptoExchange.Net/SharedApis/ResponseModels/SharedDeposit.cs @@ -1,59 +1,57 @@ -using System; +using System; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Deposit info +/// +public record SharedDeposit { /// - /// Deposit info + /// The id of the deposit /// - public record SharedDeposit + public string? Id { get; set; } + /// + /// The asset of the deposit + /// + public string Asset { get; set; } + /// + /// The quantity that was deposited + /// + public decimal Quantity { get; set; } + /// + /// Timestamp of the deposit + /// + public DateTime Timestamp { get; set; } + /// + /// Network used + /// + public string? Network { get; set; } + /// + /// Number of confirmations + /// + public int? Confirmations { get; set; } + /// + /// Transaction id + /// + public string? TransactionId { get; set; } + /// + /// Tag + /// + public string? Tag { get; set; } + /// + /// Whether the deposit was completed successfully + /// + public bool Completed { get; set; } + + /// + /// ctor + /// + public SharedDeposit(string asset, decimal quantity, bool completed, DateTime timestamp) { - /// - /// The id of the deposit - /// - public string? Id { get; set; } - /// - /// The asset of the deposit - /// - public string Asset { get; set; } - /// - /// The quantity that was deposited - /// - public decimal Quantity { get; set; } - /// - /// Timestamp of the deposit - /// - public DateTime Timestamp { get; set; } - /// - /// Network used - /// - public string? Network { get; set; } - /// - /// Number of confirmations - /// - public int? Confirmations { get; set; } - /// - /// Transaction id - /// - public string? TransactionId { get; set; } - /// - /// Tag - /// - public string? Tag { get; set; } - /// - /// Whether the deposit was completed successfully - /// - public bool Completed { get; set; } - - /// - /// ctor - /// - public SharedDeposit(string asset, decimal quantity, bool completed, DateTime timestamp) - { - Asset = asset; - Quantity = quantity; - Timestamp = timestamp; - Completed = completed; - } + Asset = asset; + Quantity = quantity; + Timestamp = timestamp; + Completed = completed; } - } diff --git a/CryptoExchange.Net/SharedApis/ResponseModels/SharedDepositAddress.cs b/CryptoExchange.Net/SharedApis/ResponseModels/SharedDepositAddress.cs index 871d23f..b876b7d 100644 --- a/CryptoExchange.Net/SharedApis/ResponseModels/SharedDepositAddress.cs +++ b/CryptoExchange.Net/SharedApis/ResponseModels/SharedDepositAddress.cs @@ -1,35 +1,33 @@ -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Deposit address info +/// +public record SharedDepositAddress { /// - /// Deposit address info + /// Asset the address is for /// - public record SharedDepositAddress + public string Asset { get; set; } + /// + /// Deposit address + /// + public string Address { get; set; } + /// + /// The network + /// + public string? Network { get; set; } + /// + /// Tag or memo + /// + public string? TagOrMemo { get; set; } + + /// + /// ctor + /// + public SharedDepositAddress(string asset, string address) { - /// - /// Asset the address is for - /// - public string Asset { get; set; } - /// - /// Deposit address - /// - public string Address { get; set; } - /// - /// The network - /// - public string? Network { get; set; } - /// - /// Tag or memo - /// - public string? TagOrMemo { get; set; } - - /// - /// ctor - /// - public SharedDepositAddress(string asset, string address) - { - Asset = asset; - Address = address; - } + Asset = asset; + Address = address; } - } diff --git a/CryptoExchange.Net/SharedApis/ResponseModels/SharedFee.cs b/CryptoExchange.Net/SharedApis/ResponseModels/SharedFee.cs index 528ed6d..1f5d475 100644 --- a/CryptoExchange.Net/SharedApis/ResponseModels/SharedFee.cs +++ b/CryptoExchange.Net/SharedApis/ResponseModels/SharedFee.cs @@ -1,30 +1,25 @@ -using System; -using System.Collections.Generic; -using System.Text; +namespace CryptoExchange.Net.SharedApis; -namespace CryptoExchange.Net.SharedApis +/// +/// Trading fee info +/// +public record SharedFee { /// - /// Trading fee info + /// Taker fee percentage /// - public record SharedFee - { - /// - /// Taker fee percentage - /// - public decimal TakerFee { get; set; } - /// - /// Maker fee percentage - /// - public decimal MakerFee { get; set; } + public decimal TakerFee { get; set; } + /// + /// Maker fee percentage + /// + public decimal MakerFee { get; set; } - /// - /// ctor - /// - public SharedFee(decimal makerFee, decimal takerFee) - { - MakerFee = makerFee; - TakerFee = takerFee; - } + /// + /// ctor + /// + public SharedFee(decimal makerFee, decimal takerFee) + { + MakerFee = makerFee; + TakerFee = takerFee; } } diff --git a/CryptoExchange.Net/SharedApis/ResponseModels/SharedFundingRate.cs b/CryptoExchange.Net/SharedApis/ResponseModels/SharedFundingRate.cs index 3454736..23594b4 100644 --- a/CryptoExchange.Net/SharedApis/ResponseModels/SharedFundingRate.cs +++ b/CryptoExchange.Net/SharedApis/ResponseModels/SharedFundingRate.cs @@ -1,28 +1,27 @@ -using System; +using System; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Funding rate +/// +public record SharedFundingRate { /// - /// Funding rate + /// The funding rate /// - public record SharedFundingRate - { - /// - /// The funding rate - /// - public decimal FundingRate { get; set; } - /// - /// Timestamp - /// - public DateTime Timestamp { get; set; } + public decimal FundingRate { get; set; } + /// + /// Timestamp + /// + public DateTime Timestamp { get; set; } - /// - /// ctor - /// - public SharedFundingRate(decimal fundingRate, DateTime timestamp) - { - FundingRate = fundingRate; - Timestamp = timestamp; - } + /// + /// ctor + /// + public SharedFundingRate(decimal fundingRate, DateTime timestamp) + { + FundingRate = fundingRate; + Timestamp = timestamp; } } diff --git a/CryptoExchange.Net/SharedApis/ResponseModels/SharedFuturesKline.cs b/CryptoExchange.Net/SharedApis/ResponseModels/SharedFuturesKline.cs index 33dd858..2cc57ec 100644 --- a/CryptoExchange.Net/SharedApis/ResponseModels/SharedFuturesKline.cs +++ b/CryptoExchange.Net/SharedApis/ResponseModels/SharedFuturesKline.cs @@ -1,43 +1,42 @@ -using System; +using System; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Mark/index price kline +/// +public record SharedFuturesKline { /// - /// Mark/index price kline + /// Open time /// - public record SharedFuturesKline - { - /// - /// Open time - /// - public DateTime OpenTime { get; set; } - /// - /// Close price - /// - public decimal ClosePrice { get; set; } - /// - /// High price - /// - public decimal HighPrice { get; set; } - /// - /// Low price - /// - public decimal LowPrice { get; set; } - /// - /// Open price - /// - public decimal OpenPrice { get; set; } + public DateTime OpenTime { get; set; } + /// + /// Close price + /// + public decimal ClosePrice { get; set; } + /// + /// High price + /// + public decimal HighPrice { get; set; } + /// + /// Low price + /// + public decimal LowPrice { get; set; } + /// + /// Open price + /// + public decimal OpenPrice { get; set; } - /// - /// ctor - /// - public SharedFuturesKline(DateTime openTime, decimal closePrice, decimal highPrice, decimal lowPrice, decimal openPrice) - { - OpenTime = openTime; - ClosePrice = closePrice; - HighPrice = highPrice; - LowPrice = lowPrice; - OpenPrice = openPrice; - } + /// + /// ctor + /// + public SharedFuturesKline(DateTime openTime, decimal closePrice, decimal highPrice, decimal lowPrice, decimal openPrice) + { + OpenTime = openTime; + ClosePrice = closePrice; + HighPrice = highPrice; + LowPrice = lowPrice; + OpenPrice = openPrice; } } diff --git a/CryptoExchange.Net/SharedApis/ResponseModels/SharedFuturesOrder.cs b/CryptoExchange.Net/SharedApis/ResponseModels/SharedFuturesOrder.cs index 38afc31..23d4f4c 100644 --- a/CryptoExchange.Net/SharedApis/ResponseModels/SharedFuturesOrder.cs +++ b/CryptoExchange.Net/SharedApis/ResponseModels/SharedFuturesOrder.cs @@ -1,128 +1,127 @@ -using System; +using System; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Futures order info +/// +public record SharedFuturesOrder : SharedSymbolModel { /// - /// Futures order info + /// Id of the order /// - public record SharedFuturesOrder : SharedSymbolModel + public string OrderId { get; set; } + /// + /// Type of the order + /// + public SharedOrderType OrderType { get; set; } + /// + /// Side of the order + /// + public SharedOrderSide Side { get; set; } + /// + /// Status of the order + /// + public SharedOrderStatus Status { get; set; } + /// + /// Time in force for the order + /// + public SharedTimeInForce? TimeInForce { get; set; } + /// + /// Position side + /// + public SharedPositionSide? PositionSide { get; set; } + /// + /// Reduce only + /// + public bool? ReduceOnly { get; set; } + /// + /// Order quantity + /// + public SharedOrderQuantity? OrderQuantity { get; set; } + /// + /// Filled quantity + /// + public SharedOrderQuantity? QuantityFilled { get; set; } + /// + /// Order price + /// + public decimal? OrderPrice { get; set; } + /// + /// Average price + /// + public decimal? AveragePrice { get; set; } + /// + /// Client order id + /// + public string? ClientOrderId { get; set; } + /// + /// Asset the fee is in + /// + public string? FeeAsset { get; set; } + /// + /// Fee paid + /// + public decimal? Fee { get; set; } + /// + /// Leverage + /// + public decimal? Leverage { get; set; } + /// + /// Timestamp the order was created + /// + public DateTime? CreateTime { get; set; } + /// + /// Last update timestamp + /// + public DateTime? UpdateTime { get; set; } + + /// + /// Last trade info, only available for websocket order updates if the API provides this data in the update + /// + public SharedUserTrade? LastTrade { get; set; } + + /// + /// Trigger price for a trigger order + /// + public decimal? TriggerPrice { get; set; } + /// + /// Whether or not the is order is a trigger order + /// + public bool? IsTriggerOrder { get; set; } + + /// + /// Take profit price + /// + public decimal? TakeProfitPrice { get; set; } + + /// + /// Stop loss price + /// + public decimal? StopLossPrice { get; set; } + + /// + /// Whether this order is to close an existing position. If this is the case quantities might not be specified + /// + public bool? IsCloseOrder { get; set; } + + /// + /// ctor + /// + public SharedFuturesOrder( + SharedSymbol? sharedSymbol, + string symbol, + string orderId, + SharedOrderType orderType, + SharedOrderSide orderSide, + SharedOrderStatus orderStatus, + DateTime? createTime) + : base(sharedSymbol, symbol) { - /// - /// Id of the order - /// - public string OrderId { get; set; } - /// - /// Type of the order - /// - public SharedOrderType OrderType { get; set; } - /// - /// Side of the order - /// - public SharedOrderSide Side { get; set; } - /// - /// Status of the order - /// - public SharedOrderStatus Status { get; set; } - /// - /// Time in force for the order - /// - public SharedTimeInForce? TimeInForce { get; set; } - /// - /// Position side - /// - public SharedPositionSide? PositionSide { get; set; } - /// - /// Reduce only - /// - public bool? ReduceOnly { get; set; } - /// - /// Order quantity - /// - public SharedOrderQuantity? OrderQuantity { get; set; } - /// - /// Filled quantity - /// - public SharedOrderQuantity? QuantityFilled { get; set; } - /// - /// Order price - /// - public decimal? OrderPrice { get; set; } - /// - /// Average price - /// - public decimal? AveragePrice { get; set; } - /// - /// Client order id - /// - public string? ClientOrderId { get; set; } - /// - /// Asset the fee is in - /// - public string? FeeAsset { get; set; } - /// - /// Fee paid - /// - public decimal? Fee { get; set; } - /// - /// Leverage - /// - public decimal? Leverage { get; set; } - /// - /// Timestamp the order was created - /// - public DateTime? CreateTime { get; set; } - /// - /// Last update timestamp - /// - public DateTime? UpdateTime { get; set; } - - /// - /// Last trade info, only available for websocket order updates if the API provides this data in the update - /// - public SharedUserTrade? LastTrade { get; set; } - - /// - /// Trigger price for a trigger order - /// - public decimal? TriggerPrice { get; set; } - /// - /// Whether or not the is order is a trigger order - /// - public bool? IsTriggerOrder { get; set; } - - /// - /// Take profit price - /// - public decimal? TakeProfitPrice { get; set; } - - /// - /// Stop loss price - /// - public decimal? StopLossPrice { get; set; } - - /// - /// Whether this order is to close an existing position. If this is the case quantities might not be specified - /// - public bool? IsCloseOrder { get; set; } - - /// - /// ctor - /// - public SharedFuturesOrder( - SharedSymbol? sharedSymbol, - string symbol, - string orderId, - SharedOrderType orderType, - SharedOrderSide orderSide, - SharedOrderStatus orderStatus, - DateTime? createTime) - : base(sharedSymbol, symbol) - { - OrderId = orderId; - OrderType = orderType; - Side = orderSide; - Status = orderStatus; - CreateTime = createTime; - } + OrderId = orderId; + OrderType = orderType; + Side = orderSide; + Status = orderStatus; + CreateTime = createTime; } } diff --git a/CryptoExchange.Net/SharedApis/ResponseModels/SharedFuturesSymbol.cs b/CryptoExchange.Net/SharedApis/ResponseModels/SharedFuturesSymbol.cs index 7f24acd..fddde1c 100644 --- a/CryptoExchange.Net/SharedApis/ResponseModels/SharedFuturesSymbol.cs +++ b/CryptoExchange.Net/SharedApis/ResponseModels/SharedFuturesSymbol.cs @@ -1,37 +1,36 @@ -using System; +using System; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Futures symbol info +/// +public record SharedFuturesSymbol : SharedSpotSymbol { /// - /// Futures symbol info + /// The size of a single contract /// - public record SharedFuturesSymbol : SharedSpotSymbol + public decimal? ContractSize { get; set; } + /// + /// Delivery time of the contract + /// + public DateTime? DeliveryTime { get; set; } + /// + /// Max short leverage setting + /// + public decimal? MaxShortLeverage { get; set; } + /// + /// Max long leverage setting + /// + public decimal? MaxLongLeverage { get; set; } + + /// + /// ctor + /// + public SharedFuturesSymbol(TradingMode symbolType, string baseAsset, string quoteAsset, string symbol, bool trading) : base(baseAsset, quoteAsset, symbol, trading, symbolType) { - /// - /// The size of a single contract - /// - public decimal? ContractSize { get; set; } - /// - /// Delivery time of the contract - /// - public DateTime? DeliveryTime { get; set; } - /// - /// Max short leverage setting - /// - public decimal? MaxShortLeverage { get; set; } - /// - /// Max long leverage setting - /// - public decimal? MaxLongLeverage { get; set; } - - /// - /// ctor - /// - public SharedFuturesSymbol(TradingMode symbolType, string baseAsset, string quoteAsset, string symbol, bool trading) : base(baseAsset, quoteAsset, symbol, trading, symbolType) - { - } - - /// - public override SharedSymbol SharedSymbol => new SharedSymbol(TradingMode, BaseAsset.ToUpperInvariant(), QuoteAsset.ToUpperInvariant(), DeliveryTime) { SymbolName = Name }; } + + /// + public override SharedSymbol SharedSymbol => new SharedSymbol(TradingMode, BaseAsset.ToUpperInvariant(), QuoteAsset.ToUpperInvariant(), DeliveryTime) { SymbolName = Name }; } diff --git a/CryptoExchange.Net/SharedApis/ResponseModels/SharedFuturesTicker.cs b/CryptoExchange.Net/SharedApis/ResponseModels/SharedFuturesTicker.cs index cfd6915..0f81c43 100644 --- a/CryptoExchange.Net/SharedApis/ResponseModels/SharedFuturesTicker.cs +++ b/CryptoExchange.Net/SharedApis/ResponseModels/SharedFuturesTicker.cs @@ -1,60 +1,59 @@ -using System; +using System; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Futures ticker info +/// +public record SharedFuturesTicker: SharedSymbolModel { /// - /// Futures ticker info + /// Last trade price /// - public record SharedFuturesTicker: SharedSymbolModel - { - /// - /// Last trade price - /// - public decimal? LastPrice { get; set; } - /// - /// High price in the last 24h - /// - public decimal? HighPrice { get; set; } - /// - /// Low price in the last 24h - /// - public decimal? LowPrice { get; set; } - /// - /// The volume in the last 24h - /// - public decimal Volume { get; set; } - /// - /// Change percentage in the last 24h - /// - public decimal? ChangePercentage { get; set; } - /// - /// Current mark price - /// - public decimal? MarkPrice { get; set; } - /// - /// Current index price - /// - public decimal? IndexPrice { get; set; } - /// - /// Current funding rate - /// - public decimal? FundingRate { get; set; } - /// - /// Next funding time - /// - public DateTime? NextFundingTime { get; set; } + public decimal? LastPrice { get; set; } + /// + /// High price in the last 24h + /// + public decimal? HighPrice { get; set; } + /// + /// Low price in the last 24h + /// + public decimal? LowPrice { get; set; } + /// + /// The volume in the last 24h + /// + public decimal Volume { get; set; } + /// + /// Change percentage in the last 24h + /// + public decimal? ChangePercentage { get; set; } + /// + /// Current mark price + /// + public decimal? MarkPrice { get; set; } + /// + /// Current index price + /// + public decimal? IndexPrice { get; set; } + /// + /// Current funding rate + /// + public decimal? FundingRate { get; set; } + /// + /// Next funding time + /// + public DateTime? NextFundingTime { get; set; } - /// - /// ctor - /// - public SharedFuturesTicker(SharedSymbol? sharedSymbol, string symbol, decimal? lastPrice, decimal? highPrice, decimal? lowPrice, decimal volume, decimal? changePercentage) - :base(sharedSymbol, symbol) - { - LastPrice = lastPrice; - HighPrice = highPrice; - LowPrice = lowPrice; - Volume = volume; - ChangePercentage = changePercentage; - } + /// + /// ctor + /// + public SharedFuturesTicker(SharedSymbol? sharedSymbol, string symbol, decimal? lastPrice, decimal? highPrice, decimal? lowPrice, decimal volume, decimal? changePercentage) + :base(sharedSymbol, symbol) + { + LastPrice = lastPrice; + HighPrice = highPrice; + LowPrice = lowPrice; + Volume = volume; + ChangePercentage = changePercentage; } } diff --git a/CryptoExchange.Net/SharedApis/ResponseModels/SharedFuturesTriggerOrder.cs b/CryptoExchange.Net/SharedApis/ResponseModels/SharedFuturesTriggerOrder.cs index 49fdbf8..da3ca8e 100644 --- a/CryptoExchange.Net/SharedApis/ResponseModels/SharedFuturesTriggerOrder.cs +++ b/CryptoExchange.Net/SharedApis/ResponseModels/SharedFuturesTriggerOrder.cs @@ -1,105 +1,102 @@ -using System; -using System.Collections.Generic; -using System.Text; +using System; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Trigger order info +/// +public record SharedFuturesTriggerOrder : SharedSymbolModel { /// - /// Trigger order info + /// The id of the trigger order /// - public record SharedFuturesTriggerOrder : SharedSymbolModel - { - /// - /// The id of the trigger order - /// - public string TriggerOrderId { get; set; } - /// - /// The id of the order that was placed when this order was activated - /// - public string? PlacedOrderId { get; set; } - /// - /// The type of the order - /// - public SharedOrderType OrderType { get; set; } - /// - /// Status of the trigger order - /// - public SharedTriggerOrderStatus Status { get; set; } - /// - /// Time in force for the order - /// - public SharedTimeInForce? TimeInForce { get; set; } - /// - /// Order quantity - /// - public SharedOrderQuantity? OrderQuantity { get; set; } - /// - /// Filled quantity - /// - public SharedOrderQuantity? QuantityFilled { get; set; } - /// - /// Order price - /// - public decimal? OrderPrice { get; set; } - /// - /// Average fill price - /// - public decimal? AveragePrice { get; set; } - /// - /// Trigger order direction - /// - public SharedTriggerOrderDirection? OrderDirection { get; set; } - /// - /// Trigger price - /// - public decimal TriggerPrice { get; set; } - /// - /// Asset the fee is in - /// - public string? FeeAsset { get; set; } - /// - /// Fee paid for the order - /// - public decimal? Fee { get; set; } - /// - /// Timestamp the order was created - /// - public DateTime? CreateTime { get; set; } - /// - /// Last update timestamp - /// - public DateTime? UpdateTime { get; set; } - /// - /// Position side for futures order - /// - public SharedPositionSide? PositionSide { get; set; } - /// - /// Client order id - /// - public string? ClientOrderId { get; set; } + public string TriggerOrderId { get; set; } + /// + /// The id of the order that was placed when this order was activated + /// + public string? PlacedOrderId { get; set; } + /// + /// The type of the order + /// + public SharedOrderType OrderType { get; set; } + /// + /// Status of the trigger order + /// + public SharedTriggerOrderStatus Status { get; set; } + /// + /// Time in force for the order + /// + public SharedTimeInForce? TimeInForce { get; set; } + /// + /// Order quantity + /// + public SharedOrderQuantity? OrderQuantity { get; set; } + /// + /// Filled quantity + /// + public SharedOrderQuantity? QuantityFilled { get; set; } + /// + /// Order price + /// + public decimal? OrderPrice { get; set; } + /// + /// Average fill price + /// + public decimal? AveragePrice { get; set; } + /// + /// Trigger order direction + /// + public SharedTriggerOrderDirection? OrderDirection { get; set; } + /// + /// Trigger price + /// + public decimal TriggerPrice { get; set; } + /// + /// Asset the fee is in + /// + public string? FeeAsset { get; set; } + /// + /// Fee paid for the order + /// + public decimal? Fee { get; set; } + /// + /// Timestamp the order was created + /// + public DateTime? CreateTime { get; set; } + /// + /// Last update timestamp + /// + public DateTime? UpdateTime { get; set; } + /// + /// Position side for futures order + /// + public SharedPositionSide? PositionSide { get; set; } + /// + /// Client order id + /// + public string? ClientOrderId { get; set; } - /// - /// ctor - /// - public SharedFuturesTriggerOrder( - SharedSymbol? sharedSymbol, - string symbol, - string triggerOrderId, - SharedOrderType orderType, - SharedTriggerOrderDirection? orderDirection, - SharedTriggerOrderStatus triggerStatus, - decimal triggerPrice, - SharedPositionSide? positionSide, - DateTime? createTime) - : base(sharedSymbol, symbol) - { - TriggerOrderId = triggerOrderId; - OrderType = orderType; - OrderDirection = orderDirection; - Status = triggerStatus; - CreateTime = createTime; - TriggerPrice = triggerPrice; - PositionSide = positionSide; - } + /// + /// ctor + /// + public SharedFuturesTriggerOrder( + SharedSymbol? sharedSymbol, + string symbol, + string triggerOrderId, + SharedOrderType orderType, + SharedTriggerOrderDirection? orderDirection, + SharedTriggerOrderStatus triggerStatus, + decimal triggerPrice, + SharedPositionSide? positionSide, + DateTime? createTime) + : base(sharedSymbol, symbol) + { + TriggerOrderId = triggerOrderId; + OrderType = orderType; + OrderDirection = orderDirection; + Status = triggerStatus; + CreateTime = createTime; + TriggerPrice = triggerPrice; + PositionSide = positionSide; } -} \ No newline at end of file +} diff --git a/CryptoExchange.Net/SharedApis/ResponseModels/SharedId.cs b/CryptoExchange.Net/SharedApis/ResponseModels/SharedId.cs index 24147b8..111135b 100644 --- a/CryptoExchange.Net/SharedApis/ResponseModels/SharedId.cs +++ b/CryptoExchange.Net/SharedApis/ResponseModels/SharedId.cs @@ -1,21 +1,20 @@ -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Id +/// +public record SharedId { + /// + /// The id + /// + public string Id { get; set; } + /// /// Id /// - public record SharedId + public SharedId(string id) { - /// - /// The id - /// - public string Id { get; set; } - - /// - /// Id - /// - public SharedId(string id) - { - Id = id; - } + Id = id; } } diff --git a/CryptoExchange.Net/SharedApis/ResponseModels/SharedKline.cs b/CryptoExchange.Net/SharedApis/ResponseModels/SharedKline.cs index 2033d22..d8d7252 100644 --- a/CryptoExchange.Net/SharedApis/ResponseModels/SharedKline.cs +++ b/CryptoExchange.Net/SharedApis/ResponseModels/SharedKline.cs @@ -1,48 +1,47 @@ -using System; +using System; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Kline info +/// +public record SharedKline { /// - /// Kline info + /// Open time /// - public record SharedKline - { - /// - /// Open time - /// - public DateTime OpenTime { get; set; } - /// - /// Close price - /// - public decimal ClosePrice { get; set; } - /// - /// High price - /// - public decimal HighPrice { get; set; } - /// - /// Low price - /// - public decimal LowPrice { get; set; } - /// - /// Open price - /// - public decimal OpenPrice { get; set; } - /// - /// Volume in the base asset - /// - public decimal Volume { get; set; } + public DateTime OpenTime { get; set; } + /// + /// Close price + /// + public decimal ClosePrice { get; set; } + /// + /// High price + /// + public decimal HighPrice { get; set; } + /// + /// Low price + /// + public decimal LowPrice { get; set; } + /// + /// Open price + /// + public decimal OpenPrice { get; set; } + /// + /// Volume in the base asset + /// + public decimal Volume { get; set; } - /// - /// ctor - /// - public SharedKline(DateTime openTime, decimal closePrice, decimal highPrice, decimal lowPrice, decimal openPrice, decimal volume) - { - OpenTime = openTime; - ClosePrice = closePrice; - HighPrice = highPrice; - LowPrice = lowPrice; - OpenPrice = openPrice; - Volume = volume; - } + /// + /// ctor + /// + public SharedKline(DateTime openTime, decimal closePrice, decimal highPrice, decimal lowPrice, decimal openPrice, decimal volume) + { + OpenTime = openTime; + ClosePrice = closePrice; + HighPrice = highPrice; + LowPrice = lowPrice; + OpenPrice = openPrice; + Volume = volume; } } diff --git a/CryptoExchange.Net/SharedApis/ResponseModels/SharedLeverage.cs b/CryptoExchange.Net/SharedApis/ResponseModels/SharedLeverage.cs index 881f22a..39395d3 100644 --- a/CryptoExchange.Net/SharedApis/ResponseModels/SharedLeverage.cs +++ b/CryptoExchange.Net/SharedApis/ResponseModels/SharedLeverage.cs @@ -1,29 +1,28 @@ -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Leverage info +/// +public record SharedLeverage { /// - /// Leverage info + /// Leverage value /// - public record SharedLeverage - { - /// - /// Leverage value - /// - public decimal Leverage { get; set; } - /// - /// Side for the leverage - /// - public SharedPositionSide? Side { get; set; } - /// - /// Margin mode - /// - public SharedMarginMode? MarginMode { get; set; } + public decimal Leverage { get; set; } + /// + /// Side for the leverage + /// + public SharedPositionSide? Side { get; set; } + /// + /// Margin mode + /// + public SharedMarginMode? MarginMode { get; set; } - /// - /// ctor - /// - public SharedLeverage(decimal leverage) - { - Leverage = leverage; - } + /// + /// ctor + /// + public SharedLeverage(decimal leverage) + { + Leverage = leverage; } } diff --git a/CryptoExchange.Net/SharedApis/ResponseModels/SharedOpenInterest.cs b/CryptoExchange.Net/SharedApis/ResponseModels/SharedOpenInterest.cs index b25b173..125b0a1 100644 --- a/CryptoExchange.Net/SharedApis/ResponseModels/SharedOpenInterest.cs +++ b/CryptoExchange.Net/SharedApis/ResponseModels/SharedOpenInterest.cs @@ -1,21 +1,20 @@ -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Open interest +/// +public record SharedOpenInterest { /// - /// Open interest + /// Current open interest /// - public record SharedOpenInterest - { - /// - /// Current open interest - /// - public decimal OpenInterest { get; set; } + public decimal OpenInterest { get; set; } - /// - /// ctor - /// - public SharedOpenInterest(decimal openInterest) - { - OpenInterest = openInterest; - } + /// + /// ctor + /// + public SharedOpenInterest(decimal openInterest) + { + OpenInterest = openInterest; } } diff --git a/CryptoExchange.Net/SharedApis/ResponseModels/SharedOrderBook.cs b/CryptoExchange.Net/SharedApis/ResponseModels/SharedOrderBook.cs index d4e85c9..7c9183e 100644 --- a/CryptoExchange.Net/SharedApis/ResponseModels/SharedOrderBook.cs +++ b/CryptoExchange.Net/SharedApis/ResponseModels/SharedOrderBook.cs @@ -1,30 +1,27 @@ -using CryptoExchange.Net.Interfaces; -using System.Collections.Generic; +using CryptoExchange.Net.Interfaces; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Order book info +/// +public record SharedOrderBook { /// - /// Order book info + /// Asks list /// - public record SharedOrderBook + public ISymbolOrderBookEntry[] Asks { get; set; } + /// + /// Bids list + /// + public ISymbolOrderBookEntry[] Bids { get; set; } + + /// + /// ctor + /// + public SharedOrderBook(ISymbolOrderBookEntry[] asks, ISymbolOrderBookEntry[] bids) { - /// - /// Asks list - /// - public ISymbolOrderBookEntry[] Asks { get; set; } - /// - /// Bids list - /// - public ISymbolOrderBookEntry[] Bids { get; set; } - - /// - /// ctor - /// - public SharedOrderBook(ISymbolOrderBookEntry[] asks, ISymbolOrderBookEntry[] bids) - { - Asks = asks; - Bids = bids; - } + Asks = asks; + Bids = bids; } - } diff --git a/CryptoExchange.Net/SharedApis/ResponseModels/SharedPosition.cs b/CryptoExchange.Net/SharedApis/ResponseModels/SharedPosition.cs index b251f71..5cf30f7 100644 --- a/CryptoExchange.Net/SharedApis/ResponseModels/SharedPosition.cs +++ b/CryptoExchange.Net/SharedApis/ResponseModels/SharedPosition.cs @@ -1,61 +1,60 @@ -using System; +using System; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Position info +/// +public record SharedPosition : SharedSymbolModel { /// - /// Position info + /// Position id /// - public record SharedPosition : SharedSymbolModel - { - /// - /// Position id - /// - public string? Id { get; set; } - /// - /// Current size of the position - /// - public decimal PositionSize { get; set; } - /// - /// Side of the position - /// - public SharedPositionSide PositionSide { get; set; } - /// - /// Average open price - /// - public decimal? AverageOpenPrice { get; set; } - /// - /// Current unrealized profit/loss - /// - public decimal? UnrealizedPnl { get; set; } - /// - /// Liquidation price - /// - public decimal? LiquidationPrice { get; set; } - /// - /// Leverage - /// - public decimal? Leverage { get; set; } - /// - /// Last update time - /// - public DateTime? UpdateTime { get; set; } - /// - /// Stop loss price for the position. Not available in all API's so might be empty even though stop loss price is set - /// - public decimal? StopLossPrice { get; set; } - /// - /// Take profit price for the position. Not available in all API's so might be empty even though stop loss price is set - /// - public decimal? TakeProfitPrice { get; set; } + public string? Id { get; set; } + /// + /// Current size of the position + /// + public decimal PositionSize { get; set; } + /// + /// Side of the position + /// + public SharedPositionSide PositionSide { get; set; } + /// + /// Average open price + /// + public decimal? AverageOpenPrice { get; set; } + /// + /// Current unrealized profit/loss + /// + public decimal? UnrealizedPnl { get; set; } + /// + /// Liquidation price + /// + public decimal? LiquidationPrice { get; set; } + /// + /// Leverage + /// + public decimal? Leverage { get; set; } + /// + /// Last update time + /// + public DateTime? UpdateTime { get; set; } + /// + /// Stop loss price for the position. Not available in all API's so might be empty even though stop loss price is set + /// + public decimal? StopLossPrice { get; set; } + /// + /// Take profit price for the position. Not available in all API's so might be empty even though stop loss price is set + /// + public decimal? TakeProfitPrice { get; set; } - /// - /// ctor - /// - public SharedPosition(SharedSymbol? sharedSymbol, string symbol, decimal positionSize, DateTime? updateTime) - : base(sharedSymbol, symbol) - { - PositionSize = positionSize; - UpdateTime = updateTime; - } + /// + /// ctor + /// + public SharedPosition(SharedSymbol? sharedSymbol, string symbol, decimal positionSize, DateTime? updateTime) + : base(sharedSymbol, symbol) + { + PositionSize = positionSize; + UpdateTime = updateTime; } } diff --git a/CryptoExchange.Net/SharedApis/ResponseModels/SharedPositionHistory.cs b/CryptoExchange.Net/SharedApis/ResponseModels/SharedPositionHistory.cs index e4f1261..7c5a9d8 100644 --- a/CryptoExchange.Net/SharedApis/ResponseModels/SharedPositionHistory.cs +++ b/CryptoExchange.Net/SharedApis/ResponseModels/SharedPositionHistory.cs @@ -1,69 +1,68 @@ -using System; +using System; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Position history +/// +public record SharedPositionHistory : SharedSymbolModel { /// - /// Position history + /// The side of the position /// - public record SharedPositionHistory : SharedSymbolModel - { - /// - /// The side of the position - /// - public SharedPositionSide PositionSide { get; set; } - /// - /// Average open price - /// - public decimal AverageOpenPrice { get; set; } - /// - /// Average close price - /// - public decimal AverageClosePrice { get; set; } - /// - /// Position size - /// - public decimal Quantity { get; set; } - /// - /// Realized profit/loss - /// - public decimal RealizedPnl { get; set; } - /// - /// Timestamp the position was closed - /// - public DateTime Timestamp { get; set; } - /// - /// Position id - /// - public string? PositionId { get; set; } - /// - /// Leverage of the position - /// - public decimal? Leverage { get; set; } - /// - /// Id of the order that closed the position - /// - public string? OrderId { get; set; } + public SharedPositionSide PositionSide { get; set; } + /// + /// Average open price + /// + public decimal AverageOpenPrice { get; set; } + /// + /// Average close price + /// + public decimal AverageClosePrice { get; set; } + /// + /// Position size + /// + public decimal Quantity { get; set; } + /// + /// Realized profit/loss + /// + public decimal RealizedPnl { get; set; } + /// + /// Timestamp the position was closed + /// + public DateTime Timestamp { get; set; } + /// + /// Position id + /// + public string? PositionId { get; set; } + /// + /// Leverage of the position + /// + public decimal? Leverage { get; set; } + /// + /// Id of the order that closed the position + /// + public string? OrderId { get; set; } - /// - /// ctor - /// - public SharedPositionHistory( - SharedSymbol? sharedSymbol, - string symbol, - SharedPositionSide side, - decimal openPrice, - decimal closePrice, - decimal quantity, - decimal realizedPnl, - DateTime timestamp) - : base(sharedSymbol, symbol) - { - PositionSide = side; - AverageOpenPrice = openPrice; - AverageClosePrice = closePrice; - Quantity = quantity; - RealizedPnl = realizedPnl; - Timestamp = timestamp; - } + /// + /// ctor + /// + public SharedPositionHistory( + SharedSymbol? sharedSymbol, + string symbol, + SharedPositionSide side, + decimal openPrice, + decimal closePrice, + decimal quantity, + decimal realizedPnl, + DateTime timestamp) + : base(sharedSymbol, symbol) + { + PositionSide = side; + AverageOpenPrice = openPrice; + AverageClosePrice = closePrice; + Quantity = quantity; + RealizedPnl = realizedPnl; + Timestamp = timestamp; } } diff --git a/CryptoExchange.Net/SharedApis/ResponseModels/SharedPositionModeResult.cs b/CryptoExchange.Net/SharedApis/ResponseModels/SharedPositionModeResult.cs index eba0c4e..f506c80 100644 --- a/CryptoExchange.Net/SharedApis/ResponseModels/SharedPositionModeResult.cs +++ b/CryptoExchange.Net/SharedApis/ResponseModels/SharedPositionModeResult.cs @@ -1,21 +1,20 @@ -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Position mode result +/// +public record SharedPositionModeResult { /// - /// Position mode result + /// The current position mode /// - public record SharedPositionModeResult - { - /// - /// The current position mode - /// - public SharedPositionMode PositionMode { get; set; } + public SharedPositionMode PositionMode { get; set; } - /// - /// ctor - /// - public SharedPositionModeResult(SharedPositionMode positionMode) - { - PositionMode = positionMode; - } + /// + /// ctor + /// + public SharedPositionModeResult(SharedPositionMode positionMode) + { + PositionMode = positionMode; } } diff --git a/CryptoExchange.Net/SharedApis/ResponseModels/SharedSpotOrder.cs b/CryptoExchange.Net/SharedApis/ResponseModels/SharedSpotOrder.cs index 577232f..e1719c0 100644 --- a/CryptoExchange.Net/SharedApis/ResponseModels/SharedSpotOrder.cs +++ b/CryptoExchange.Net/SharedApis/ResponseModels/SharedSpotOrder.cs @@ -1,100 +1,99 @@ -using System; +using System; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Spot order info +/// +public record SharedSpotOrder : SharedSymbolModel { /// - /// Spot order info + /// The id of the order /// - public record SharedSpotOrder : SharedSymbolModel + public string OrderId { get; set; } + /// + /// The type of the order + /// + public SharedOrderType OrderType { get; set; } + /// + /// The side of the order + /// + public SharedOrderSide Side { get; set; } + /// + /// Status of the order + /// + public SharedOrderStatus Status { get; set; } + /// + /// Time in force for the order + /// + public SharedTimeInForce? TimeInForce { get; set; } + /// + /// Order quantity + /// + public SharedOrderQuantity? OrderQuantity { get; set; } + /// + /// Filled quantity + /// + public SharedOrderQuantity? QuantityFilled { get; set; } + /// + /// Order price + /// + public decimal? OrderPrice { get; set; } + /// + /// Average fill price + /// + public decimal? AveragePrice { get; set; } + /// + /// Client order id + /// + public string? ClientOrderId { get; set; } + /// + /// Asset the fee is in + /// + public string? FeeAsset { get; set; } + /// + /// Fee paid for the order + /// + public decimal? Fee { get; set; } + /// + /// Timestamp the order was created + /// + public DateTime? CreateTime { get; set; } + /// + /// Last update timestamp + /// + public DateTime? UpdateTime { get; set; } + /// + /// Last trade info, only available for websocket order updates if the API provides this data in the update + /// + public SharedUserTrade? LastTrade { get; set; } + + /// + /// Trigger price for a trigger order + /// + public decimal? TriggerPrice { get; set; } + /// + /// Whether or not the is order is a trigger order + /// + public bool IsTriggerOrder { get; set; } + + /// + /// ctor + /// + public SharedSpotOrder( + SharedSymbol? sharedSymbol, + string symbol, + string orderId, + SharedOrderType orderType, + SharedOrderSide orderSide, + SharedOrderStatus orderStatus, + DateTime? createTime) + : base(sharedSymbol, symbol) { - /// - /// The id of the order - /// - public string OrderId { get; set; } - /// - /// The type of the order - /// - public SharedOrderType OrderType { get; set; } - /// - /// The side of the order - /// - public SharedOrderSide Side { get; set; } - /// - /// Status of the order - /// - public SharedOrderStatus Status { get; set; } - /// - /// Time in force for the order - /// - public SharedTimeInForce? TimeInForce { get; set; } - /// - /// Order quantity - /// - public SharedOrderQuantity? OrderQuantity { get; set; } - /// - /// Filled quantity - /// - public SharedOrderQuantity? QuantityFilled { get; set; } - /// - /// Order price - /// - public decimal? OrderPrice { get; set; } - /// - /// Average fill price - /// - public decimal? AveragePrice { get; set; } - /// - /// Client order id - /// - public string? ClientOrderId { get; set; } - /// - /// Asset the fee is in - /// - public string? FeeAsset { get; set; } - /// - /// Fee paid for the order - /// - public decimal? Fee { get; set; } - /// - /// Timestamp the order was created - /// - public DateTime? CreateTime { get; set; } - /// - /// Last update timestamp - /// - public DateTime? UpdateTime { get; set; } - /// - /// Last trade info, only available for websocket order updates if the API provides this data in the update - /// - public SharedUserTrade? LastTrade { get; set; } - - /// - /// Trigger price for a trigger order - /// - public decimal? TriggerPrice { get; set; } - /// - /// Whether or not the is order is a trigger order - /// - public bool IsTriggerOrder { get; set; } - - /// - /// ctor - /// - public SharedSpotOrder( - SharedSymbol? sharedSymbol, - string symbol, - string orderId, - SharedOrderType orderType, - SharedOrderSide orderSide, - SharedOrderStatus orderStatus, - DateTime? createTime) - : base(sharedSymbol, symbol) - { - OrderId = orderId; - OrderType = orderType; - Side = orderSide; - Status = orderStatus; - CreateTime = createTime; - } + OrderId = orderId; + OrderType = orderType; + Side = orderSide; + Status = orderStatus; + CreateTime = createTime; } } diff --git a/CryptoExchange.Net/SharedApis/ResponseModels/SharedSpotSymbol.cs b/CryptoExchange.Net/SharedApis/ResponseModels/SharedSpotSymbol.cs index ef8a9bd..29e0aed 100644 --- a/CryptoExchange.Net/SharedApis/ResponseModels/SharedSpotSymbol.cs +++ b/CryptoExchange.Net/SharedApis/ResponseModels/SharedSpotSymbol.cs @@ -1,79 +1,78 @@ -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Symbol info +/// +public record SharedSpotSymbol { /// - /// Symbol info + /// The trading mode of the symbol /// - public record SharedSpotSymbol + public TradingMode TradingMode { get; set; } + /// + /// Base asset of the symbol + /// + public string BaseAsset { get; set; } + /// + /// Quote asset of the symbol + /// + public string QuoteAsset { get; set; } + /// + /// The name of the symbol + /// + public string Name { get; set; } + /// + /// Minimal quantity of an order in the base asset + /// + public decimal? MinTradeQuantity { get; set; } + /// + /// Minimal notional value (quantity * price) of an order + /// + public decimal? MinNotionalValue { get; set; } + /// + /// Max quantity of an order in the base asset + /// + public decimal? MaxTradeQuantity { get; set; } + /// + /// Step by which the quantity should increase + /// + public decimal? QuantityStep { get; set; } + /// + /// Step by which the price should increase + /// + public decimal? PriceStep { get; set; } + /// + /// The max amount of decimal places for quantity + /// + public int? QuantityDecimals { get; set; } + /// + /// The max amount of decimal places for price + /// + public int? PriceDecimals { get; set; } + /// + /// The max amount of significant figures to use for price. For example with value of 5 these values are valid: 0.00001, 0.12300, 123.53, 12345, but this is not: 12345.1 + /// + public int? PriceSignificantFigures { get; set; } + /// + /// Whether the symbol is currently available for trading + /// + public bool Trading { get; set; } + + /// + /// ctor + /// + public SharedSpotSymbol(string baseAsset, string quoteAsset, string symbol, bool trading, TradingMode? tradingMode = null) { - /// - /// The trading mode of the symbol - /// - public TradingMode TradingMode { get; set; } - /// - /// Base asset of the symbol - /// - public string BaseAsset { get; set; } - /// - /// Quote asset of the symbol - /// - public string QuoteAsset { get; set; } - /// - /// The name of the symbol - /// - public string Name { get; set; } - /// - /// Minimal quantity of an order in the base asset - /// - public decimal? MinTradeQuantity { get; set; } - /// - /// Minimal notional value (quantity * price) of an order - /// - public decimal? MinNotionalValue { get; set; } - /// - /// Max quantity of an order in the base asset - /// - public decimal? MaxTradeQuantity { get; set; } - /// - /// Step by which the quantity should increase - /// - public decimal? QuantityStep { get; set; } - /// - /// Step by which the price should increase - /// - public decimal? PriceStep { get; set; } - /// - /// The max amount of decimal places for quantity - /// - public int? QuantityDecimals { get; set; } - /// - /// The max amount of decimal places for price - /// - public int? PriceDecimals { get; set; } - /// - /// The max amount of significant figures to use for price. For example with value of 5 these values are valid: 0.00001, 0.12300, 123.53, 12345, but this is not: 12345.1 - /// - public int? PriceSignificantFigures { get; set; } - /// - /// Whether the symbol is currently available for trading - /// - public bool Trading { get; set; } - - /// - /// ctor - /// - public SharedSpotSymbol(string baseAsset, string quoteAsset, string symbol, bool trading, TradingMode? tradingMode = null) - { - TradingMode = tradingMode ?? TradingMode.Spot; - BaseAsset = baseAsset; - QuoteAsset = quoteAsset; - Name = symbol; - Trading = trading; - } - - /// - /// The SharedSymbol of this symbol - /// - /// - public virtual SharedSymbol SharedSymbol => new SharedSymbol(TradingMode, BaseAsset.ToUpperInvariant(), QuoteAsset.ToUpperInvariant()) { SymbolName = Name }; + TradingMode = tradingMode ?? TradingMode.Spot; + BaseAsset = baseAsset; + QuoteAsset = quoteAsset; + Name = symbol; + Trading = trading; } + + /// + /// The SharedSymbol of this symbol + /// + /// + public virtual SharedSymbol SharedSymbol => new SharedSymbol(TradingMode, BaseAsset.ToUpperInvariant(), QuoteAsset.ToUpperInvariant()) { SymbolName = Name }; } diff --git a/CryptoExchange.Net/SharedApis/ResponseModels/SharedSpotTicker.cs b/CryptoExchange.Net/SharedApis/ResponseModels/SharedSpotTicker.cs index c19ee01..2f06d21 100644 --- a/CryptoExchange.Net/SharedApis/ResponseModels/SharedSpotTicker.cs +++ b/CryptoExchange.Net/SharedApis/ResponseModels/SharedSpotTicker.cs @@ -1,46 +1,45 @@ -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Ticker info +/// +public record SharedSpotTicker: SharedSymbolModel { /// - /// Ticker info + /// Last trade price /// - public record SharedSpotTicker: SharedSymbolModel - { - /// - /// Last trade price - /// - public decimal? LastPrice { get; set; } - /// - /// Highest price in last 24h - /// - public decimal? HighPrice { get; set; } - /// - /// Lowest price in last 24h - /// - public decimal? LowPrice { get; set; } - /// - /// Trade volume in base asset in the last 24h - /// - public decimal Volume { get; set; } - /// - /// Trade volume in quote asset in the last 24h - /// - public decimal? QuoteVolume { get; set; } - /// - /// Change percentage in the last 24h - /// - public decimal? ChangePercentage { get; set; } + public decimal? LastPrice { get; set; } + /// + /// Highest price in last 24h + /// + public decimal? HighPrice { get; set; } + /// + /// Lowest price in last 24h + /// + public decimal? LowPrice { get; set; } + /// + /// Trade volume in base asset in the last 24h + /// + public decimal Volume { get; set; } + /// + /// Trade volume in quote asset in the last 24h + /// + public decimal? QuoteVolume { get; set; } + /// + /// Change percentage in the last 24h + /// + public decimal? ChangePercentage { get; set; } - /// - /// ctor - /// - public SharedSpotTicker(SharedSymbol? sharedSymbol, string symbol, decimal? lastPrice, decimal? highPrice, decimal? lowPrice, decimal volume, decimal? changePercentage) - : base(sharedSymbol, symbol) - { - LastPrice = lastPrice; - HighPrice = highPrice; - LowPrice = lowPrice; - Volume = volume; - ChangePercentage = changePercentage; - } + /// + /// ctor + /// + public SharedSpotTicker(SharedSymbol? sharedSymbol, string symbol, decimal? lastPrice, decimal? highPrice, decimal? lowPrice, decimal volume, decimal? changePercentage) + : base(sharedSymbol, symbol) + { + LastPrice = lastPrice; + HighPrice = highPrice; + LowPrice = lowPrice; + Volume = volume; + ChangePercentage = changePercentage; } } diff --git a/CryptoExchange.Net/SharedApis/ResponseModels/SharedSpotTriggerOrder.cs b/CryptoExchange.Net/SharedApis/ResponseModels/SharedSpotTriggerOrder.cs index fedade0..2e6be31 100644 --- a/CryptoExchange.Net/SharedApis/ResponseModels/SharedSpotTriggerOrder.cs +++ b/CryptoExchange.Net/SharedApis/ResponseModels/SharedSpotTriggerOrder.cs @@ -1,99 +1,96 @@ -using System; -using System.Collections.Generic; -using System.Text; +using System; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Trigger order info +/// +public record SharedSpotTriggerOrder : SharedSymbolModel { /// - /// Trigger order info + /// The id of the trigger order /// - public record SharedSpotTriggerOrder : SharedSymbolModel - { - /// - /// The id of the trigger order - /// - public string TriggerOrderId { get; set; } - /// - /// The id of the order that was placed when this order was activated - /// - public string? PlacedOrderId { get; set; } - /// - /// The type of the order - /// - public SharedOrderType OrderType { get; set; } - /// - /// Status of the trigger order - /// - public SharedTriggerOrderStatus Status { get; set; } - /// - /// Time in force for the order - /// - public SharedTimeInForce? TimeInForce { get; set; } - /// - /// Order quantity - /// - public SharedOrderQuantity? OrderQuantity { get; set; } - /// - /// Filled quantity - /// - public SharedOrderQuantity? QuantityFilled { get; set; } - /// - /// Order price - /// - public decimal? OrderPrice { get; set; } - /// - /// Average fill price - /// - public decimal? AveragePrice { get; set; } - /// - /// Trigger order direction - /// - public SharedTriggerOrderDirection OrderDirection { get; set; } - /// - /// Trigger price - /// - public decimal TriggerPrice { get; set; } - /// - /// Asset the fee is in - /// - public string? FeeAsset { get; set; } - /// - /// Fee paid for the order - /// - public decimal? Fee { get; set; } - /// - /// Timestamp the order was created - /// - public DateTime? CreateTime { get; set; } - /// - /// Last update timestamp - /// - public DateTime? UpdateTime { get; set; } - /// - /// Client order id - /// - public string? ClientOrderId { get; set; } + public string TriggerOrderId { get; set; } + /// + /// The id of the order that was placed when this order was activated + /// + public string? PlacedOrderId { get; set; } + /// + /// The type of the order + /// + public SharedOrderType OrderType { get; set; } + /// + /// Status of the trigger order + /// + public SharedTriggerOrderStatus Status { get; set; } + /// + /// Time in force for the order + /// + public SharedTimeInForce? TimeInForce { get; set; } + /// + /// Order quantity + /// + public SharedOrderQuantity? OrderQuantity { get; set; } + /// + /// Filled quantity + /// + public SharedOrderQuantity? QuantityFilled { get; set; } + /// + /// Order price + /// + public decimal? OrderPrice { get; set; } + /// + /// Average fill price + /// + public decimal? AveragePrice { get; set; } + /// + /// Trigger order direction + /// + public SharedTriggerOrderDirection OrderDirection { get; set; } + /// + /// Trigger price + /// + public decimal TriggerPrice { get; set; } + /// + /// Asset the fee is in + /// + public string? FeeAsset { get; set; } + /// + /// Fee paid for the order + /// + public decimal? Fee { get; set; } + /// + /// Timestamp the order was created + /// + public DateTime? CreateTime { get; set; } + /// + /// Last update timestamp + /// + public DateTime? UpdateTime { get; set; } + /// + /// Client order id + /// + public string? ClientOrderId { get; set; } - /// - /// ctor - /// - public SharedSpotTriggerOrder( - SharedSymbol? sharedSymbol, - string symbol, - string triggerOrderId, - SharedOrderType orderType, - SharedTriggerOrderDirection orderDirection, - SharedTriggerOrderStatus triggerStatus, - decimal triggerPrice, - DateTime? createTime) - : base(sharedSymbol, symbol) - { - TriggerOrderId = triggerOrderId; - OrderType = orderType; - OrderDirection = orderDirection; - Status = triggerStatus; - CreateTime = createTime; - TriggerPrice = triggerPrice; - } + /// + /// ctor + /// + public SharedSpotTriggerOrder( + SharedSymbol? sharedSymbol, + string symbol, + string triggerOrderId, + SharedOrderType orderType, + SharedTriggerOrderDirection orderDirection, + SharedTriggerOrderStatus triggerStatus, + decimal triggerPrice, + DateTime? createTime) + : base(sharedSymbol, symbol) + { + TriggerOrderId = triggerOrderId; + OrderType = orderType; + OrderDirection = orderDirection; + Status = triggerStatus; + CreateTime = createTime; + TriggerPrice = triggerPrice; } -} \ No newline at end of file +} diff --git a/CryptoExchange.Net/SharedApis/ResponseModels/SharedSymbolModel.cs b/CryptoExchange.Net/SharedApis/ResponseModels/SharedSymbolModel.cs index d15849f..ce4e0a7 100644 --- a/CryptoExchange.Net/SharedApis/ResponseModels/SharedSymbolModel.cs +++ b/CryptoExchange.Net/SharedApis/ResponseModels/SharedSymbolModel.cs @@ -1,30 +1,25 @@ -using System; -using System.Collections.Generic; -using System.Text; +namespace CryptoExchange.Net.SharedApis; -namespace CryptoExchange.Net.SharedApis +/// +/// Symbol model +/// +public record SharedSymbolModel { /// - /// Symbol model + /// SharedSymbol, only filled when the related GetSpotSymbolsAsync or GetFuturesSymbolsAsync method has been called previously /// - public record SharedSymbolModel - { - /// - /// SharedSymbol, only filled when the related GetSpotSymbolsAsync or GetFuturesSymbolsAsync method has been called previously - /// - public SharedSymbol? SharedSymbol { get; set; } - /// - /// Symbol name - /// - public string Symbol { get; set; } + public SharedSymbol? SharedSymbol { get; set; } + /// + /// Symbol name + /// + public string Symbol { get; set; } - /// - /// ctor - /// - public SharedSymbolModel(SharedSymbol? sharedSymbol, string symbol) - { - Symbol = symbol; - SharedSymbol = sharedSymbol; - } + /// + /// ctor + /// + public SharedSymbolModel(SharedSymbol? sharedSymbol, string symbol) + { + Symbol = symbol; + SharedSymbol = sharedSymbol; } } diff --git a/CryptoExchange.Net/SharedApis/ResponseModels/SharedTrade.cs b/CryptoExchange.Net/SharedApis/ResponseModels/SharedTrade.cs index 4355b45..5e4b48a 100644 --- a/CryptoExchange.Net/SharedApis/ResponseModels/SharedTrade.cs +++ b/CryptoExchange.Net/SharedApis/ResponseModels/SharedTrade.cs @@ -1,37 +1,36 @@ -using System; +using System; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Public trade info +/// +public record SharedTrade { /// - /// Public trade info + /// Quantity of the trade /// - public record SharedTrade - { - /// - /// Quantity of the trade - /// - public decimal Quantity { get; set; } - /// - /// Price of the trade - /// - public decimal Price { get; set; } - /// - /// Trade time - /// - public DateTime Timestamp { get; set; } - /// - /// Trade side. Buy means that the taker took an ask order of the order book, sell means the taker took a bid order of the order book. - /// - public SharedOrderSide? Side { get; set; } + public decimal Quantity { get; set; } + /// + /// Price of the trade + /// + public decimal Price { get; set; } + /// + /// Trade time + /// + public DateTime Timestamp { get; set; } + /// + /// Trade side. Buy means that the taker took an ask order of the order book, sell means the taker took a bid order of the order book. + /// + public SharedOrderSide? Side { get; set; } - /// - /// ctor - /// - public SharedTrade(decimal quantity, decimal price, DateTime timestamp) - { - Quantity = quantity; - Price = price; - Timestamp = timestamp; - } + /// + /// ctor + /// + public SharedTrade(decimal quantity, decimal price, DateTime timestamp) + { + Quantity = quantity; + Price = price; + Timestamp = timestamp; } } diff --git a/CryptoExchange.Net/SharedApis/ResponseModels/SharedUserTrade.cs b/CryptoExchange.Net/SharedApis/ResponseModels/SharedUserTrade.cs index 1791ce6..69848dd 100644 --- a/CryptoExchange.Net/SharedApis/ResponseModels/SharedUserTrade.cs +++ b/CryptoExchange.Net/SharedApis/ResponseModels/SharedUserTrade.cs @@ -1,62 +1,61 @@ -using System; +using System; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// A user trade +/// +public record SharedUserTrade : SharedSymbolModel { /// - /// A user trade + /// The trade id /// - public record SharedUserTrade : SharedSymbolModel - { - /// - /// The trade id - /// - public string Id { get; set; } - /// - /// Traded quantity - /// - public decimal Quantity { get; set; } - /// - /// Trade price - /// - public decimal Price { get; set; } - /// - /// Trade timestamp - /// - public DateTime Timestamp { get; set; } - /// - /// The order id - /// - public string OrderId { get; set; } - /// - /// Side of the trade - /// - public SharedOrderSide? Side { get; set; } - /// - /// Fee paid for the trade - /// - public decimal? Fee { get; set; } - /// - /// The asset the fee is in - /// - public string? FeeAsset { get; set; } - /// - /// Trade role - /// - public SharedRole? Role { get; set; } + public string Id { get; set; } + /// + /// Traded quantity + /// + public decimal Quantity { get; set; } + /// + /// Trade price + /// + public decimal Price { get; set; } + /// + /// Trade timestamp + /// + public DateTime Timestamp { get; set; } + /// + /// The order id + /// + public string OrderId { get; set; } + /// + /// Side of the trade + /// + public SharedOrderSide? Side { get; set; } + /// + /// Fee paid for the trade + /// + public decimal? Fee { get; set; } + /// + /// The asset the fee is in + /// + public string? FeeAsset { get; set; } + /// + /// Trade role + /// + public SharedRole? Role { get; set; } - /// - /// ctor - /// - public SharedUserTrade(SharedSymbol? sharedSymbol, string symbol, string orderId, string id, SharedOrderSide? side, decimal quantity, decimal price, DateTime timestamp) - : base(sharedSymbol, symbol) - { - Symbol = symbol; - OrderId = orderId; - Id = id; - Side = side; - Quantity = quantity; - Price = price; - Timestamp = timestamp; - } + /// + /// ctor + /// + public SharedUserTrade(SharedSymbol? sharedSymbol, string symbol, string orderId, string id, SharedOrderSide? side, decimal quantity, decimal price, DateTime timestamp) + : base(sharedSymbol, symbol) + { + Symbol = symbol; + OrderId = orderId; + Id = id; + Side = side; + Quantity = quantity; + Price = price; + Timestamp = timestamp; } } diff --git a/CryptoExchange.Net/SharedApis/ResponseModels/SharedWithdrawal.cs b/CryptoExchange.Net/SharedApis/ResponseModels/SharedWithdrawal.cs index 190b359..42d7a1f 100644 --- a/CryptoExchange.Net/SharedApis/ResponseModels/SharedWithdrawal.cs +++ b/CryptoExchange.Net/SharedApis/ResponseModels/SharedWithdrawal.cs @@ -1,68 +1,66 @@ -using System; +using System; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// A withdrawal record +/// +public record SharedWithdrawal { /// - /// A withdrawal record + /// The id of the withdrawal /// - public record SharedWithdrawal + public string? Id { get; set; } + /// + /// The asset that was withdrawn + /// + public string Asset { get; set; } + /// + /// The withdrawal address + /// + public string Address { get; set; } + /// + /// Quantity that was withdrawn + /// + public decimal Quantity { get; set; } + /// + /// The timestamp of the withdrawal + /// + public DateTime Timestamp { get; set; } + /// + /// Whether the withdrawal was successfully completed + /// + public bool Completed { get; set; } + /// + /// Tag + /// + public string? Tag { get; set; } + /// + /// Used network + /// + public string? Network { get; set; } + /// + /// Transaction id + /// + public string? TransactionId { get; set; } + /// + /// Number of confirmations + /// + public int? Confirmations { get; set; } + /// + /// Fee paid + /// + public decimal? Fee { get; set; } + + /// + /// ctor + /// + public SharedWithdrawal(string asset, string address, decimal quantity, bool completed, DateTime timestamp) { - /// - /// The id of the withdrawal - /// - public string? Id { get; set; } - /// - /// The asset that was withdrawn - /// - public string Asset { get; set; } - /// - /// The withdrawal address - /// - public string Address { get; set; } - /// - /// Quantity that was withdrawn - /// - public decimal Quantity { get; set; } - /// - /// The timestamp of the withdrawal - /// - public DateTime Timestamp { get; set; } - /// - /// Whether the withdrawal was successfully completed - /// - public bool Completed { get; set; } - /// - /// Tag - /// - public string? Tag { get; set; } - /// - /// Used network - /// - public string? Network { get; set; } - /// - /// Transaction id - /// - public string? TransactionId { get; set; } - /// - /// Number of confirmations - /// - public int? Confirmations { get; set; } - /// - /// Fee paid - /// - public decimal? Fee { get; set; } - - /// - /// ctor - /// - public SharedWithdrawal(string asset, string address, decimal quantity, bool completed, DateTime timestamp) - { - Asset = asset; - Address = address; - Quantity = quantity; - Completed = completed; - Timestamp = timestamp; - } + Asset = asset; + Address = address; + Quantity = quantity; + Completed = completed; + Timestamp = timestamp; } - } diff --git a/CryptoExchange.Net/SharedApis/SharedQuantity.cs b/CryptoExchange.Net/SharedApis/SharedQuantity.cs index 722c1b7..ac84bc2 100644 --- a/CryptoExchange.Net/SharedApis/SharedQuantity.cs +++ b/CryptoExchange.Net/SharedApis/SharedQuantity.cs @@ -1,125 +1,121 @@ -using CryptoExchange.Net.Converters.SystemTextJson; -using System; -using System.Collections.Generic; -using System.Text; +using CryptoExchange.Net.Converters.SystemTextJson; using System.Text.Json.Serialization; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// Quantity reference +/// +public record SharedQuantityReference { /// - /// Quantity reference + /// Quantity denoted in the base asset of the symbol /// - public record SharedQuantityReference - { - /// - /// Quantity denoted in the base asset of the symbol - /// - public decimal? QuantityInBaseAsset { get; set; } - /// - /// Quantity denoted in the quote asset of the symbol - /// - public decimal? QuantityInQuoteAsset { get; set; } - /// - /// Quantity denoted in the number of contracts - /// - public decimal? QuantityInContracts { get; set; } - - /// - /// ctor - /// - internal SharedQuantityReference(decimal? baseAssetQuantity, decimal? quoteAssetQuantity, decimal? contractQuantity) - { - QuantityInBaseAsset = baseAssetQuantity; - QuantityInQuoteAsset = quoteAssetQuantity; - QuantityInContracts = contractQuantity; - } - } + public decimal? QuantityInBaseAsset { get; set; } + /// + /// Quantity denoted in the quote asset of the symbol + /// + public decimal? QuantityInQuoteAsset { get; set; } + /// + /// Quantity denoted in the number of contracts + /// + public decimal? QuantityInContracts { get; set; } /// - /// Quantity for an order + /// ctor /// - [JsonConverter(typeof(SharedQuantityConverter))] - public record SharedQuantity : SharedQuantityReference + internal SharedQuantityReference(decimal? baseAssetQuantity, decimal? quoteAssetQuantity, decimal? contractQuantity) + { + QuantityInBaseAsset = baseAssetQuantity; + QuantityInQuoteAsset = quoteAssetQuantity; + QuantityInContracts = contractQuantity; + } +} + +/// +/// Quantity for an order +/// +[JsonConverter(typeof(SharedQuantityConverter))] +public record SharedQuantity : SharedQuantityReference +{ + private SharedQuantity(decimal? baseAssetQuantity, decimal? quoteAssetQuantity, decimal? contractQuantity) + : base(baseAssetQuantity, quoteAssetQuantity, contractQuantity) + { + } + + /// + /// ctor + /// + public SharedQuantity() : base(null, null, null) { } + + /// + /// Specify quantity in base asset + /// + public static SharedQuantity Base(decimal quantity) => new SharedQuantity(quantity, null, null); + /// + /// Specify quantity in quote asset + /// + public static SharedQuantity Quote(decimal quantity) => new SharedQuantity(null, quantity, null); + /// + /// Specify quantity in number of contracts + /// + public static SharedQuantity Contracts(decimal quantity) => new SharedQuantity(null, null, quantity); + + /// + /// Get the base asset quantity from a quote quantity using a price + /// + /// Quantity in quote asset to convert + /// Price to use for conversion + /// The max number of decimal places for the result + /// The lot size (step per quantity) for the base asset + public static SharedQuantity BaseFromQuote(decimal quoteQuantity, decimal price, int decimalPlaces = 8, decimal lotSize = 0.00000001m) + => new SharedQuantity(ExchangeHelpers.ApplyRules(quoteQuantity / price, decimalPlaces, lotSize), null, null); + /// + /// Get the quote asset quantity from a base quantity using a price + /// + /// Quantity in base asset to convert + /// Price to use for conversion + /// The max number of decimal places for the result + /// The lot size (step per quantity) for the quote asset + public static SharedQuantity QuoteFromBase(decimal baseQuantity, decimal price, int decimalPlaces = 8, decimal lotSize = 0.00000001m) + => new SharedQuantity(ExchangeHelpers.ApplyRules(baseQuantity * price, decimalPlaces, lotSize), null, null); + /// + /// Get a quantity in number of contracts from a base asset + /// + /// Quantity in base asset to convert + /// The contract size of a single contract + /// The max number of decimal places for the result + /// The lot size (step per quantity) for the contract + public static SharedQuantity ContractsFromBase(decimal baseQuantity, decimal contractSize, int decimalPlaces = 8, decimal lotSize = 0.00000001m) + => new SharedQuantity(ExchangeHelpers.ApplyRules(baseQuantity / contractSize, decimalPlaces, lotSize), null, null); + /// + /// Get a quantity in number of contracts from a quote asset + /// + /// Quantity in quote asset to convert + /// The contract size of a single contract + /// The price to use for conversion + /// The max number of decimal places for the result + /// The lot size (step per quantity) for the contract + public static SharedQuantity ContractsFromQuote(decimal quoteQuantity, decimal contractSize, decimal price, int decimalPlaces = 8, decimal lotSize = 0.00000001m) + => new SharedQuantity(ExchangeHelpers.ApplyRules(quoteQuantity / price / contractSize, decimalPlaces, lotSize), null, null); +} + +/// +/// Order quantity +/// +[JsonConverter(typeof(SharedOrderQuantityConverter))] +public record SharedOrderQuantity : SharedQuantityReference +{ + /// + /// ctor + /// + public SharedOrderQuantity(): base(null, null,null) { } + + /// + /// ctor + /// + public SharedOrderQuantity(decimal? baseAssetQuantity = null, decimal? quoteAssetQuantity = null, decimal? contractQuantity = null) + : base(baseAssetQuantity, quoteAssetQuantity, contractQuantity) { - private SharedQuantity(decimal? baseAssetQuantity, decimal? quoteAssetQuantity, decimal? contractQuantity) - : base(baseAssetQuantity, quoteAssetQuantity, contractQuantity) - { - } - - /// - /// ctor - /// - public SharedQuantity() : base(null, null, null) { } - - /// - /// Specify quantity in base asset - /// - public static SharedQuantity Base(decimal quantity) => new SharedQuantity(quantity, null, null); - /// - /// Specify quantity in quote asset - /// - public static SharedQuantity Quote(decimal quantity) => new SharedQuantity(null, quantity, null); - /// - /// Specify quantity in number of contracts - /// - public static SharedQuantity Contracts(decimal quantity) => new SharedQuantity(null, null, quantity); - - /// - /// Get the base asset quantity from a quote quantity using a price - /// - /// Quantity in quote asset to convert - /// Price to use for conversion - /// The max number of decimal places for the result - /// The lot size (step per quantity) for the base asset - public static SharedQuantity BaseFromQuote(decimal quoteQuantity, decimal price, int decimalPlaces = 8, decimal lotSize = 0.00000001m) - => new SharedQuantity(ExchangeHelpers.ApplyRules(quoteQuantity / price, decimalPlaces, lotSize), null, null); - /// - /// Get the quote asset quantity from a base quantity using a price - /// - /// Quantity in base asset to convert - /// Price to use for conversion - /// The max number of decimal places for the result - /// The lot size (step per quantity) for the quote asset - public static SharedQuantity QuoteFromBase(decimal baseQuantity, decimal price, int decimalPlaces = 8, decimal lotSize = 0.00000001m) - => new SharedQuantity(ExchangeHelpers.ApplyRules(baseQuantity * price, decimalPlaces, lotSize), null, null); - /// - /// Get a quantity in number of contracts from a base asset - /// - /// Quantity in base asset to convert - /// The contract size of a single contract - /// The max number of decimal places for the result - /// The lot size (step per quantity) for the contract - public static SharedQuantity ContractsFromBase(decimal baseQuantity, decimal contractSize, int decimalPlaces = 8, decimal lotSize = 0.00000001m) - => new SharedQuantity(ExchangeHelpers.ApplyRules(baseQuantity / contractSize, decimalPlaces, lotSize), null, null); - /// - /// Get a quantity in number of contracts from a quote asset - /// - /// Quantity in quote asset to convert - /// The contract size of a single contract - /// The price to use for conversion - /// The max number of decimal places for the result - /// The lot size (step per quantity) for the contract - public static SharedQuantity ContractsFromQuote(decimal quoteQuantity, decimal contractSize, decimal price, int decimalPlaces = 8, decimal lotSize = 0.00000001m) - => new SharedQuantity(ExchangeHelpers.ApplyRules(quoteQuantity / price / contractSize, decimalPlaces, lotSize), null, null); - } - - /// - /// Order quantity - /// - [JsonConverter(typeof(SharedOrderQuantityConverter))] - public record SharedOrderQuantity : SharedQuantityReference - { - /// - /// ctor - /// - public SharedOrderQuantity(): base(null, null,null) { } - - /// - /// ctor - /// - public SharedOrderQuantity(decimal? baseAssetQuantity = null, decimal? quoteAssetQuantity = null, decimal? contractQuantity = null) - : base(baseAssetQuantity, quoteAssetQuantity, contractQuantity) - { - } } } diff --git a/CryptoExchange.Net/SharedApis/SharedSymbol.cs b/CryptoExchange.Net/SharedApis/SharedSymbol.cs index 92c0e3c..6fb60d8 100644 --- a/CryptoExchange.Net/SharedApis/SharedSymbol.cs +++ b/CryptoExchange.Net/SharedApis/SharedSymbol.cs @@ -1,69 +1,66 @@ -using CryptoExchange.Net.Converters.SystemTextJson; -using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Converters.SystemTextJson; using System; -using System.Collections.Generic; using System.Text.Json.Serialization; -namespace CryptoExchange.Net.SharedApis +namespace CryptoExchange.Net.SharedApis; + +/// +/// A symbol representation based on a base and quote asset +/// +[JsonConverter(typeof(SharedSymbolConverter))] +public record SharedSymbol { /// - /// A symbol representation based on a base and quote asset + /// The base asset of the symbol /// - [JsonConverter(typeof(SharedSymbolConverter))] - public record SharedSymbol + public string BaseAsset { get; set; } + /// + /// The quote asset of the symbol + /// + public string QuoteAsset { get; set; } + /// + /// The symbol name, can be used to overwrite the default formatted name + /// + public string? SymbolName { get; set; } + /// + /// The trading mode of the symbol. This determines how the base and quote asset should be formatted into the symbol name + /// + public TradingMode TradingMode { get; set; } + /// + /// Delivery time of the symbol, used for delivery futures to format the symbol name + /// + public DateTime? DeliverTime { get; set; } + + /// + /// Create a new SharedSymbol + /// + public SharedSymbol(TradingMode tradingMode, string baseAsset, string quoteAsset, DateTime? deliverTime = null) { - /// - /// The base asset of the symbol - /// - public string BaseAsset { get; set; } - /// - /// The quote asset of the symbol - /// - public string QuoteAsset { get; set; } - /// - /// The symbol name, can be used to overwrite the default formatted name - /// - public string? SymbolName { get; set; } - /// - /// The trading mode of the symbol. This determines how the base and quote asset should be formatted into the symbol name - /// - public TradingMode TradingMode { get; set; } - /// - /// Delivery time of the symbol, used for delivery futures to format the symbol name - /// - public DateTime? DeliverTime { get; set; } + TradingMode = tradingMode; + BaseAsset = baseAsset; + QuoteAsset = quoteAsset; + DeliverTime = deliverTime; + } - /// - /// Create a new SharedSymbol - /// - public SharedSymbol(TradingMode tradingMode, string baseAsset, string quoteAsset, DateTime? deliverTime = null) - { - TradingMode = tradingMode; - BaseAsset = baseAsset; - QuoteAsset = quoteAsset; - DeliverTime = deliverTime; - } + /// + /// Create a new SharedSymbol and override the formatted name + /// + public SharedSymbol(TradingMode tradingMode, string baseAsset, string quoteAsset, string symbolName) + { + TradingMode = tradingMode; + BaseAsset = baseAsset; + QuoteAsset = quoteAsset; + SymbolName = symbolName; + } - /// - /// Create a new SharedSymbol and override the formatted name - /// - public SharedSymbol(TradingMode tradingMode, string baseAsset, string quoteAsset, string symbolName) - { - TradingMode = tradingMode; - BaseAsset = baseAsset; - QuoteAsset = quoteAsset; - SymbolName = symbolName; - } + /// + /// Get the symbol name using the provided formatting function + /// + public string GetSymbol(Func format) + { + if (!string.IsNullOrEmpty(SymbolName)) + return SymbolName!; - /// - /// Get the symbol name using the provided formatting function - /// - public string GetSymbol(Func format) - { - if (!string.IsNullOrEmpty(SymbolName)) - return SymbolName!; - - return format(BaseAsset, QuoteAsset, TradingMode, DeliverTime); - } + return format(BaseAsset, QuoteAsset, TradingMode, DeliverTime); } } diff --git a/CryptoExchange.Net/Sockets/CryptoExchangeWebSocketClient.cs b/CryptoExchange.Net/Sockets/CryptoExchangeWebSocketClient.cs index 817c90e..6a403f0 100644 --- a/CryptoExchange.Net/Sockets/CryptoExchangeWebSocketClient.cs +++ b/CryptoExchange.Net/Sockets/CryptoExchangeWebSocketClient.cs @@ -1,7 +1,9 @@ -using CryptoExchange.Net.Interfaces; +using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Logging.Extensions; using CryptoExchange.Net.Objects; +#if NET5_0_OR_GREATER using CryptoExchange.Net.Objects.Errors; +#endif using CryptoExchange.Net.Objects.Sockets; using CryptoExchange.Net.RateLimiting; using Microsoft.Extensions.Logging; @@ -17,899 +19,898 @@ using System.Net.WebSockets; using System.Threading; using System.Threading.Tasks; -namespace CryptoExchange.Net.Sockets +namespace CryptoExchange.Net.Sockets; + +/// +/// A wrapper around the ClientWebSocket +/// +public class CryptoExchangeWebSocketClient : IWebsocket { - /// - /// A wrapper around the ClientWebSocket - /// - public class CryptoExchangeWebSocketClient : IWebsocket + enum ProcessState { - enum ProcessState + Idle, + Processing, + WaitingForClose, + Reconnecting + } + + internal static int _lastStreamId; + private static readonly object _streamIdLock = new(); + private static readonly ArrayPool _receiveBufferPool = ArrayPool.Shared; + + private readonly AsyncResetEvent _sendEvent; + private readonly ConcurrentQueue _sendBuffer; + private readonly SemaphoreSlim _closeSem; + + private ClientWebSocket _socket; + private CancellationTokenSource _ctsSource; + private DateTime _lastReceivedMessagesUpdate; + private Task? _processTask; + private Task? _closeTask; + private bool _stopRequested; + private bool _disposed; + private ProcessState _processState; + private DateTime _lastReconnectTime; + private readonly string _baseAddress; + private int _reconnectAttempt; + private readonly int _receiveBufferSize; + + private const int _defaultReceiveBufferSize = 1048576; + private const int _sendBufferSize = 4096; + + /// + /// Received messages, the size and the timestamp + /// + protected readonly List _receivedMessages; + + /// + /// Received messages lock + /// + protected readonly object _receivedMessagesLock; + + /// + /// Log + /// + protected ILogger _logger; + + /// + public int Id { get; } + + /// + public WebSocketParameters Parameters { get; } + + /// + /// The timestamp this socket has been active for the last time + /// + public DateTime LastActionTime { get; private set; } + + /// + public Uri Uri => Parameters.Uri; + + /// + public virtual bool IsClosed => _socket.State == WebSocketState.Closed; + + /// + public virtual bool IsOpen => _socket.State == WebSocketState.Open && !_ctsSource.IsCancellationRequested; + + /// + public double IncomingKbps + { + get { - Idle, - Processing, - WaitingForClose, - Reconnecting - } - - internal static int _lastStreamId; - private static readonly object _streamIdLock = new(); - private static readonly ArrayPool _receiveBufferPool = ArrayPool.Shared; - - private readonly AsyncResetEvent _sendEvent; - private readonly ConcurrentQueue _sendBuffer; - private readonly SemaphoreSlim _closeSem; - - private ClientWebSocket _socket; - private CancellationTokenSource _ctsSource; - private DateTime _lastReceivedMessagesUpdate; - private Task? _processTask; - private Task? _closeTask; - private bool _stopRequested; - private bool _disposed; - private ProcessState _processState; - private DateTime _lastReconnectTime; - private readonly string _baseAddress; - private int _reconnectAttempt; - private readonly int _receiveBufferSize; - - private const int _defaultReceiveBufferSize = 1048576; - private const int _sendBufferSize = 4096; - - /// - /// Received messages, the size and the timestamp - /// - protected readonly List _receivedMessages; - - /// - /// Received messages lock - /// - protected readonly object _receivedMessagesLock; - - /// - /// Log - /// - protected ILogger _logger; - - /// - public int Id { get; } - - /// - public WebSocketParameters Parameters { get; } - - /// - /// The timestamp this socket has been active for the last time - /// - public DateTime LastActionTime { get; private set; } - - /// - public Uri Uri => Parameters.Uri; - - /// - public virtual bool IsClosed => _socket.State == WebSocketState.Closed; - - /// - public virtual bool IsOpen => _socket.State == WebSocketState.Open && !_ctsSource.IsCancellationRequested; - - /// - public double IncomingKbps - { - get + lock (_receivedMessagesLock) { - lock (_receivedMessagesLock) - { - UpdateReceivedMessages(); + UpdateReceivedMessages(); - if (_receivedMessages.Count == 0) - return 0; + if (_receivedMessages.Count == 0) + return 0; - return Math.Round(_receivedMessages.Sum(v => v.Bytes) / 1000d / 3d); - } + return Math.Round(_receivedMessages.Sum(v => v.Bytes) / 1000d / 3d); } } + } - /// - public event Func? OnClose; + /// + public event Func? OnClose; - /// - public event Func, Task>? OnStreamMessage; + /// + public event Func, Task>? OnStreamMessage; - /// - public event Func? OnRequestSent; + /// + public event Func? OnRequestSent; - /// - public event Func? OnRequestRateLimited; + /// + public event Func? OnRequestRateLimited; - /// - public event Func? OnConnectRateLimited; + /// + public event Func? OnConnectRateLimited; - /// - public event Func? OnError; + /// + public event Func? OnError; - /// - public event Func? OnOpen; + /// + public event Func? OnOpen; - /// - public event Func? OnReconnecting; + /// + public event Func? OnReconnecting; - /// - public event Func? OnReconnected; - /// - public Func>? GetReconnectionUrl { get; set; } + /// + public event Func? OnReconnected; + /// + public Func>? GetReconnectionUrl { get; set; } - /// - /// ctor - /// - /// The log object to use - /// The parameters for this socket - public CryptoExchangeWebSocketClient(ILogger logger, WebSocketParameters websocketParameters) - { - Id = NextStreamId(); - _logger = logger; + /// + /// ctor + /// + /// The log object to use + /// The parameters for this socket + public CryptoExchangeWebSocketClient(ILogger logger, WebSocketParameters websocketParameters) + { + Id = NextStreamId(); + _logger = logger; - Parameters = websocketParameters; - _receivedMessages = new List(); - _sendEvent = new AsyncResetEvent(); - _sendBuffer = new ConcurrentQueue(); - _ctsSource = new CancellationTokenSource(); - _receivedMessagesLock = new object(); - _receiveBufferSize = websocketParameters.ReceiveBufferSize ?? _defaultReceiveBufferSize; + Parameters = websocketParameters; + _receivedMessages = new List(); + _sendEvent = new AsyncResetEvent(); + _sendBuffer = new ConcurrentQueue(); + _ctsSource = new CancellationTokenSource(); + _receivedMessagesLock = new object(); + _receiveBufferSize = websocketParameters.ReceiveBufferSize ?? _defaultReceiveBufferSize; - _closeSem = new SemaphoreSlim(1, 1); - _socket = CreateSocket(); - _baseAddress = $"{Uri.Scheme}://{Uri.Host}"; - } + _closeSem = new SemaphoreSlim(1, 1); + _socket = CreateSocket(); + _baseAddress = $"{Uri.Scheme}://{Uri.Host}"; + } - /// - public void UpdateProxy(ApiProxy? proxy) - { - Parameters.Proxy = proxy; - } + /// + public void UpdateProxy(ApiProxy? proxy) + { + Parameters.Proxy = proxy; + } - /// - public virtual async Task ConnectAsync(CancellationToken ct) - { - var connectResult = await ConnectInternalAsync(ct).ConfigureAwait(false); - if (!connectResult) - return connectResult; - - await (OnOpen?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false); - _processTask = ProcessAsync(); + /// + public virtual async Task ConnectAsync(CancellationToken ct) + { + var connectResult = await ConnectInternalAsync(ct).ConfigureAwait(false); + if (!connectResult) return connectResult; - } + + await (OnOpen?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false); + _processTask = ProcessAsync(); + return connectResult; + } - /// - /// Create the socket object - /// - private ClientWebSocket CreateSocket() + /// + /// Create the socket object + /// + private ClientWebSocket CreateSocket() + { + var cookieContainer = new CookieContainer(); + foreach (var cookie in Parameters.Cookies) + cookieContainer.Add(new Cookie(cookie.Key, cookie.Value)); + + var socket = new ClientWebSocket(); + try { - var cookieContainer = new CookieContainer(); - foreach (var cookie in Parameters.Cookies) - cookieContainer.Add(new Cookie(cookie.Key, cookie.Value)); - - var socket = new ClientWebSocket(); - try - { - socket.Options.Cookies = cookieContainer; - foreach (var header in Parameters.Headers) - socket.Options.SetRequestHeader(header.Key, header.Value); - socket.Options.KeepAliveInterval = Parameters.KeepAliveInterval ?? TimeSpan.Zero; - if (System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription.StartsWith(".NET Framework")) - socket.Options.SetBuffer(65536, 65536); // Setting it to anything bigger than 65536 throws an exception in .net framework - else - socket.Options.SetBuffer(_receiveBufferSize, _sendBufferSize); - if (Parameters.Proxy != null) - SetProxy(socket, Parameters.Proxy); + socket.Options.Cookies = cookieContainer; + foreach (var header in Parameters.Headers) + socket.Options.SetRequestHeader(header.Key, header.Value); + socket.Options.KeepAliveInterval = Parameters.KeepAliveInterval ?? TimeSpan.Zero; + if (System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription.StartsWith(".NET Framework")) + socket.Options.SetBuffer(65536, 65536); // Setting it to anything bigger than 65536 throws an exception in .net framework + else + socket.Options.SetBuffer(_receiveBufferSize, _sendBufferSize); + if (Parameters.Proxy != null) + SetProxy(socket, Parameters.Proxy); #if NET6_0_OR_GREATER - socket.Options.CollectHttpResponseDetails = true; + socket.Options.CollectHttpResponseDetails = true; #endif #if NET9_0_OR_GREATER - socket.Options.KeepAliveTimeout = Parameters.KeepAliveTimeout ?? TimeSpan.FromSeconds(10); + socket.Options.KeepAliveTimeout = Parameters.KeepAliveTimeout ?? TimeSpan.FromSeconds(10); #endif - } - catch (PlatformNotSupportedException) - { - // Options are not supported on certain platforms (WebAssembly for instance) - // best we can do it try to connect without setting options. - } - - return socket; + } + catch (PlatformNotSupportedException) + { + // Options are not supported on certain platforms (WebAssembly for instance) + // best we can do it try to connect without setting options. } - private async Task ConnectInternalAsync(CancellationToken ct) + return socket; + } + + private async Task ConnectInternalAsync(CancellationToken ct) + { + _logger.SocketConnecting(Id); + try { - _logger.SocketConnecting(Id); - try + if (Parameters.RateLimiter != null) { - if (Parameters.RateLimiter != null) - { - var definition = new RequestDefinition(Uri.AbsolutePath, HttpMethod.Get) { ConnectionId = Id }; - var limitResult = await Parameters.RateLimiter.ProcessAsync(_logger, Id, RateLimitItemType.Connection, definition, _baseAddress, null, 1, Parameters.RateLimitingBehavior, null, _ctsSource.Token).ConfigureAwait(false); - if (!limitResult) - return new CallResult(new ClientRateLimitError("Connection limit reached")); - } - - using CancellationTokenSource tcs = new(TimeSpan.FromSeconds(10)); - using var linked = CancellationTokenSource.CreateLinkedTokenSource(tcs.Token, _ctsSource.Token, ct); - await _socket.ConnectAsync(Uri, linked.Token).ConfigureAwait(false); + var definition = new RequestDefinition(Uri.AbsolutePath, HttpMethod.Get) { ConnectionId = Id }; + var limitResult = await Parameters.RateLimiter.ProcessAsync(_logger, Id, RateLimitItemType.Connection, definition, _baseAddress, null, 1, Parameters.RateLimitingBehavior, null, _ctsSource.Token).ConfigureAwait(false); + if (!limitResult) + return new CallResult(new ClientRateLimitError("Connection limit reached")); } - catch (Exception e) - { - if (ct.IsCancellationRequested) - { - _logger.SocketConnectingCanceled(Id); - } - else if (!_ctsSource.IsCancellationRequested) - { - // if _ctsSource was canceled this was already logged - _logger.SocketConnectionFailed(Id, e.Message, e); - } - if (e is WebSocketException we) - { + using CancellationTokenSource tcs = new(TimeSpan.FromSeconds(10)); + using var linked = CancellationTokenSource.CreateLinkedTokenSource(tcs.Token, _ctsSource.Token, ct); + await _socket.ConnectAsync(Uri, linked.Token).ConfigureAwait(false); + } + catch (Exception e) + { + if (ct.IsCancellationRequested) + { + _logger.SocketConnectingCanceled(Id); + } + else if (!_ctsSource.IsCancellationRequested) + { + // if _ctsSource was canceled this was already logged + _logger.SocketConnectionFailed(Id, e.Message, e); + } + + if (e is WebSocketException we) + { #if (NET6_0_OR_GREATER) - if (_socket.HttpStatusCode == HttpStatusCode.TooManyRequests) - { - await (OnConnectRateLimited?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false); - return new CallResult(new ServerRateLimitError(we.Message, we)); - } - - if (_socket.HttpStatusCode == HttpStatusCode.Unauthorized) - { - return new CallResult(new ServerError(new ErrorInfo(ErrorType.Unauthorized, "Server returned status code `401` when `101` was expected"))); - } -#else - // ClientWebSocket.HttpStatusCode is only available in .NET6+ https://learn.microsoft.com/en-us/dotnet/api/system.net.websockets.clientwebsocket.httpstatuscode?view=net-8.0 - // Try to read 429 from the message instead - if (we.Message.Contains("429")) - { - await (OnConnectRateLimited?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false); - return new CallResult(new ServerRateLimitError(we.Message, we)); - } -#endif + if (_socket.HttpStatusCode == HttpStatusCode.TooManyRequests) + { + await (OnConnectRateLimited?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false); + return new CallResult(new ServerRateLimitError(we.Message, we)); } - return new CallResult(new CantConnectError(e)); + if (_socket.HttpStatusCode == HttpStatusCode.Unauthorized) + { + return new CallResult(new ServerError(new ErrorInfo(ErrorType.Unauthorized, "Server returned status code `401` when `101` was expected"))); + } +#else + // ClientWebSocket.HttpStatusCode is only available in .NET6+ https://learn.microsoft.com/en-us/dotnet/api/system.net.websockets.clientwebsocket.httpstatuscode?view=net-8.0 + // Try to read 429 from the message instead + if (we.Message.Contains("429")) + { + await (OnConnectRateLimited?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false); + return new CallResult(new ServerRateLimitError(we.Message, we)); + } +#endif } - _logger.SocketConnected(Id, Uri); - return CallResult.SuccessResult; + return new CallResult(new CantConnectError(e)); } - /// - private async Task ProcessAsync() + _logger.SocketConnected(Id, Uri); + return CallResult.SuccessResult; + } + + /// + private async Task ProcessAsync() + { + while (!_stopRequested) { + _logger.SocketStartingProcessing(Id); + SetProcessState(ProcessState.Processing); + var sendTask = SendLoopAsync(); + var receiveTask = ReceiveLoopAsync(); + var timeoutTask = Parameters.Timeout != null && Parameters.Timeout > TimeSpan.FromSeconds(0) ? CheckTimeoutAsync() : Task.CompletedTask; + await Task.WhenAll(sendTask, receiveTask, timeoutTask).ConfigureAwait(false); + _logger.SocketFinishedProcessing(Id); + + SetProcessState(ProcessState.WaitingForClose); + while (_closeTask == null) + await Task.Delay(50).ConfigureAwait(false); + + await _closeTask.ConfigureAwait(false); + if (!_stopRequested) + _closeTask = null; + + if (Parameters.ReconnectPolicy == ReconnectPolicy.Disabled) + { + SetProcessState(ProcessState.Idle); + await (OnClose?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false); + return; + } + + if (!_stopRequested) + { + SetProcessState(ProcessState.Reconnecting); + await (OnReconnecting?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false); + } + + // Delay here to prevent very rapid looping when a connection to the server is accepted and immediately disconnected + var initialDelay = GetReconnectDelay(); + await Task.Delay(initialDelay).ConfigureAwait(false); + while (!_stopRequested) { - _logger.SocketStartingProcessing(Id); + _logger.SocketAttemptReconnect(Id); + var task = GetReconnectionUrl?.Invoke(); + if (task != null) + { + var reconnectUri = await task.ConfigureAwait(false); + if (reconnectUri != null && Parameters.Uri.ToString() != reconnectUri.ToString()) + { + _logger.SocketSetReconnectUri(Id, reconnectUri); + Parameters.Uri = reconnectUri; + } + } + + _socket?.Dispose(); + _socket = CreateSocket(); + _ctsSource.Dispose(); + _ctsSource = new CancellationTokenSource(); + while (_sendBuffer.TryDequeue(out _)) { } // Clear send buffer + + _reconnectAttempt++; + var connected = await ConnectInternalAsync(default).ConfigureAwait(false); + if (!connected) + { + // Delay between reconnect attempts + var delay = GetReconnectDelay(); + await Task.Delay(delay).ConfigureAwait(false); + continue; + } + + _reconnectAttempt = 0; + _lastReconnectTime = DateTime.UtcNow; + + // Set to processing before reconnect handling SetProcessState(ProcessState.Processing); - var sendTask = SendLoopAsync(); - var receiveTask = ReceiveLoopAsync(); - var timeoutTask = Parameters.Timeout != null && Parameters.Timeout > TimeSpan.FromSeconds(0) ? CheckTimeoutAsync() : Task.CompletedTask; - await Task.WhenAll(sendTask, receiveTask, timeoutTask).ConfigureAwait(false); - _logger.SocketFinishedProcessing(Id); - - SetProcessState(ProcessState.WaitingForClose); - while (_closeTask == null) - await Task.Delay(50).ConfigureAwait(false); - - await _closeTask.ConfigureAwait(false); - if (!_stopRequested) - _closeTask = null; - - if (Parameters.ReconnectPolicy == ReconnectPolicy.Disabled) - { - SetProcessState(ProcessState.Idle); - await (OnClose?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false); - return; - } - - if (!_stopRequested) - { - SetProcessState(ProcessState.Reconnecting); - await (OnReconnecting?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false); - } - - // Delay here to prevent very rapid looping when a connection to the server is accepted and immediately disconnected - var initialDelay = GetReconnectDelay(); - await Task.Delay(initialDelay).ConfigureAwait(false); - - while (!_stopRequested) - { - _logger.SocketAttemptReconnect(Id); - var task = GetReconnectionUrl?.Invoke(); - if (task != null) - { - var reconnectUri = await task.ConfigureAwait(false); - if (reconnectUri != null && Parameters.Uri.ToString() != reconnectUri.ToString()) - { - _logger.SocketSetReconnectUri(Id, reconnectUri); - Parameters.Uri = reconnectUri; - } - } - - _socket?.Dispose(); - _socket = CreateSocket(); - _ctsSource.Dispose(); - _ctsSource = new CancellationTokenSource(); - while (_sendBuffer.TryDequeue(out _)) { } // Clear send buffer - - _reconnectAttempt++; - var connected = await ConnectInternalAsync(default).ConfigureAwait(false); - if (!connected) - { - // Delay between reconnect attempts - var delay = GetReconnectDelay(); - await Task.Delay(delay).ConfigureAwait(false); - continue; - } - - _reconnectAttempt = 0; - _lastReconnectTime = DateTime.UtcNow; - - // Set to processing before reconnect handling - SetProcessState(ProcessState.Processing); - await (OnReconnected?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false); - break; - } + await (OnReconnected?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false); + break; } - - SetProcessState(ProcessState.Idle); } - private TimeSpan GetReconnectDelay() + SetProcessState(ProcessState.Idle); + } + + private TimeSpan GetReconnectDelay() + { + if (_reconnectAttempt == 0) { - if (_reconnectAttempt == 0) - { - // Means this is directly after disconnecting. Only delay if the last reconnect time is very recent - var sinceLastReconnect = DateTime.UtcNow - _lastReconnectTime; - if (sinceLastReconnect < TimeSpan.FromSeconds(5)) - return TimeSpan.FromSeconds(5) - sinceLastReconnect; + // Means this is directly after disconnecting. Only delay if the last reconnect time is very recent + var sinceLastReconnect = DateTime.UtcNow - _lastReconnectTime; + if (sinceLastReconnect < TimeSpan.FromSeconds(5)) + return TimeSpan.FromSeconds(5) - sinceLastReconnect; - return TimeSpan.FromMilliseconds(1); - } - - var delay = Parameters.ReconnectPolicy == ReconnectPolicy.FixedDelay ? Parameters.ReconnectInterval : TimeSpan.FromSeconds(Math.Pow(2, Math.Min(5, _reconnectAttempt))); - if (delay > TimeSpan.Zero) - return delay; return TimeSpan.FromMilliseconds(1); } - /// - public virtual bool Send(int id, string data, int weight) + var delay = Parameters.ReconnectPolicy == ReconnectPolicy.FixedDelay ? Parameters.ReconnectInterval : TimeSpan.FromSeconds(Math.Pow(2, Math.Min(5, _reconnectAttempt))); + if (delay > TimeSpan.Zero) + return delay; + return TimeSpan.FromMilliseconds(1); + } + + /// + public virtual bool Send(int id, string data, int weight) + { + if (_ctsSource.IsCancellationRequested || _processState != ProcessState.Processing) + return false; + + var bytes = Parameters.Encoding.GetBytes(data); + _logger.SocketAddingBytesToSendBuffer(Id, id, bytes); + _sendBuffer.Enqueue(new SendItem { Id = id, Type = WebSocketMessageType.Text, Weight = weight, Bytes = bytes }); + _sendEvent.Set(); + return true; + } + + /// + public virtual bool Send(int id, byte[] data, int weight) + { + if (_ctsSource.IsCancellationRequested || _processState != ProcessState.Processing) + return false; + + _logger.SocketAddingBytesToSendBuffer(Id, id, data); + _sendBuffer.Enqueue(new SendItem { Id = id, Type = WebSocketMessageType.Binary, Weight = weight, Bytes = data }); + _sendEvent.Set(); + return true; + } + + /// + public virtual async Task ReconnectAsync() + { + if (_processState != ProcessState.Processing && IsOpen) + return; + + _logger.SocketReconnectRequested(Id); + _closeTask = CloseInternalAsync(); + await _closeTask.ConfigureAwait(false); + } + + /// + public virtual async Task CloseAsync() + { + await _closeSem.WaitAsync().ConfigureAwait(false); + _stopRequested = true; + + try { - if (_ctsSource.IsCancellationRequested || _processState != ProcessState.Processing) - return false; - - var bytes = Parameters.Encoding.GetBytes(data); - _logger.SocketAddingBytesToSendBuffer(Id, id, bytes); - _sendBuffer.Enqueue(new SendItem { Id = id, Type = WebSocketMessageType.Text, Weight = weight, Bytes = bytes }); - _sendEvent.Set(); - return true; - } - - /// - public virtual bool Send(int id, byte[] data, int weight) - { - if (_ctsSource.IsCancellationRequested || _processState != ProcessState.Processing) - return false; - - _logger.SocketAddingBytesToSendBuffer(Id, id, data); - _sendBuffer.Enqueue(new SendItem { Id = id, Type = WebSocketMessageType.Binary, Weight = weight, Bytes = data }); - _sendEvent.Set(); - return true; - } - - /// - public virtual async Task ReconnectAsync() - { - if (_processState != ProcessState.Processing && IsOpen) + if (_closeTask?.IsCompleted == false) + { + _logger.SocketCloseAsyncWaitingForExistingCloseTask(Id); + await _closeTask.ConfigureAwait(false); return; + } - _logger.SocketReconnectRequested(Id); + if (!IsOpen) + { + _logger.SocketCloseAsyncSocketNotOpen(Id); + return; + } + + _logger.SocketClosing(Id); _closeTask = CloseInternalAsync(); - await _closeTask.ConfigureAwait(false); + } + finally + { + _closeSem.Release(); } - /// - public virtual async Task CloseAsync() + await _closeTask.ConfigureAwait(false); + if(_processTask != null) + await _processTask.ConfigureAwait(false); + await (OnClose?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false); + _logger.SocketClosed(Id); + } + + /// + /// Internal close method + /// + /// + private async Task CloseInternalAsync() + { + if (_disposed) + return; + + try { - await _closeSem.WaitAsync().ConfigureAwait(false); - _stopRequested = true; - - try + if (_socket.State == WebSocketState.CloseReceived) { - if (_closeTask?.IsCompleted == false) - { - _logger.SocketCloseAsyncWaitingForExistingCloseTask(Id); - await _closeTask.ConfigureAwait(false); - return; - } - - if (!IsOpen) - { - _logger.SocketCloseAsyncSocketNotOpen(Id); - return; - } - - _logger.SocketClosing(Id); - _closeTask = CloseInternalAsync(); + await _socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", default).ConfigureAwait(false); } - finally + else if (_socket.State == WebSocketState.Open) { - _closeSem.Release(); + await _socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "Closing", default).ConfigureAwait(false); + var startWait = DateTime.UtcNow; + while (_socket.State != WebSocketState.Closed && _socket.State != WebSocketState.Aborted) + { + // Wait until we receive close confirmation + await Task.Delay(10).ConfigureAwait(false); + if (DateTime.UtcNow - startWait > TimeSpan.FromSeconds(1)) + break; // Wait for max 1 second, then just abort the connection + } } - - await _closeTask.ConfigureAwait(false); - if(_processTask != null) - await _processTask.ConfigureAwait(false); - await (OnClose?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false); - _logger.SocketClosed(Id); + } + catch (Exception) + { + // Can sometimes throw an exception when socket is in aborted state due to timing + // Websocket is set to Aborted state when the cancelation token is set during SendAsync/ReceiveAsync + // So socket might go to aborted state, might still be open } - /// - /// Internal close method - /// - /// - private async Task CloseInternalAsync() - { - if (_disposed) - return; + _ctsSource.Cancel(); + } - try - { - if (_socket.State == WebSocketState.CloseReceived) - { - await _socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", default).ConfigureAwait(false); - } - else if (_socket.State == WebSocketState.Open) - { - await _socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "Closing", default).ConfigureAwait(false); - var startWait = DateTime.UtcNow; - while (_socket.State != WebSocketState.Closed && _socket.State != WebSocketState.Aborted) - { - // Wait until we receive close confirmation - await Task.Delay(10).ConfigureAwait(false); - if (DateTime.UtcNow - startWait > TimeSpan.FromSeconds(1)) - break; // Wait for max 1 second, then just abort the connection - } - } - } - catch (Exception) - { - // Can sometimes throw an exception when socket is in aborted state due to timing - // Websocket is set to Aborted state when the cancelation token is set during SendAsync/ReceiveAsync - // So socket might go to aborted state, might still be open - } + /// + /// Dispose the socket + /// + public void Dispose() + { + if (_disposed) + return; + if (_ctsSource?.IsCancellationRequested == false) _ctsSource.Cancel(); - } - /// - /// Dispose the socket - /// - public void Dispose() + _logger.SocketDisposing(Id); + _disposed = true; + _socket.Dispose(); + _ctsSource?.Dispose(); + _sendEvent.Dispose(); + _logger.SocketDisposed(Id); + } + + /// + /// Loop for sending data + /// + /// + private async Task SendLoopAsync() + { + var requestDefinition = new RequestDefinition(Uri.AbsolutePath, HttpMethod.Get) { ConnectionId = Id }; + try { - if (_disposed) - return; - - if (_ctsSource?.IsCancellationRequested == false) - _ctsSource.Cancel(); - - _logger.SocketDisposing(Id); - _disposed = true; - _socket.Dispose(); - _ctsSource?.Dispose(); - _sendEvent.Dispose(); - _logger.SocketDisposed(Id); - } - - /// - /// Loop for sending data - /// - /// - private async Task SendLoopAsync() - { - var requestDefinition = new RequestDefinition(Uri.AbsolutePath, HttpMethod.Get) { ConnectionId = Id }; - try + while (true) { - while (true) + try { - try - { - if (_sendBuffer.IsEmpty) - await _sendEvent.WaitAsync(ct: _ctsSource.Token).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - break; - } + if (_sendBuffer.IsEmpty) + await _sendEvent.WaitAsync(ct: _ctsSource.Token).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + break; + } - if (_ctsSource.IsCancellationRequested) - break; + if (_ctsSource.IsCancellationRequested) + break; - while (_sendBuffer.TryDequeue(out var data)) + while (_sendBuffer.TryDequeue(out var data)) + { + if (Parameters.RateLimiter != null) { - if (Parameters.RateLimiter != null) - { - try - { - var limitResult = await Parameters.RateLimiter.ProcessAsync(_logger, data.Id, RateLimitItemType.Request, requestDefinition, _baseAddress, null, data.Weight, Parameters.RateLimitingBehavior, null, _ctsSource.Token).ConfigureAwait(false); - if (!limitResult) - { - await (OnRequestRateLimited?.Invoke(data.Id) ?? Task.CompletedTask).ConfigureAwait(false); - continue; - } - } - catch (OperationCanceledException) - { - // canceled - break; - } - } - try { - await _socket.SendAsync(new ArraySegment(data.Bytes, 0, data.Bytes.Length), data.Type, true, _ctsSource.Token).ConfigureAwait(false); - await (OnRequestSent?.Invoke(data.Id) ?? Task.CompletedTask).ConfigureAwait(false); - _logger.SocketSentBytes(Id, data.Id, data.Bytes.Length); + var limitResult = await Parameters.RateLimiter.ProcessAsync(_logger, data.Id, RateLimitItemType.Request, requestDefinition, _baseAddress, null, data.Weight, Parameters.RateLimitingBehavior, null, _ctsSource.Token).ConfigureAwait(false); + if (!limitResult) + { + await (OnRequestRateLimited?.Invoke(data.Id) ?? Task.CompletedTask).ConfigureAwait(false); + continue; + } } catch (OperationCanceledException) { // canceled break; } - catch (Exception ioe) - { - // Connection closed unexpectedly, .NET framework - await (OnError?.Invoke(ioe) ?? Task.CompletedTask).ConfigureAwait(false); - if (_closeTask?.IsCompleted != false) - _closeTask = CloseInternalAsync(); - break; - } - } - } - } - catch (Exception e) - { - // Because this is running in a separate task and not awaited until the socket gets closed - // any exception here will crash the send processing, but do so silently unless the socket get's stopped. - // Make sure we at least let the owner know there was an error - _logger.SocketSendLoopStoppedWithException(Id, e.Message, e); - await (OnError?.Invoke(e) ?? Task.CompletedTask).ConfigureAwait(false); - if (_closeTask?.IsCompleted != false) - _closeTask = CloseInternalAsync(); - } - finally - { - _logger.SocketSendLoopFinished(Id); - } - } - - /// - /// Loop for receiving and reassembling data - /// - /// - private async Task ReceiveLoopAsync() - { - byte[] rentedBuffer = _receiveBufferPool.Rent(_receiveBufferSize); - var buffer = new ArraySegment(rentedBuffer); - try - { - while (true) - { - if (_ctsSource.IsCancellationRequested) - break; - - MemoryStream? multipartStream = null; - WebSocketReceiveResult? receiveResult = null; - bool multiPartMessage = false; - while (true) - { - try - { - receiveResult = await _socket.ReceiveAsync(buffer, _ctsSource.Token).ConfigureAwait(false); - lock (_receivedMessagesLock) - _receivedMessages.Add(new ReceiveItem(DateTime.UtcNow, receiveResult.Count)); - } - catch (OperationCanceledException ex) - { - if (ex.InnerException?.InnerException?.Message.Contains("KeepAliveTimeout") == true) - { - // Specific case that the websocket connection got closed because of a ping frame timeout - // Unfortunately doesn't seem to be a nicer way to catch - _logger.SocketPingTimeout(Id); - } - - if (_closeTask?.IsCompleted != false) - _closeTask = CloseInternalAsync(); - - // canceled - break; - } - catch (Exception wse) - { - if (!_ctsSource.Token.IsCancellationRequested && !_stopRequested) - // Connection closed unexpectedly - await (OnError?.Invoke(wse) ?? Task.CompletedTask).ConfigureAwait(false); - - if (_closeTask?.IsCompleted != false) - _closeTask = CloseInternalAsync(); - break; - } - - if (receiveResult.MessageType == WebSocketMessageType.Close) - { - // Connection closed - if (_socket.State == WebSocketState.CloseReceived) - { - // Close received means it server initiated, we should send a confirmation and close the socket - _logger.SocketReceivedCloseMessage(Id, receiveResult.CloseStatus.ToString()!, receiveResult.CloseStatusDescription ?? string.Empty); - if (_closeTask?.IsCompleted != false) - _closeTask = CloseInternalAsync(); - } - else - { - // Means the socket is now closed and we were the one initiating it - _logger.SocketReceivedCloseConfirmation(Id, receiveResult.CloseStatus.ToString()!, receiveResult.CloseStatusDescription ?? string.Empty); - } - - break; - } - - if (!receiveResult.EndOfMessage) - { - // We received data, but it is not complete, write it to a memory stream for reassembling - multiPartMessage = true; - _logger.SocketReceivedPartialMessage(Id, receiveResult.Count); - - // Write the data to a memory stream to be reassembled later - if (multipartStream == null) - multipartStream = new MemoryStream(); - multipartStream.Write(buffer.Array!, buffer.Offset, receiveResult.Count); - } - else - { - if (!multiPartMessage) - { - // Received a complete message and it's not multi part - _logger.SocketReceivedSingleMessage(Id, receiveResult.Count); - await ProcessData(receiveResult.MessageType, new ReadOnlyMemory(buffer.Array!, buffer.Offset, receiveResult.Count)).ConfigureAwait(false); - } - else - { - // Received the end of a multipart message, write to memory stream for reassembling - _logger.SocketReceivedPartialMessage(Id, receiveResult.Count); - multipartStream!.Write(buffer.Array!, buffer.Offset, receiveResult.Count); - } - - break; - } - } - - lock (_receivedMessagesLock) - UpdateReceivedMessages(); - - if (receiveResult?.MessageType == WebSocketMessageType.Close) - { - // Received close message - break; - } - - if (receiveResult == null || _ctsSource.IsCancellationRequested) - { - // Error during receiving or cancellation requested, stop. - break; - } - - if (multiPartMessage) - { - // When the connection gets interrupted we might not have received a full message - if (receiveResult?.EndOfMessage == true) - { - _logger.SocketReassembledMessage(Id, multipartStream!.Length); - // Get the underlying buffer of the memory stream holding the written data and delimit it (GetBuffer return the full array, not only the written part) - await ProcessData(receiveResult.MessageType, new ReadOnlyMemory(multipartStream.GetBuffer(), 0, (int)multipartStream.Length)).ConfigureAwait(false); - } - else - { - _logger.SocketDiscardIncompleteMessage(Id, multipartStream!.Length); - } - } - } - } - catch(Exception e) - { - // Because this is running in a separate task and not awaited until the socket gets closed - // any exception here will crash the receive processing, but do so silently unless the socket gets stopped. - // Make sure we at least let the owner know there was an error - _logger.SocketReceiveLoopStoppedWithException(Id, e); - await (OnError?.Invoke(e) ?? Task.CompletedTask).ConfigureAwait(false); - if (_closeTask?.IsCompleted != false) - _closeTask = CloseInternalAsync(); - } - finally - { - _receiveBufferPool.Return(rentedBuffer, true); - _logger.SocketReceiveLoopFinished(Id); - } - } - - /// - /// Process a stream message - /// - /// - /// - /// - protected async Task ProcessData(WebSocketMessageType type, ReadOnlyMemory data) - { - LastActionTime = DateTime.UtcNow; - await (OnStreamMessage?.Invoke(type, data) ?? Task.CompletedTask).ConfigureAwait(false); - } - - /// - /// Checks if there is no data received for a period longer than the specified timeout - /// - /// - protected async Task CheckTimeoutAsync() - { - _logger.SocketStartingTaskForNoDataReceivedCheck(Id, Parameters.Timeout); - LastActionTime = DateTime.UtcNow; - try - { - while (true) - { - if (_ctsSource.IsCancellationRequested) - return; - - if (DateTime.UtcNow - LastActionTime > Parameters.Timeout) - { - _logger.SocketNoDataReceiveTimoutReconnect(Id, Parameters.Timeout); - _ = ReconnectAsync().ConfigureAwait(false); - return; } try { - await Task.Delay(500, _ctsSource.Token).ConfigureAwait(false); + await _socket.SendAsync(new ArraySegment(data.Bytes, 0, data.Bytes.Length), data.Type, true, _ctsSource.Token).ConfigureAwait(false); + await (OnRequestSent?.Invoke(data.Id) ?? Task.CompletedTask).ConfigureAwait(false); + _logger.SocketSentBytes(Id, data.Id, data.Bytes.Length); } catch (OperationCanceledException) { // canceled break; } + catch (Exception ioe) + { + // Connection closed unexpectedly, .NET framework + await (OnError?.Invoke(ioe) ?? Task.CompletedTask).ConfigureAwait(false); + if (_closeTask?.IsCompleted != false) + _closeTask = CloseInternalAsync(); + break; + } } } - catch (Exception e) - { - // Because this is running in a separate task and not awaited until the socket gets closed - // any exception here will stop the timeout checking, but do so silently unless the socket get's stopped. - // Make sure we at least let the owner know there was an error - await (OnError?.Invoke(e) ?? Task.CompletedTask).ConfigureAwait(false); - } } - - /// - /// Get the next identifier - /// - /// - private static int NextStreamId() + catch (Exception e) { - lock (_streamIdLock) - { - _lastStreamId++; - return _lastStreamId; - } + // Because this is running in a separate task and not awaited until the socket gets closed + // any exception here will crash the send processing, but do so silently unless the socket get's stopped. + // Make sure we at least let the owner know there was an error + _logger.SocketSendLoopStoppedWithException(Id, e.Message, e); + await (OnError?.Invoke(e) ?? Task.CompletedTask).ConfigureAwait(false); + if (_closeTask?.IsCompleted != false) + _closeTask = CloseInternalAsync(); } - - /// - /// Update the received messages list, removing messages received longer than 3s ago - /// - protected void UpdateReceivedMessages() + finally { - var checkTime = DateTime.UtcNow; - if (checkTime - _lastReceivedMessagesUpdate > TimeSpan.FromSeconds(1)) + _logger.SocketSendLoopFinished(Id); + } + } + + /// + /// Loop for receiving and reassembling data + /// + /// + private async Task ReceiveLoopAsync() + { + byte[] rentedBuffer = _receiveBufferPool.Rent(_receiveBufferSize); + var buffer = new ArraySegment(rentedBuffer); + try + { + while (true) { - for (var i = 0; i < _receivedMessages.Count; i++) + if (_ctsSource.IsCancellationRequested) + break; + + MemoryStream? multipartStream = null; + WebSocketReceiveResult? receiveResult = null; + bool multiPartMessage = false; + while (true) { - var msg = _receivedMessages[i]; - if (checkTime - msg.Timestamp > TimeSpan.FromSeconds(3)) + try { - _receivedMessages.Remove(msg); - i--; + receiveResult = await _socket.ReceiveAsync(buffer, _ctsSource.Token).ConfigureAwait(false); + lock (_receivedMessagesLock) + _receivedMessages.Add(new ReceiveItem(DateTime.UtcNow, receiveResult.Count)); + } + catch (OperationCanceledException ex) + { + if (ex.InnerException?.InnerException?.Message.Contains("KeepAliveTimeout") == true) + { + // Specific case that the websocket connection got closed because of a ping frame timeout + // Unfortunately doesn't seem to be a nicer way to catch + _logger.SocketPingTimeout(Id); + } + + if (_closeTask?.IsCompleted != false) + _closeTask = CloseInternalAsync(); + + // canceled + break; + } + catch (Exception wse) + { + if (!_ctsSource.Token.IsCancellationRequested && !_stopRequested) + // Connection closed unexpectedly + await (OnError?.Invoke(wse) ?? Task.CompletedTask).ConfigureAwait(false); + + if (_closeTask?.IsCompleted != false) + _closeTask = CloseInternalAsync(); + break; + } + + if (receiveResult.MessageType == WebSocketMessageType.Close) + { + // Connection closed + if (_socket.State == WebSocketState.CloseReceived) + { + // Close received means it server initiated, we should send a confirmation and close the socket + _logger.SocketReceivedCloseMessage(Id, receiveResult.CloseStatus.ToString()!, receiveResult.CloseStatusDescription ?? string.Empty); + if (_closeTask?.IsCompleted != false) + _closeTask = CloseInternalAsync(); + } + else + { + // Means the socket is now closed and we were the one initiating it + _logger.SocketReceivedCloseConfirmation(Id, receiveResult.CloseStatus.ToString()!, receiveResult.CloseStatusDescription ?? string.Empty); + } + + break; + } + + if (!receiveResult.EndOfMessage) + { + // We received data, but it is not complete, write it to a memory stream for reassembling + multiPartMessage = true; + _logger.SocketReceivedPartialMessage(Id, receiveResult.Count); + + // Write the data to a memory stream to be reassembled later + if (multipartStream == null) + multipartStream = new MemoryStream(); + multipartStream.Write(buffer.Array!, buffer.Offset, receiveResult.Count); + } + else + { + if (!multiPartMessage) + { + // Received a complete message and it's not multi part + _logger.SocketReceivedSingleMessage(Id, receiveResult.Count); + await ProcessData(receiveResult.MessageType, new ReadOnlyMemory(buffer.Array!, buffer.Offset, receiveResult.Count)).ConfigureAwait(false); + } + else + { + // Received the end of a multipart message, write to memory stream for reassembling + _logger.SocketReceivedPartialMessage(Id, receiveResult.Count); + multipartStream!.Write(buffer.Array!, buffer.Offset, receiveResult.Count); + } + + break; } } - _lastReceivedMessagesUpdate = checkTime; + lock (_receivedMessagesLock) + UpdateReceivedMessages(); + + if (receiveResult?.MessageType == WebSocketMessageType.Close) + { + // Received close message + break; + } + + if (receiveResult == null || _ctsSource.IsCancellationRequested) + { + // Error during receiving or cancellation requested, stop. + break; + } + + if (multiPartMessage) + { + // When the connection gets interrupted we might not have received a full message + if (receiveResult?.EndOfMessage == true) + { + _logger.SocketReassembledMessage(Id, multipartStream!.Length); + // Get the underlying buffer of the memory stream holding the written data and delimit it (GetBuffer return the full array, not only the written part) + await ProcessData(receiveResult.MessageType, new ReadOnlyMemory(multipartStream.GetBuffer(), 0, (int)multipartStream.Length)).ConfigureAwait(false); + } + else + { + _logger.SocketDiscardIncompleteMessage(Id, multipartStream!.Length); + } + } } } - - /// - /// Set proxy on socket - /// - /// - /// - /// - protected virtual void SetProxy(ClientWebSocket socket, ApiProxy proxy) + catch(Exception e) { - if (!Uri.TryCreate($"{proxy.Host}:{proxy.Port}", UriKind.Absolute, out var uri)) - throw new ArgumentException("Proxy settings invalid, {proxy.Host}:{proxy.Port} not a valid URI", nameof(proxy)); + // Because this is running in a separate task and not awaited until the socket gets closed + // any exception here will crash the receive processing, but do so silently unless the socket gets stopped. + // Make sure we at least let the owner know there was an error + _logger.SocketReceiveLoopStoppedWithException(Id, e); + await (OnError?.Invoke(e) ?? Task.CompletedTask).ConfigureAwait(false); + if (_closeTask?.IsCompleted != false) + _closeTask = CloseInternalAsync(); + } + finally + { + _receiveBufferPool.Return(rentedBuffer, true); + _logger.SocketReceiveLoopFinished(Id); + } + } - socket.Options.Proxy = uri?.Scheme == null - ? socket.Options.Proxy = new WebProxy(proxy.Host, proxy.Port) - : socket.Options.Proxy = new WebProxy + /// + /// Process a stream message + /// + /// + /// + /// + protected async Task ProcessData(WebSocketMessageType type, ReadOnlyMemory data) + { + LastActionTime = DateTime.UtcNow; + await (OnStreamMessage?.Invoke(type, data) ?? Task.CompletedTask).ConfigureAwait(false); + } + + /// + /// Checks if there is no data received for a period longer than the specified timeout + /// + /// + protected async Task CheckTimeoutAsync() + { + _logger.SocketStartingTaskForNoDataReceivedCheck(Id, Parameters.Timeout); + LastActionTime = DateTime.UtcNow; + try + { + while (true) + { + if (_ctsSource.IsCancellationRequested) + return; + + if (DateTime.UtcNow - LastActionTime > Parameters.Timeout) { - Address = uri - }; + _logger.SocketNoDataReceiveTimoutReconnect(Id, Parameters.Timeout); + _ = ReconnectAsync().ConfigureAwait(false); + return; + } - if (proxy.Login != null) - socket.Options.Proxy.Credentials = new NetworkCredential(proxy.Login, proxy.Password); + try + { + await Task.Delay(500, _ctsSource.Token).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + // canceled + break; + } + } } - - private void SetProcessState(ProcessState state) + catch (Exception e) { - if (_processState == state) - return; - - _logger.SocketProcessingStateChanged(Id, _processState.ToString(), state.ToString()); - _processState = state; + // Because this is running in a separate task and not awaited until the socket gets closed + // any exception here will stop the timeout checking, but do so silently unless the socket get's stopped. + // Make sure we at least let the owner know there was an error + await (OnError?.Invoke(e) ?? Task.CompletedTask).ConfigureAwait(false); } } /// - /// Message info + /// Get the next identifier /// - public struct SendItem + /// + private static int NextStreamId() { - /// - /// The request id - /// - public int Id { get; set; } - - /// - /// The request weight - /// - public int Weight { get; set; } - - /// - /// Timestamp the request was sent - /// - public DateTime SendTime { get; set; } - - /// - /// Message type - /// - public WebSocketMessageType Type { get; set; } - - /// - /// The bytes to send - /// - public byte[] Bytes { get; set; } + lock (_streamIdLock) + { + _lastStreamId++; + return _lastStreamId; + } } /// - /// Received message info + /// Update the received messages list, removing messages received longer than 3s ago /// - public struct ReceiveItem + protected void UpdateReceivedMessages() { - /// - /// Timestamp of the received data - /// - public DateTime Timestamp { get; set; } - /// - /// Number of bytes received - /// - public int Bytes { get; set; } - - /// - /// ctor - /// - /// - /// - public ReceiveItem(DateTime timestamp, int bytes) + var checkTime = DateTime.UtcNow; + if (checkTime - _lastReceivedMessagesUpdate > TimeSpan.FromSeconds(1)) { - Timestamp = timestamp; - Bytes = bytes; + for (var i = 0; i < _receivedMessages.Count; i++) + { + var msg = _receivedMessages[i]; + if (checkTime - msg.Timestamp > TimeSpan.FromSeconds(3)) + { + _receivedMessages.Remove(msg); + i--; + } + } + + _lastReceivedMessagesUpdate = checkTime; } } + + /// + /// Set proxy on socket + /// + /// + /// + /// + protected virtual void SetProxy(ClientWebSocket socket, ApiProxy proxy) + { + if (!Uri.TryCreate($"{proxy.Host}:{proxy.Port}", UriKind.Absolute, out var uri)) + throw new ArgumentException("Proxy settings invalid, {proxy.Host}:{proxy.Port} not a valid URI", nameof(proxy)); + + socket.Options.Proxy = uri?.Scheme == null + ? socket.Options.Proxy = new WebProxy(proxy.Host, proxy.Port) + : socket.Options.Proxy = new WebProxy + { + Address = uri + }; + + if (proxy.Login != null) + socket.Options.Proxy.Credentials = new NetworkCredential(proxy.Login, proxy.Password); + } + + private void SetProcessState(ProcessState state) + { + if (_processState == state) + return; + + _logger.SocketProcessingStateChanged(Id, _processState.ToString(), state.ToString()); + _processState = state; + } +} + +/// +/// Message info +/// +public struct SendItem +{ + /// + /// The request id + /// + public int Id { get; set; } + + /// + /// The request weight + /// + public int Weight { get; set; } + + /// + /// Timestamp the request was sent + /// + public DateTime SendTime { get; set; } + + /// + /// Message type + /// + public WebSocketMessageType Type { get; set; } + + /// + /// The bytes to send + /// + public byte[] Bytes { get; set; } +} + +/// +/// Received message info +/// +public struct ReceiveItem +{ + /// + /// Timestamp of the received data + /// + public DateTime Timestamp { get; set; } + /// + /// Number of bytes received + /// + public int Bytes { get; set; } + + /// + /// ctor + /// + /// + /// + public ReceiveItem(DateTime timestamp, int bytes) + { + Timestamp = timestamp; + Bytes = bytes; + } } diff --git a/CryptoExchange.Net/Sockets/DedicatedConnectionConfig.cs b/CryptoExchange.Net/Sockets/DedicatedConnectionConfig.cs index af21b0e..696d9da 100644 --- a/CryptoExchange.Net/Sockets/DedicatedConnectionConfig.cs +++ b/CryptoExchange.Net/Sockets/DedicatedConnectionConfig.cs @@ -1,32 +1,31 @@ -namespace CryptoExchange.Net.Sockets +namespace CryptoExchange.Net.Sockets; + +/// +/// Dedicated connection configuration +/// +public class DedicatedConnectionConfig { /// - /// Dedicated connection configuration + /// Socket address /// - public class DedicatedConnectionConfig - { - /// - /// Socket address - /// - public string SocketAddress { get; set; } = string.Empty; - /// - /// authenticated - /// - public bool Authenticated { get; set; } - } - + public string SocketAddress { get; set; } = string.Empty; /// - /// Dedicated connection state + /// authenticated /// - public class DedicatedConnectionState - { - /// - /// Whether the connection is a dedicated request connection - /// - public bool IsDedicatedRequestConnection { get; set; } - /// - /// Whether the dedication request connection should be authenticated - /// - public bool Authenticated { get; set; } - } + public bool Authenticated { get; set; } +} + +/// +/// Dedicated connection state +/// +public class DedicatedConnectionState +{ + /// + /// Whether the connection is a dedicated request connection + /// + public bool IsDedicatedRequestConnection { get; set; } + /// + /// Whether the dedication request connection should be authenticated + /// + public bool Authenticated { get; set; } } diff --git a/CryptoExchange.Net/Sockets/MessageMatcher.cs b/CryptoExchange.Net/Sockets/MessageMatcher.cs index 2ca7107..2ded141 100644 --- a/CryptoExchange.Net/Sockets/MessageMatcher.cs +++ b/CryptoExchange.Net/Sockets/MessageMatcher.cs @@ -1,180 +1,177 @@ -using CryptoExchange.Net.Interfaces; +using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects.Sockets; using System; using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; -namespace CryptoExchange.Net.Sockets +namespace CryptoExchange.Net.Sockets; + +/// +/// Message link type +/// +public enum MessageLinkType { /// - /// Message link type + /// Match when the listen id matches fully to the value /// - public enum MessageLinkType + Full, + /// + /// Match when the listen id starts with the value + /// + StartsWith +} + +/// +/// Matches a message listen id to a specific listener +/// +public class MessageMatcher +{ + /// + /// Linkers in this matcher + /// + public MessageHandlerLink[] HandlerLinks { get; } + + /// + /// ctor + /// + private MessageMatcher(params MessageHandlerLink[] links) { - /// - /// Match when the listen id matches fully to the value - /// - Full, - /// - /// Match when the listen id starts with the value - /// - StartsWith + HandlerLinks = links; } /// - /// Matches a message listen id to a specific listener + /// Create message matcher /// - public class MessageMatcher + public static MessageMatcher Create(string value) { - /// - /// Linkers in this matcher - /// - public MessageHandlerLink[] HandlerLinks { get; } - - /// - /// ctor - /// - private MessageMatcher(params MessageHandlerLink[] links) - { - HandlerLinks = links; - } - - /// - /// Create message matcher - /// - public static MessageMatcher Create(string value) - { - return new MessageMatcher(new MessageHandlerLink(MessageLinkType.Full, value, (con, msg) => new CallResult(default, msg.OriginalData, null))); - } - - /// - /// Create message matcher - /// - public static MessageMatcher Create(string value, Func, CallResult> handler) - { - return new MessageMatcher(new MessageHandlerLink(MessageLinkType.Full, value, handler)); - } - - /// - /// Create message matcher - /// - public static MessageMatcher Create(IEnumerable values, Func, CallResult> handler) - { - return new MessageMatcher(values.Select(x => new MessageHandlerLink(MessageLinkType.Full, x, handler)).ToArray()); - } - - /// - /// Create message matcher - /// - public static MessageMatcher Create(MessageLinkType type, string value, Func, CallResult> handler) - { - return new MessageMatcher(new MessageHandlerLink(type, value, handler)); - } - - /// - /// Create message matcher - /// - public static MessageMatcher Create(params MessageHandlerLink[] linkers) - { - return new MessageMatcher(linkers); - } - - /// - /// Whether this matcher contains a specific link - /// - public bool ContainsCheck(MessageHandlerLink link) => HandlerLinks.Any(x => x.Type == link.Type && x.Value == link.Value); - - /// - /// Get any handler links matching with the listen id - /// - public List GetHandlerLinks(string listenId) => HandlerLinks.Where(x => x.Check(listenId)).ToList(); - - /// - public override string ToString() => string.Join(",", HandlerLinks.Select(x => x.ToString())); + return new MessageMatcher(new MessageHandlerLink(MessageLinkType.Full, value, (con, msg) => new CallResult(default, msg.OriginalData, null))); } /// - /// Message handler link + /// Create message matcher /// - public abstract class MessageHandlerLink + public static MessageMatcher Create(string value, Func, CallResult> handler) { - /// - /// Type of check - /// - public MessageLinkType Type { get; } - /// - /// String value of the check - /// - public string Value { get; } - /// - /// Deserialization type - /// - public abstract Type GetDeserializationType(IMessageAccessor accessor); - - /// - /// ctor - /// - public MessageHandlerLink(MessageLinkType type, string value) - { - Type = type; - Value = value; - } - - /// - /// Whether this listen id matches this link - /// - public bool Check(string listenId) - { - if (Type == MessageLinkType.Full) - return Value.Equals(listenId, StringComparison.Ordinal); - - return listenId.StartsWith(Value, StringComparison.Ordinal); - } - - /// - /// Message handler - /// - public abstract CallResult Handle(SocketConnection connection, DataEvent message); - - /// - public override string ToString() => $"{Type} match for \"{Value}\""; + return new MessageMatcher(new MessageHandlerLink(MessageLinkType.Full, value, handler)); } /// - /// Message handler link + /// Create message matcher /// - public class MessageHandlerLink: MessageHandlerLink + public static MessageMatcher Create(IEnumerable values, Func, CallResult> handler) { - private Func, CallResult> _handler; + return new MessageMatcher(values.Select(x => new MessageHandlerLink(MessageLinkType.Full, x, handler)).ToArray()); + } - /// - public override Type GetDeserializationType(IMessageAccessor accessor) => typeof(TServer); + /// + /// Create message matcher + /// + public static MessageMatcher Create(MessageLinkType type, string value, Func, CallResult> handler) + { + return new MessageMatcher(new MessageHandlerLink(type, value, handler)); + } - /// - /// ctor - /// - public MessageHandlerLink(string value, Func, CallResult> handler) - : this(MessageLinkType.Full, value, handler) - { - } + /// + /// Create message matcher + /// + public static MessageMatcher Create(params MessageHandlerLink[] linkers) + { + return new MessageMatcher(linkers); + } - /// - /// ctor - /// - public MessageHandlerLink(MessageLinkType type, string value, Func, CallResult> handler) - : base(type, value) - { - _handler = handler; - } + /// + /// Whether this matcher contains a specific link + /// + public bool ContainsCheck(MessageHandlerLink link) => HandlerLinks.Any(x => x.Type == link.Type && x.Value == link.Value); + + /// + /// Get any handler links matching with the listen id + /// + public List GetHandlerLinks(string listenId) => HandlerLinks.Where(x => x.Check(listenId)).ToList(); + + /// + public override string ToString() => string.Join(",", HandlerLinks.Select(x => x.ToString())); +} + +/// +/// Message handler link +/// +public abstract class MessageHandlerLink +{ + /// + /// Type of check + /// + public MessageLinkType Type { get; } + /// + /// String value of the check + /// + public string Value { get; } + /// + /// Deserialization type + /// + public abstract Type GetDeserializationType(IMessageAccessor accessor); + + /// + /// ctor + /// + public MessageHandlerLink(MessageLinkType type, string value) + { + Type = type; + Value = value; + } + + /// + /// Whether this listen id matches this link + /// + public bool Check(string listenId) + { + if (Type == MessageLinkType.Full) + return Value.Equals(listenId, StringComparison.Ordinal); + + return listenId.StartsWith(Value, StringComparison.Ordinal); + } + + /// + /// Message handler + /// + public abstract CallResult Handle(SocketConnection connection, DataEvent message); + + /// + public override string ToString() => $"{Type} match for \"{Value}\""; +} + +/// +/// Message handler link +/// +public class MessageHandlerLink: MessageHandlerLink +{ + private Func, CallResult> _handler; + + /// + public override Type GetDeserializationType(IMessageAccessor accessor) => typeof(TServer); + + /// + /// ctor + /// + public MessageHandlerLink(string value, Func, CallResult> handler) + : this(MessageLinkType.Full, value, handler) + { + } + + /// + /// ctor + /// + public MessageHandlerLink(MessageLinkType type, string value, Func, CallResult> handler) + : base(type, value) + { + _handler = handler; + } - /// - public override CallResult Handle(SocketConnection connection, DataEvent message) - { - return _handler(connection, message.As((TServer)message.Data)); - } + /// + public override CallResult Handle(SocketConnection connection, DataEvent message) + { + return _handler(connection, message.As((TServer)message.Data)); } } diff --git a/CryptoExchange.Net/Sockets/PeriodicTaskRegistration.cs b/CryptoExchange.Net/Sockets/PeriodicTaskRegistration.cs index 9c532bb..adb72dd 100644 --- a/CryptoExchange.Net/Sockets/PeriodicTaskRegistration.cs +++ b/CryptoExchange.Net/Sockets/PeriodicTaskRegistration.cs @@ -1,28 +1,27 @@ -using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects; using System; -namespace CryptoExchange.Net.Sockets +namespace CryptoExchange.Net.Sockets; + +/// +/// Periodic task registration +/// +public class PeriodicTaskRegistration { /// - /// Periodic task registration + /// Identifier /// - public class PeriodicTaskRegistration - { - /// - /// Identifier - /// - public string Identifier { get; set; } = string.Empty; - /// - /// Interval of query - /// - public TimeSpan Interval { get; set; } - /// - /// Delegate for getting the query - /// - public Func QueryDelegate { get; set; } = null!; - /// - /// Callback after query - /// - public Action? Callback { get; set; } - } + public string Identifier { get; set; } = string.Empty; + /// + /// Interval of query + /// + public TimeSpan Interval { get; set; } + /// + /// Delegate for getting the query + /// + public Func QueryDelegate { get; set; } = null!; + /// + /// Callback after query + /// + public Action? Callback { get; set; } } diff --git a/CryptoExchange.Net/Sockets/Query.cs b/CryptoExchange.Net/Sockets/Query.cs index 9d38368..e9393b1 100644 --- a/CryptoExchange.Net/Sockets/Query.cs +++ b/CryptoExchange.Net/Sockets/Query.cs @@ -1,243 +1,266 @@ -using CryptoExchange.Net.Interfaces; +using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects.Sockets; using System; -using System.Collections.Generic; -using System.Linq; using System.Threading; using System.Threading.Tasks; -namespace CryptoExchange.Net.Sockets +namespace CryptoExchange.Net.Sockets; + +/// +/// Query +/// +public abstract class Query : IMessageProcessor, IDisposable { /// - /// Query + /// Unique identifier /// - public abstract class Query : IMessageProcessor + public int Id { get; } = ExchangeHelpers.NextId(); + + /// + /// Has this query been completed + /// + public bool Completed { get; set; } + + /// + /// Timeout for the request + /// + public TimeSpan? RequestTimeout { get; set; } + + /// + /// What should happen if the query times out + /// + public TimeoutBehavior TimeoutBehavior { get; set; } = TimeoutBehavior.Fail; + + /// + /// The number of required responses. Can be more than 1 when for example subscribing multiple symbols streams in a single request, + /// and each symbol receives it's own confirmation response + /// + public int RequiredResponses { get; set; } = 1; + + /// + /// The current number of responses received on this query + /// + public int CurrentResponses { get; set; } + + /// + /// Timestamp of when the request was send + /// + public DateTime RequestTimestamp { get; set; } + + /// + /// Result + /// + public CallResult? Result { get; set; } + + /// + /// Response + /// + public object? Response { get; set; } + + /// + /// Wait event for the calling message processing thread + /// + public AsyncResetEvent? ContinueAwaiter { get; set; } + + /// + /// Matcher for this query + /// + public MessageMatcher MessageMatcher { get; set; } = null!; + + /// + /// The query request object + /// + public object Request { get; set; } + + /// + /// If this is a private request + /// + public bool Authenticated { get; } + + /// + /// Weight of the query + /// + public int Weight { get; } + + /// + /// Whether the query should wait for a response or not + /// + public bool ExpectsResponse { get; set; } = true; + + /// + /// Wait event for response + /// + protected AsyncResetEvent _event; + + /// + /// Cancellation token + /// + protected CancellationTokenSource? _cts; + private bool _disposedValue; + + /// + /// ctor + /// + /// + /// + /// + public Query(object request, bool authenticated, int weight = 1) { - /// - /// Unique identifier - /// - public int Id { get; } = ExchangeHelpers.NextId(); - - /// - /// Has this query been completed - /// - public bool Completed { get; set; } - - /// - /// Timeout for the request - /// - public TimeSpan? RequestTimeout { get; set; } - - /// - /// What should happen if the query times out - /// - public TimeoutBehavior TimeoutBehavior { get; set; } = TimeoutBehavior.Fail; - - /// - /// The number of required responses. Can be more than 1 when for example subscribing multiple symbols streams in a single request, - /// and each symbol receives it's own confirmation response - /// - public int RequiredResponses { get; set; } = 1; - - /// - /// The current number of responses received on this query - /// - public int CurrentResponses { get; set; } - - /// - /// Timestamp of when the request was send - /// - public DateTime RequestTimestamp { get; set; } - - /// - /// Result - /// - public CallResult? Result { get; set; } - - /// - /// Response - /// - public object? Response { get; set; } - - /// - /// Wait event for the calling message processing thread - /// - public AsyncResetEvent? ContinueAwaiter { get; set; } - - /// - /// Matcher for this query - /// - public MessageMatcher MessageMatcher { get; set; } = null!; - - /// - /// The query request object - /// - public object Request { get; set; } - - /// - /// If this is a private request - /// - public bool Authenticated { get; } - - /// - /// Weight of the query - /// - public int Weight { get; } - - /// - /// Whether the query should wait for a response or not - /// - public bool ExpectsResponse { get; set; } = true; - - /// - /// Wait event for response - /// - protected AsyncResetEvent _event; - - /// - /// Cancellation token - /// - protected CancellationTokenSource? _cts; - - /// - /// ctor - /// - /// - /// - /// - public Query(object request, bool authenticated, int weight = 1) - { - _event = new AsyncResetEvent(false, false); - - Authenticated = authenticated; - Request = request; - Weight = weight; - } - - /// - /// Signal that the request has been send and the timeout timer should start - /// - public void IsSend(TimeSpan timeout) - { - RequestTimestamp = DateTime.UtcNow; - if (ExpectsResponse) - { - // Start timeout countdown - _cts = new CancellationTokenSource(timeout); - _cts.Token.Register(Timeout, false); - } - else - { - Completed = true; - Result = CallResult.SuccessResult; - _event.Set(); - } - } - - /// - /// Wait until timeout or the request is completed - /// - /// - /// Cancellation token - /// - public async Task WaitAsync(TimeSpan timeout, CancellationToken ct) => await _event.WaitAsync(timeout, ct).ConfigureAwait(false); - - /// - public virtual CallResult Deserialize(IMessageAccessor message, Type type) => message.Deserialize(type); - - /// - /// Mark request as timeout - /// - public abstract void Timeout(); - - /// - /// Mark request as failed - /// - /// - public abstract void Fail(Error error); - - /// - /// Handle a response message - /// - public abstract Task Handle(SocketConnection connection, DataEvent message, MessageHandlerLink check); + _event = new AsyncResetEvent(false, false); + Authenticated = authenticated; + Request = request; + Weight = weight; } /// - /// Query + /// Signal that the request has been send and the timeout timer should start /// - /// The type to be returned to the caller - public abstract class Query : Query + public void IsSend(TimeSpan timeout) { - /// - /// The typed call result - /// - public CallResult? TypedResult => (CallResult?)Result; - - /// - /// ctor - /// - /// - /// - /// - protected Query(object request, bool authenticated, int weight = 1) : base(request, authenticated, weight) + RequestTimestamp = DateTime.UtcNow; + if (ExpectsResponse) { + // Start timeout countdown + _cts = new CancellationTokenSource(timeout); + _cts.Token.Register(Timeout, false); } - - /// - public override async Task Handle(SocketConnection connection, DataEvent message, MessageHandlerLink check) + else { - if (!PreCheckMessage(connection, message)) - return CallResult.SuccessResult; - - CurrentResponses++; - if (CurrentResponses == RequiredResponses) - Response = message.Data; - - if (Result?.Success != false) - // If an error result is already set don't override that - Result = check.Handle(connection, message); - - if (CurrentResponses == RequiredResponses) - { - Completed = true; - _event.Set(); - if (ContinueAwaiter != null) - await ContinueAwaiter.WaitAsync().ConfigureAwait(false); - } - - return Result; - } - - /// - /// Validate if a message is actually processable by this query - /// - public virtual bool PreCheckMessage(SocketConnection connection, DataEvent message) => true; - - /// - public override void Timeout() - { - if (Completed) - return; - Completed = true; - if (TimeoutBehavior == TimeoutBehavior.Fail) - Result = new CallResult(new TimeoutError()); - else - Result = new CallResult(default, null, default); - - ContinueAwaiter?.Set(); - _event.Set(); - } - - /// - public override void Fail(Error error) - { - Result = new CallResult(error); - Completed = true; - ContinueAwaiter?.Set(); + Result = CallResult.SuccessResult; _event.Set(); } } + + /// + /// Wait until timeout or the request is completed + /// + /// + /// Cancellation token + /// + public async Task WaitAsync(TimeSpan timeout, CancellationToken ct) => await _event.WaitAsync(timeout, ct).ConfigureAwait(false); + + /// + public virtual CallResult Deserialize(IMessageAccessor accessor, Type type) => accessor.Deserialize(type); + + /// + /// Mark request as timeout + /// + public abstract void Timeout(); + + /// + /// Mark request as failed + /// + public abstract void Fail(Error error); + + /// + /// Handle a response message + /// + public abstract Task Handle(SocketConnection connection, DataEvent message, MessageHandlerLink matchedHandler); + + /// + /// Dispose + /// + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + // TODO: dispose managed state (managed objects) + _cts?.Dispose(); + _event.Dispose(); + } + + _disposedValue = true; + } + } + + /// + /// Dispose + /// + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } +} + +/// +/// Query +/// +/// The type to be returned to the caller +public abstract class Query : Query +{ + /// + /// The typed call result + /// + public CallResult? TypedResult => (CallResult?)Result; + + /// + /// ctor + /// + /// + /// + /// + protected Query(object request, bool authenticated, int weight = 1) : base(request, authenticated, weight) + { + } + + /// + public override async Task Handle(SocketConnection connection, DataEvent message, MessageHandlerLink matchedHandler) + { + if (!PreCheckMessage(connection, message)) + return CallResult.SuccessResult; + + CurrentResponses++; + if (CurrentResponses == RequiredResponses) + Response = message.Data; + + if (Result?.Success != false) + // If an error result is already set don't override that + Result = matchedHandler.Handle(connection, message); + + if (CurrentResponses == RequiredResponses) + { + Completed = true; + _event.Set(); + if (ContinueAwaiter != null) + await ContinueAwaiter.WaitAsync().ConfigureAwait(false); + } + + return Result; + } + + /// + /// Validate if a message is actually processable by this query + /// + public virtual bool PreCheckMessage(SocketConnection connection, DataEvent message) => true; + + /// + public override void Timeout() + { + if (Completed) + return; + + Completed = true; + if (TimeoutBehavior == TimeoutBehavior.Fail) + Result = new CallResult(new TimeoutError()); + else + Result = new CallResult(default, null, default); + + ContinueAwaiter?.Set(); + _event.Set(); + } + + /// + public override void Fail(Error error) + { + Result = new CallResult(error); + Completed = true; + ContinueAwaiter?.Set(); + _event.Set(); + } } diff --git a/CryptoExchange.Net/Sockets/SocketConnection.cs b/CryptoExchange.Net/Sockets/SocketConnection.cs index 9c8d84e..91a3cd2 100644 --- a/CryptoExchange.Net/Sockets/SocketConnection.cs +++ b/CryptoExchange.Net/Sockets/SocketConnection.cs @@ -1,4 +1,4 @@ -using CryptoExchange.Net.Interfaces; +using CryptoExchange.Net.Interfaces; using System; using System.Collections.Generic; using System.Linq; @@ -12,1145 +12,1144 @@ using CryptoExchange.Net.Clients; using CryptoExchange.Net.Logging.Extensions; using System.Threading; -namespace CryptoExchange.Net.Sockets +namespace CryptoExchange.Net.Sockets; + +/// +/// A single socket connection to the server +/// +public class SocketConnection : IDisposable { /// - /// A single socket connection to the server + /// State of a the connection /// - public class SocketConnection + /// The id of the socket connection + /// The connection URI + /// Number of subscriptions on this socket + /// Socket status + /// If the connection is authenticated + /// Download speed over this socket + /// Number of non-completed queries + /// State for each subscription on this socket + public record SocketConnectionState( + int Id, + string Address, + int Subscriptions, + SocketStatus Status, + bool Authenticated, + double DownloadSpeed, + int PendingQueries, + List SubscriptionStates + ); + + /// + /// Connection lost event + /// + public event Action? ConnectionLost; + + /// + /// Connection closed and no reconnect is happening + /// + public event Action? ConnectionClosed; + + /// + /// Failed to resubscribe all subscription on the reconnected socket + /// + public event Action? ResubscribingFailed; + + /// + /// Connecting restored event + /// + public event Action? ConnectionRestored; + + /// + /// The connection is paused event + /// + public event Action? ActivityPaused; + + /// + /// The connection is unpaused event + /// + public event Action? ActivityUnpaused; + + /// + /// Unhandled message event + /// + public event Action? UnhandledMessage; + + /// + /// Connection was rate limited and couldn't be established + /// + public event Func? ConnectRateLimitedAsync; + + /// + /// The amount of subscriptions on this connection + /// + public int UserSubscriptionCount { - /// - /// State of a the connection - /// - /// The id of the socket connection - /// The connection URI - /// Number of subscriptions on this socket - /// Socket status - /// If the connection is authenticated - /// Download speed over this socket - /// Number of non-completed queries - /// State for each subscription on this socket - public record SocketConnectionState( - int Id, - string Address, - int Subscriptions, - SocketStatus Status, - bool Authenticated, - double DownloadSpeed, - int PendingQueries, - List SubscriptionStates - ); - - /// - /// Connection lost event - /// - public event Action? ConnectionLost; - - /// - /// Connection closed and no reconnect is happening - /// - public event Action? ConnectionClosed; - - /// - /// Failed to resubscribe all subscription on the reconnected socket - /// - public event Action? ResubscribingFailed; - - /// - /// Connecting restored event - /// - public event Action? ConnectionRestored; - - /// - /// The connection is paused event - /// - public event Action? ActivityPaused; - - /// - /// The connection is unpaused event - /// - public event Action? ActivityUnpaused; - - /// - /// Unhandled message event - /// - public event Action? UnhandledMessage; - - /// - /// Connection was rate limited and couldn't be established - /// - public Func? ConnectRateLimitedAsync; - - /// - /// The amount of subscriptions on this connection - /// - public int UserSubscriptionCount - { - get - { - lock(_listenersLock) - return _listeners.OfType().Count(h => h.UserSubscription); - } - } - - /// - /// Get a copy of the current message subscriptions - /// - public Subscription[] Subscriptions - { - get - { - lock(_listenersLock) - return _listeners.OfType().Where(h => h.UserSubscription).ToArray(); - } - } - - /// - /// If the connection has been authenticated - /// - public bool Authenticated { get; set; } - - /// - /// If connection is made - /// - public bool Connected => _socket.IsOpen; - - /// - /// The unique ID of the socket - /// - public int SocketId => _socket.Id; - - /// - /// The current kilobytes per second of data being received, averaged over the last 3 seconds - /// - public double IncomingKbps => _socket.IncomingKbps; - - /// - /// The connection uri - /// - public Uri ConnectionUri => _socket.Uri; - - /// - /// The API client the connection is for - /// - public SocketApiClient ApiClient { get; set; } - - /// - /// Time of disconnecting - /// - public DateTime? DisconnectTime { get; set; } - - /// - /// Tag for identification - /// - public string Tag { get; set; } - - /// - /// Additional properties for this connection - /// - public Dictionary Properties { get; set; } - - /// - /// If activity is paused - /// - public bool PausedActivity - { - get => _pausedActivity; - set - { - if (_pausedActivity != value) - { - _pausedActivity = value; - _logger.ActivityPaused(SocketId, value); - if(_pausedActivity) _ = Task.Run(() => ActivityPaused?.Invoke()); - else _ = Task.Run(() => ActivityUnpaused?.Invoke()); - } - } - } - - /// - /// Status of the socket connection - /// - public SocketStatus Status - { - get => _status; - private set - { - if (_status == value) - return; - - var oldStatus = _status; - _status = value; - _logger.SocketStatusChanged(SocketId, oldStatus, value); - } - } - - /// - /// Info on whether this connection is a dedicated request connection - /// - public DedicatedConnectionState DedicatedRequestConnection { get; internal set; } = new DedicatedConnectionState(); - - /// - /// Current subscription topics on this connection - /// - public string[] Topics - { - get - { - lock (_listenersLock) - return _listeners.OfType().Select(x => x.Topic).Where(t => t != null).ToArray()!; - } - } - - /// - /// The number of current pending requests - /// - public int PendingRequests - { - get - { - lock (_listenersLock) - return _listeners.OfType().Where(x => !x.Completed).Count(); - } - } - - private bool _pausedActivity; - private readonly object _listenersLock; - private readonly List _listeners; - private readonly ILogger _logger; - private SocketStatus _status; - - private readonly IMessageSerializer _serializer; - private IByteMessageAccessor? _stringMessageAccessor; - private IByteMessageAccessor? _byteMessageAccessor; - - /// - /// The task that is sending periodic data on the websocket. Can be used for sending Ping messages every x seconds or similar. Not necessary. - /// - protected Task? periodicTask; - - /// - /// Wait event for the periodicTask - /// - protected AsyncResetEvent? periodicEvent; - - /// - /// The underlying websocket - /// - private readonly IWebsocket _socket; - - /// - /// Cache for deserialization, only caches for a single message - /// - private readonly Dictionary _deserializationCache = new Dictionary(); - - /// - /// New socket connection - /// - /// The logger - /// The api client - /// The socket - /// - public SocketConnection(ILogger logger, SocketApiClient apiClient, IWebsocket socket, string tag) - { - _logger = logger; - ApiClient = apiClient; - Tag = tag; - Properties = new Dictionary(); - - _socket = socket; - _socket.OnStreamMessage += HandleStreamMessage; - _socket.OnRequestSent += HandleRequestSentAsync; - _socket.OnRequestRateLimited += HandleRequestRateLimitedAsync; - _socket.OnConnectRateLimited += HandleConnectRateLimitedAsync; - _socket.OnOpen += HandleOpenAsync; - _socket.OnClose += HandleCloseAsync; - _socket.OnReconnecting += HandleReconnectingAsync; - _socket.OnReconnected += HandleReconnectedAsync; - _socket.OnError += HandleErrorAsync; - _socket.GetReconnectionUrl = GetReconnectionUrlAsync; - - _listenersLock = new object(); - _listeners = new List(); - - _serializer = apiClient.CreateSerializer(); - } - - /// - /// Handler for a socket opening - /// - protected virtual Task HandleOpenAsync() - { - Status = SocketStatus.Connected; - PausedActivity = false; - return Task.CompletedTask; - } - - /// - /// Handler for a socket closing without reconnect - /// - protected virtual Task HandleCloseAsync() - { - Status = SocketStatus.Closed; - Authenticated = false; - - lock (_listenersLock) - { - foreach (var subscription in _listeners.OfType().Where(l => l.UserSubscription)) - subscription.Reset(); - - foreach (var query in _listeners.OfType().ToList()) - { - query.Fail(new WebError("Connection interrupted")); - _listeners.Remove(query); - } - } - - _ = Task.Run(() => ConnectionClosed?.Invoke()); - return Task.CompletedTask; - } - - /// - /// Handler for a socket losing connection and starting reconnect - /// - protected virtual Task HandleReconnectingAsync() - { - Status = SocketStatus.Reconnecting; - DisconnectTime = DateTime.UtcNow; - Authenticated = false; - - lock (_listenersLock) - { - foreach (var subscription in _listeners.OfType().Where(l => l.UserSubscription)) - subscription.Reset(); - - foreach (var query in _listeners.OfType().ToList()) - { - query.Fail(new WebError("Connection interrupted")); - _listeners.Remove(query); - } - } - - _ = Task.Run(() => ConnectionLost?.Invoke()); - return Task.CompletedTask; - } - - /// - /// Get the url to connect to when reconnecting - /// - /// - protected virtual async Task GetReconnectionUrlAsync() - { - return await ApiClient.GetReconnectUriAsync(this).ConfigureAwait(false); - } - - /// - /// Handler for a socket which has reconnected - /// - protected virtual Task HandleReconnectedAsync() - { - Status = SocketStatus.Resubscribing; - - lock (_listenersLock) - { - foreach (var query in _listeners.OfType().ToList()) - { - query.Fail(new WebError("Connection interrupted")); - _listeners.Remove(query); - } - } - - // Can't wait for this as it would cause a deadlock - _ = Task.Run(async () => - { - try - { - var reconnectSuccessful = await ProcessReconnectAsync().ConfigureAwait(false); - if (!reconnectSuccessful) - { - _logger.FailedReconnectProcessing(SocketId, reconnectSuccessful.Error!.ToString()); - _ = Task.Run(() => ResubscribingFailed?.Invoke(reconnectSuccessful.Error)); - _ = _socket.ReconnectAsync().ConfigureAwait(false); - } - else - { - Status = SocketStatus.Connected; - _ = Task.Run(() => - { - ConnectionRestored?.Invoke(DateTime.UtcNow - DisconnectTime!.Value); - DisconnectTime = null; - }); - } - } - catch(Exception ex) - { - _logger.UnknownExceptionWhileProcessingReconnection(SocketId, ex); - _ = _socket.ReconnectAsync().ConfigureAwait(false); - } - }); - - return Task.CompletedTask; - } - - /// - /// Handler for an error on a websocket - /// - /// The exception - protected virtual Task HandleErrorAsync(Exception e) - { - if (e is WebSocketException wse) - _logger.WebSocketErrorCodeAndDetails(SocketId, wse.WebSocketErrorCode, wse.Message, wse); - else - _logger.WebSocketError(SocketId, e.Message, e); - - return Task.CompletedTask; - } - - /// - /// Handler for whenever a request is rate limited and rate limit behavior is set to fail - /// - /// - /// - protected virtual Task HandleRequestRateLimitedAsync(int requestId) - { - Query? query; - lock (_listenersLock) - { - query = _listeners.OfType().FirstOrDefault(x => x.Id == requestId); - } - - if (query == null) - return Task.CompletedTask; - - query.Fail(new ClientRateLimitError("Connection rate limit reached")); - return Task.CompletedTask; - } - - /// - /// Handler for whenever a connection was rate limited and couldn't be established - /// - /// - protected async virtual Task HandleConnectRateLimitedAsync() - { - if (ConnectRateLimitedAsync is not null) - await ConnectRateLimitedAsync().ConfigureAwait(false); - } - - /// - /// Handler for whenever a request is sent over the websocket - /// - /// Id of the request sent - protected virtual Task HandleRequestSentAsync(int requestId) - { - Query? query; - lock (_listenersLock) - { - query = _listeners.OfType().FirstOrDefault(x => x.Id == requestId); - } - - if (query == null) - { - _logger.MessageSentNotPending(SocketId, requestId); - return Task.CompletedTask; - } - - query.IsSend(query.RequestTimeout ?? ApiClient.ClientOptions.RequestTimeout); - return Task.CompletedTask; - } - - /// - /// Handle a message - /// - protected virtual async Task HandleStreamMessage(WebSocketMessageType type, ReadOnlyMemory data) - { - var sw = Stopwatch.StartNew(); - var receiveTime = DateTime.UtcNow; - string? originalData = null; - - // 1. Decrypt/Preprocess if necessary - data = ApiClient.PreprocessStreamMessage(this, type, data); - - // 2. Read data into accessor - IByteMessageAccessor accessor; - if (type == WebSocketMessageType.Binary) - accessor = _stringMessageAccessor ??= ApiClient.CreateAccessor(type); - else - accessor = _byteMessageAccessor ??= ApiClient.CreateAccessor(type); - - var result = accessor.Read(data); - try - { - bool outputOriginalData = ApiClient.ApiOptions.OutputOriginalData ?? ApiClient.ClientOptions.OutputOriginalData; - if (outputOriginalData) - { - originalData = accessor.GetOriginalString(); - _logger.ReceivedData(SocketId, originalData); - } - - if (!accessor.IsValid && !ApiClient.ProcessUnparsableMessages) - { - _logger.FailedToParse(SocketId, result.Error!.Message ?? result.Error!.ErrorDescription!); - return; - } - - // 3. Determine the identifying properties of this message - var listenId = ApiClient.GetListenerIdentifier(accessor); - if (listenId == null) - { - originalData ??= "[OutputOriginalData is false]"; - if (!ApiClient.UnhandledMessageExpected) - _logger.FailedToEvaluateMessage(SocketId, originalData); - - UnhandledMessage?.Invoke(accessor); - return; - } - - bool processed = false; - var totalUserTime = 0; - - List localListeners; - lock(_listenersLock) - localListeners = _listeners.ToList(); - - foreach(var processor in localListeners) - { - foreach(var listener in processor.MessageMatcher.GetHandlerLinks(listenId)) - { - processed = true; - _logger.ProcessorMatched(SocketId, listener.ToString(), listenId); - - // 4. Determine the type to deserialize to for this processor - var messageType = listener.GetDeserializationType(accessor); - if (messageType == null) - { - _logger.ReceivedMessageNotRecognized(SocketId, processor.Id); - continue; - } - - if (processor is Subscription subscriptionProcessor && !subscriptionProcessor.Confirmed) - { - // If this message is for this listener then it is automatically confirmed, even if the subscription is not (yet) confirmed - subscriptionProcessor.Confirmed = true; - if (subscriptionProcessor.SubscriptionQuery?.TimeoutBehavior == TimeoutBehavior.Succeed) - // If this subscription has a query waiting for a timeout (success if there is no error response) - // then time it out now as the data is being received, so we assume it's successful - subscriptionProcessor.SubscriptionQuery.Timeout(); - } - - // 5. Deserialize the message - _deserializationCache.TryGetValue(messageType, out var deserialized); - - if (deserialized == null) - { - var desResult = processor.Deserialize(accessor, messageType); - if (!desResult) - { - _logger.FailedToDeserializeMessage(SocketId, desResult.Error?.ToString(), desResult.Error?.Exception); - continue; - } - - deserialized = desResult.Data; - _deserializationCache.Add(messageType, deserialized); - } - - // 6. Pass the message to the handler - try - { - var innerSw = Stopwatch.StartNew(); - await processor.Handle(this, new DataEvent(deserialized, null, null, originalData, receiveTime, null), listener).ConfigureAwait(false); - if (processor is Query query && query.RequiredResponses != 1) - _logger.LogDebug($"[Sckt {SocketId}] [Req {query.Id}] responses: {query.CurrentResponses}/{query.RequiredResponses}"); - totalUserTime += (int)innerSw.ElapsedMilliseconds; - } - catch (Exception ex) - { - _logger.UserMessageProcessingFailed(SocketId, ex.Message, ex); - if (processor is Subscription subscription) - subscription.InvokeExceptionHandler(ex); - } - - } - } - - if (!processed) - { - if (!ApiClient.UnhandledMessageExpected) - { - List listenerIds; - lock (_listenersLock) - listenerIds = _listeners.Select(l => l.MessageMatcher.ToString()).ToList(); - - _logger.ReceivedMessageNotMatchedToAnyListener(SocketId, listenId, string.Join(",", listenerIds)); - UnhandledMessage?.Invoke(accessor); - } - - return; - } - - _logger.MessageProcessed(SocketId, sw.ElapsedMilliseconds, sw.ElapsedMilliseconds - totalUserTime); - } - finally - { - _deserializationCache.Clear(); - accessor.Clear(); - } - } - - /// - /// Connect the websocket - /// - /// - public async Task ConnectAsync(CancellationToken ct) => await _socket.ConnectAsync(ct).ConfigureAwait(false); - - /// - /// Retrieve the underlying socket - /// - /// - public IWebsocket GetSocket() => _socket; - - /// - /// Trigger a reconnect of the socket connection - /// - /// - public async Task TriggerReconnectAsync() => await _socket.ReconnectAsync().ConfigureAwait(false); - - /// - /// Update the proxy setting and reconnect - /// - /// New proxy setting - public async Task UpdateProxy(ApiProxy? proxy) - { - _socket.UpdateProxy(proxy); - await TriggerReconnectAsync().ConfigureAwait(false); - } - - /// - /// Close the connection - /// - /// - public async Task CloseAsync() - { - if (Status == SocketStatus.Closed || Status == SocketStatus.Disposed) - return; - - if (ApiClient.socketConnections.ContainsKey(SocketId)) - ApiClient.socketConnections.TryRemove(SocketId, out _); - - lock (_listenersLock) - { - foreach (var subscription in _listeners.OfType()) - { - if (subscription.CancellationTokenRegistration.HasValue) - subscription.CancellationTokenRegistration.Value.Dispose(); - } - } - - await _socket.CloseAsync().ConfigureAwait(false); - _socket.Dispose(); - } - - /// - /// Close a subscription on this connection. If all subscriptions on this connection are closed the connection gets closed as well - /// - /// Subscription to close - /// - public async Task CloseAsync(Subscription subscription) - { - // If we are resubscribing this subscription at this moment we'll want to wait for a bit until it is finished to avoid concurrency issues - while (subscription.IsResubscribing) - await Task.Delay(50).ConfigureAwait(false); - - subscription.Closed = true; - - if (Status == SocketStatus.Closing || Status == SocketStatus.Closed || Status == SocketStatus.Disposed) - return; - - _logger.ClosingSubscription(SocketId, subscription.Id); - if (subscription.CancellationTokenRegistration.HasValue) - subscription.CancellationTokenRegistration.Value.Dispose(); - - bool anyDuplicateSubscription; - lock (_listenersLock) - anyDuplicateSubscription = _listeners.OfType().Any(x => x != subscription && x.MessageMatcher.HandlerLinks.All(l => subscription.MessageMatcher.ContainsCheck(l))); - - bool shouldCloseConnection; - lock (_listenersLock) - shouldCloseConnection = _listeners.OfType().All(r => !r.UserSubscription || r.Closed) && !DedicatedRequestConnection.IsDedicatedRequestConnection; - - if (!anyDuplicateSubscription) - { - bool needUnsub; - lock (_listenersLock) - needUnsub = _listeners.Contains(subscription) && !shouldCloseConnection; - - if (needUnsub && _socket.IsOpen) - await UnsubscribeAsync(subscription).ConfigureAwait(false); - } - else - { - _logger.NotUnsubscribingSubscriptionBecauseDuplicateRunning(SocketId); - } - - if (Status == SocketStatus.Closing) - { - _logger.AlreadyClosing(SocketId); - return; - } - - if (shouldCloseConnection) - { - Status = SocketStatus.Closing; - _logger.ClosingNoMoreSubscriptions(SocketId); - await CloseAsync().ConfigureAwait(false); - } - - lock (_listenersLock) - _listeners.Remove(subscription); - } - - /// - /// Dispose the connection - /// - public void Dispose() - { - Status = SocketStatus.Disposed; - periodicEvent?.Set(); - periodicEvent?.Dispose(); - _socket.Dispose(); - } - - /// - /// Whether or not a new subscription can be added to this connection - /// - /// - public bool CanAddSubscription() => Status == SocketStatus.None || Status == SocketStatus.Connected; - - /// - /// Add a subscription to this connection - /// - /// - public bool AddSubscription(Subscription subscription) - { - if (Status != SocketStatus.None && Status != SocketStatus.Connected) - return false; - - lock (_listenersLock) - _listeners.Add(subscription); - - if (subscription.UserSubscription) - _logger.AddingNewSubscription(SocketId, subscription.Id, UserSubscriptionCount); - return true; - } - - /// - /// Get a subscription on this connection by id - /// - /// - public Subscription? GetSubscription(int id) - { - lock (_listenersLock) - return _listeners.OfType().SingleOrDefault(s => s.Id == id); - } - - /// - /// Get the state of the connection - /// - /// - public SocketConnectionState GetState(bool includeSubDetails) - { - return new SocketConnectionState( - SocketId, - ConnectionUri.AbsoluteUri, - UserSubscriptionCount, - Status, - Authenticated, - IncomingKbps, - PendingQueries: _listeners.OfType().Count(x => !x.Completed), - includeSubDetails ? Subscriptions.Select(sub => sub.GetState()).ToList() : new List() - ); - } - - /// - /// Send a query request and wait for an answer - /// - /// Query to send - /// Wait event for when the socket message handler can continue - /// Cancellation token - /// - public virtual async Task SendAndWaitQueryAsync(Query query, AsyncResetEvent? continueEvent = null, CancellationToken ct = default) - { - await SendAndWaitIntAsync(query, continueEvent, ct).ConfigureAwait(false); - return query.Result ?? new CallResult(new TimeoutError()); - } - - /// - /// Send a query request and wait for an answer - /// - /// Expected result type - /// Query to send - /// Wait event for when the socket message handler can continue - /// Cancellation token - /// - public virtual async Task> SendAndWaitQueryAsync(Query query, AsyncResetEvent? continueEvent = null, CancellationToken ct = default) - { - await SendAndWaitIntAsync(query, continueEvent, ct).ConfigureAwait(false); - return query.TypedResult ?? new CallResult(new TimeoutError()); - } - - private async Task SendAndWaitIntAsync(Query query, AsyncResetEvent? continueEvent, CancellationToken ct = default) + get { lock(_listenersLock) - _listeners.Add(query); + return _listeners.OfType().Count(h => h.UserSubscription); + } + } - query.ContinueAwaiter = continueEvent; - var sendResult = Send(query.Id, query.Request, query.Weight); - if (!sendResult) + /// + /// Get a copy of the current message subscriptions + /// + public Subscription[] Subscriptions + { + get + { + lock(_listenersLock) + return _listeners.OfType().Where(h => h.UserSubscription).ToArray(); + } + } + + /// + /// If the connection has been authenticated + /// + public bool Authenticated { get; set; } + + /// + /// If connection is made + /// + public bool Connected => _socket.IsOpen; + + /// + /// The unique ID of the socket + /// + public int SocketId => _socket.Id; + + /// + /// The current kilobytes per second of data being received, averaged over the last 3 seconds + /// + public double IncomingKbps => _socket.IncomingKbps; + + /// + /// The connection uri + /// + public Uri ConnectionUri => _socket.Uri; + + /// + /// The API client the connection is for + /// + public SocketApiClient ApiClient { get; set; } + + /// + /// Time of disconnecting + /// + public DateTime? DisconnectTime { get; set; } + + /// + /// Tag for identification + /// + public string Tag { get; set; } + + /// + /// Additional properties for this connection + /// + public Dictionary Properties { get; set; } + + /// + /// If activity is paused + /// + public bool PausedActivity + { + get => _pausedActivity; + set + { + if (_pausedActivity != value) { - query.Fail(sendResult.Error!); - lock (_listenersLock) - _listeners.Remove(query); + _pausedActivity = value; + _logger.ActivityPaused(SocketId, value); + if(_pausedActivity) _ = Task.Run(() => ActivityPaused?.Invoke()); + else _ = Task.Run(() => ActivityUnpaused?.Invoke()); + } + } + } + + /// + /// Status of the socket connection + /// + public SocketStatus Status + { + get => _status; + private set + { + if (_status == value) return; - } + var oldStatus = _status; + _status = value; + _logger.SocketStatusChanged(SocketId, oldStatus, value); + } + } + + /// + /// Info on whether this connection is a dedicated request connection + /// + public DedicatedConnectionState DedicatedRequestConnection { get; internal set; } = new DedicatedConnectionState(); + + /// + /// Current subscription topics on this connection + /// + public string[] Topics + { + get + { + lock (_listenersLock) + return _listeners.OfType().Select(x => x.Topic).Where(t => t != null).ToArray()!; + } + } + + /// + /// The number of current pending requests + /// + public int PendingRequests + { + get + { + lock (_listenersLock) + return _listeners.OfType().Where(x => !x.Completed).Count(); + } + } + + private bool _pausedActivity; + private readonly object _listenersLock; + private readonly List _listeners; + private readonly ILogger _logger; + private SocketStatus _status; + + private readonly IMessageSerializer _serializer; + private IByteMessageAccessor? _stringMessageAccessor; + private IByteMessageAccessor? _byteMessageAccessor; + + /// + /// The task that is sending periodic data on the websocket. Can be used for sending Ping messages every x seconds or similar. Not necessary. + /// + protected Task? periodicTask; + + /// + /// Wait event for the periodicTask + /// + protected AsyncResetEvent? periodicEvent; + + /// + /// The underlying websocket + /// + private readonly IWebsocket _socket; + + /// + /// Cache for deserialization, only caches for a single message + /// + private readonly Dictionary _deserializationCache = new Dictionary(); + + /// + /// New socket connection + /// + /// The logger + /// The api client + /// The socket + /// + public SocketConnection(ILogger logger, SocketApiClient apiClient, IWebsocket socket, string tag) + { + _logger = logger; + ApiClient = apiClient; + Tag = tag; + Properties = new Dictionary(); + + _socket = socket; + _socket.OnStreamMessage += HandleStreamMessage; + _socket.OnRequestSent += HandleRequestSentAsync; + _socket.OnRequestRateLimited += HandleRequestRateLimitedAsync; + _socket.OnConnectRateLimited += HandleConnectRateLimitedAsync; + _socket.OnOpen += HandleOpenAsync; + _socket.OnClose += HandleCloseAsync; + _socket.OnReconnecting += HandleReconnectingAsync; + _socket.OnReconnected += HandleReconnectedAsync; + _socket.OnError += HandleErrorAsync; + _socket.GetReconnectionUrl = GetReconnectionUrlAsync; + + _listenersLock = new object(); + _listeners = new List(); + + _serializer = apiClient.CreateSerializer(); + } + + /// + /// Handler for a socket opening + /// + protected virtual Task HandleOpenAsync() + { + Status = SocketStatus.Connected; + PausedActivity = false; + return Task.CompletedTask; + } + + /// + /// Handler for a socket closing without reconnect + /// + protected virtual Task HandleCloseAsync() + { + Status = SocketStatus.Closed; + Authenticated = false; + + lock (_listenersLock) + { + foreach (var subscription in _listeners.OfType().Where(l => l.UserSubscription)) + subscription.Reset(); + + foreach (var query in _listeners.OfType().ToList()) + { + query.Fail(new WebError("Connection interrupted")); + _listeners.Remove(query); + } + } + + _ = Task.Run(() => ConnectionClosed?.Invoke()); + return Task.CompletedTask; + } + + /// + /// Handler for a socket losing connection and starting reconnect + /// + protected virtual Task HandleReconnectingAsync() + { + Status = SocketStatus.Reconnecting; + DisconnectTime = DateTime.UtcNow; + Authenticated = false; + + lock (_listenersLock) + { + foreach (var subscription in _listeners.OfType().Where(l => l.UserSubscription)) + subscription.Reset(); + + foreach (var query in _listeners.OfType().ToList()) + { + query.Fail(new WebError("Connection interrupted")); + _listeners.Remove(query); + } + } + + _ = Task.Run(() => ConnectionLost?.Invoke()); + return Task.CompletedTask; + } + + /// + /// Get the url to connect to when reconnecting + /// + /// + protected virtual async Task GetReconnectionUrlAsync() + { + return await ApiClient.GetReconnectUriAsync(this).ConfigureAwait(false); + } + + /// + /// Handler for a socket which has reconnected + /// + protected virtual Task HandleReconnectedAsync() + { + Status = SocketStatus.Resubscribing; + + lock (_listenersLock) + { + foreach (var query in _listeners.OfType().ToList()) + { + query.Fail(new WebError("Connection interrupted")); + _listeners.Remove(query); + } + } + + // Can't wait for this as it would cause a deadlock + _ = Task.Run(async () => + { try { - while (!ct.IsCancellationRequested) + var reconnectSuccessful = await ProcessReconnectAsync().ConfigureAwait(false); + if (!reconnectSuccessful) { - if (!_socket.IsOpen) + _logger.FailedReconnectProcessing(SocketId, reconnectSuccessful.Error!.ToString()); + _ = Task.Run(() => ResubscribingFailed?.Invoke(reconnectSuccessful.Error)); + _ = _socket.ReconnectAsync().ConfigureAwait(false); + } + else + { + Status = SocketStatus.Connected; + _ = Task.Run(() => { - query.Fail(new WebError("Socket not open")); - return; - } - - if (query.Completed) - return; - - await query.WaitAsync(TimeSpan.FromMilliseconds(500), ct).ConfigureAwait(false); - - if (query.Completed) - return; + ConnectionRestored?.Invoke(DateTime.UtcNow - DisconnectTime!.Value); + DisconnectTime = null; + }); } - - if (ct.IsCancellationRequested) - { - query.Fail(new CancellationRequestedError()); - return; - } - } - finally - { - lock (_listenersLock) - _listeners.Remove(query); - } - } - - /// - /// Send data over the websocket connection - /// - /// The type of the object to send - /// The request id - /// The object to send - /// The weight of the message - public virtual CallResult Send(int requestId, T obj, int weight) - { - if (_serializer is IByteMessageSerializer byteSerializer) - { - return SendBytes(requestId, byteSerializer.Serialize(obj), weight); - } - else if (_serializer is IStringMessageSerializer stringSerializer) - { - if (obj is string str) - return Send(requestId, str, weight); - - str = stringSerializer.Serialize(obj); - return Send(requestId, str, weight); - } - - throw new Exception("Unknown serializer when sending message"); - } - - /// - /// Send byte data over the websocket connection - /// - /// The data to send - /// The weight of the message - /// The id of the request - public virtual CallResult SendBytes(int requestId, byte[] data, int weight) - { - if (ApiClient.MessageSendSizeLimit != null && data.Length > ApiClient.MessageSendSizeLimit.Value) - { - var info = $"Message to send exceeds the max server message size ({data.Length} vs {ApiClient.MessageSendSizeLimit.Value} bytes). Split the request into batches to keep below this limit"; - _logger.LogWarning("[Sckt {SocketId}] [Req {RequestId}] {Info}", SocketId, requestId, info); - return new CallResult(new InvalidOperationError(info)); - } - - if (!_socket.IsOpen) - { - _logger.LogWarning("[Sckt {SocketId}] [Req {RequestId}] failed to send, socket no longer open", SocketId, requestId); - return new CallResult(new WebError("Failed to send message, socket no longer open")); - } - - _logger.SendingByteData(SocketId, requestId, data.Length); - try - { - if (!_socket.Send(requestId, data, weight)) - return new CallResult(new WebError("Failed to send message, connection not open")); - - return CallResult.SuccessResult; - } - catch (Exception ex) - { - return new CallResult(new WebError("Failed to send message: " + ex.Message, exception: ex)); - } - } - - /// - /// Send string data over the websocket connection - /// - /// The data to send - /// The weight of the message - /// The id of the request - public virtual CallResult Send(int requestId, string data, int weight) - { - if (ApiClient.MessageSendSizeLimit != null && data.Length > ApiClient.MessageSendSizeLimit.Value) - { - var info = $"Message to send exceeds the max server message size ({data.Length} vs {ApiClient.MessageSendSizeLimit.Value} bytes). Split the request into batches to keep below this limit"; - _logger.LogWarning("[Sckt {SocketId}] [Req {RequestId}] {Info}", SocketId, requestId, info); - return new CallResult(new InvalidOperationError(info)); - } - - if (!_socket.IsOpen) - { - _logger.LogWarning("[Sckt {SocketId}] [Req {RequestId}] failed to send, socket no longer open", SocketId, requestId); - return new CallResult(new WebError("Failed to send message, socket no longer open")); - } - - _logger.SendingData(SocketId, requestId, data); - try - { - if (!_socket.Send(requestId, data, weight)) - return new CallResult(new WebError("Failed to send message, connection not open")); - - return CallResult.SuccessResult; } catch(Exception ex) { - return new CallResult(new WebError("Failed to send message: " + ex.Message, exception: ex)); + _logger.UnknownExceptionWhileProcessingReconnection(SocketId, ex); + _ = _socket.ReconnectAsync().ConfigureAwait(false); } + }); + + return Task.CompletedTask; + } + + /// + /// Handler for an error on a websocket + /// + /// The exception + protected virtual Task HandleErrorAsync(Exception e) + { + if (e is WebSocketException wse) + _logger.WebSocketErrorCodeAndDetails(SocketId, wse.WebSocketErrorCode, wse.Message, wse); + else + _logger.WebSocketError(SocketId, e.Message, e); + + return Task.CompletedTask; + } + + /// + /// Handler for whenever a request is rate limited and rate limit behavior is set to fail + /// + /// + /// + protected virtual Task HandleRequestRateLimitedAsync(int requestId) + { + Query? query; + lock (_listenersLock) + { + query = _listeners.OfType().FirstOrDefault(x => x.Id == requestId); } - private async Task ProcessReconnectAsync() + if (query == null) + return Task.CompletedTask; + + query.Fail(new ClientRateLimitError("Connection rate limit reached")); + return Task.CompletedTask; + } + + /// + /// Handler for whenever a connection was rate limited and couldn't be established + /// + /// + protected async virtual Task HandleConnectRateLimitedAsync() + { + if (ConnectRateLimitedAsync is not null) + await ConnectRateLimitedAsync().ConfigureAwait(false); + } + + /// + /// Handler for whenever a request is sent over the websocket + /// + /// Id of the request sent + protected virtual Task HandleRequestSentAsync(int requestId) + { + Query? query; + lock (_listenersLock) { - if (!_socket.IsOpen) - return new CallResult(new WebError("Socket not connected")); - - if (!DedicatedRequestConnection.IsDedicatedRequestConnection) - { - bool anySubscriptions; - lock (_listenersLock) - anySubscriptions = _listeners.OfType().Any(s => s.UserSubscription); - if (!anySubscriptions) - { - // No need to resubscribe anything - _logger.NothingToResubscribeCloseConnection(SocketId); - _ = _socket.CloseAsync(); - return CallResult.SuccessResult; - } - } - - bool anyAuthenticated; - lock (_listenersLock) - { - anyAuthenticated = _listeners.OfType().Any(s => s.Authenticated) - || (DedicatedRequestConnection.IsDedicatedRequestConnection && DedicatedRequestConnection.Authenticated); - } - - if (anyAuthenticated) - { - // If we reconnected a authenticated connection we need to re-authenticate - var authResult = await ApiClient.AuthenticateSocketAsync(this).ConfigureAwait(false); - if (!authResult) - { - _logger.FailedAuthenticationDisconnectAndRecoonect(SocketId); - return authResult; - } - - Authenticated = true; - _logger.AuthenticationSucceeded(SocketId); - } - - // Foreach subscription which is subscribed by a subscription request we will need to resend that request to resubscribe - int batch = 0; - int batchSize = ApiClient.ClientOptions.MaxConcurrentResubscriptionsPerSocket; - while (true) - { - if (!_socket.IsOpen) - return new CallResult(new WebError("Socket not connected")); - - List subList; - lock (_listenersLock) - subList = _listeners.OfType().Where(x => !x.Closed).Skip(batch * batchSize).Take(batchSize).ToList(); - - if (subList.Count == 0) - break; - - var taskList = new List>(); - foreach (var subscription in subList) - { - subscription.ConnectionInvocations = 0; - if (subscription.Closed) - // Can be closed during resubscribing - continue; - - subscription.IsResubscribing = true; - var result = await ApiClient.RevitalizeRequestAsync(subscription).ConfigureAwait(false); - if (!result) - { - _logger.FailedRequestRevitalization(SocketId, result.Error?.ToString()); - subscription.IsResubscribing = false; - return result; - } - - var subQuery = subscription.CreateSubscriptionQuery(this); - if (subQuery == null) - { - subscription.IsResubscribing = false; - continue; - } - - var waitEvent = new AsyncResetEvent(false); - taskList.Add(SendAndWaitQueryAsync(subQuery, waitEvent).ContinueWith((r) => - { - subscription.IsResubscribing = false; - subscription.HandleSubQueryResponse(subQuery.Response!); - waitEvent.Set(); - if (r.Result.Success) - subscription.Confirmed = true; - return r.Result; - })); - } - - await Task.WhenAll(taskList).ConfigureAwait(false); - if (taskList.Any(t => !t.Result.Success)) - return taskList.First(t => !t.Result.Success).Result; - - batch++; - } - - if (!_socket.IsOpen) - return new CallResult(new WebError("Socket not connected")); - - _logger.AllSubscriptionResubscribed(SocketId); - return CallResult.SuccessResult; + query = _listeners.OfType().FirstOrDefault(x => x.Id == requestId); } - internal async Task UnsubscribeAsync(Subscription subscription) + if (query == null) { - var unsubscribeRequest = subscription.CreateUnsubscriptionQuery(this); - if (unsubscribeRequest == null) + _logger.MessageSentNotPending(SocketId, requestId); + return Task.CompletedTask; + } + + query.IsSend(query.RequestTimeout ?? ApiClient.ClientOptions.RequestTimeout); + return Task.CompletedTask; + } + + /// + /// Handle a message + /// + protected virtual async Task HandleStreamMessage(WebSocketMessageType type, ReadOnlyMemory data) + { + var sw = Stopwatch.StartNew(); + var receiveTime = DateTime.UtcNow; + string? originalData = null; + + // 1. Decrypt/Preprocess if necessary + data = ApiClient.PreprocessStreamMessage(this, type, data); + + // 2. Read data into accessor + IByteMessageAccessor accessor; + if (type == WebSocketMessageType.Binary) + accessor = _stringMessageAccessor ??= ApiClient.CreateAccessor(type); + else + accessor = _byteMessageAccessor ??= ApiClient.CreateAccessor(type); + + var result = accessor.Read(data); + try + { + bool outputOriginalData = ApiClient.ApiOptions.OutputOriginalData ?? ApiClient.ClientOptions.OutputOriginalData; + if (outputOriginalData) + { + originalData = accessor.GetOriginalString(); + _logger.ReceivedData(SocketId, originalData); + } + + if (!accessor.IsValid && !ApiClient.ProcessUnparsableMessages) + { + _logger.FailedToParse(SocketId, result.Error!.Message ?? result.Error!.ErrorDescription!); return; + } - await SendAndWaitQueryAsync(unsubscribeRequest).ConfigureAwait(false); - _logger.SubscriptionUnsubscribed(SocketId, subscription.Id); - } - - internal async Task ResubscribeAsync(Subscription subscription) - { - if (!_socket.IsOpen) - return new CallResult(new WebError("Socket is not connected")); - - var subQuery = subscription.CreateSubscriptionQuery(this); - if (subQuery == null) - return CallResult.SuccessResult; - - var result = await SendAndWaitQueryAsync(subQuery).ConfigureAwait(false); - subscription.HandleSubQueryResponse(subQuery.Response!); - return result; - } - - /// - /// Periodically sends data over a socket connection - /// - /// Identifier for the periodic send - /// How often - /// Method returning the query to send - /// The callback for processing the response - public virtual void QueryPeriodic(string identifier, TimeSpan interval, Func queryDelegate, Action? callback) - { - if (queryDelegate == null) - throw new ArgumentNullException(nameof(queryDelegate)); - - periodicEvent = new AsyncResetEvent(); - periodicTask = Task.Run(async () => + // 3. Determine the identifying properties of this message + var listenId = ApiClient.GetListenerIdentifier(accessor); + if (listenId == null) { - while (Status != SocketStatus.Disposed - && Status != SocketStatus.Closed - && Status != SocketStatus.Closing) + originalData ??= "[OutputOriginalData is false]"; + if (!ApiClient.UnhandledMessageExpected) + _logger.FailedToEvaluateMessage(SocketId, originalData); + + UnhandledMessage?.Invoke(accessor); + return; + } + + bool processed = false; + var totalUserTime = 0; + + List localListeners; + lock(_listenersLock) + localListeners = _listeners.ToList(); + + foreach(var processor in localListeners) + { + foreach(var listener in processor.MessageMatcher.GetHandlerLinks(listenId)) { - await periodicEvent.WaitAsync(interval).ConfigureAwait(false); - if (Status == SocketStatus.Disposed - || Status == SocketStatus.Closed - || Status == SocketStatus.Closing) + processed = true; + _logger.ProcessorMatched(SocketId, listener.ToString(), listenId); + + // 4. Determine the type to deserialize to for this processor + var messageType = listener.GetDeserializationType(accessor); + if (messageType == null) { - break; + _logger.ReceivedMessageNotRecognized(SocketId, processor.Id); + continue; } - if (!Connected) - continue; + if (processor is Subscription subscriptionProcessor && !subscriptionProcessor.Confirmed) + { + // If this message is for this listener then it is automatically confirmed, even if the subscription is not (yet) confirmed + subscriptionProcessor.Confirmed = true; + if (subscriptionProcessor.SubscriptionQuery?.TimeoutBehavior == TimeoutBehavior.Succeed) + // If this subscription has a query waiting for a timeout (success if there is no error response) + // then time it out now as the data is being received, so we assume it's successful + subscriptionProcessor.SubscriptionQuery.Timeout(); + } - var query = queryDelegate(this); - if (query == null) - continue; + // 5. Deserialize the message + _deserializationCache.TryGetValue(messageType, out var deserialized); - _logger.SendingPeriodic(SocketId, identifier); + if (deserialized == null) + { + var desResult = processor.Deserialize(accessor, messageType); + if (!desResult) + { + _logger.FailedToDeserializeMessage(SocketId, desResult.Error?.ToString(), desResult.Error?.Exception); + continue; + } + deserialized = desResult.Data; + _deserializationCache.Add(messageType, deserialized); + } + + // 6. Pass the message to the handler try { - var result = await SendAndWaitQueryAsync(query).ConfigureAwait(false); - callback?.Invoke(this, result); + var innerSw = Stopwatch.StartNew(); + await processor.Handle(this, new DataEvent(deserialized, null, null, originalData, receiveTime, null), listener).ConfigureAwait(false); + if (processor is Query query && query.RequiredResponses != 1) + _logger.LogDebug("[Sckt {SocketId}] [Req {RequestId}] responses: {CurrentResponses}/{RequiredResponses}", SocketId, query.Id, query.CurrentResponses, query.RequiredResponses); + totalUserTime += (int)innerSw.ElapsedMilliseconds; } catch (Exception ex) { - _logger.PeriodicSendFailed(SocketId, identifier, ex.Message, ex); + _logger.UserMessageProcessingFailed(SocketId, ex.Message, ex); + if (processor is Subscription subscription) + subscription.InvokeExceptionHandler(ex); } + } - }); + } + + if (!processed) + { + if (!ApiClient.UnhandledMessageExpected) + { + List listenerIds; + lock (_listenersLock) + listenerIds = _listeners.Select(l => l.MessageMatcher.ToString()).ToList(); + + _logger.ReceivedMessageNotMatchedToAnyListener(SocketId, listenId, string.Join(",", listenerIds)); + UnhandledMessage?.Invoke(accessor); + } + + return; + } + + _logger.MessageProcessed(SocketId, sw.ElapsedMilliseconds, sw.ElapsedMilliseconds - totalUserTime); + } + finally + { + _deserializationCache.Clear(); + accessor.Clear(); + } + } + + /// + /// Connect the websocket + /// + /// + public async Task ConnectAsync(CancellationToken ct) => await _socket.ConnectAsync(ct).ConfigureAwait(false); + + /// + /// Retrieve the underlying socket + /// + /// + public IWebsocket GetSocket() => _socket; + + /// + /// Trigger a reconnect of the socket connection + /// + /// + public async Task TriggerReconnectAsync() => await _socket.ReconnectAsync().ConfigureAwait(false); + + /// + /// Update the proxy setting and reconnect + /// + /// New proxy setting + public async Task UpdateProxy(ApiProxy? proxy) + { + _socket.UpdateProxy(proxy); + await TriggerReconnectAsync().ConfigureAwait(false); + } + + /// + /// Close the connection + /// + /// + public async Task CloseAsync() + { + if (Status == SocketStatus.Closed || Status == SocketStatus.Disposed) + return; + + if (ApiClient.socketConnections.ContainsKey(SocketId)) + ApiClient.socketConnections.TryRemove(SocketId, out _); + + lock (_listenersLock) + { + foreach (var subscription in _listeners.OfType()) + { + if (subscription.CancellationTokenRegistration.HasValue) + subscription.CancellationTokenRegistration.Value.Dispose(); + } } - /// - /// Status of the socket connection - /// - public enum SocketStatus + await _socket.CloseAsync().ConfigureAwait(false); + _socket.Dispose(); + } + + /// + /// Close a subscription on this connection. If all subscriptions on this connection are closed the connection gets closed as well + /// + /// Subscription to close + /// + public async Task CloseAsync(Subscription subscription) + { + // If we are resubscribing this subscription at this moment we'll want to wait for a bit until it is finished to avoid concurrency issues + while (subscription.IsResubscribing) + await Task.Delay(50).ConfigureAwait(false); + + subscription.Closed = true; + + if (Status == SocketStatus.Closing || Status == SocketStatus.Closed || Status == SocketStatus.Disposed) + return; + + _logger.ClosingSubscription(SocketId, subscription.Id); + if (subscription.CancellationTokenRegistration.HasValue) + subscription.CancellationTokenRegistration.Value.Dispose(); + + bool anyDuplicateSubscription; + lock (_listenersLock) + anyDuplicateSubscription = _listeners.OfType().Any(x => x != subscription && x.MessageMatcher.HandlerLinks.All(l => subscription.MessageMatcher.ContainsCheck(l))); + + bool shouldCloseConnection; + lock (_listenersLock) + shouldCloseConnection = _listeners.OfType().All(r => !r.UserSubscription || r.Closed) && !DedicatedRequestConnection.IsDedicatedRequestConnection; + + if (!anyDuplicateSubscription) { - /// - /// None/Initial - /// - None, - /// - /// Connected - /// - Connected, - /// - /// Reconnecting - /// - Reconnecting, - /// - /// Resubscribing on reconnected socket - /// - Resubscribing, - /// - /// Closing - /// - Closing, - /// - /// Closed - /// - Closed, - /// - /// Disposed - /// - Disposed + bool needUnsub; + lock (_listenersLock) + needUnsub = _listeners.Contains(subscription) && !shouldCloseConnection; + + if (needUnsub && _socket.IsOpen) + await UnsubscribeAsync(subscription).ConfigureAwait(false); } + else + { + _logger.NotUnsubscribingSubscriptionBecauseDuplicateRunning(SocketId); + } + + if (Status == SocketStatus.Closing) + { + _logger.AlreadyClosing(SocketId); + return; + } + + if (shouldCloseConnection) + { + Status = SocketStatus.Closing; + _logger.ClosingNoMoreSubscriptions(SocketId); + await CloseAsync().ConfigureAwait(false); + } + + lock (_listenersLock) + _listeners.Remove(subscription); + } + + /// + /// Dispose the connection + /// + public void Dispose() + { + Status = SocketStatus.Disposed; + periodicEvent?.Set(); + periodicEvent?.Dispose(); + _socket.Dispose(); + } + + /// + /// Whether or not a new subscription can be added to this connection + /// + /// + public bool CanAddSubscription() => Status == SocketStatus.None || Status == SocketStatus.Connected; + + /// + /// Add a subscription to this connection + /// + /// + public bool AddSubscription(Subscription subscription) + { + if (Status != SocketStatus.None && Status != SocketStatus.Connected) + return false; + + lock (_listenersLock) + _listeners.Add(subscription); + + if (subscription.UserSubscription) + _logger.AddingNewSubscription(SocketId, subscription.Id, UserSubscriptionCount); + return true; + } + + /// + /// Get a subscription on this connection by id + /// + /// + public Subscription? GetSubscription(int id) + { + lock (_listenersLock) + return _listeners.OfType().SingleOrDefault(s => s.Id == id); + } + + /// + /// Get the state of the connection + /// + /// + public SocketConnectionState GetState(bool includeSubDetails) + { + return new SocketConnectionState( + SocketId, + ConnectionUri.AbsoluteUri, + UserSubscriptionCount, + Status, + Authenticated, + IncomingKbps, + PendingQueries: _listeners.OfType().Count(x => !x.Completed), + includeSubDetails ? Subscriptions.Select(sub => sub.GetState()).ToList() : new List() + ); + } + + /// + /// Send a query request and wait for an answer + /// + /// Query to send + /// Wait event for when the socket message handler can continue + /// Cancellation token + /// + public virtual async Task SendAndWaitQueryAsync(Query query, AsyncResetEvent? continueEvent = null, CancellationToken ct = default) + { + await SendAndWaitIntAsync(query, continueEvent, ct).ConfigureAwait(false); + return query.Result ?? new CallResult(new TimeoutError()); + } + + /// + /// Send a query request and wait for an answer + /// + /// Expected result type + /// Query to send + /// Wait event for when the socket message handler can continue + /// Cancellation token + /// + public virtual async Task> SendAndWaitQueryAsync(Query query, AsyncResetEvent? continueEvent = null, CancellationToken ct = default) + { + await SendAndWaitIntAsync(query, continueEvent, ct).ConfigureAwait(false); + return query.TypedResult ?? new CallResult(new TimeoutError()); + } + + private async Task SendAndWaitIntAsync(Query query, AsyncResetEvent? continueEvent, CancellationToken ct = default) + { + lock(_listenersLock) + _listeners.Add(query); + + query.ContinueAwaiter = continueEvent; + var sendResult = Send(query.Id, query.Request, query.Weight); + if (!sendResult) + { + query.Fail(sendResult.Error!); + lock (_listenersLock) + _listeners.Remove(query); + return; + } + + try + { + while (!ct.IsCancellationRequested) + { + if (!_socket.IsOpen) + { + query.Fail(new WebError("Socket not open")); + return; + } + + if (query.Completed) + return; + + await query.WaitAsync(TimeSpan.FromMilliseconds(500), ct).ConfigureAwait(false); + + if (query.Completed) + return; + } + + if (ct.IsCancellationRequested) + { + query.Fail(new CancellationRequestedError()); + return; + } + } + finally + { + lock (_listenersLock) + _listeners.Remove(query); + } + } + + /// + /// Send data over the websocket connection + /// + /// The type of the object to send + /// The request id + /// The object to send + /// The weight of the message + public virtual CallResult Send(int requestId, T obj, int weight) + { + if (_serializer is IByteMessageSerializer byteSerializer) + { + return SendBytes(requestId, byteSerializer.Serialize(obj), weight); + } + else if (_serializer is IStringMessageSerializer stringSerializer) + { + if (obj is string str) + return Send(requestId, str, weight); + + str = stringSerializer.Serialize(obj); + return Send(requestId, str, weight); + } + + throw new Exception("Unknown serializer when sending message"); + } + + /// + /// Send byte data over the websocket connection + /// + /// The data to send + /// The weight of the message + /// The id of the request + public virtual CallResult SendBytes(int requestId, byte[] data, int weight) + { + if (ApiClient.MessageSendSizeLimit != null && data.Length > ApiClient.MessageSendSizeLimit.Value) + { + var info = $"Message to send exceeds the max server message size ({data.Length} vs {ApiClient.MessageSendSizeLimit.Value} bytes). Split the request into batches to keep below this limit"; + _logger.LogWarning("[Sckt {SocketId}] [Req {RequestId}] {Info}", SocketId, requestId, info); + return new CallResult(new InvalidOperationError(info)); + } + + if (!_socket.IsOpen) + { + _logger.LogWarning("[Sckt {SocketId}] [Req {RequestId}] failed to send, socket no longer open", SocketId, requestId); + return new CallResult(new WebError("Failed to send message, socket no longer open")); + } + + _logger.SendingByteData(SocketId, requestId, data.Length); + try + { + if (!_socket.Send(requestId, data, weight)) + return new CallResult(new WebError("Failed to send message, connection not open")); + + return CallResult.SuccessResult; + } + catch (Exception ex) + { + return new CallResult(new WebError("Failed to send message: " + ex.Message, exception: ex)); + } + } + + /// + /// Send string data over the websocket connection + /// + /// The data to send + /// The weight of the message + /// The id of the request + public virtual CallResult Send(int requestId, string data, int weight) + { + if (ApiClient.MessageSendSizeLimit != null && data.Length > ApiClient.MessageSendSizeLimit.Value) + { + var info = $"Message to send exceeds the max server message size ({data.Length} vs {ApiClient.MessageSendSizeLimit.Value} bytes). Split the request into batches to keep below this limit"; + _logger.LogWarning("[Sckt {SocketId}] [Req {RequestId}] {Info}", SocketId, requestId, info); + return new CallResult(new InvalidOperationError(info)); + } + + if (!_socket.IsOpen) + { + _logger.LogWarning("[Sckt {SocketId}] [Req {RequestId}] failed to send, socket no longer open", SocketId, requestId); + return new CallResult(new WebError("Failed to send message, socket no longer open")); + } + + _logger.SendingData(SocketId, requestId, data); + try + { + if (!_socket.Send(requestId, data, weight)) + return new CallResult(new WebError("Failed to send message, connection not open")); + + return CallResult.SuccessResult; + } + catch(Exception ex) + { + return new CallResult(new WebError("Failed to send message: " + ex.Message, exception: ex)); + } + } + + private async Task ProcessReconnectAsync() + { + if (!_socket.IsOpen) + return new CallResult(new WebError("Socket not connected")); + + if (!DedicatedRequestConnection.IsDedicatedRequestConnection) + { + bool anySubscriptions; + lock (_listenersLock) + anySubscriptions = _listeners.OfType().Any(s => s.UserSubscription); + if (!anySubscriptions) + { + // No need to resubscribe anything + _logger.NothingToResubscribeCloseConnection(SocketId); + _ = _socket.CloseAsync(); + return CallResult.SuccessResult; + } + } + + bool anyAuthenticated; + lock (_listenersLock) + { + anyAuthenticated = _listeners.OfType().Any(s => s.Authenticated) + || (DedicatedRequestConnection.IsDedicatedRequestConnection && DedicatedRequestConnection.Authenticated); + } + + if (anyAuthenticated) + { + // If we reconnected a authenticated connection we need to re-authenticate + var authResult = await ApiClient.AuthenticateSocketAsync(this).ConfigureAwait(false); + if (!authResult) + { + _logger.FailedAuthenticationDisconnectAndRecoonect(SocketId); + return authResult; + } + + Authenticated = true; + _logger.AuthenticationSucceeded(SocketId); + } + + // Foreach subscription which is subscribed by a subscription request we will need to resend that request to resubscribe + int batch = 0; + int batchSize = ApiClient.ClientOptions.MaxConcurrentResubscriptionsPerSocket; + while (true) + { + if (!_socket.IsOpen) + return new CallResult(new WebError("Socket not connected")); + + List subList; + lock (_listenersLock) + subList = _listeners.OfType().Where(x => !x.Closed).Skip(batch * batchSize).Take(batchSize).ToList(); + + if (subList.Count == 0) + break; + + var taskList = new List>(); + foreach (var subscription in subList) + { + subscription.ConnectionInvocations = 0; + if (subscription.Closed) + // Can be closed during resubscribing + continue; + + subscription.IsResubscribing = true; + var result = await ApiClient.RevitalizeRequestAsync(subscription).ConfigureAwait(false); + if (!result) + { + _logger.FailedRequestRevitalization(SocketId, result.Error?.ToString()); + subscription.IsResubscribing = false; + return result; + } + + var subQuery = subscription.CreateSubscriptionQuery(this); + if (subQuery == null) + { + subscription.IsResubscribing = false; + continue; + } + + var waitEvent = new AsyncResetEvent(false); + taskList.Add(SendAndWaitQueryAsync(subQuery, waitEvent).ContinueWith((r) => + { + subscription.IsResubscribing = false; + subscription.HandleSubQueryResponse(subQuery.Response!); + waitEvent.Set(); + if (r.Result.Success) + subscription.Confirmed = true; + return r.Result; + })); + } + + await Task.WhenAll(taskList).ConfigureAwait(false); + if (taskList.Any(t => !t.Result.Success)) + return taskList.First(t => !t.Result.Success).Result; + + batch++; + } + + if (!_socket.IsOpen) + return new CallResult(new WebError("Socket not connected")); + + _logger.AllSubscriptionResubscribed(SocketId); + return CallResult.SuccessResult; + } + + internal async Task UnsubscribeAsync(Subscription subscription) + { + var unsubscribeRequest = subscription.CreateUnsubscriptionQuery(this); + if (unsubscribeRequest == null) + return; + + await SendAndWaitQueryAsync(unsubscribeRequest).ConfigureAwait(false); + _logger.SubscriptionUnsubscribed(SocketId, subscription.Id); + } + + internal async Task ResubscribeAsync(Subscription subscription) + { + if (!_socket.IsOpen) + return new CallResult(new WebError("Socket is not connected")); + + var subQuery = subscription.CreateSubscriptionQuery(this); + if (subQuery == null) + return CallResult.SuccessResult; + + var result = await SendAndWaitQueryAsync(subQuery).ConfigureAwait(false); + subscription.HandleSubQueryResponse(subQuery.Response!); + return result; + } + + /// + /// Periodically sends data over a socket connection + /// + /// Identifier for the periodic send + /// How often + /// Method returning the query to send + /// The callback for processing the response + public virtual void QueryPeriodic(string identifier, TimeSpan interval, Func queryDelegate, Action? callback) + { + if (queryDelegate == null) + throw new ArgumentNullException(nameof(queryDelegate)); + + periodicEvent = new AsyncResetEvent(); + periodicTask = Task.Run(async () => + { + while (Status != SocketStatus.Disposed + && Status != SocketStatus.Closed + && Status != SocketStatus.Closing) + { + await periodicEvent.WaitAsync(interval).ConfigureAwait(false); + if (Status == SocketStatus.Disposed + || Status == SocketStatus.Closed + || Status == SocketStatus.Closing) + { + break; + } + + if (!Connected) + continue; + + var query = queryDelegate(this); + if (query == null) + continue; + + _logger.SendingPeriodic(SocketId, identifier); + + try + { + var result = await SendAndWaitQueryAsync(query).ConfigureAwait(false); + callback?.Invoke(this, result); + } + catch (Exception ex) + { + _logger.PeriodicSendFailed(SocketId, identifier, ex.Message, ex); + } + } + }); + } + + /// + /// Status of the socket connection + /// + public enum SocketStatus + { + /// + /// None/Initial + /// + None, + /// + /// Connected + /// + Connected, + /// + /// Reconnecting + /// + Reconnecting, + /// + /// Resubscribing on reconnected socket + /// + Resubscribing, + /// + /// Closing + /// + Closing, + /// + /// Closed + /// + Closed, + /// + /// Disposed + /// + Disposed } } diff --git a/CryptoExchange.Net/Sockets/Subscription.cs b/CryptoExchange.Net/Sockets/Subscription.cs index df0b66b..0a52cb7 100644 --- a/CryptoExchange.Net/Sockets/Subscription.cs +++ b/CryptoExchange.Net/Sockets/Subscription.cs @@ -1,241 +1,238 @@ -using CryptoExchange.Net.Interfaces; +using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects.Sockets; using Microsoft.Extensions.Logging; using System; -using System.Collections.Generic; -using System.Linq; using System.Threading; using System.Threading.Tasks; -namespace CryptoExchange.Net.Sockets +namespace CryptoExchange.Net.Sockets; + +/// +/// Socket subscription +/// +public abstract class Subscription : IMessageProcessor { /// - /// Socket subscription + /// Subscription id /// - public abstract class Subscription : IMessageProcessor + public int Id { get; set; } + + /// + /// Total amount of invocations + /// + public int TotalInvocations { get; set; } + + /// + /// Amount of invocation during this connection + /// + public int ConnectionInvocations { get; set; } + + /// + /// Is it a user subscription + /// + public bool UserSubscription { get; set; } + + /// + /// Has the subscription been confirmed + /// + public bool Confirmed { get; set; } + + /// + /// Is the subscription closed + /// + public bool Closed { get; set; } + + /// + /// Is the subscription currently resubscribing + /// + public bool IsResubscribing { get; set; } + + /// + /// Logger + /// + protected readonly ILogger _logger; + + /// + /// If the subscription is a private subscription and needs authentication + /// + public bool Authenticated { get; } + + /// + /// Matcher for this subscription + /// + public MessageMatcher MessageMatcher { get; set; } = null!; + + /// + /// Cancellation token registration + /// + public CancellationTokenRegistration? CancellationTokenRegistration { get; set; } + + /// + /// Exception event + /// + public event Action? Exception; + + /// + /// Subscription topic + /// + public string? Topic { get; set; } + + /// + /// The subscribe query for this subscription + /// + public Query? SubscriptionQuery { get; private set; } + + /// + /// The unsubscribe query for this subscription + /// + public Query? UnsubscriptionQuery { get; private set; } + + /// + /// ctor + /// + public Subscription(ILogger logger, bool authenticated, bool userSubscription = true) { - /// - /// Subscription id - /// - public int Id { get; set; } + _logger = logger; + Authenticated = authenticated; + UserSubscription = userSubscription; + Id = ExchangeHelpers.NextId(); + } - /// - /// Total amount of invocations - /// - public int TotalInvocations { get; set; } + /// + /// Create a new subscription query + /// + public Query? CreateSubscriptionQuery(SocketConnection connection) + { + var query = GetSubQuery(connection); + SubscriptionQuery = query; + return query; + } - /// - /// Amount of invocation during this connection - /// - public int ConnectionInvocations { get; set; } + /// + /// Get the subscribe query to send when subscribing + /// + /// + protected abstract Query? GetSubQuery(SocketConnection connection); - /// - /// Is it a user subscription - /// - public bool UserSubscription { get; set; } - - /// - /// Has the subscription been confirmed - /// - public bool Confirmed { get; set; } + /// + /// Handle a subscription query response + /// + /// + public virtual void HandleSubQueryResponse(object message) { } - /// - /// Is the subscription closed - /// - public bool Closed { get; set; } + /// + /// Handle an unsubscription query response + /// + /// + public virtual void HandleUnsubQueryResponse(object message) { } - /// - /// Is the subscription currently resubscribing - /// - public bool IsResubscribing { get; set; } + /// + /// Create a new unsubscription query + /// + public Query? CreateUnsubscriptionQuery(SocketConnection connection) + { + var query = GetUnsubQuery(connection); + UnsubscriptionQuery = query; + return query; + } - /// - /// Logger - /// - protected readonly ILogger _logger; + /// + /// Get the unsubscribe query to send when unsubscribing + /// + /// + protected abstract Query? GetUnsubQuery(SocketConnection connection); - /// - /// If the subscription is a private subscription and needs authentication - /// - public bool Authenticated { get; } + /// + public virtual CallResult Deserialize(IMessageAccessor accessor, Type type) => accessor.Deserialize(type); - /// - /// Matcher for this subscription - /// - public MessageMatcher MessageMatcher { get; set; } = null!; + /// + /// Handle an update message + /// + public Task Handle(SocketConnection connection, DataEvent message, MessageHandlerLink matchedHandler) + { + ConnectionInvocations++; + TotalInvocations++; + return Task.FromResult(matchedHandler.Handle(connection, message)); + } - /// - /// Cancellation token registration - /// - public CancellationTokenRegistration? CancellationTokenRegistration { get; set; } + /// + /// Reset the subscription + /// + public void Reset() + { + Confirmed = false; + DoHandleReset(); + } - /// - /// Exception event - /// - public event Action? Exception; + /// + /// Connection has been reset, do any logic for resetting the subscription + /// + public virtual void DoHandleReset() { } - /// - /// Subscription topic - /// - public string? Topic { get; set; } + /// + /// Invoke the exception event + /// + /// + public void InvokeExceptionHandler(Exception e) + { + Exception?.Invoke(e); + } - /// - /// The subscribe query for this subscription - /// - public Query? SubscriptionQuery { get; private set; } + /// + /// State of this subscription + /// + /// The id of the subscription + /// True when the subscription query is handled (either accepted or rejected) + /// Number of times this subscription got a message + /// Matcher for this subscription + public record SubscriptionState( + int Id, + bool Confirmed, + int Invocations, + MessageMatcher ListenMatcher + ); - /// - /// The unsubscribe query for this subscription - /// - public Query? UnsubscriptionQuery { get; private set; } + /// + /// Get the state of this subscription + /// + /// + public SubscriptionState GetState() + { + return new SubscriptionState(Id, Confirmed, TotalInvocations, MessageMatcher); + } +} - /// - /// ctor - /// - public Subscription(ILogger logger, bool authenticated, bool userSubscription = true) - { - _logger = logger; - Authenticated = authenticated; - UserSubscription = userSubscription; - Id = ExchangeHelpers.NextId(); - } - - /// - /// Create a new subscription query - /// - public Query? CreateSubscriptionQuery(SocketConnection connection) - { - var query = GetSubQuery(connection); - SubscriptionQuery = query; - return query; - } - - /// - /// Get the subscribe query to send when subscribing - /// - /// - protected abstract Query? GetSubQuery(SocketConnection connection); - - /// - /// Handle a subscription query response - /// - /// - public virtual void HandleSubQueryResponse(object message) { } - - /// - /// Handle an unsubscription query response - /// - /// - public virtual void HandleUnsubQueryResponse(object message) { } - - /// - /// Create a new unsubscription query - /// - public Query? CreateUnsubscriptionQuery(SocketConnection connection) - { - var query = GetUnsubQuery(connection); - UnsubscriptionQuery = query; - return query; - } - - /// - /// Get the unsubscribe query to send when unsubscribing - /// - /// - protected abstract Query? GetUnsubQuery(SocketConnection connection); - - /// - public virtual CallResult Deserialize(IMessageAccessor message, Type type) => message.Deserialize(type); - - /// - /// Handle an update message - /// - public Task Handle(SocketConnection connection, DataEvent message, MessageHandlerLink matcher) - { - ConnectionInvocations++; - TotalInvocations++; - return Task.FromResult(matcher.Handle(connection, message)); - } - - /// - /// Reset the subscription - /// - public void Reset() - { - Confirmed = false; - DoHandleReset(); - } - - /// - /// Connection has been reset, do any logic for resetting the subscription - /// - public virtual void DoHandleReset() { } - - /// - /// Invoke the exception event - /// - /// - public void InvokeExceptionHandler(Exception e) - { - Exception?.Invoke(e); - } - - /// - /// State of this subscription - /// - /// The id of the subscription - /// True when the subscription query is handled (either accepted or rejected) - /// Number of times this subscription got a message - /// Matcher for this subscription - public record SubscriptionState( - int Id, - bool Confirmed, - int Invocations, - MessageMatcher ListenMatcher - ); - - /// - /// Get the state of this subscription - /// - /// - public SubscriptionState GetState() - { - return new SubscriptionState(Id, Confirmed, TotalInvocations, MessageMatcher); - } +/// +public abstract class Subscription : Subscription +{ + /// + /// ctor + /// + /// + /// + protected Subscription(ILogger logger, bool authenticated) : base(logger, authenticated) + { } /// - public abstract class Subscription : Subscription - { - /// - /// ctor - /// - /// - /// - protected Subscription(ILogger logger, bool authenticated) : base(logger, authenticated) - { - } + public override void HandleSubQueryResponse(object message) + => HandleSubQueryResponse((TSubResponse)message); - /// - public override void HandleSubQueryResponse(object message) - => HandleSubQueryResponse((TSubResponse)message); + /// + /// Handle a subscription query response + /// + /// + public virtual void HandleSubQueryResponse(TSubResponse message) { } - /// - /// Handle a subscription query response - /// - /// - public virtual void HandleSubQueryResponse(TSubResponse message) { } + /// + public override void HandleUnsubQueryResponse(object message) + => HandleUnsubQueryResponse((TUnsubResponse)message); - /// - public override void HandleUnsubQueryResponse(object message) - => HandleUnsubQueryResponse((TUnsubResponse)message); + /// + /// Handle an unsubscription query response + /// + /// + public virtual void HandleUnsubQueryResponse(TUnsubResponse message) { } - /// - /// Handle an unsubscription query response - /// - /// - public virtual void HandleUnsubQueryResponse(TUnsubResponse message) { } - - } } diff --git a/CryptoExchange.Net/Sockets/SystemSubscription.cs b/CryptoExchange.Net/Sockets/SystemSubscription.cs index 97fe449..58cb97b 100644 --- a/CryptoExchange.Net/Sockets/SystemSubscription.cs +++ b/CryptoExchange.Net/Sockets/SystemSubscription.cs @@ -1,30 +1,25 @@ -using CryptoExchange.Net.Interfaces; -using CryptoExchange.Net.Objects; -using CryptoExchange.Net.Objects.Sockets; using Microsoft.Extensions.Logging; -using System; -namespace CryptoExchange.Net.Sockets +namespace CryptoExchange.Net.Sockets; + +/// +/// A system subscription +/// +public abstract class SystemSubscription : Subscription { /// - /// A system subscription + /// ctor /// - public abstract class SystemSubscription : Subscription + /// + /// + public SystemSubscription(ILogger logger, bool authenticated = false) : base(logger, authenticated, false) { - /// - /// ctor - /// - /// - /// - public SystemSubscription(ILogger logger, bool authenticated = false) : base(logger, authenticated, false) - { - Confirmed = true; - } - - /// - protected override Query? GetSubQuery(SocketConnection connection) => null; - - /// - protected override Query? GetUnsubQuery(SocketConnection connection) => null; + Confirmed = true; } + + /// + protected override Query? GetSubQuery(SocketConnection connection) => null; + + /// + protected override Query? GetUnsubQuery(SocketConnection connection) => null; } diff --git a/CryptoExchange.Net/Sockets/WebsocketFactory.cs b/CryptoExchange.Net/Sockets/WebsocketFactory.cs index 286d37d..7ee3390 100644 --- a/CryptoExchange.Net/Sockets/WebsocketFactory.cs +++ b/CryptoExchange.Net/Sockets/WebsocketFactory.cs @@ -1,18 +1,17 @@ -using CryptoExchange.Net.Interfaces; +using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Objects.Sockets; using Microsoft.Extensions.Logging; -namespace CryptoExchange.Net.Sockets +namespace CryptoExchange.Net.Sockets; + +/// +/// Default websocket factory implementation +/// +public class WebsocketFactory : IWebsocketFactory { - /// - /// Default websocket factory implementation - /// - public class WebsocketFactory : IWebsocketFactory + /// + public IWebsocket CreateWebsocket(ILogger logger, WebSocketParameters parameters) { - /// - public IWebsocket CreateWebsocket(ILogger logger, WebSocketParameters parameters) - { - return new CryptoExchangeWebSocketClient(logger, parameters); - } + return new CryptoExchangeWebSocketClient(logger, parameters); } } diff --git a/CryptoExchange.Net/Testing/Comparers/SystemTextJsonComparer.cs b/CryptoExchange.Net/Testing/Comparers/SystemTextJsonComparer.cs index eaa61d6..c54c195 100644 --- a/CryptoExchange.Net/Testing/Comparers/SystemTextJsonComparer.cs +++ b/CryptoExchange.Net/Testing/Comparers/SystemTextJsonComparer.cs @@ -1,11 +1,10 @@ -using System; +using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Linq; using System.Text.Json; -using System.Text.Json.Nodes; using System.Text.Json.Serialization; using CryptoExchange.Net.Converters; using CryptoExchange.Net.Converters.SystemTextJson; @@ -15,80 +14,309 @@ using CryptoExchange.Net.Converters.SystemTextJson; #pragma warning disable IL2075 #pragma warning disable IL3050 -namespace CryptoExchange.Net.Testing.Comparers +namespace CryptoExchange.Net.Testing.Comparers; + +internal class SystemTextJsonComparer { - internal class SystemTextJsonComparer + internal static void CompareData( + string method, + object? resultData, + string json, + string? nestedJsonProperty, + List? ignoreProperties = null, + bool userSingleArrayItem = false) { - internal static void CompareData( - string method, - object? resultData, - string json, - string? nestedJsonProperty, - List? ignoreProperties = null, - bool userSingleArrayItem = false) + var jsonObject = JsonDocument.Parse(json).RootElement; + if (nestedJsonProperty != null) { - var jsonObject = JsonDocument.Parse(json).RootElement; - if (nestedJsonProperty != null) + var nested = nestedJsonProperty.Split('.'); + foreach (var nest in nested) { - var nested = nestedJsonProperty.Split('.'); - foreach (var nest in nested) + if (int.TryParse(nest, out var index)) + jsonObject = jsonObject![index]; + else + jsonObject = jsonObject!.GetProperty(nest); + } + } + + if (userSingleArrayItem) + jsonObject = jsonObject[0]; + + + if (resultData == null) + { + if (jsonObject.ValueKind == JsonValueKind.Null) + return; + + if (jsonObject.ValueKind == JsonValueKind.Object && jsonObject.GetPropertyCount() == 0) + return; + + throw new Exception("ResultData null"); + } + + if (resultData.GetType().GetInterfaces().Contains(typeof(IDictionary))) + { + var dict = (IDictionary)resultData; + var jObj = jsonObject!; + foreach (var dictProp in jObj.EnumerateObject()) + { + if (!dict.Contains(dictProp.Name)) + throw new Exception($"{method}: Dictionary has no value for {dictProp.Name} while input json `{dictProp.Name}` has value {dictProp.Value}"); + + if (dictProp.Value.ValueKind == JsonValueKind.Object) { - if (int.TryParse(nest, out var index)) - jsonObject = jsonObject![index]; - else - jsonObject = jsonObject!.GetProperty(nest); + // TODO Some additional checking for objects + foreach (var prop in dictProp.Value.EnumerateObject()) + CheckObject(method, prop, dict[dictProp.Name]!, ignoreProperties!); + } + else + { + if (dict[dictProp.Name] == default && dictProp.Value.ValueKind != JsonValueKind.Null) + { + if (dictProp.Value.ToString() == "") + continue; + + // Property value not correct + throw new Exception($"{method}: Dictionary entry `{dictProp.Name}` has no value while input json has value {dictProp.Value}"); + } } } - - if (userSingleArrayItem) - jsonObject = jsonObject[0]; - - - if (resultData == null) + } + else if (jsonObject!.ValueKind == JsonValueKind.Array) + { + if (resultData is IEnumerable list) { - if (jsonObject.ValueKind == JsonValueKind.Null) - return; - - if (jsonObject.ValueKind == JsonValueKind.Object && jsonObject.GetPropertyCount() == 0) - return; - - throw new Exception("ResultData null"); - } - - if (resultData.GetType().GetInterfaces().Contains(typeof(IDictionary))) - { - var dict = (IDictionary)resultData; - var jObj = jsonObject!; - foreach (var dictProp in jObj.EnumerateObject()) + var enumerator = list.GetEnumerator(); + foreach (var jObj in jsonObject.EnumerateArray()) { - if (!dict.Contains(dictProp.Name)) - throw new Exception($"{method}: Dictionary has no value for {dictProp.Name} while input json `{dictProp.Name}` has value {dictProp.Value}"); - - if (dictProp.Value.ValueKind == JsonValueKind.Object) + if (!enumerator.MoveNext()) { - // TODO Some additional checking for objects - foreach (var prop in dictProp.Value.EnumerateObject()) - CheckObject(method, prop, dict[dictProp.Name]!, ignoreProperties!); } - else - { - if (dict[dictProp.Name] == default && dictProp.Value.ValueKind != JsonValueKind.Null) - { - if (dictProp.Value.ToString() == "") - continue; - // Property value not correct - throw new Exception($"{method}: Dictionary entry `{dictProp.Name}` has no value while input json has value {dictProp.Value}"); + if (jObj.ValueKind == JsonValueKind.Object) + { + foreach (var subProp in jObj.EnumerateObject()) + { + if (ignoreProperties?.Contains(subProp.Name) == true) + continue; + CheckObject(method, subProp, enumerator.Current, ignoreProperties!); } } + else if (jObj.ValueKind == JsonValueKind.Array) + { + var resultObj = enumerator.Current; + if (resultObj is string) + // string list + continue; + + var resultProps = resultObj.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast().SingleOrDefault())); + var arrayConverterProperty = resultObj.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true).FirstOrDefault(); + var jsonConverter = ((JsonConverterAttribute)arrayConverterProperty!).ConverterType; + if (jsonConverter != typeof(ArrayConverter<>)) + // Not array converter? + continue; + + int i = 0; + foreach (var item in jObj.EnumerateObject()) + { + var arrayProp = resultProps.Where(p => p.Item2 != null).FirstOrDefault(p => p.Item2!.Index == i).p; + if (arrayProp != null) + CheckPropertyValue(method, item.Value, arrayProp.GetValue(resultObj), arrayProp.PropertyType, arrayProp.Name, "Array index " + i, ignoreProperties!); + i++; + } + } + else + { + var value = enumerator.Current; + if (value == default && jObj.ValueKind != JsonValueKind.Null) + throw new Exception($"{method}: Array has no value while input json array has value {jObj}"); + } } } - else if (jsonObject!.ValueKind == JsonValueKind.Array) + else { - if (resultData is IEnumerable list) + var resultProps = resultData.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast().SingleOrDefault())); + int i = 0; + foreach (var item in jsonObject.EnumerateArray()) + { + var arrayProp = resultProps.Where(p => p.Item2 != null).FirstOrDefault(p => p.Item2!.Index == i).p; + if (arrayProp != null) + CheckPropertyValue(method, item, arrayProp.GetValue(resultData), arrayProp.PropertyType, arrayProp.Name, "Array index " + i, ignoreProperties!); + i++; + } + } + } + else if (jsonObject.ValueKind == JsonValueKind.Object) + { + foreach (var item in jsonObject.EnumerateObject()) + { + //if (item is JProperty prop) + //{ + if (ignoreProperties?.Contains(item.Name) == true) + continue; + + CheckObject(method, item, resultData, ignoreProperties); + //} + } + } + else + { + //? + } + + Debug.WriteLine($"Successfully validated {method}"); + } + + private static void CheckObject(string method, JsonProperty prop, object obj, List? ignoreProperties) + { + var publicProperties = obj.GetType().GetProperties( + System.Reflection.BindingFlags.Public + | System.Reflection.BindingFlags.GetProperty + | System.Reflection.BindingFlags.SetProperty + | System.Reflection.BindingFlags.Instance).Select(p => (p, ((JsonPropertyNameAttribute?)p.GetCustomAttributes(typeof(JsonPropertyNameAttribute), true).SingleOrDefault())?.Name)); + + var internalProperties = obj.GetType().GetProperties( + System.Reflection.BindingFlags.NonPublic + | System.Reflection.BindingFlags.GetProperty + | System.Reflection.BindingFlags.SetProperty + | System.Reflection.BindingFlags.Instance) + .Where(p => p.CustomAttributes.Any(x => x.AttributeType == typeof(JsonIncludeAttribute))) + .Select(p => (p, ((JsonPropertyNameAttribute?)p.GetCustomAttributes(typeof(JsonPropertyNameAttribute), true).SingleOrDefault())?.Name)); + + var resultProperties = publicProperties.Concat(internalProperties); + + // Property has a value + var property = resultProperties.SingleOrDefault(p => p.Name == prop.Name).p; + property ??= resultProperties.SingleOrDefault(p => p.p.Name == prop.Name).p; + + if (property is null) + // Property not found + throw new Exception($"{method}: Missing property `{prop.Name}` on `{obj.GetType().Name}`"); + + var getMethod = property.GetGetMethod(); + if (getMethod is null) + // There is no getter, so probably just a set for an alternative json name + return; + + var propertyValue = property.GetValue(obj); + CheckPropertyValue(method, prop.Value, propertyValue, property.PropertyType, property.Name, prop.Name, ignoreProperties); + } + + private static void CheckPropertyValue(string method, JsonElement propValue, object? propertyValue, Type propertyType, string? propertyName = null, string? propName = null, List? ignoreProperties = null) + { + if (propertyValue == default && propValue.ValueKind != JsonValueKind.Null && !string.IsNullOrEmpty(propValue.ToString())) + { + if (propertyType == typeof(DateTime?) && (propValue.ToString() == "" || propValue.ToString() == "0" || propValue.ToString() == "-1" || propValue.ToString() == "01/01/0001 00:00:00")) + return; + + // Property value not correct + if (propValue.ToString() != "0") + throw new Exception($"{method}: Property `{propertyName}` has no value while input json `{propName}` has value {propValue}"); + } + + if ((propertyValue == default && (propValue.ValueKind == JsonValueKind.Null || string.IsNullOrEmpty(propValue.ToString()))) || propValue.ToString() == "0") + return; + + if (propertyValue!.GetType().GetInterfaces().Contains(typeof(IDictionary))) + { + var dict = (IDictionary)propertyValue; + foreach (var dictProp in propValue.EnumerateObject()) + { + if (!dict.Contains(dictProp.Name)) + throw new Exception($"{method}: Property `{propertyName}` has no value while input json `{propName}` has value {propValue}"); + + if (dictProp.Value.ValueKind == JsonValueKind.Object) + { + CheckPropertyValue(method, dictProp.Value, dict[dictProp.Name]!, dict[dictProp.Name]!.GetType(), null, null, ignoreProperties); + } + else + { + if (dict[dictProp.Name] == default && dictProp.Value.ValueKind != JsonValueKind.Null) + // Property value not correct + throw new Exception($"{method}: Dictionary entry `{dictProp.Name}` has no value while input json has value {propValue} for"); + } + } + } + else if (propertyValue.GetType().GetInterfaces().Contains(typeof(IEnumerable)) + && propertyValue.GetType() != typeof(string)) + { + if (propValue.ValueKind != JsonValueKind.Array) + return; + + var list = (IEnumerable)propertyValue; + var enumerator = list.GetEnumerator(); + foreach (var jToken in propValue.EnumerateArray()) + { + var moved = enumerator.MoveNext(); + if (!moved) + throw new Exception("Enumeration not moved; incorrect amount of results?"); + + var typeConverter = enumerator.Current.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true); + if (typeConverter.Length != 0 && ((JsonConverterAttribute)typeConverter.First()).ConverterType != typeof(ArrayConverter<>)) + // Custom converter for the type, skip + continue; + + if (jToken.ValueKind == JsonValueKind.Object) + { + foreach (var subProp in jToken.EnumerateObject()) + { + if (ignoreProperties?.Contains(subProp.Name) == true) + continue; + + CheckObject(method, subProp, enumerator.Current, ignoreProperties); + } + } + else if (jToken.ValueKind == JsonValueKind.Array) + { + var resultObj = enumerator.Current; + var resultProps = resultObj.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast().SingleOrDefault())); + var arrayConverterProperty = resultObj.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true).FirstOrDefault(); + var jsonConverter = ((JsonConverterAttribute)arrayConverterProperty!).ConverterType; + if (jsonConverter != typeof(ArrayConverter<>)) + // Not array converter? + continue; + + int i = 0; + foreach (var item in jToken.EnumerateArray()) + { + var arrayProp = resultProps.Where(p => p.Item2 != null).FirstOrDefault(p => p.Item2!.Index == i).p; + if (arrayProp != null) + CheckPropertyValue(method, item, arrayProp.GetValue(resultObj), arrayProp.PropertyType, arrayProp.Name, "Array index " + i, ignoreProperties); + + i++; + } + } + else + { + var value = enumerator.Current; + if (value == default && jToken.ValueKind != JsonValueKind.Null) + throw new Exception($"{method}: Property `{propertyName}` has no value while input json `{propName}` has value {jToken}"); + + CheckValues(method, propertyName!, propertyType, jToken, value!); + } + } + } + else + { + if (propValue.ValueKind == JsonValueKind.Object) + { + foreach (var item in propValue.EnumerateObject()) + { + //if (item is JProperty prop) + //{ + if (ignoreProperties?.Contains(item.Name) == true) + continue; + + CheckObject(method, item, propertyValue, ignoreProperties); + //} + } + } + else if (propValue.ValueKind == JsonValueKind.Array) + { + if (propertyValue is IEnumerable list) { var enumerator = list.GetEnumerator(); - foreach (var jObj in jsonObject.EnumerateArray()) + foreach (var jObj in propValue.EnumerateArray()) { if (!enumerator.MoveNext()) { @@ -106,10 +334,6 @@ namespace CryptoExchange.Net.Testing.Comparers else if (jObj.ValueKind == JsonValueKind.Array) { var resultObj = enumerator.Current; - if (resultObj is string) - // string list - continue; - var resultProps = resultObj.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast().SingleOrDefault())); var arrayConverterProperty = resultObj.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true).FirstOrDefault(); var jsonConverter = ((JsonConverterAttribute)arrayConverterProperty!).ConverterType; @@ -118,11 +342,11 @@ namespace CryptoExchange.Net.Testing.Comparers continue; int i = 0; - foreach (var item in jObj.EnumerateObject()) + foreach (var item in jObj.EnumerateArray()) { - var arrayProp = resultProps.Where(p => p.Item2 != null).FirstOrDefault(p => p.Item2!.Index == i).p; + var arrayProp = resultProps.SingleOrDefault(p => p.Item2!.Index == i).p; if (arrayProp != null) - CheckPropertyValue(method, item.Value, arrayProp.GetValue(resultObj), arrayProp.PropertyType, arrayProp.Name, "Array index " + i, ignoreProperties!); + CheckPropertyValue(method, item, arrayProp.GetValue(resultObj), arrayProp.PropertyType, arrayProp.Name, "Array index " + i, ignoreProperties!); i++; } } @@ -136,317 +360,91 @@ namespace CryptoExchange.Net.Testing.Comparers } else { - var resultProps = resultData.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast().SingleOrDefault())); + var resultProps = propertyValue.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast().SingleOrDefault())); int i = 0; - foreach (var item in jsonObject.EnumerateArray()) + foreach (var item in propValue.EnumerateArray()) { var arrayProp = resultProps.Where(p => p.Item2 != null).FirstOrDefault(p => p.Item2!.Index == i).p; if (arrayProp != null) - CheckPropertyValue(method, item, arrayProp.GetValue(resultData), arrayProp.PropertyType, arrayProp.Name, "Array index " + i, ignoreProperties!); + CheckPropertyValue(method, item, arrayProp.GetValue(propertyValue), arrayProp.PropertyType, arrayProp.Name, "Array index " + i, ignoreProperties!); i++; } } } - else if (jsonObject.ValueKind == JsonValueKind.Object) - { - foreach (var item in jsonObject.EnumerateObject()) - { - //if (item is JProperty prop) - //{ - if (ignoreProperties?.Contains(item.Name) == true) - continue; - - CheckObject(method, item, resultData, ignoreProperties); - //} - } - } else { - //? - } - - Debug.WriteLine($"Successfully validated {method}"); - } - - private static void CheckObject(string method, JsonProperty prop, object obj, List? ignoreProperties) - { - var publicProperties = obj.GetType().GetProperties( - System.Reflection.BindingFlags.Public - | System.Reflection.BindingFlags.GetProperty - | System.Reflection.BindingFlags.SetProperty - | System.Reflection.BindingFlags.Instance).Select(p => (p, ((JsonPropertyNameAttribute?)p.GetCustomAttributes(typeof(JsonPropertyNameAttribute), true).SingleOrDefault())?.Name)); - - var internalProperties = obj.GetType().GetProperties( - System.Reflection.BindingFlags.NonPublic - | System.Reflection.BindingFlags.GetProperty - | System.Reflection.BindingFlags.SetProperty - | System.Reflection.BindingFlags.Instance) - .Where(p => p.CustomAttributes.Any(x => x.AttributeType == typeof(JsonIncludeAttribute))) - .Select(p => (p, ((JsonPropertyNameAttribute?)p.GetCustomAttributes(typeof(JsonPropertyNameAttribute), true).SingleOrDefault())?.Name)); - - var resultProperties = publicProperties.Concat(internalProperties); - - // Property has a value - var property = resultProperties.SingleOrDefault(p => p.Name == prop.Name).p; - property ??= resultProperties.SingleOrDefault(p => p.p.Name == prop.Name).p; - - if (property is null) - // Property not found - throw new Exception($"{method}: Missing property `{prop.Name}` on `{obj.GetType().Name}`"); - - var getMethod = property.GetGetMethod(); - if (getMethod is null) - // There is no getter, so probably just a set for an alternative json name - return; - - var propertyValue = property.GetValue(obj); - CheckPropertyValue(method, prop.Value, propertyValue, property.PropertyType, property.Name, prop.Name, ignoreProperties); - } - - private static void CheckPropertyValue(string method, JsonElement propValue, object? propertyValue, Type propertyType, string? propertyName = null, string? propName = null, List? ignoreProperties = null) - { - if (propertyValue == default && propValue.ValueKind != JsonValueKind.Null && !string.IsNullOrEmpty(propValue.ToString())) - { - if (propertyType == typeof(DateTime?) && (propValue.ToString() == "" || propValue.ToString() == "0" || propValue.ToString() == "-1" || propValue.ToString() == "01/01/0001 00:00:00")) - return; - - // Property value not correct - if (propValue.ToString() != "0") - throw new Exception($"{method}: Property `{propertyName}` has no value while input json `{propName}` has value {propValue}"); - } - - if ((propertyValue == default && (propValue.ValueKind == JsonValueKind.Null || string.IsNullOrEmpty(propValue.ToString()))) || propValue.ToString() == "0") - return; - - if (propertyValue!.GetType().GetInterfaces().Contains(typeof(IDictionary))) - { - var dict = (IDictionary)propertyValue; - foreach (var dictProp in propValue.EnumerateObject()) - { - if (!dict.Contains(dictProp.Name)) - throw new Exception($"{method}: Property `{propertyName}` has no value while input json `{propName}` has value {propValue}"); - - if (dictProp.Value.ValueKind == JsonValueKind.Object) - { - CheckPropertyValue(method, dictProp.Value, dict[dictProp.Name]!, dict[dictProp.Name]!.GetType(), null, null, ignoreProperties); - } - else - { - if (dict[dictProp.Name] == default && dictProp.Value.ValueKind != JsonValueKind.Null) - // Property value not correct - throw new Exception($"{method}: Dictionary entry `{dictProp.Name}` has no value while input json has value {propValue} for"); - } - } - } - else if (propertyValue.GetType().GetInterfaces().Contains(typeof(IEnumerable)) - && propertyValue.GetType() != typeof(string)) - { - if (propValue.ValueKind != JsonValueKind.Array) - return; - - var list = (IEnumerable)propertyValue; - var enumerator = list.GetEnumerator(); - foreach (var jToken in propValue.EnumerateArray()) - { - var moved = enumerator.MoveNext(); - if (!moved) - throw new Exception("Enumeration not moved; incorrect amount of results?"); - - var typeConverter = enumerator.Current.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true); - if (typeConverter.Length != 0 && ((JsonConverterAttribute)typeConverter.First()).ConverterType != typeof(ArrayConverter<>)) - // Custom converter for the type, skip - continue; - - if (jToken.ValueKind == JsonValueKind.Object) - { - foreach (var subProp in jToken.EnumerateObject()) - { - if (ignoreProperties?.Contains(subProp.Name) == true) - continue; - - CheckObject(method, subProp, enumerator.Current, ignoreProperties); - } - } - else if (jToken.ValueKind == JsonValueKind.Array) - { - var resultObj = enumerator.Current; - var resultProps = resultObj.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast().SingleOrDefault())); - var arrayConverterProperty = resultObj.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true).FirstOrDefault(); - var jsonConverter = ((JsonConverterAttribute)arrayConverterProperty!).ConverterType; - if (jsonConverter != typeof(ArrayConverter<>)) - // Not array converter? - continue; - - int i = 0; - foreach (var item in jToken.EnumerateArray()) - { - var arrayProp = resultProps.Where(p => p.Item2 != null).FirstOrDefault(p => p.Item2!.Index == i).p; - if (arrayProp != null) - CheckPropertyValue(method, item, arrayProp.GetValue(resultObj), arrayProp.PropertyType, arrayProp.Name, "Array index " + i, ignoreProperties); - - i++; - } - } - else - { - var value = enumerator.Current; - if (value == default && jToken.ValueKind != JsonValueKind.Null) - throw new Exception($"{method}: Property `{propertyName}` has no value while input json `{propName}` has value {jToken}"); - - CheckValues(method, propertyName!, propertyType, jToken, value!); - } - } - } - else - { - if (propValue.ValueKind == JsonValueKind.Object) - { - foreach (var item in propValue.EnumerateObject()) - { - //if (item is JProperty prop) - //{ - if (ignoreProperties?.Contains(item.Name) == true) - continue; - - CheckObject(method, item, propertyValue, ignoreProperties); - //} - } - } - else if (propValue.ValueKind == JsonValueKind.Array) - { - if (propertyValue is IEnumerable list) - { - var enumerator = list.GetEnumerator(); - foreach (var jObj in propValue.EnumerateArray()) - { - if (!enumerator.MoveNext()) - { - } - - if (jObj.ValueKind == JsonValueKind.Object) - { - foreach (var subProp in jObj.EnumerateObject()) - { - if (ignoreProperties?.Contains(subProp.Name) == true) - continue; - CheckObject(method, subProp, enumerator.Current, ignoreProperties!); - } - } - else if (jObj.ValueKind == JsonValueKind.Array) - { - var resultObj = enumerator.Current; - var resultProps = resultObj.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast().SingleOrDefault())); - var arrayConverterProperty = resultObj.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true).FirstOrDefault(); - var jsonConverter = ((JsonConverterAttribute)arrayConverterProperty!).ConverterType; - if (jsonConverter != typeof(ArrayConverter<>)) - // Not array converter? - continue; - - int i = 0; - foreach (var item in jObj.EnumerateArray()) - { - var arrayProp = resultProps.SingleOrDefault(p => p.Item2!.Index == i).p; - if (arrayProp != null) - CheckPropertyValue(method, item, arrayProp.GetValue(resultObj), arrayProp.PropertyType, arrayProp.Name, "Array index " + i, ignoreProperties!); - i++; - } - } - else - { - var value = enumerator.Current; - if (value == default && jObj.ValueKind != JsonValueKind.Null) - throw new Exception($"{method}: Array has no value while input json array has value {jObj}"); - } - } - } - else - { - var resultProps = propertyValue.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast().SingleOrDefault())); - int i = 0; - foreach (var item in propValue.EnumerateArray()) - { - var arrayProp = resultProps.Where(p => p.Item2 != null).FirstOrDefault(p => p.Item2!.Index == i).p; - if (arrayProp != null) - CheckPropertyValue(method, item, arrayProp.GetValue(propertyValue), arrayProp.PropertyType, arrayProp.Name, "Array index " + i, ignoreProperties!); - i++; - } - } - } - else - { - CheckValues(method, propertyName!, propertyType, propValue, propertyValue); - } - } - } - - private static void CheckValues(string method, string property, Type propertyType, JsonElement jsonValue, object objectValue) - { - if (jsonValue.ValueKind == JsonValueKind.String) - { - var stringValue = jsonValue.GetString(); - if (objectValue is decimal dec) - { - if (ExchangeHelpers.ParseDecimal(stringValue!) != dec) - throw new Exception($"{method}: {property} not equal: {stringValue} vs {dec}"); - } - else if (objectValue is DateTime time) - { - if (!string.IsNullOrEmpty(stringValue) && time != DateTimeConverter.ParseFromString(stringValue!)) - throw new Exception($"{method}: {property} not equal: {stringValue} vs {time}"); - } - else if (objectValue is bool bl) - { - if (bl && (stringValue != "1" && stringValue != "true" && stringValue != "True")) - throw new Exception($"{method}: {property} not equal: {stringValue} vs {bl}"); - if (!bl && (stringValue != "0" && stringValue != "-1" && stringValue != "false" && stringValue != "False")) - throw new Exception($"{method}: {property} not equal: {stringValue} vs {bl}"); - } - else if (propertyType.IsEnum || Nullable.GetUnderlyingType(propertyType)?.IsEnum == true) - { - // TODO enum comparing - } - else if (!stringValue!.Equals(Convert.ToString(objectValue, CultureInfo.InvariantCulture), StringComparison.InvariantCultureIgnoreCase)) - { - throw new Exception($"{method}: {property} not equal: {stringValue} vs {objectValue}"); - } - } - else if (jsonValue.ValueKind == JsonValueKind.Number) - { - var value = jsonValue.GetDecimal(); - if (objectValue is DateTime time) - { - if (time != DateTimeConverter.ParseFromDouble((double)value)) - throw new Exception($"{method}: {property} not equal: {DateTimeConverter.ParseFromDouble((double)value!)} vs {time}"); - } - else if (propertyType.IsEnum || Nullable.GetUnderlyingType(propertyType)?.IsEnum == true) - { - // TODO enum comparing - } - else if(objectValue is decimal dec) - { - if (dec != value) - throw new Exception($"{method}: {property} not equal: {dec} vs {value}"); - } - else if (objectValue is double dbl) - { - if ((decimal)dbl != value) - throw new Exception($"{method}: {property} not equal: {dbl} vs {value}"); - } - else if(objectValue is string objStr) - { - if (objStr != value.ToString()) - throw new Exception($"{method}: {property} not equal: {value} vs {objStr}"); - } - else if (value != Convert.ToInt64(objectValue, CultureInfo.InvariantCulture)) - { - throw new Exception($"{method}: {property} not equal: {value} vs {Convert.ToInt64(objectValue)}"); - } - } - else if (jsonValue.ValueKind == JsonValueKind.True || jsonValue.ValueKind == JsonValueKind.False) - { - if (jsonValue.GetBoolean() != (bool)objectValue) - throw new Exception($"{method}: {property} not equal: {jsonValue.GetBoolean()} vs {(bool)objectValue}"); + CheckValues(method, propertyName!, propertyType, propValue, propertyValue); } } } + + private static void CheckValues(string method, string property, Type propertyType, JsonElement jsonValue, object objectValue) + { + if (jsonValue.ValueKind == JsonValueKind.String) + { + var stringValue = jsonValue.GetString(); + if (objectValue is decimal dec) + { + if (ExchangeHelpers.ParseDecimal(stringValue!) != dec) + throw new Exception($"{method}: {property} not equal: {stringValue} vs {dec}"); + } + else if (objectValue is DateTime time) + { + if (!string.IsNullOrEmpty(stringValue) && time != DateTimeConverter.ParseFromString(stringValue!)) + throw new Exception($"{method}: {property} not equal: {stringValue} vs {time}"); + } + else if (objectValue is bool bl) + { + if (bl && (stringValue != "1" && stringValue != "true" && stringValue != "True")) + throw new Exception($"{method}: {property} not equal: {stringValue} vs {bl}"); + if (!bl && (stringValue != "0" && stringValue != "-1" && stringValue != "false" && stringValue != "False")) + throw new Exception($"{method}: {property} not equal: {stringValue} vs {bl}"); + } + else if (propertyType.IsEnum || Nullable.GetUnderlyingType(propertyType)?.IsEnum == true) + { + // TODO enum comparing + } + else if (!stringValue!.Equals(Convert.ToString(objectValue, CultureInfo.InvariantCulture), StringComparison.InvariantCultureIgnoreCase)) + { + throw new Exception($"{method}: {property} not equal: {stringValue} vs {objectValue}"); + } + } + else if (jsonValue.ValueKind == JsonValueKind.Number) + { + var value = jsonValue.GetDecimal(); + if (objectValue is DateTime time) + { + if (time != DateTimeConverter.ParseFromDouble((double)value)) + throw new Exception($"{method}: {property} not equal: {DateTimeConverter.ParseFromDouble((double)value!)} vs {time}"); + } + else if (propertyType.IsEnum || Nullable.GetUnderlyingType(propertyType)?.IsEnum == true) + { + // TODO enum comparing + } + else if(objectValue is decimal dec) + { + if (dec != value) + throw new Exception($"{method}: {property} not equal: {dec} vs {value}"); + } + else if (objectValue is double dbl) + { + if ((decimal)dbl != value) + throw new Exception($"{method}: {property} not equal: {dbl} vs {value}"); + } + else if(objectValue is string objStr) + { + if (objStr != value.ToString()) + throw new Exception($"{method}: {property} not equal: {value} vs {objStr}"); + } + else if (value != Convert.ToInt64(objectValue, CultureInfo.InvariantCulture)) + { + throw new Exception($"{method}: {property} not equal: {value} vs {Convert.ToInt64(objectValue)}"); + } + } + else if (jsonValue.ValueKind == JsonValueKind.True || jsonValue.ValueKind == JsonValueKind.False) + { + if (jsonValue.GetBoolean() != (bool)objectValue) + throw new Exception($"{method}: {property} not equal: {jsonValue.GetBoolean()} vs {(bool)objectValue}"); + } + } } diff --git a/CryptoExchange.Net/Testing/EnumValueTraceListener.cs b/CryptoExchange.Net/Testing/EnumValueTraceListener.cs index 3b13193..577f651 100644 --- a/CryptoExchange.Net/Testing/EnumValueTraceListener.cs +++ b/CryptoExchange.Net/Testing/EnumValueTraceListener.cs @@ -1,32 +1,31 @@ -using System; +using System; using System.Diagnostics; -namespace CryptoExchange.Net.Testing +namespace CryptoExchange.Net.Testing; + +internal class EnumValueTraceListener : TraceListener { - internal class EnumValueTraceListener : TraceListener + public override void Write(string? message) { - public override void Write(string? message) - { - if (message == null) - return; + if (message == null) + return; - if (message.Contains("Cannot map")) - throw new Exception("Enum value error: " + message); + if (message.Contains("Cannot map")) + throw new Exception("Enum value error: " + message); - if (message.Contains("Received null enum value")) - throw new Exception("Enum null error: " + message); - } + if (message.Contains("Received null enum value")) + throw new Exception("Enum null error: " + message); + } - public override void WriteLine(string? message) - { - if (message == null) - return; + public override void WriteLine(string? message) + { + if (message == null) + return; - if (message.Contains("Cannot map")) - throw new Exception("Enum value error: " + message); + if (message.Contains("Cannot map")) + throw new Exception("Enum value error: " + message); - if (message.Contains("Received null enum value")) - throw new Exception("Enum null error: " + message); - } + if (message.Contains("Received null enum value")) + throw new Exception("Enum null error: " + message); } } diff --git a/CryptoExchange.Net/Testing/Implementations/TestAuthTimeProvider.cs b/CryptoExchange.Net/Testing/Implementations/TestAuthTimeProvider.cs index ac6fdfa..a1d7dd4 100644 --- a/CryptoExchange.Net/Testing/Implementations/TestAuthTimeProvider.cs +++ b/CryptoExchange.Net/Testing/Implementations/TestAuthTimeProvider.cs @@ -1,17 +1,16 @@ -using CryptoExchange.Net.Interfaces; +using CryptoExchange.Net.Interfaces; using System; -namespace CryptoExchange.Net.Testing.Implementations +namespace CryptoExchange.Net.Testing.Implementations; + +internal class TestAuthTimeProvider : IAuthTimeProvider { - internal class TestAuthTimeProvider : IAuthTimeProvider + private readonly DateTime _timestamp; + + public TestAuthTimeProvider(DateTime timestamp) { - private readonly DateTime _timestamp; - - public TestAuthTimeProvider(DateTime timestamp) - { - _timestamp = timestamp; - } - - public DateTime GetTime() => _timestamp; + _timestamp = timestamp; } + + public DateTime GetTime() => _timestamp; } diff --git a/CryptoExchange.Net/Testing/Implementations/TestNonceProvider.cs b/CryptoExchange.Net/Testing/Implementations/TestNonceProvider.cs index 5f97248..b450977 100644 --- a/CryptoExchange.Net/Testing/Implementations/TestNonceProvider.cs +++ b/CryptoExchange.Net/Testing/Implementations/TestNonceProvider.cs @@ -1,23 +1,22 @@ -using CryptoExchange.Net.Interfaces; +using CryptoExchange.Net.Interfaces; -namespace CryptoExchange.Net.Testing.Implementations +namespace CryptoExchange.Net.Testing.Implementations; + +/// +/// Test implementation for nonce provider, returning a prespecified nonce +/// +public class TestNonceProvider : INonceProvider { + private readonly long _nonce; + /// - /// Test implementation for nonce provider, returning a prespecified nonce + /// ctor /// - public class TestNonceProvider : INonceProvider + public TestNonceProvider(long nonce) { - private readonly long _nonce; - - /// - /// ctor - /// - public TestNonceProvider(long nonce) - { - _nonce = nonce; - } - - /// - public long GetNonce() => _nonce; + _nonce = nonce; } + + /// + public long GetNonce() => _nonce; } diff --git a/CryptoExchange.Net/Testing/Implementations/TestRequest.cs b/CryptoExchange.Net/Testing/Implementations/TestRequest.cs index dc6ba5a..7627f5b 100644 --- a/CryptoExchange.Net/Testing/Implementations/TestRequest.cs +++ b/CryptoExchange.Net/Testing/Implementations/TestRequest.cs @@ -1,4 +1,4 @@ -using CryptoExchange.Net.Interfaces; +using CryptoExchange.Net.Interfaces; using System; using System.Collections.Generic; using System.Net.Http; @@ -6,47 +6,46 @@ using System.Text; using System.Threading; using System.Threading.Tasks; -namespace CryptoExchange.Net.Testing.Implementations +namespace CryptoExchange.Net.Testing.Implementations; + +internal class TestRequest : IRequest { - internal class TestRequest : IRequest - { - private readonly List> _headers = new(); - private readonly TestResponse _response; + private readonly List> _headers = new(); + private readonly TestResponse _response; - public string Accept { set { } } + public string Accept { set { } } - public string? Content { get; private set; } + public string? Content { get; private set; } - public HttpMethod Method { get; set; } + public HttpMethod Method { get; set; } - public Uri Uri { get; set; } + public Uri Uri { get; set; } - public int RequestId { get; set; } + public int RequestId { get; set; } #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. - public TestRequest(TestResponse response) + public TestRequest(TestResponse response) #pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. - { - _response = response; - } + { + _response = response; + } - public void AddHeader(string key, string value) - { - _headers.Add(new KeyValuePair(key, new[] { value })); - } + public void AddHeader(string key, string value) + { + _headers.Add(new KeyValuePair(key, new[] { value })); + } - public KeyValuePair[] GetHeaders() => _headers.ToArray(); + public KeyValuePair[] GetHeaders() => _headers.ToArray(); - public Task GetResponseAsync(CancellationToken cancellationToken) => Task.FromResult(_response); + public Task GetResponseAsync(CancellationToken cancellationToken) => Task.FromResult(_response); - public void SetContent(byte[] data) - { - Content = Encoding.UTF8.GetString(data); - } + public void SetContent(byte[] data) + { + Content = Encoding.UTF8.GetString(data); + } - public void SetContent(string data, string contentType) - { - Content = data; - } + public void SetContent(string data, string contentType) + { + Content = data; } } diff --git a/CryptoExchange.Net/Testing/Implementations/TestRequestFactory.cs b/CryptoExchange.Net/Testing/Implementations/TestRequestFactory.cs index 293c2ea..450c5e7 100644 --- a/CryptoExchange.Net/Testing/Implementations/TestRequestFactory.cs +++ b/CryptoExchange.Net/Testing/Implementations/TestRequestFactory.cs @@ -1,31 +1,30 @@ -using CryptoExchange.Net.Interfaces; +using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Objects; using System; using System.Net.Http; -namespace CryptoExchange.Net.Testing.Implementations +namespace CryptoExchange.Net.Testing.Implementations; + +internal class TestRequestFactory : IRequestFactory { - internal class TestRequestFactory : IRequestFactory + private readonly TestRequest _request; + + public TestRequestFactory(TestRequest request) { - private readonly TestRequest _request; - - public TestRequestFactory(TestRequest request) - { - _request = request; - } - - public void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? httpClient = null) - { - } - - public IRequest Create(HttpMethod method, Uri uri, int requestId) - { - _request.Method = method; - _request.Uri = uri; - _request.RequestId = requestId; - return _request; - } - - public void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout) {} + _request = request; } + + public void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? httpClient = null) + { + } + + public IRequest Create(HttpMethod method, Uri uri, int requestId) + { + _request.Method = method; + _request.Uri = uri; + _request.RequestId = requestId; + return _request; + } + + public void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout) {} } diff --git a/CryptoExchange.Net/Testing/Implementations/TestResponse.cs b/CryptoExchange.Net/Testing/Implementations/TestResponse.cs index 53d59e4..4020db5 100644 --- a/CryptoExchange.Net/Testing/Implementations/TestResponse.cs +++ b/CryptoExchange.Net/Testing/Implementations/TestResponse.cs @@ -1,34 +1,33 @@ -using CryptoExchange.Net.Interfaces; +using CryptoExchange.Net.Interfaces; using System.Collections.Generic; using System.IO; using System.Net; using System.Threading.Tasks; -namespace CryptoExchange.Net.Testing.Implementations +namespace CryptoExchange.Net.Testing.Implementations; + +internal class TestResponse : IResponse { - internal class TestResponse : IResponse + private readonly Stream _response; + + public HttpStatusCode StatusCode { get; } + + public bool IsSuccessStatusCode { get; } + + public long? ContentLength { get; } + + public KeyValuePair[] ResponseHeaders { get; } = []; + + public TestResponse(HttpStatusCode code, Stream response) { - private readonly Stream _response; - - public HttpStatusCode StatusCode { get; } - - public bool IsSuccessStatusCode { get; } - - public long? ContentLength { get; } - - public KeyValuePair[] ResponseHeaders { get; } = new KeyValuePair[0]; - - public TestResponse(HttpStatusCode code, Stream response) - { - StatusCode = code; - IsSuccessStatusCode = code == HttpStatusCode.OK; - _response = response; - } - - public void Close() - { - } - - public Task GetResponseStreamAsync() => Task.FromResult(_response); + StatusCode = code; + IsSuccessStatusCode = code == HttpStatusCode.OK; + _response = response; } + + public void Close() + { + } + + public Task GetResponseStreamAsync() => Task.FromResult(_response); } diff --git a/CryptoExchange.Net/Testing/Implementations/TestSocket.cs b/CryptoExchange.Net/Testing/Implementations/TestSocket.cs index 5df0dae..ece5cce 100644 --- a/CryptoExchange.Net/Testing/Implementations/TestSocket.cs +++ b/CryptoExchange.Net/Testing/Implementations/TestSocket.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Net.WebSockets; using System.Text; using System.Text.Json; @@ -10,104 +10,103 @@ using CryptoExchange.Net.Objects; #pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code #pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling. -namespace CryptoExchange.Net.Testing.Implementations +namespace CryptoExchange.Net.Testing.Implementations; + +internal class TestSocket : IWebsocket { - internal class TestSocket : IWebsocket - { - public event Action? OnMessageSend; + public event Action? OnMessageSend; - public bool CanConnect { get; set; } = true; - public bool Connected { get; set; } + public bool CanConnect { get; set; } = true; + public bool Connected { get; set; } - public event Func? OnClose; + public event Func? OnClose; #pragma warning disable 0067 - public event Func? OnReconnected; - public event Func? OnReconnecting; - public event Func? OnRequestRateLimited; - public event Func? OnConnectRateLimited; - public event Func? OnError; + public event Func? OnReconnected; + public event Func? OnReconnecting; + public event Func? OnRequestRateLimited; + public event Func? OnConnectRateLimited; + public event Func? OnError; #pragma warning restore 0067 - public event Func? OnRequestSent; - public event Func, Task>? OnStreamMessage; - public event Func? OnOpen; + public event Func? OnRequestSent; + public event Func, Task>? OnStreamMessage; + public event Func? OnOpen; - public int Id { get; } - public bool IsClosed => !Connected; - public bool IsOpen => Connected; - public double IncomingKbps => 0; - public Uri Uri { get; set; } - public Func>? GetReconnectionUrl { get; set; } + public int Id { get; } + public bool IsClosed => !Connected; + public bool IsOpen => Connected; + public double IncomingKbps => 0; + public Uri Uri { get; set; } + public Func>? GetReconnectionUrl { get; set; } - public static int lastId = 0; - public static object lastIdLock = new object(); + public static int lastId; + public static object lastIdLock = new(); - public TestSocket(string address) + public TestSocket(string address) + { + Uri = new Uri(address); + lock (lastIdLock) { - Uri = new Uri(address); - lock (lastIdLock) - { - Id = lastId + 1; - lastId++; - } + Id = lastId + 1; + lastId++; } - - public Task ConnectAsync(CancellationToken ct) - { - Connected = CanConnect; - return Task.FromResult(CanConnect ? new CallResult(null) : new CallResult(new CantConnectError())); - } - - public bool Send(int requestId, string data, int weight) - { - if (!Connected) - throw new Exception("Socket not connected"); - - OnRequestSent?.Invoke(requestId); - OnMessageSend?.Invoke(data); - return true; - } - - public bool Send(int requestId, byte[] data, int weight) - { - if (!Connected) - throw new Exception("Socket not connected"); - - OnRequestSent?.Invoke(requestId); - OnMessageSend?.Invoke(Encoding.UTF8.GetString(data)); - return true; - } - - - public Task CloseAsync() - { - Connected = false; - return Task.FromResult(0); - } - - public void InvokeClose() - { - Connected = false; - OnClose?.Invoke(); - } - - public void InvokeOpen() - { - OnOpen?.Invoke(); - } - - public void InvokeMessage(string data) - { - OnStreamMessage?.Invoke(WebSocketMessageType.Text, new ReadOnlyMemory(Encoding.UTF8.GetBytes(data))).Wait(); - } - - public void InvokeMessage(T data) - { - OnStreamMessage?.Invoke(WebSocketMessageType.Text, new ReadOnlyMemory(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(data)))).Wait(); - } - - public Task ReconnectAsync() => throw new NotImplementedException(); - public void Dispose() { } - - public void UpdateProxy(ApiProxy? proxy) => throw new NotImplementedException(); } + + public Task ConnectAsync(CancellationToken ct) + { + Connected = CanConnect; + return Task.FromResult(CanConnect ? new CallResult(null) : new CallResult(new CantConnectError())); + } + + public bool Send(int requestId, string data, int weight) + { + if (!Connected) + throw new Exception("Socket not connected"); + + OnRequestSent?.Invoke(requestId); + OnMessageSend?.Invoke(data); + return true; + } + + public bool Send(int requestId, byte[] data, int weight) + { + if (!Connected) + throw new Exception("Socket not connected"); + + OnRequestSent?.Invoke(requestId); + OnMessageSend?.Invoke(Encoding.UTF8.GetString(data)); + return true; + } + + + public Task CloseAsync() + { + Connected = false; + return Task.FromResult(0); + } + + public void InvokeClose() + { + Connected = false; + OnClose?.Invoke(); + } + + public void InvokeOpen() + { + OnOpen?.Invoke(); + } + + public void InvokeMessage(string data) + { + OnStreamMessage?.Invoke(WebSocketMessageType.Text, new ReadOnlyMemory(Encoding.UTF8.GetBytes(data))).Wait(); + } + + public void InvokeMessage(T data) + { + OnStreamMessage?.Invoke(WebSocketMessageType.Text, new ReadOnlyMemory(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(data)))).Wait(); + } + + public Task ReconnectAsync() => throw new NotImplementedException(); + public void Dispose() { } + + public void UpdateProxy(ApiProxy? proxy) => throw new NotImplementedException(); } diff --git a/CryptoExchange.Net/Testing/Implementations/TestWebsocketFactory.cs b/CryptoExchange.Net/Testing/Implementations/TestWebsocketFactory.cs index 3fac7e9..4605ca9 100644 --- a/CryptoExchange.Net/Testing/Implementations/TestWebsocketFactory.cs +++ b/CryptoExchange.Net/Testing/Implementations/TestWebsocketFactory.cs @@ -1,17 +1,16 @@ -using CryptoExchange.Net.Interfaces; +using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Objects.Sockets; using Microsoft.Extensions.Logging; -namespace CryptoExchange.Net.Testing.Implementations -{ - internal class TestWebsocketFactory : IWebsocketFactory - { - private readonly TestSocket _socket; - public TestWebsocketFactory(TestSocket socket) - { - _socket = socket; - } +namespace CryptoExchange.Net.Testing.Implementations; - public IWebsocket CreateWebsocket(ILogger logger, WebSocketParameters parameters) => _socket; +internal class TestWebsocketFactory : IWebsocketFactory +{ + private readonly TestSocket _socket; + public TestWebsocketFactory(TestSocket socket) + { + _socket = socket; } + + public IWebsocket CreateWebsocket(ILogger logger, WebSocketParameters parameters) => _socket; } diff --git a/CryptoExchange.Net/Testing/RestIntegrationTest.cs b/CryptoExchange.Net/Testing/RestIntegrationTest.cs index 18b359b..b9a7941 100644 --- a/CryptoExchange.Net/Testing/RestIntegrationTest.cs +++ b/CryptoExchange.Net/Testing/RestIntegrationTest.cs @@ -1,4 +1,4 @@ -using CryptoExchange.Net.Interfaces; +using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Objects; using Microsoft.Extensions.Logging; using System; @@ -6,126 +6,125 @@ using System.Diagnostics; using System.Linq.Expressions; using System.Threading.Tasks; -namespace CryptoExchange.Net.Testing +namespace CryptoExchange.Net.Testing; + +/// +/// Base class for executing REST API integration tests +/// +/// Client type +public abstract class RestIntegrationTest { /// - /// Base class for executing REST API integration tests + /// Get a client instance /// - /// Client type - public abstract class RestIntegrationTest + /// + /// + public abstract TClient GetClient(ILoggerFactory loggerFactory); + + /// + /// Whether the test should be run. By default integration tests aren't executed, can be set to true to force execution. + /// + public virtual bool Run { get; set; } + + /// + /// Whether API credentials are provided and thus authenticated calls can be executed. Should be set in the GetClient implementation. + /// + public bool Authenticated { get; set; } + + /// + /// Create a client + /// + /// + protected TClient CreateClient() { - /// - /// Get a client instance - /// - /// - /// - public abstract TClient GetClient(ILoggerFactory loggerFactory); + var fact = new LoggerFactory(); + fact.AddProvider(new TraceLoggerProvider()); + return GetClient(fact); + } - /// - /// Whether the test should be run. By default integration tests aren't executed, can be set to true to force execution. - /// - public virtual bool Run { get; set; } + /// + /// Check if integration tests should be executed + /// + /// + protected bool ShouldRun() + { + var integrationTests = Environment.GetEnvironmentVariable("INTEGRATION"); + if (!Run && integrationTests != "1") + return false; - /// - /// Whether API credentials are provided and thus authenticated calls can be executed. Should be set in the GetClient implementation. - /// - public bool Authenticated { get; set; } + return true; + } - /// - /// Create a client - /// - /// - protected TClient CreateClient() + /// + /// Execute a REST endpoint call and check for any errors or warnings. + /// + /// Type of response + /// The call expression + /// Whether this is an authenticated request + public async Task RunAndCheckResult(Expression>>> expression, bool authRequest) + { + if (!ShouldRun()) + return; + + var client = CreateClient(); + + var expressionBody = (MethodCallExpression)expression.Body; + if (authRequest && !Authenticated) { - var fact = new LoggerFactory(); - fact.AddProvider(new TraceLoggerProvider()); - return GetClient(fact); + Debug.WriteLine($"Skipping {expressionBody.Method.Name}, not authenticated"); + return; } - /// - /// Check if integration tests should be executed - /// - /// - protected bool ShouldRun() - { - var integrationTests = Environment.GetEnvironmentVariable("INTEGRATION"); - if (!Run && integrationTests != "1") - return false; + var listener = new EnumValueTraceListener(); + Trace.Listeners.Add(listener); - return true; + WebCallResult result; + try + { + result = await expression.Compile().Invoke(client).ConfigureAwait(false); + } + catch (Exception ex) + { + throw new Exception($"Method {expressionBody.Method.Name} threw an exception: " + ex.ToLogString()); + } + finally + { + Trace.Listeners.Remove(listener); } - /// - /// Execute a REST endpoint call and check for any errors or warnings. - /// - /// Type of response - /// The call expression - /// Whether this is an authenticated request - public async Task RunAndCheckResult(Expression>>> expression, bool authRequest) + if (!result.Success) + throw new Exception($"Method {expressionBody.Method.Name} returned error: " + result.Error); + + Debug.WriteLine($"{expressionBody.Method.Name} {result}"); + } + + /// + /// Start an order book implementation and expect it to sync and produce an update + /// + public async Task TestOrderBook(ISymbolOrderBook book) + { + if (!ShouldRun()) + return; + + var bookHasChanged = false; + book.OnStatusChange += (_, news) => { - if (!ShouldRun()) - return; - - var client = CreateClient(); - - var expressionBody = (MethodCallExpression)expression.Body; - if (authRequest && !Authenticated) - { - Debug.WriteLine($"Skipping {expressionBody.Method.Name}, not authenticated"); - return; - } - - var listener = new EnumValueTraceListener(); - Trace.Listeners.Add(listener); - - WebCallResult result; - try - { - result = await expression.Compile().Invoke(client).ConfigureAwait(false); - } - catch (Exception ex) - { - throw new Exception($"Method {expressionBody.Method.Name} threw an exception: " + ex.ToLogString()); - } - finally - { - Trace.Listeners.Remove(listener); - } - - if (!result.Success) - throw new Exception($"Method {expressionBody.Method.Name} returned error: " + result.Error); - - Debug.WriteLine($"{expressionBody.Method.Name} {result}"); - } - - /// - /// Start an order book implementation and expect it to sync and produce an update - /// - public async Task TestOrderBook(ISymbolOrderBook book) + if (news == OrderBookStatus.Reconnecting) + throw new Exception($"Book reconnecting"); + }; + book.OnOrderBookUpdate += (change) => { - if (!ShouldRun()) - return; + bookHasChanged = true; + }; - var bookHasChanged = false; - book.OnStatusChange += (_, news) => - { - if (news == OrderBookStatus.Reconnecting) - throw new Exception($"Book reconnecting"); - }; - book.OnOrderBookUpdate += (change) => - { - bookHasChanged = true; - }; + var result = await book.StartAsync().ConfigureAwait(false); + if (!result) + throw new Exception($"Book failed to start: " + result.Error); - var result = await book.StartAsync().ConfigureAwait(false); - if (!result) - throw new Exception($"Book failed to start: " + result.Error); + await Task.Delay(5000).ConfigureAwait(false); + await book.StopAsync().ConfigureAwait(false); - await Task.Delay(5000).ConfigureAwait(false); - await book.StopAsync().ConfigureAwait(false); - - if (!bookHasChanged) - throw new Exception($"Expected book to have changed at least once"); - } + if (!bookHasChanged) + throw new Exception($"Expected book to have changed at least once"); } } diff --git a/CryptoExchange.Net/Testing/RestRequestValidator.cs b/CryptoExchange.Net/Testing/RestRequestValidator.cs index 1bc0bea..b998ae1 100644 --- a/CryptoExchange.Net/Testing/RestRequestValidator.cs +++ b/CryptoExchange.Net/Testing/RestRequestValidator.cs @@ -1,4 +1,4 @@ -using CryptoExchange.Net.Clients; +using CryptoExchange.Net.Clients; using CryptoExchange.Net.Objects; using CryptoExchange.Net.Testing.Comparers; using System; @@ -9,177 +9,176 @@ using System.Net.Http; using System.Text; using System.Threading.Tasks; -namespace CryptoExchange.Net.Testing +namespace CryptoExchange.Net.Testing; + +/// +/// Validator for REST requests, comparing path, http method, authentication and response parsing +/// +/// The Rest client +public class RestRequestValidator where TClient : BaseRestClient { + private readonly TClient _client; + private readonly Func _isAuthenticated; + private readonly string _folder; + private readonly string _baseAddress; + private readonly string? _nestedPropertyForCompare; + /// - /// Validator for REST requests, comparing path, http method, authentication and response parsing + /// ctor /// - /// The Rest client - public class RestRequestValidator where TClient : BaseRestClient + /// Client to test + /// Folder for json test values + /// The base address that is expected + /// Func for checking if the request is authenticated + /// Property to use for compare + public RestRequestValidator(TClient client, string folder, string baseAddress, Func isAuthenticated, string? nestedPropertyForCompare = null) { - private readonly TClient _client; - private readonly Func _isAuthenticated; - private readonly string _folder; - private readonly string _baseAddress; - private readonly string? _nestedPropertyForCompare; + _client = client; + _folder = folder; + _baseAddress = baseAddress; + _nestedPropertyForCompare = nestedPropertyForCompare; + _isAuthenticated = isAuthenticated; + } - /// - /// ctor - /// - /// Client to test - /// Folder for json test values - /// The base address that is expected - /// Func for checking if the request is authenticated - /// Property to use for compare - public RestRequestValidator(TClient client, string folder, string baseAddress, Func isAuthenticated, string? nestedPropertyForCompare = null) + /// + /// Validate a request + /// + /// Expected response type + /// Method invocation + /// Method name for looking up json test values + /// Use nested json property for compare + /// Ignore certain properties + /// Use the first item of an json array response + /// Whether to skip the response model validation + /// + /// + public Task ValidateAsync( + Func>> methodInvoke, + string name, + string? nestedJsonProperty = null, + List? ignoreProperties = null, + bool useSingleArrayItem = false, + bool skipResponseValidation = false) + => ValidateAsync(methodInvoke, name, nestedJsonProperty, ignoreProperties, useSingleArrayItem, skipResponseValidation); + + /// + /// Validate a request + /// + /// Expected response type + /// The concrete response type + /// Method invocation + /// Method name for looking up json test values + /// Use nested json property for compare + /// Ignore certain properties + /// Use the first item of an json array response + /// Whether to skip the response model validation + /// + /// + public async Task ValidateAsync( + Func>> methodInvoke, + string name, + string? nestedJsonProperty = null, + List? ignoreProperties = null, + bool useSingleArrayItem = false, + bool skipResponseValidation = false) where TActualResponse : TResponse + { + var listener = new EnumValueTraceListener(); + Trace.Listeners.Add(listener); + + var path = Directory.GetParent(Environment.CurrentDirectory)!.Parent!.Parent!.FullName; + FileStream file; + try { - _client = client; - _folder = folder; - _baseAddress = baseAddress; - _nestedPropertyForCompare = nestedPropertyForCompare; - _isAuthenticated = isAuthenticated; + file = File.OpenRead(Path.Combine(path, _folder, $"{name}.txt")); + } + catch (FileNotFoundException) + { + throw new Exception($"Response file not found for {name}: {path}"); } - /// - /// Validate a request - /// - /// Expected response type - /// Method invocation - /// Method name for looking up json test values - /// Use nested json property for compare - /// Ignore certain properties - /// Use the first item of an json array response - /// Whether to skip the response model validation - /// - /// - public Task ValidateAsync( - Func>> methodInvoke, - string name, - string? nestedJsonProperty = null, - List? ignoreProperties = null, - bool useSingleArrayItem = false, - bool skipResponseValidation = false) - => ValidateAsync(methodInvoke, name, nestedJsonProperty, ignoreProperties, useSingleArrayItem, skipResponseValidation); + var buffer = new byte[file.Length]; + await file.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false); + file.Close(); - /// - /// Validate a request - /// - /// Expected response type - /// The concrete response type - /// Method invocation - /// Method name for looking up json test values - /// Use nested json property for compare - /// Ignore certain properties - /// Use the first item of an json array response - /// Whether to skip the response model validation - /// - /// - public async Task ValidateAsync( - Func>> methodInvoke, - string name, - string? nestedJsonProperty = null, - List? ignoreProperties = null, - bool useSingleArrayItem = false, - bool skipResponseValidation = false) where TActualResponse : TResponse + var data = Encoding.UTF8.GetString(buffer); + using var reader = new StringReader(data); + var expectedMethod = reader.ReadLine(); + var expectedPath = reader.ReadLine(); + var expectedAuth = bool.Parse(reader.ReadLine()!); + var response = reader.ReadToEnd(); + + TestHelpers.ConfigureRestClient(_client, response, System.Net.HttpStatusCode.OK); + var result = await methodInvoke(_client).ConfigureAwait(false); + + // Check request/response properties + if (result.Error != null) + throw new Exception(name + " returned error " + result.Error); + if (_isAuthenticated(result.AsDataless()) != expectedAuth) + throw new Exception(name + $" authentication not matched. Expected: {expectedAuth}, Actual: {_isAuthenticated(result.AsDataless())}"); + if (result.RequestMethod != new HttpMethod(expectedMethod!)) + throw new Exception(name + $" http method not matched. Expected {expectedMethod}, Actual: {result.RequestMethod}"); + if (expectedPath != result.RequestUrl!.Replace(_baseAddress, "").Split(new char[] { '?' })[0]) + throw new Exception(name + $" path not matched. Expected: {expectedPath}, Actual: {result.RequestUrl!.Replace(_baseAddress, "").Split(new char[] { '?' })[0]}"); + + if (!skipResponseValidation) { - var listener = new EnumValueTraceListener(); - Trace.Listeners.Add(listener); - - var path = Directory.GetParent(Environment.CurrentDirectory)!.Parent!.Parent!.FullName; - FileStream file; - try - { - file = File.OpenRead(Path.Combine(path, _folder, $"{name}.txt")); - } - catch (FileNotFoundException) - { - throw new Exception($"Response file not found for {name}: {path}"); - } - - var buffer = new byte[file.Length]; - await file.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false); - file.Close(); - - var data = Encoding.UTF8.GetString(buffer); - using var reader = new StringReader(data); - var expectedMethod = reader.ReadLine(); - var expectedPath = reader.ReadLine(); - var expectedAuth = bool.Parse(reader.ReadLine()!); - var response = reader.ReadToEnd(); - - TestHelpers.ConfigureRestClient(_client, response, System.Net.HttpStatusCode.OK); - var result = await methodInvoke(_client).ConfigureAwait(false); - - // Check request/response properties - if (result.Error != null) - throw new Exception(name + " returned error " + result.Error); - if (_isAuthenticated(result.AsDataless()) != expectedAuth) - throw new Exception(name + $" authentication not matched. Expected: {expectedAuth}, Actual: {_isAuthenticated(result.AsDataless())}"); - if (result.RequestMethod != new HttpMethod(expectedMethod!)) - throw new Exception(name + $" http method not matched. Expected {expectedMethod}, Actual: {result.RequestMethod}"); - if (expectedPath != result.RequestUrl!.Replace(_baseAddress, "").Split(new char[] { '?' })[0]) - throw new Exception(name + $" path not matched. Expected: {expectedPath}, Actual: {result.RequestUrl!.Replace(_baseAddress, "").Split(new char[] { '?' })[0]}"); - - if (!skipResponseValidation) - { - // Check response data - object responseData = (TActualResponse)result.Data!; - SystemTextJsonComparer.CompareData(name, responseData, response, nestedJsonProperty ?? _nestedPropertyForCompare, ignoreProperties, useSingleArrayItem); - } - - Trace.Listeners.Remove(listener); + // Check response data + object responseData = (TActualResponse)result.Data!; + SystemTextJsonComparer.CompareData(name, responseData, response, nestedJsonProperty ?? _nestedPropertyForCompare, ignoreProperties, useSingleArrayItem); } - /// - /// Validate a request - /// - /// Method invocation - /// Method name for looking up json test values - /// - /// - public async Task ValidateAsync( - Func> methodInvoke, - string name) + Trace.Listeners.Remove(listener); + } + + /// + /// Validate a request + /// + /// Method invocation + /// Method name for looking up json test values + /// + /// + public async Task ValidateAsync( + Func> methodInvoke, + string name) + { + var listener = new EnumValueTraceListener(); + Trace.Listeners.Add(listener); + + var path = Directory.GetParent(Environment.CurrentDirectory)!.Parent!.Parent!.FullName; + FileStream file; + try { - var listener = new EnumValueTraceListener(); - Trace.Listeners.Add(listener); - - var path = Directory.GetParent(Environment.CurrentDirectory)!.Parent!.Parent!.FullName; - FileStream file; - try - { - file = File.OpenRead(Path.Combine(path, _folder, $"{name}.txt")); - } - catch (FileNotFoundException) - { - throw new Exception($"Response file not found for {name}: {path}"); - } - - var buffer = new byte[file.Length]; - await file.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false); - file.Close(); - - var data = Encoding.UTF8.GetString(buffer); - using var reader = new StringReader(data); - var expectedMethod = reader.ReadLine(); - var expectedPath = reader.ReadLine(); - var expectedAuth = bool.Parse(reader.ReadLine()!); - var response = reader.ReadToEnd(); - - TestHelpers.ConfigureRestClient(_client, response, System.Net.HttpStatusCode.OK); - var result = await methodInvoke(_client).ConfigureAwait(false); - - // Check request/response properties - if (result.Error != null) - throw new Exception(name + " returned error " + result.Error); - if (_isAuthenticated(result) != expectedAuth) - throw new Exception(name + $" authentication not matched. Expected: {expectedAuth}, Actual: {_isAuthenticated(result)}"); - if (result.RequestMethod != new HttpMethod(expectedMethod!)) - throw new Exception(name + $" http method not matched. Expected {expectedMethod}, Actual: {result.RequestMethod}"); - if (expectedPath != result.RequestUrl!.Replace(_baseAddress, "").Split(new char[] { '?' })[0]) - throw new Exception(name + $" path not matched. Expected: {expectedPath}, Actual: {result.RequestUrl!.Replace(_baseAddress, "").Split(new char[] { '?' })[0]}"); - - Trace.Listeners.Remove(listener); + file = File.OpenRead(Path.Combine(path, _folder, $"{name}.txt")); } + catch (FileNotFoundException) + { + throw new Exception($"Response file not found for {name}: {path}"); + } + + var buffer = new byte[file.Length]; + await file.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false); + file.Close(); + + var data = Encoding.UTF8.GetString(buffer); + using var reader = new StringReader(data); + var expectedMethod = reader.ReadLine(); + var expectedPath = reader.ReadLine(); + var expectedAuth = bool.Parse(reader.ReadLine()!); + var response = reader.ReadToEnd(); + + TestHelpers.ConfigureRestClient(_client, response, System.Net.HttpStatusCode.OK); + var result = await methodInvoke(_client).ConfigureAwait(false); + + // Check request/response properties + if (result.Error != null) + throw new Exception(name + " returned error " + result.Error); + if (_isAuthenticated(result) != expectedAuth) + throw new Exception(name + $" authentication not matched. Expected: {expectedAuth}, Actual: {_isAuthenticated(result)}"); + if (result.RequestMethod != new HttpMethod(expectedMethod!)) + throw new Exception(name + $" http method not matched. Expected {expectedMethod}, Actual: {result.RequestMethod}"); + if (expectedPath != result.RequestUrl!.Replace(_baseAddress, "").Split(new char[] { '?' })[0]) + throw new Exception(name + $" path not matched. Expected: {expectedPath}, Actual: {result.RequestUrl!.Replace(_baseAddress, "").Split(new char[] { '?' })[0]}"); + + Trace.Listeners.Remove(listener); } } diff --git a/CryptoExchange.Net/Testing/SocketIntegrationTest.cs b/CryptoExchange.Net/Testing/SocketIntegrationTest.cs index d00ed06..8ea1573 100644 --- a/CryptoExchange.Net/Testing/SocketIntegrationTest.cs +++ b/CryptoExchange.Net/Testing/SocketIntegrationTest.cs @@ -1,4 +1,3 @@ -using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects.Sockets; using Microsoft.Extensions.Logging; @@ -8,111 +7,110 @@ using System.Linq.Expressions; using System.Threading; using System.Threading.Tasks; -namespace CryptoExchange.Net.Testing +namespace CryptoExchange.Net.Testing; + +/// +/// Base class for executing websocket API integration tests +/// +/// Client type +public abstract class SocketIntegrationTest { /// - /// Base class for executing websocket API integration tests + /// Get a client instance /// - /// Client type - public abstract class SocketIntegrationTest + /// + /// + public abstract TClient GetClient(ILoggerFactory loggerFactory); + + /// + /// Whether the test should be run. By default integration tests aren't executed, can be set to true to force execution. + /// + public virtual bool Run { get; set; } + + /// + /// Whether API credentials are provided and thus authenticated calls can be executed. Should be set in the GetClient implementation. + /// + public bool Authenticated { get; set; } + + /// + /// Create a client + /// + /// + protected TClient CreateClient() { - /// - /// Get a client instance - /// - /// - /// - public abstract TClient GetClient(ILoggerFactory loggerFactory); + var fact = new LoggerFactory(); + fact.AddProvider(new TraceLoggerProvider()); + return GetClient(fact); + } - /// - /// Whether the test should be run. By default integration tests aren't executed, can be set to true to force execution. - /// - public virtual bool Run { get; set; } + /// + /// Check if integration tests should be executed + /// + /// + protected bool ShouldRun() + { + var integrationTests = Environment.GetEnvironmentVariable("INTEGRATION"); + if (!Run && integrationTests != "1") + return false; - /// - /// Whether API credentials are provided and thus authenticated calls can be executed. Should be set in the GetClient implementation. - /// - public bool Authenticated { get; set; } + return true; + } - /// - /// Create a client - /// - /// - protected TClient CreateClient() + /// + /// Execute a REST endpoint call and check for any errors or warnings. + /// + /// Type of the update + /// The call expression + /// Whether an update is expected + /// Whether this is an authenticated request + public async Task RunAndCheckUpdate(Expression>, Task>>> expression, bool expectUpdate, bool authRequest) + { + if (!ShouldRun()) + return; + + var client = CreateClient(); + + var expressionBody = (MethodCallExpression)expression.Body; + if (authRequest && !Authenticated) { - var fact = new LoggerFactory(); - fact.AddProvider(new TraceLoggerProvider()); - return GetClient(fact); + Debug.WriteLine($"Skipping {expressionBody.Method.Name}, not authenticated"); + return; } - /// - /// Check if integration tests should be executed - /// - /// - protected bool ShouldRun() - { - var integrationTests = Environment.GetEnvironmentVariable("INTEGRATION"); - if (!Run && integrationTests != "1") - return false; + var listener = new EnumValueTraceListener(); + Trace.Listeners.Add(listener); - return true; + var evnt = new ManualResetEvent(!expectUpdate); + DataEvent? receivedUpdate = null; + var updateHandler = (DataEvent update) => + { + receivedUpdate = update; + evnt.Set(); + }; + + CallResult result; + try + { + result = await expression.Compile().Invoke(client, updateHandler).ConfigureAwait(false); + } + catch (Exception ex) + { + throw new Exception($"Method {expressionBody.Method.Name} threw an exception: " + ex.ToLogString()); + } + finally + { + Trace.Listeners.Remove(listener); } - /// - /// Execute a REST endpoint call and check for any errors or warnings. - /// - /// Type of the update - /// The call expression - /// Whether an update is expected - /// Whether this is an authenticated request - public async Task RunAndCheckUpdate(Expression>, Task>>> expression, bool expectUpdate, bool authRequest) - { - if (!ShouldRun()) - return; + if (!result.Success) + throw new Exception($"Method {expressionBody.Method.Name} returned error: " + result.Error); - var client = CreateClient(); + evnt.WaitOne(TimeSpan.FromSeconds(10)); - var expressionBody = (MethodCallExpression)expression.Body; - if (authRequest && !Authenticated) - { - Debug.WriteLine($"Skipping {expressionBody.Method.Name}, not authenticated"); - return; - } + if (expectUpdate && receivedUpdate == null) + throw new Exception($"Method {expressionBody.Method.Name} has not received an update"); - var listener = new EnumValueTraceListener(); - Trace.Listeners.Add(listener); - - var evnt = new ManualResetEvent(!expectUpdate); - DataEvent? receivedUpdate = null; - var updateHandler = (DataEvent update) => - { - receivedUpdate = update; - evnt.Set(); - }; - - CallResult result; - try - { - result = await expression.Compile().Invoke(client, updateHandler).ConfigureAwait(false); - } - catch (Exception ex) - { - throw new Exception($"Method {expressionBody.Method.Name} threw an exception: " + ex.ToLogString()); - } - finally - { - Trace.Listeners.Remove(listener); - } - - if (!result.Success) - throw new Exception($"Method {expressionBody.Method.Name} returned error: " + result.Error); - - evnt.WaitOne(TimeSpan.FromSeconds(10)); - - if (expectUpdate && receivedUpdate == null) - throw new Exception($"Method {expressionBody.Method.Name} has not received an update"); - - await result.Data.CloseAsync().ConfigureAwait(false); - Debug.WriteLine($"{expressionBody.Method.Name} {result}"); - } + await result.Data.CloseAsync().ConfigureAwait(false); + Debug.WriteLine($"{expressionBody.Method.Name} {result}"); } } diff --git a/CryptoExchange.Net/Testing/SocketRequestValidator.cs b/CryptoExchange.Net/Testing/SocketRequestValidator.cs index 24446cd..ed440d2 100644 --- a/CryptoExchange.Net/Testing/SocketRequestValidator.cs +++ b/CryptoExchange.Net/Testing/SocketRequestValidator.cs @@ -1,6 +1,5 @@ -using CryptoExchange.Net.Clients; +using CryptoExchange.Net.Clients; using CryptoExchange.Net.Objects; -using CryptoExchange.Net.Objects.Sockets; using CryptoExchange.Net.Testing.Comparers; using System; using System.Collections.Generic; @@ -11,176 +10,175 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; -namespace CryptoExchange.Net.Testing -{ - /// - /// Validator for websocket subscriptions, checking expected requests and responses and comparing update models - /// - /// - public class SocketRequestValidator where TClient : BaseSocketClient - { - private readonly string _baseAddress = "wss://localhost"; - private readonly string _folder; - private readonly string? _nestedPropertyForCompare; +namespace CryptoExchange.Net.Testing; - /// - /// ctor - /// - /// Folder for json test values - /// Property to use for compare - public SocketRequestValidator(string folder, string? nestedPropertyForCompare = null) +/// +/// Validator for websocket subscriptions, checking expected requests and responses and comparing update models +/// +/// +public class SocketRequestValidator where TClient : BaseSocketClient +{ + private readonly string _baseAddress = "wss://localhost"; + private readonly string _folder; + private readonly string? _nestedPropertyForCompare; + + /// + /// ctor + /// + /// Folder for json test values + /// Property to use for compare + public SocketRequestValidator(string folder, string? nestedPropertyForCompare = null) + { + _folder = folder; + _nestedPropertyForCompare = nestedPropertyForCompare; + } + + /// + /// Validate a subscription + /// + /// Expected response type + /// Client to test + /// Subscription method invocation + /// Method name for looking up json test values + /// Chose nested property to use for comparing + /// Use nested json property for compare + /// Ignore certain properties + /// Use the first item of an array update + /// Whether to skip the response model validation + /// + /// + public async Task ValidateAsync( + TClient client, + Func>> methodInvoke, + string name, + Func? responseMapper = null, + string? nestedJsonProperty = null, + List? ignoreProperties = null, + bool useSingleArrayItem = false, + bool skipResponseValidation = false) + { + var listener = new EnumValueTraceListener(); + Trace.Listeners.Add(listener); + + var path = Directory.GetParent(Environment.CurrentDirectory)!.Parent!.Parent!.FullName; + FileStream file ; + try { - _folder = folder; - _nestedPropertyForCompare = nestedPropertyForCompare; + file = File.OpenRead(Path.Combine(path, _folder, $"{name}.txt")); + } + catch (FileNotFoundException) + { + throw new Exception("Response file not found"); } - /// - /// Validate a subscription - /// - /// Expected response type - /// Client to test - /// Subscription method invocation - /// Method name for looking up json test values - /// Chose nested property to use for comparing - /// Use nested json property for compare - /// Ignore certain properties - /// Use the first item of an array update - /// Whether to skip the response model validation - /// - /// - public async Task ValidateAsync( - TClient client, - Func>> methodInvoke, - string name, - Func? responseMapper = null, - string? nestedJsonProperty = null, - List? ignoreProperties = null, - bool useSingleArrayItem = false, - bool skipResponseValidation = false) + var buffer = new byte[file.Length]; + await file.ReadAsync(buffer, 0, (int)file.Length).ConfigureAwait(false); + file.Close(); + + var data = Encoding.UTF8.GetString(buffer); + using var reader = new StringReader(data); + + var socket = TestHelpers.ConfigureSocketClient(client, _baseAddress); + + var waiter = new AutoResetEvent(false); + string? lastMessage = null; + socket.OnMessageSend += (x) => { - var listener = new EnumValueTraceListener(); - Trace.Listeners.Add(listener); - - var path = Directory.GetParent(Environment.CurrentDirectory)!.Parent!.Parent!.FullName; - FileStream file ; - try + lastMessage = x; + waiter.Set(); + }; + + // Invoke subscription method + var task = methodInvoke(client); + + var replaceValues = new Dictionary(); + while (true) + { + var line = reader.ReadLine(); + if (line == null) + break; + + if (line.StartsWith("> ")) { - file = File.OpenRead(Path.Combine(path, _folder, $"{name}.txt")); - } - catch (FileNotFoundException) - { - throw new Exception("Response file not found"); - } + // Expect a message from client to server + waiter.WaitOne(TimeSpan.FromSeconds(5)); - var buffer = new byte[file.Length]; - await file.ReadAsync(buffer, 0, (int)file.Length).ConfigureAwait(false); - file.Close(); + if (lastMessage == null) + throw new Exception($"{name} expected {line} to be send to server but did not receive anything"); - var data = Encoding.UTF8.GetString(buffer); - using var reader = new StringReader(data); - - var socket = TestHelpers.ConfigureSocketClient(client, _baseAddress); - - var waiter = new AutoResetEvent(false); - string? lastMessage = null; - socket.OnMessageSend += (x) => - { - lastMessage = x; - waiter.Set(); - }; - - // Invoke subscription method - var task = methodInvoke(client); - - var replaceValues = new Dictionary(); - while (true) - { - var line = reader.ReadLine(); - if (line == null) - break; - - if (line.StartsWith("> ")) + var lastMessageJson = JsonDocument.Parse(lastMessage).RootElement; + var expectedJson = JsonDocument.Parse(line.Substring(2)).RootElement; + if (expectedJson.ValueKind == JsonValueKind.Object) { - // Expect a message from client to server - waiter.WaitOne(TimeSpan.FromSeconds(5)); - - if (lastMessage == null) - throw new Exception($"{name} expected {line} to be send to server but did not receive anything"); - - var lastMessageJson = JsonDocument.Parse(lastMessage).RootElement; - var expectedJson = JsonDocument.Parse(line.Substring(2)).RootElement; - if (expectedJson.ValueKind == JsonValueKind.Object) + foreach (var item in expectedJson.EnumerateObject()) { - foreach (var item in expectedJson.EnumerateObject()) + if (item.Value.ValueKind == JsonValueKind.Object) { - if (item.Value.ValueKind == JsonValueKind.Object) + foreach (var innerItem in item.Value.EnumerateObject()) { - foreach (var innerItem in item.Value.EnumerateObject()) + if (innerItem.Value.ToString().StartsWith("|") && innerItem.Value.ToString().EndsWith("|")) { - if (innerItem.Value.ToString().StartsWith("|") && innerItem.Value.ToString().EndsWith("|")) - { - // |x| values are used to replace parts of response messages - if (!lastMessageJson.GetProperty(item.Name).TryGetProperty(innerItem.Name, out var prop)) - continue; + // |x| values are used to replace parts of response messages + if (!lastMessageJson.GetProperty(item.Name).TryGetProperty(innerItem.Name, out var prop)) + continue; - replaceValues.Add(innerItem.Value.ToString(), prop.ValueKind == JsonValueKind.String ? prop.GetString()! : prop.GetInt64().ToString()!); - } + replaceValues.Add(innerItem.Value.ToString(), prop.ValueKind == JsonValueKind.String ? prop.GetString()! : prop.GetInt64().ToString()!); } } - - if (item.Value.ToString().StartsWith("|") && item.Value.ToString().EndsWith("|")) - { - // |x| values are used to replace parts of response messages - if (!lastMessageJson.TryGetProperty(item.Name, out var prop)) - continue; - - replaceValues.Add(item.Value.ToString(), prop.ValueKind == JsonValueKind.String ? prop.GetString()! : prop.GetInt64().ToString()!); - } - else if (!lastMessageJson.TryGetProperty(item.Name, out var prop)) - { - } - else if (lastMessageJson.GetProperty(item.Name).ValueKind == JsonValueKind.String && lastMessageJson.GetProperty(item.Name).GetString() != item.Value.ToString() && ignoreProperties?.Contains(item.Name) != true) - { - throw new Exception($"{name} Expected {item.Name} to be {item.Value}, but was {lastMessageJson.GetProperty(item.Name).GetString()}"); - } - else - { - // TODO check arrays and sub-objects - - } } - // TODO check arrays and sub-objects + if (item.Value.ToString().StartsWith("|") && item.Value.ToString().EndsWith("|")) + { + // |x| values are used to replace parts of response messages + if (!lastMessageJson.TryGetProperty(item.Name, out var prop)) + continue; + + replaceValues.Add(item.Value.ToString(), prop.ValueKind == JsonValueKind.String ? prop.GetString()! : prop.GetInt64().ToString()!); + } + else if (!lastMessageJson.TryGetProperty(item.Name, out var prop)) + { + } + else if (lastMessageJson.GetProperty(item.Name).ValueKind == JsonValueKind.String && lastMessageJson.GetProperty(item.Name).GetString() != item.Value.ToString() && ignoreProperties?.Contains(item.Name) != true) + { + throw new Exception($"{name} Expected {item.Name} to be {item.Value}, but was {lastMessageJson.GetProperty(item.Name).GetString()}"); + } + else + { + // TODO check arrays and sub-objects + + } } - } - else if (line.StartsWith("< ")) - { - // Expect a message from server to client - foreach(var item in replaceValues) - line = line.Replace(item.Key, item.Value); + // TODO check arrays and sub-objects - socket.InvokeMessage(line.Substring(2)); - } - else - { - // A update message from server to client - var compareData = reader.ReadToEnd(); - foreach (var item in replaceValues) - compareData = compareData.Replace(item.Key, item.Value); - - socket.InvokeMessage(compareData); - - await task.ConfigureAwait(false); - object? result = task.Result.Data; - if (responseMapper != null) - result = responseMapper(task.Result.Data); - - if (!skipResponseValidation) - SystemTextJsonComparer.CompareData(name, result, compareData, nestedJsonProperty ?? _nestedPropertyForCompare, ignoreProperties, useSingleArrayItem); } } + else if (line.StartsWith("< ")) + { + // Expect a message from server to client + foreach(var item in replaceValues) + line = line.Replace(item.Key, item.Value); - Trace.Listeners.Remove(listener); + socket.InvokeMessage(line.Substring(2)); + } + else + { + // A update message from server to client + var compareData = reader.ReadToEnd(); + foreach (var item in replaceValues) + compareData = compareData.Replace(item.Key, item.Value); + + socket.InvokeMessage(compareData); + + await task.ConfigureAwait(false); + object? result = task.Result.Data; + if (responseMapper != null) + result = responseMapper(task.Result.Data); + + if (!skipResponseValidation) + SystemTextJsonComparer.CompareData(name, result, compareData, nestedJsonProperty ?? _nestedPropertyForCompare, ignoreProperties, useSingleArrayItem); + } } + + Trace.Listeners.Remove(listener); } } diff --git a/CryptoExchange.Net/Testing/SocketSubscriptionValidator.cs b/CryptoExchange.Net/Testing/SocketSubscriptionValidator.cs index f3bdee2..4792831 100644 --- a/CryptoExchange.Net/Testing/SocketSubscriptionValidator.cs +++ b/CryptoExchange.Net/Testing/SocketSubscriptionValidator.cs @@ -1,4 +1,4 @@ -using CryptoExchange.Net.Clients; +using CryptoExchange.Net.Clients; using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects.Sockets; using CryptoExchange.Net.Testing.Comparers; @@ -11,191 +11,190 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; -namespace CryptoExchange.Net.Testing -{ - /// - /// Validator for websocket subscriptions, checking expected requests and responses and comparing update models - /// - /// - public class SocketSubscriptionValidator where TClient : BaseSocketClient - { - private readonly TClient _client; - private readonly string _folder; - private readonly string _baseAddress; - private readonly string? _nestedPropertyForCompare; +namespace CryptoExchange.Net.Testing; - /// - /// ctor - /// - /// Client to test - /// Folder for json test values - /// The base address that is expected - /// Property to use for compare - public SocketSubscriptionValidator(TClient client, string folder, string baseAddress, string? nestedPropertyForCompare = null) +/// +/// Validator for websocket subscriptions, checking expected requests and responses and comparing update models +/// +/// +public class SocketSubscriptionValidator where TClient : BaseSocketClient +{ + private readonly TClient _client; + private readonly string _folder; + private readonly string _baseAddress; + private readonly string? _nestedPropertyForCompare; + + /// + /// ctor + /// + /// Client to test + /// Folder for json test values + /// The base address that is expected + /// Property to use for compare + public SocketSubscriptionValidator(TClient client, string folder, string baseAddress, string? nestedPropertyForCompare = null) + { + _client = client; + _folder = folder; + _baseAddress = baseAddress; + _nestedPropertyForCompare = nestedPropertyForCompare; + } + + /// + /// Validate a subscription + /// + /// The expected update type + /// Subscription method invocation + /// Method name for looking up json test values + /// Use nested json property for compare + /// Ignore certain properties + /// Use the first item of an array update + /// Path + /// Whether to skip comparing the json model with the update model + /// + /// + public async Task ValidateAsync( + Func>, Task>> methodInvoke, + string name, + string? nestedJsonProperty = null, + List? ignoreProperties = null, + string? addressPath = null, + bool? useFirstUpdateItem = null, + bool? skipUpdateValidation = null) + { + var listener = new EnumValueTraceListener(); + Trace.Listeners.Add(listener); + + var path = Directory.GetParent(Environment.CurrentDirectory)!.Parent!.Parent!.FullName; + FileStream file ; + try { - _client = client; - _folder = folder; - _baseAddress = baseAddress; - _nestedPropertyForCompare = nestedPropertyForCompare; + file = File.OpenRead(Path.Combine(path, _folder, $"{name}.txt")); + } + catch (FileNotFoundException) + { + throw new Exception("Response file not found"); } - /// - /// Validate a subscription - /// - /// The expected update type - /// Subscription method invocation - /// Method name for looking up json test values - /// Use nested json property for compare - /// Ignore certain properties - /// Use the first item of an array update - /// Path - /// Whether to skip comparing the json model with the update model - /// - /// - public async Task ValidateAsync( - Func>, Task>> methodInvoke, - string name, - string? nestedJsonProperty = null, - List? ignoreProperties = null, - string? addressPath = null, - bool? useFirstUpdateItem = null, - bool? skipUpdateValidation = null) + var buffer = new byte[file.Length]; + await file.ReadAsync(buffer, 0, (int)file.Length).ConfigureAwait(false); + file.Close(); + + var data = Encoding.UTF8.GetString(buffer); + using var reader = new StringReader(data); + + var socket = TestHelpers.ConfigureSocketClient(_client, addressPath == null ? _baseAddress : _baseAddress.AppendPath(addressPath)); + + var waiter = new AutoResetEvent(false); + string? lastMessage = null; + socket.OnMessageSend += (x) => { - var listener = new EnumValueTraceListener(); - Trace.Listeners.Add(listener); - - var path = Directory.GetParent(Environment.CurrentDirectory)!.Parent!.Parent!.FullName; - FileStream file ; - try + lastMessage = x; + waiter.Set(); + }; + + TUpdate? update = default; + // Invoke subscription method + try + { + var task = methodInvoke(_client, x => { update = x.Data; }); + } + catch(Exception) + { + throw; + } + + var replaceValues = new Dictionary(); + while (true) + { + var line = reader.ReadLine(); + if (line == null) + break; + + if (line.StartsWith("> ")) { - file = File.OpenRead(Path.Combine(path, _folder, $"{name}.txt")); - } - catch (FileNotFoundException) - { - throw new Exception("Response file not found"); - } + // Expect a message from client to server + waiter.WaitOne(TimeSpan.FromSeconds(1)); - var buffer = new byte[file.Length]; - await file.ReadAsync(buffer, 0, (int)file.Length).ConfigureAwait(false); - file.Close(); + if (lastMessage == null) + throw new Exception($"{name} expected to {line} to be send to server but did not receive anything"); - var data = Encoding.UTF8.GetString(buffer); - using var reader = new StringReader(data); - - var socket = TestHelpers.ConfigureSocketClient(_client, addressPath == null ? _baseAddress : _baseAddress.AppendPath(addressPath)); - - var waiter = new AutoResetEvent(false); - string? lastMessage = null; - socket.OnMessageSend += (x) => - { - lastMessage = x; - waiter.Set(); - }; - - TUpdate? update = default; - // Invoke subscription method - try - { - var task = methodInvoke(_client, x => { update = x.Data; }); - } - catch(Exception) - { - throw; - } - - var replaceValues = new Dictionary(); - while (true) - { - var line = reader.ReadLine(); - if (line == null) - break; - - if (line.StartsWith("> ")) + var lastMessageJson = JsonDocument.Parse(lastMessage).RootElement; + var expectedJson = JsonDocument.Parse(line.Substring(2)).RootElement; + if (expectedJson.ValueKind == JsonValueKind.Object) { - // Expect a message from client to server - waiter.WaitOne(TimeSpan.FromSeconds(1)); - - if (lastMessage == null) - throw new Exception($"{name} expected to {line} to be send to server but did not receive anything"); - - var lastMessageJson = JsonDocument.Parse(lastMessage).RootElement; - var expectedJson = JsonDocument.Parse(line.Substring(2)).RootElement; - if (expectedJson.ValueKind == JsonValueKind.Object) + foreach (var item in expectedJson.EnumerateObject()) { - foreach (var item in expectedJson.EnumerateObject()) + if (item.Value.ValueKind == JsonValueKind.Object) { - if (item.Value.ValueKind == JsonValueKind.Object) + foreach (var innerItem in item.Value.EnumerateObject()) { - foreach (var innerItem in item.Value.EnumerateObject()) + if (innerItem.Value.ToString().StartsWith("|") && innerItem.Value.ToString().EndsWith("|")) { - if (innerItem.Value.ToString().StartsWith("|") && innerItem.Value.ToString().EndsWith("|")) - { - // |x| values are used to replace parts of response messages - if (!lastMessageJson.GetProperty(item.Name).TryGetProperty(innerItem.Name, out var prop)) - continue; + // |x| values are used to replace parts of response messages + if (!lastMessageJson.GetProperty(item.Name).TryGetProperty(innerItem.Name, out var prop)) + continue; - replaceValues.Add(innerItem.Value.ToString(), prop.ValueKind == JsonValueKind.String ? prop.GetString()! : prop.GetInt64().ToString()!); - } + replaceValues.Add(innerItem.Value.ToString(), prop.ValueKind == JsonValueKind.String ? prop.GetString()! : prop.GetInt64().ToString()!); } } - - if (item.Value.ToString().StartsWith("|") && item.Value.ToString().EndsWith("|")) - { - // |x| values are used to replace parts of response messages - if (!lastMessageJson.TryGetProperty(item.Name, out var prop)) - continue; - - replaceValues.Add(item.Value.ToString(), prop.ValueKind == JsonValueKind.String ? prop.GetString()! : prop.GetInt64().ToString()!); - } - else if (item.Value.ToString() == "-999") - { - // |x| values are used to replace parts of response messages - if (!lastMessageJson.TryGetProperty(item.Name, out var prop)) - continue; - - replaceValues.Add(item.Value.ToString(), prop.GetDecimal().ToString()!); - } - else if (lastMessageJson.GetProperty(item.Name).ValueKind == JsonValueKind.String && lastMessageJson.GetProperty(item.Name).GetString() != item.Value.ToString() && ignoreProperties?.Contains(item.Name) != true) - { - throw new Exception($"{name} Expected {item.Name} to be {item.Value}, but was {lastMessageJson.GetProperty(item.Name).GetString()}"); - } - else - { - // TODO check arrays and sub-objects - - } } - // TODO check arrays and sub-objects + if (item.Value.ToString().StartsWith("|") && item.Value.ToString().EndsWith("|")) + { + // |x| values are used to replace parts of response messages + if (!lastMessageJson.TryGetProperty(item.Name, out var prop)) + continue; + + replaceValues.Add(item.Value.ToString(), prop.ValueKind == JsonValueKind.String ? prop.GetString()! : prop.GetInt64().ToString()!); + } + else if (item.Value.ToString() == "-999") + { + // |x| values are used to replace parts of response messages + if (!lastMessageJson.TryGetProperty(item.Name, out var prop)) + continue; + + replaceValues.Add(item.Value.ToString(), prop.GetDecimal().ToString()!); + } + else if (lastMessageJson.GetProperty(item.Name).ValueKind == JsonValueKind.String && lastMessageJson.GetProperty(item.Name).GetString() != item.Value.ToString() && ignoreProperties?.Contains(item.Name) != true) + { + throw new Exception($"{name} Expected {item.Name} to be {item.Value}, but was {lastMessageJson.GetProperty(item.Name).GetString()}"); + } + else + { + // TODO check arrays and sub-objects + + } } - } - else if (line.StartsWith("< ")) - { - // Expect a message from server to client - foreach (var item in replaceValues) - line = line.Replace(item.Key, item.Value); + // TODO check arrays and sub-objects - socket.InvokeMessage(line.Substring(2)); - } - else - { - // A update message from server to client - var compareData = reader.ReadToEnd(); - foreach (var item in replaceValues) - compareData = compareData.Replace(item.Key, item.Value); - - socket.InvokeMessage(compareData); - - if (update == null) - throw new Exception($"{name} Update send to client did not trigger in update handler"); - - if (skipUpdateValidation != true) - SystemTextJsonComparer.CompareData(name, update, compareData, nestedJsonProperty ?? _nestedPropertyForCompare, ignoreProperties, useFirstUpdateItem ?? false); } } + else if (line.StartsWith("< ")) + { + // Expect a message from server to client + foreach (var item in replaceValues) + line = line.Replace(item.Key, item.Value); - await _client.UnsubscribeAllAsync().ConfigureAwait(false); - Trace.Listeners.Remove(listener); + socket.InvokeMessage(line.Substring(2)); + } + else + { + // A update message from server to client + var compareData = reader.ReadToEnd(); + foreach (var item in replaceValues) + compareData = compareData.Replace(item.Key, item.Value); + + socket.InvokeMessage(compareData); + + if (update == null) + throw new Exception($"{name} Update send to client did not trigger in update handler"); + + if (skipUpdateValidation != true) + SystemTextJsonComparer.CompareData(name, update, compareData, nestedJsonProperty ?? _nestedPropertyForCompare, ignoreProperties, useFirstUpdateItem ?? false); + } } + + await _client.UnsubscribeAllAsync().ConfigureAwait(false); + Trace.Listeners.Remove(listener); } } diff --git a/CryptoExchange.Net/Testing/TestHelpers.cs b/CryptoExchange.Net/Testing/TestHelpers.cs index 169d186..f83c711 100644 --- a/CryptoExchange.Net/Testing/TestHelpers.cs +++ b/CryptoExchange.Net/Testing/TestHelpers.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; @@ -20,186 +20,185 @@ using CryptoExchange.Net.Testing.Implementations; #pragma warning disable IL2075 #pragma warning disable IL3050 -namespace CryptoExchange.Net.Testing +namespace CryptoExchange.Net.Testing; + +/// +/// Testing helpers +/// +public class TestHelpers { - /// - /// Testing helpers - /// - public class TestHelpers + [ExcludeFromCodeCoverage] + internal static bool AreEqual(T? self, T? to, params string[] ignore) where T : class { - [ExcludeFromCodeCoverage] - internal static bool AreEqual(T? self, T? to, params string[] ignore) where T : class + if (self != null && to != null) { - if (self != null && to != null) + var type = self.GetType(); + var ignoreList = new List(ignore); + foreach (var pi in type.GetProperties(BindingFlags.Public | BindingFlags.Instance)) { - var type = self.GetType(); - var ignoreList = new List(ignore); - foreach (var pi in type.GetProperties(BindingFlags.Public | BindingFlags.Instance)) + if (ignoreList.Contains(pi.Name)) + continue; + + var selfValue = type.GetProperty(pi.Name)!.GetValue(self, null); + var toValue = type.GetProperty(pi.Name)!.GetValue(to, null); + + if (pi.PropertyType.IsClass && !pi.PropertyType.Module.ScopeName.Equals("System.Private.CoreLib.dll")) { - if (ignoreList.Contains(pi.Name)) + // Check of "CommonLanguageRuntimeLibrary" is needed because string is also a class + if (AreEqual(selfValue, toValue, ignore)) continue; - var selfValue = type.GetProperty(pi.Name)!.GetValue(self, null); - var toValue = type.GetProperty(pi.Name)!.GetValue(to, null); - - if (pi.PropertyType.IsClass && !pi.PropertyType.Module.ScopeName.Equals("System.Private.CoreLib.dll")) - { - // Check of "CommonLanguageRuntimeLibrary" is needed because string is also a class - if (AreEqual(selfValue, toValue, ignore)) - continue; - - return false; - } - - if (selfValue != toValue && (selfValue == null || !selfValue.Equals(toValue))) - return false; + return false; } - return true; + if (selfValue != toValue && (selfValue == null || !selfValue.Equals(toValue))) + return false; } - return self == to; + return true; } - internal static TestSocket ConfigureSocketClient(T client, string address) where T : BaseSocketClient + return self == to; + } + + internal static TestSocket ConfigureSocketClient(T client, string address) where T : BaseSocketClient + { + var socket = new TestSocket(address); + foreach (var apiClient in client.ApiClients.OfType()) { - var socket = new TestSocket(address); - foreach (var apiClient in client.ApiClients.OfType()) + apiClient.SocketFactory = new TestWebsocketFactory(socket); + } + return socket; + } + + internal static void ConfigureRestClient(T client, string data, HttpStatusCode code) where T : BaseRestClient + { + foreach (var apiClient in client.ApiClients.OfType()) + { + var expectedBytes = Encoding.UTF8.GetBytes(data); + var responseStream = new MemoryStream(); + responseStream.Write(expectedBytes, 0, expectedBytes.Length); + responseStream.Seek(0, SeekOrigin.Begin); + + var response = new TestResponse(code, responseStream); + var request = new TestRequest(response); + + var factory = new TestRequestFactory(request); + apiClient.RequestFactory = factory; + } + } + + /// + /// Check a signature matches the expected signature + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static void CheckSignature( + RestApiClient client, + AuthenticationProvider authProvider, + HttpMethod method, + string path, + Func?, IDictionary?, IDictionary?, string> getSignature, + string expectedSignature, + Dictionary? parameters = null, + DateTime? time = null, + bool disableOrdering = false, + bool compareCase = true, + string host = "https://test.test-api.com") + { + parameters ??= new Dictionary { - apiClient.SocketFactory = new TestWebsocketFactory(socket); - } - return socket; - } + { "test", 123 }, + { "test2", "abc" } + }; - internal static void ConfigureRestClient(T client, string data, HttpStatusCode code) where T : BaseRestClient - { - foreach (var apiClient in client.ApiClients.OfType()) - { - var expectedBytes = Encoding.UTF8.GetBytes(data); - var responseStream = new MemoryStream(); - responseStream.Write(expectedBytes, 0, expectedBytes.Length); - responseStream.Seek(0, SeekOrigin.Begin); + if (disableOrdering) + client.OrderParameters = false; - var response = new TestResponse(code, responseStream); - var request = new TestRequest(response); + var uriParams = client.ParameterPositions[method] == HttpMethodParameterPosition.InUri ? client.CreateParameterDictionary(parameters) : null; + var bodyParams = client.ParameterPositions[method] == HttpMethodParameterPosition.InBody ? client.CreateParameterDictionary(parameters) : null; - var factory = new TestRequestFactory(request); - apiClient.RequestFactory = factory; - } - } - - /// - /// Check a signature matches the expected signature - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - public static void CheckSignature( - RestApiClient client, - AuthenticationProvider authProvider, - HttpMethod method, - string path, - Func?, IDictionary?, IDictionary?, string> getSignature, - string expectedSignature, - Dictionary? parameters = null, - DateTime? time = null, - bool disableOrdering = false, - bool compareCase = true, - string host = "https://test.test-api.com") - { - parameters ??= new Dictionary + var requestDefinition = new RestRequestConfiguration( + new RequestDefinition(path, method) { - { "test", 123 }, - { "test2", "abc" } - }; - - if (disableOrdering) - client.OrderParameters = false; - - var uriParams = client.ParameterPositions[method] == HttpMethodParameterPosition.InUri ? client.CreateParameterDictionary(parameters) : null; - var bodyParams = client.ParameterPositions[method] == HttpMethodParameterPosition.InBody ? client.CreateParameterDictionary(parameters) : null; - - var requestDefinition = new RestRequestConfiguration( - new RequestDefinition(path, method) - { - Authenticated = true - }, - host, - uriParams ?? new Dictionary(), - bodyParams ?? new Dictionary(), - new Dictionary(), - client.ArraySerialization, - client.ParameterPositions[method], - client.RequestBodyFormat - ); - - authProvider.TimeProvider = new TestAuthTimeProvider(time ?? new DateTime(2024, 01, 01, 0, 0, 0, DateTimeKind.Utc)); - authProvider.ProcessRequest( - client, - requestDefinition + Authenticated = true + }, + host, + uriParams ?? new Dictionary(), + bodyParams ?? new Dictionary(), + new Dictionary(), + client.ArraySerialization, + client.ParameterPositions[method], + client.RequestBodyFormat ); - var signature = getSignature(requestDefinition.QueryParameters, requestDefinition.BodyParameters, requestDefinition.Headers); + authProvider.TimeProvider = new TestAuthTimeProvider(time ?? new DateTime(2024, 01, 01, 0, 0, 0, DateTimeKind.Utc)); + authProvider.ProcessRequest( + client, + requestDefinition + ); - if (!string.Equals(signature, expectedSignature, compareCase ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase)) - throw new Exception($"Signatures do not match. Expected: {expectedSignature}, Actual: {signature}"); - } + var signature = getSignature(requestDefinition.QueryParameters, requestDefinition.BodyParameters, requestDefinition.Headers); - /// - /// Scan the TClient rest client type for missing interface methods - /// - /// - /// - public static void CheckForMissingRestInterfaces(string[]? excludeInterfaces = null) + if (!string.Equals(signature, expectedSignature, compareCase ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase)) + throw new Exception($"Signatures do not match. Expected: {expectedSignature}, Actual: {signature}"); + } + + /// + /// Scan the TClient rest client type for missing interface methods + /// + /// + /// + public static void CheckForMissingRestInterfaces(string[]? excludeInterfaces = null) + { + CheckForMissingInterfaces(typeof(TClient), typeof(Task), excludeInterfaces); + } + + /// + /// Scan the TClient socket client type for missing interface methods + /// + /// + /// + public static void CheckForMissingSocketInterfaces(string[]? excludeInterfaces = null) + { + CheckForMissingInterfaces(typeof(TClient), typeof(Task>), excludeInterfaces); + } + + private static void CheckForMissingInterfaces(Type clientType, Type implementationTypes, string[]? excludeInterfaces = null) + { + var assembly = Assembly.GetAssembly(clientType); + var interfaceType = clientType.GetInterface("I" + clientType.Name); + var clientInterfaces = assembly!.GetTypes() + .Where(t => t.Name.StartsWith("I" + clientType.Name) + && !t.Name.EndsWith("Shared") + && (excludeInterfaces?.Contains(t.Name) != true)); + + foreach (var clientInterface in clientInterfaces) { - CheckForMissingInterfaces(typeof(TClient), typeof(Task), excludeInterfaces); - } - - /// - /// Scan the TClient socket client type for missing interface methods - /// - /// - /// - public static void CheckForMissingSocketInterfaces(string[]? excludeInterfaces = null) - { - CheckForMissingInterfaces(typeof(TClient), typeof(Task>), excludeInterfaces); - } - - private static void CheckForMissingInterfaces(Type clientType, Type implementationTypes, string[]? excludeInterfaces = null) - { - var assembly = Assembly.GetAssembly(clientType); - var interfaceType = clientType.GetInterface("I" + clientType.Name); - var clientInterfaces = assembly!.GetTypes() - .Where(t => t.Name.StartsWith("I" + clientType.Name) - && !t.Name.EndsWith("Shared") - && (excludeInterfaces?.Contains(t.Name) != true)); - - foreach (var clientInterface in clientInterfaces) + var implementations = assembly.GetTypes().Where(t => clientInterface.IsAssignableFrom(t) && !t.IsInterface && t != clientInterface); + foreach (var implementation in implementations) { - var implementations = assembly.GetTypes().Where(t => clientInterface.IsAssignableFrom(t) && !t.IsInterface && t != clientInterface); - foreach (var implementation in implementations) + int methods = 0; + foreach (var method in implementation.GetMethods().Where(m => implementationTypes.IsAssignableFrom(m.ReturnType))) { - int methods = 0; - foreach (var method in implementation.GetMethods().Where(m => implementationTypes.IsAssignableFrom(m.ReturnType))) - { - var interfaceMethod = - clientInterface.GetMethod(method.Name, method.GetParameters().Select(p => p.ParameterType).ToArray()) - ?? clientInterface.GetInterfaces().Select(x => x.GetMethod(method.Name, method.GetParameters().Select(p => p.ParameterType).ToArray())).FirstOrDefault() - ?? throw new Exception($"Missing interface for method {method.Name} in {implementation.Name} implementing interface {clientInterface.Name}"); - methods++; - } - - Debug.WriteLine($"{clientInterface.Name} {methods} methods validated"); + var interfaceMethod = + clientInterface.GetMethod(method.Name, method.GetParameters().Select(p => p.ParameterType).ToArray()) + ?? clientInterface.GetInterfaces().Select(x => x.GetMethod(method.Name, method.GetParameters().Select(p => p.ParameterType).ToArray())).FirstOrDefault() + ?? throw new Exception($"Missing interface for method {method.Name} in {implementation.Name} implementing interface {clientInterface.Name}"); + methods++; } + + Debug.WriteLine($"{clientInterface.Name} {methods} methods validated"); } } } diff --git a/CryptoExchange.Net/Trackers/CompareValue.cs b/CryptoExchange.Net/Trackers/CompareValue.cs index a6fe033..a541c2e 100644 --- a/CryptoExchange.Net/Trackers/CompareValue.cs +++ b/CryptoExchange.Net/Trackers/CompareValue.cs @@ -1,34 +1,30 @@ -using System; -using System.Collections.Generic; -using System.Text; +using System; -namespace CryptoExchange.Net.Trackers +namespace CryptoExchange.Net.Trackers; + +/// +/// Compare value +/// +public record CompareValue { + /// + /// The value difference + /// + public decimal? Difference { get; set; } + /// + /// The value difference percentage + /// + public decimal? PercentageDifference { get; set; } /// - /// Compare value + /// ctor /// - public record CompareValue + public CompareValue(decimal? value1, decimal? value2) { - /// - /// The value difference - /// - public decimal? Difference { get; set; } - /// - /// The value difference percentage - /// - public decimal? PercentageDifference { get; set; } + if (value1 == null || value2 == null) + return; - /// - /// ctor - /// - public CompareValue(decimal? value1, decimal? value2) - { - if (value1 == null || value2 == null) - return; - - Difference = value2 - value1; - PercentageDifference = value1.Value == 0 ? null : Math.Round(value2.Value / value1.Value * 100 - 100, 4); - } + Difference = value2 - value1; + PercentageDifference = value1.Value == 0 ? null : Math.Round(value2.Value / value1.Value * 100 - 100, 4); } } diff --git a/CryptoExchange.Net/Trackers/Klines/IKlineTracker.cs b/CryptoExchange.Net/Trackers/Klines/IKlineTracker.cs index 11b7afc..7d347ef 100644 --- a/CryptoExchange.Net/Trackers/Klines/IKlineTracker.cs +++ b/CryptoExchange.Net/Trackers/Klines/IKlineTracker.cs @@ -1,105 +1,103 @@ -using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects; using CryptoExchange.Net.SharedApis; using System; -using System.Collections.Generic; using System.Threading.Tasks; -namespace CryptoExchange.Net.Trackers.Klines +namespace CryptoExchange.Net.Trackers.Klines; + +/// +/// A tracker for kline data of a symbol +/// +public interface IKlineTracker { /// - /// A tracker for kline data of a symbol + /// The total number of klines /// - public interface IKlineTracker - { - /// - /// The total number of klines - /// - int Count { get; } + int Count { get; } - /// - /// Exchange name - /// - string Exchange { get; } + /// + /// Exchange name + /// + string Exchange { get; } - /// - /// Symbol name - /// - string SymbolName { get; } + /// + /// Symbol name + /// + string SymbolName { get; } - /// - /// Symbol - /// - SharedSymbol Symbol { get; } + /// + /// Symbol + /// + SharedSymbol Symbol { get; } - /// - /// The max number of klines tracked - /// - int? Limit { get; } + /// + /// The max number of klines tracked + /// + int? Limit { get; } - /// - /// The max age of the data tracked - /// - TimeSpan? Period { get; } + /// + /// The max age of the data tracked + /// + TimeSpan? Period { get; } - /// - /// From which timestamp the trades are registered - /// - DateTime? SyncedFrom { get; } + /// + /// From which timestamp the trades are registered + /// + DateTime? SyncedFrom { get; } - /// - /// Sync status - /// - SyncStatus Status { get; } + /// + /// Sync status + /// + SyncStatus Status { get; } - /// - /// Get the last kline - /// - SharedKline? Last { get; } + /// + /// Get the last kline + /// + SharedKline? Last { get; } - /// - /// Event for when a new kline is added - /// - event Func? OnAdded; - /// - /// Event for when a kline is removed because it's no longer within the period/limit window - /// - event Func? OnRemoved; - /// - /// Event for when a kline is updated - /// - event Func OnUpdated; - /// - /// Event for when the sync status changes - /// - event Func? OnStatusChanged; + /// + /// Event for when a new kline is added + /// + event Func? OnAdded; + /// + /// Event for when a kline is removed because it's no longer within the period/limit window + /// + event Func? OnRemoved; + /// + /// Event for when a kline is updated + /// + event Func OnUpdated; + /// + /// Event for when the sync status changes + /// + event Func? OnStatusChanged; - /// - /// Start synchronization - /// - /// - Task StartAsync(bool startWithSnapshot = true); + /// + /// Start synchronization + /// + /// + Task StartAsync(bool startWithSnapshot = true); - /// - /// Stop synchronization - /// - /// - Task StopAsync(); + /// + /// Stop synchronization + /// + /// + Task StopAsync(); - /// - /// Get the data tracked - /// - /// Start timestamp to get the data from, defaults to tracked data start time - /// End timestamp to get the data until, defaults to current time - /// - SharedKline[] GetData(DateTime? fromTimestamp = null, DateTime? toTimestamp = null); + /// + /// Get the data tracked + /// + /// Start timestamp to get the data from, defaults to tracked data start time + /// End timestamp to get the data until, defaults to current time + /// + SharedKline[] GetData(DateTime? fromTimestamp = null, DateTime? toTimestamp = null); - /// - /// Get statistics on the klines - /// - /// Start timestamp to get the data from, defaults to tracked data start time - /// End timestamp to get the data until, defaults to current time - /// - KlinesStats GetStats(DateTime? fromTimestamp = null, DateTime? toTimestamp = null); + /// + /// Get statistics on the klines + /// + /// Start timestamp to get the data from, defaults to tracked data start time + /// End timestamp to get the data until, defaults to current time + /// + KlinesStats GetStats(DateTime? fromTimestamp = null, DateTime? toTimestamp = null); - } } \ No newline at end of file diff --git a/CryptoExchange.Net/Trackers/Klines/KlineTracker.cs b/CryptoExchange.Net/Trackers/Klines/KlineTracker.cs index cfc4c33..eac7ea4 100644 --- a/CryptoExchange.Net/Trackers/Klines/KlineTracker.cs +++ b/CryptoExchange.Net/Trackers/Klines/KlineTracker.cs @@ -1,4 +1,4 @@ -using CryptoExchange.Net.Logging.Extensions; +using CryptoExchange.Net.Logging.Extensions; using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects.Sockets; using CryptoExchange.Net.SharedApis; @@ -6,474 +6,472 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using System.Threading.Tasks; -namespace CryptoExchange.Net.Trackers.Klines +namespace CryptoExchange.Net.Trackers.Klines; + +/// +public class KlineTracker : IKlineTracker { - /// - public class KlineTracker : IKlineTracker + private readonly IKlineSocketClient _socketClient; + private readonly IKlineRestClient _restClient; + private SyncStatus _status; + private bool _startWithSnapshot; + + /// + /// The internal data structure + /// + protected readonly SortedDictionary _data = new SortedDictionary(); + /// + /// The pre-snapshot queue buffering updates received before the snapshot is set and which will be applied after the snapshot was set + /// + protected readonly List _preSnapshotQueue = new List(); + /// + /// Lock for accessing _data + /// + protected readonly object _lock = new object(); + /// + /// The last time the window was applied + /// + protected DateTime _lastWindowApplied = DateTime.MinValue; + /// + /// Whether or not the data has changed since last window was applied + /// + protected bool _changed; + /// + /// The kline interval + /// + protected readonly SharedKlineInterval _interval; + /// + /// Whether the snapshot has been set + /// + protected bool _snapshotSet; + /// + /// Logger + /// + protected readonly ILogger _logger; + /// + /// Update subscription + /// + protected UpdateSubscription? _updateSubscription; + + /// + /// The timestamp of the first item + /// + protected DateTime? _firstTimestamp; + + /// + public SyncStatus Status { - private readonly IKlineSocketClient _socketClient; - private readonly IKlineRestClient _restClient; - private SyncStatus _status; - private bool _startWithSnapshot; - - /// - /// The internal data structure - /// - protected readonly SortedDictionary _data = new SortedDictionary(); - /// - /// The pre-snapshot queue buffering updates received before the snapshot is set and which will be applied after the snapshot was set - /// - protected readonly List _preSnapshotQueue = new List(); - /// - /// Lock for accessing _data - /// - protected readonly object _lock = new object(); - /// - /// The last time the window was applied - /// - protected DateTime _lastWindowApplied = DateTime.MinValue; - /// - /// Whether or not the data has changed since last window was applied - /// - protected bool _changed = false; - /// - /// The kline interval - /// - protected readonly SharedKlineInterval _interval; - /// - /// Whether the snapshot has been set - /// - protected bool _snapshotSet; - /// - /// Logger - /// - protected readonly ILogger _logger; - /// - /// Update subscription - /// - protected UpdateSubscription? _updateSubscription; - - /// - /// The timestamp of the first item - /// - protected DateTime? _firstTimestamp; - - /// - public SyncStatus Status + get => _status; + set { - get => _status; - set - { - if (value == _status) - return; - - var old = _status; - _status = value; - _logger.KlineTrackerStatusChanged(SymbolName, old, value); - OnStatusChanged?.Invoke(old, _status); - } - } - - /// - public string Exchange { get; } - - /// - public string SymbolName { get; } - - /// - public SharedSymbol Symbol { get; } - - /// - public int? Limit { get; } - /// - public TimeSpan? Period { get; } - - /// - public DateTime? SyncedFrom - { - get - { - if (Period == null) - return _firstTimestamp; - - var max = DateTime.UtcNow - Period.Value; - if (_firstTimestamp > max) - return _firstTimestamp; - - return max; - } - } - - /// - public int Count - { - get - { - lock (_lock) - { - ApplyWindow(true); - return _data.Count; - } - } - } - - /// - public SharedKline? Last - { - get - { - lock (_lock) - { - ApplyWindow(true); - return _data.LastOrDefault().Value; - } - } - } - - /// - public event Func? OnAdded; - /// - public event Func? OnUpdated; - /// - public event Func? OnRemoved; - /// - public event Func? OnStatusChanged; - - /// - /// ctor - /// - public KlineTracker( - ILogger? logger, - IKlineRestClient restClient, - IKlineSocketClient socketClient, - SharedSymbol symbol, - SharedKlineInterval interval, - int? limit = null, - TimeSpan? period = null) - { - _logger = logger ?? new NullLogger(); - Symbol = symbol; - SymbolName = socketClient.FormatSymbol(symbol.BaseAsset, symbol.QuoteAsset, symbol.TradingMode, symbol.DeliverTime); - Exchange = restClient.Exchange; - Limit = limit; - Period = period; - _interval = interval; - _socketClient = socketClient; - _restClient = restClient; - } - - /// - public async Task StartAsync(bool startWithSnapshot = true) - { - if (Status != SyncStatus.Disconnected) - throw new InvalidOperationException($"Can't start syncing unless state is {SyncStatus.Disconnected}. Current state: {Status}"); - - _startWithSnapshot = startWithSnapshot; - Status = SyncStatus.Syncing; - _logger.KlineTrackerStarting(SymbolName); - - var subResult = await _socketClient.SubscribeToKlineUpdatesAsync(new SubscribeKlineRequest(Symbol, _interval), - update => - { - AddOrUpdate(update.Data); - }).ConfigureAwait(false); - - if (!subResult) - { - _logger.KlineTrackerStartFailed(SymbolName, subResult.Error!.Message ?? subResult.Error!.ErrorDescription!, subResult.Error.Exception); - Status = SyncStatus.Disconnected; - return subResult; - } - - _updateSubscription = subResult.Data; - _updateSubscription.ConnectionLost += HandleConnectionLost; - _updateSubscription.ConnectionClosed += HandleConnectionClosed; - _updateSubscription.ConnectionRestored += HandleConnectionRestored; - - var startResult = await DoStartAsync().ConfigureAwait(false); - if (!startResult) - { - _ = subResult.Data.CloseAsync(); - Status = SyncStatus.Disconnected; - return new CallResult(startResult.Error!); - } - - Status = SyncStatus.Synced; - _logger.KlineTrackerStarted(SymbolName); - return CallResult.SuccessResult; - } - - /// - public async Task StopAsync() - { - _logger.KlineTrackerStopping(SymbolName); - Status = SyncStatus.Disconnected; - await DoStopAsync().ConfigureAwait(false); - _data.Clear(); - _preSnapshotQueue.Clear(); - _logger.KlineTrackerStopped(SymbolName); - } - - /// - /// The start procedure needed for kline syncing, generally subscribing to an update stream and requesting the snapshot - /// - /// - protected virtual async Task DoStartAsync() - { - if (!_startWithSnapshot) - return CallResult.SuccessResult; - - var startTime = Period == null ? (DateTime?)null : DateTime.UtcNow.Add(-Period.Value); - if (_restClient.GetKlinesOptions.MaxAge != null && DateTime.UtcNow.Add(-_restClient.GetKlinesOptions.MaxAge.Value) > startTime) - startTime = DateTime.UtcNow.Add(-_restClient.GetKlinesOptions.MaxAge.Value); - - var limit = Math.Min(_restClient.GetKlinesOptions.MaxLimit, Limit ?? 100); - - var request = new GetKlinesRequest(Symbol, _interval, startTime, DateTime.UtcNow, limit: limit); - var data = new List(); - await foreach (var result in ExchangeHelpers.ExecutePages(_restClient.GetKlinesAsync, request).ConfigureAwait(false)) - { - if (!result) - return result; - - if (Limit != null && data.Count > Limit) - break; - - data.AddRange(result.Data); - } - - SetInitialData(data); - return CallResult.SuccessResult; - } - - /// - /// The stop procedure needed, generally stopping the update stream - /// - /// - protected virtual Task DoStopAsync() => _updateSubscription?.CloseAsync() ?? Task.CompletedTask; - - /// - public KlinesStats GetStats(DateTime? fromTimestamp = null, DateTime? toTimestamp = null) - { - var compareTime = SyncedFrom?.AddSeconds(-2); - var stats = GetStats(GetData(fromTimestamp, toTimestamp)); - stats.Complete = (fromTimestamp == null || fromTimestamp >= compareTime) && (toTimestamp == null || toTimestamp >= compareTime); - return stats; - } - - private KlinesStats GetStats(IEnumerable klines) - { - if (!klines.Any()) - return new KlinesStats(); - - return new KlinesStats - { - KlineCount = klines.Count(), - FirstOpenTime = klines.First().OpenTime, - LastOpenTime = klines.Last().OpenTime, - HighPrice = klines.Select(d => d.LowPrice).Max(), - LowPrice = klines.Select(d => d.HighPrice).Min(), - Volume = klines.Select(d => d.Volume).Sum(), - AverageVolume = Math.Round(klines.OrderByDescending(d => d.OpenTime).Skip(1).Select(d => d.Volume).DefaultIfEmpty().Average(), 8) - }; - } - - /// - public SharedKline[] GetData(DateTime? since = null, DateTime? until = null) - { - lock (_lock) - { - ApplyWindow(true); - - IEnumerable result = _data.Values; - if (since != null) - result = result.Where(d => d.OpenTime >= since); - if (until != null) - result = result.Where(d => d.OpenTime <= until); - - return result.ToArray(); - } - } - - /// - /// Set the initial kline data snapshot - /// - /// - protected void SetInitialData(IEnumerable data) - { - lock (_lock) - { - _data.Clear(); - - IEnumerable items = data.OrderByDescending(d => d.OpenTime); - if (Limit != null) - items = items.Take(Limit.Value); - if (Period != null) - items = items.Where(e => e.OpenTime >= DateTime.UtcNow.Add(-Period.Value)); - - foreach (var item in items.OrderBy(d => d.OpenTime)) - _data.Add(item.OpenTime, item); - - _snapshotSet = true; - - foreach (var item in _preSnapshotQueue) - { - if (_data.ContainsKey(item.OpenTime)) - continue; - - _data.Add(item.OpenTime, item); - } - - _firstTimestamp = _data.Min(v => v.Key); - ApplyWindow(false); - _logger.KlineTrackerInitialDataSet(SymbolName, _data.Last().Key); - } - } - - /// - /// Add or update a kline - /// - /// - protected void AddOrUpdate(SharedKline item) => AddOrUpdate(new[] { item }); - - /// - /// Add or update klines - /// - /// - protected void AddOrUpdate(IEnumerable items) - { - lock (_lock) - { - if (_restClient != null && _startWithSnapshot && !_snapshotSet) - { - _preSnapshotQueue.AddRange(items); - return; - } - - foreach (var item in items) - { - if (_data.TryGetValue(item.OpenTime, out var existing)) - { - _data.Remove(item.OpenTime); - _data.Add(item.OpenTime, item); - OnUpdated?.Invoke(item); - _logger.KlineTrackerKlineUpdated(SymbolName, _data.Last().Key); - } - else - { - _data.Add(item.OpenTime, item); - OnAdded?.Invoke(item); - _logger.KlineTrackerKlineAdded(SymbolName, _data.Last().Key); - } - } - - _firstTimestamp = _data.Min(x => x.Key); - _changed = true; - - SetSyncStatus(); - ApplyWindow(true); - } - } - - private void ApplyWindow(bool broadcastEvents) - { - if (!_changed && (DateTime.UtcNow - _lastWindowApplied) < TimeSpan.FromSeconds(1)) + if (value == _status) return; - if (Period != null) - { - var compareDate = DateTime.UtcNow.Add(-Period.Value); - for (var i = 0; i < _data.Count; i++) - { - var item = _data.ElementAt(0); - if (item.Key >= compareDate) - break; - - _data.Remove(item.Key); - if (broadcastEvents) - OnRemoved?.Invoke(item.Value); - } - } - - if (Limit != null && _data.Count > Limit.Value) - { - var toRemove = Math.Max(0, _data.Count - Limit.Value); - for (var i = 0; i < toRemove; i++) - { - var item = _data.ElementAt(0); - _data.Remove(item.Key); - if (broadcastEvents) - OnRemoved?.Invoke(item.Value); - } - } - - _lastWindowApplied = DateTime.UtcNow; - _changed = false; - } - - private void HandleConnectionLost() - { - _logger.KlineTrackerConnectionLost(SymbolName); - if (Status != SyncStatus.Disconnected) - { - Status = SyncStatus.Syncing; - _snapshotSet = false; - _firstTimestamp = null; - _preSnapshotQueue.Clear(); - } - } - - private void HandleConnectionClosed() - { - _logger.KlineTrackerConnectionClosed(SymbolName); - Status = SyncStatus.Disconnected; - _ = StopAsync(); - } - - private async void HandleConnectionRestored(TimeSpan _) - { - Status = SyncStatus.Syncing; - var success = false; - while (!success) - { - if (Status != SyncStatus.Syncing) - return; - - var resyncResult = await DoStartAsync().ConfigureAwait(false); - success = resyncResult; - } - - _logger.KlineTrackerConnectionRestored(SymbolName); - SetSyncStatus(); - } - - private void SetSyncStatus() - { - if (Status == SyncStatus.Synced) - return; - - if (Period != null) - { - if (_firstTimestamp <= DateTime.UtcNow - Period.Value) - Status = SyncStatus.Synced; - else - Status = SyncStatus.PartiallySynced; - } - - if (Limit != null) - { - if (_data.Count == Limit.Value) - Status = SyncStatus.Synced; - else - Status = SyncStatus.PartiallySynced; - } - - if (Period == null && Limit == null) - Status = SyncStatus.Synced; + var old = _status; + _status = value; + _logger.KlineTrackerStatusChanged(SymbolName, old, value); + OnStatusChanged?.Invoke(old, _status); } } + + /// + public string Exchange { get; } + + /// + public string SymbolName { get; } + + /// + public SharedSymbol Symbol { get; } + + /// + public int? Limit { get; } + /// + public TimeSpan? Period { get; } + + /// + public DateTime? SyncedFrom + { + get + { + if (Period == null) + return _firstTimestamp; + + var max = DateTime.UtcNow - Period.Value; + if (_firstTimestamp > max) + return _firstTimestamp; + + return max; + } + } + + /// + public int Count + { + get + { + lock (_lock) + { + ApplyWindow(true); + return _data.Count; + } + } + } + + /// + public SharedKline? Last + { + get + { + lock (_lock) + { + ApplyWindow(true); + return _data.LastOrDefault().Value; + } + } + } + + /// + public event Func? OnAdded; + /// + public event Func? OnUpdated; + /// + public event Func? OnRemoved; + /// + public event Func? OnStatusChanged; + + /// + /// ctor + /// + public KlineTracker( + ILogger? logger, + IKlineRestClient restClient, + IKlineSocketClient socketClient, + SharedSymbol symbol, + SharedKlineInterval interval, + int? limit = null, + TimeSpan? period = null) + { + _logger = logger ?? new NullLogger(); + Symbol = symbol; + SymbolName = socketClient.FormatSymbol(symbol.BaseAsset, symbol.QuoteAsset, symbol.TradingMode, symbol.DeliverTime); + Exchange = restClient.Exchange; + Limit = limit; + Period = period; + _interval = interval; + _socketClient = socketClient; + _restClient = restClient; + } + + /// + public async Task StartAsync(bool startWithSnapshot = true) + { + if (Status != SyncStatus.Disconnected) + throw new InvalidOperationException($"Can't start syncing unless state is {SyncStatus.Disconnected}. Current state: {Status}"); + + _startWithSnapshot = startWithSnapshot; + Status = SyncStatus.Syncing; + _logger.KlineTrackerStarting(SymbolName); + + var subResult = await _socketClient.SubscribeToKlineUpdatesAsync(new SubscribeKlineRequest(Symbol, _interval), + update => + { + AddOrUpdate(update.Data); + }).ConfigureAwait(false); + + if (!subResult) + { + _logger.KlineTrackerStartFailed(SymbolName, subResult.Error!.Message ?? subResult.Error!.ErrorDescription!, subResult.Error.Exception); + Status = SyncStatus.Disconnected; + return subResult; + } + + _updateSubscription = subResult.Data; + _updateSubscription.ConnectionLost += HandleConnectionLost; + _updateSubscription.ConnectionClosed += HandleConnectionClosed; + _updateSubscription.ConnectionRestored += HandleConnectionRestored; + + var startResult = await DoStartAsync().ConfigureAwait(false); + if (!startResult) + { + _ = subResult.Data.CloseAsync(); + Status = SyncStatus.Disconnected; + return new CallResult(startResult.Error!); + } + + Status = SyncStatus.Synced; + _logger.KlineTrackerStarted(SymbolName); + return CallResult.SuccessResult; + } + + /// + public async Task StopAsync() + { + _logger.KlineTrackerStopping(SymbolName); + Status = SyncStatus.Disconnected; + await DoStopAsync().ConfigureAwait(false); + _data.Clear(); + _preSnapshotQueue.Clear(); + _logger.KlineTrackerStopped(SymbolName); + } + + /// + /// The start procedure needed for kline syncing, generally subscribing to an update stream and requesting the snapshot + /// + /// + protected virtual async Task DoStartAsync() + { + if (!_startWithSnapshot) + return CallResult.SuccessResult; + + var startTime = Period == null ? (DateTime?)null : DateTime.UtcNow.Add(-Period.Value); + if (_restClient.GetKlinesOptions.MaxAge != null && DateTime.UtcNow.Add(-_restClient.GetKlinesOptions.MaxAge.Value) > startTime) + startTime = DateTime.UtcNow.Add(-_restClient.GetKlinesOptions.MaxAge.Value); + + var limit = Math.Min(_restClient.GetKlinesOptions.MaxLimit, Limit ?? 100); + + var request = new GetKlinesRequest(Symbol, _interval, startTime, DateTime.UtcNow, limit: limit); + var data = new List(); + await foreach (var result in ExchangeHelpers.ExecutePages(_restClient.GetKlinesAsync, request).ConfigureAwait(false)) + { + if (!result) + return result; + + if (Limit != null && data.Count > Limit) + break; + + data.AddRange(result.Data); + } + + SetInitialData(data); + return CallResult.SuccessResult; + } + + /// + /// The stop procedure needed, generally stopping the update stream + /// + /// + protected virtual Task DoStopAsync() => _updateSubscription?.CloseAsync() ?? Task.CompletedTask; + + /// + public KlinesStats GetStats(DateTime? fromTimestamp = null, DateTime? toTimestamp = null) + { + var compareTime = SyncedFrom?.AddSeconds(-2); + var stats = GetStats(GetData(fromTimestamp, toTimestamp)); + stats.Complete = (fromTimestamp == null || fromTimestamp >= compareTime) && (toTimestamp == null || toTimestamp >= compareTime); + return stats; + } + + private static KlinesStats GetStats(IEnumerable klines) + { + if (!klines.Any()) + return new KlinesStats(); + + return new KlinesStats + { + KlineCount = klines.Count(), + FirstOpenTime = klines.First().OpenTime, + LastOpenTime = klines.Last().OpenTime, + HighPrice = klines.Select(d => d.LowPrice).Max(), + LowPrice = klines.Select(d => d.HighPrice).Min(), + Volume = klines.Select(d => d.Volume).Sum(), + AverageVolume = Math.Round(klines.OrderByDescending(d => d.OpenTime).Skip(1).Select(d => d.Volume).DefaultIfEmpty().Average(), 8) + }; + } + + /// + public SharedKline[] GetData(DateTime? fromTimestamp = null, DateTime? toTimestamp = null) + { + lock (_lock) + { + ApplyWindow(true); + + IEnumerable result = _data.Values; + if (fromTimestamp != null) + result = result.Where(d => d.OpenTime >= fromTimestamp); + if (toTimestamp != null) + result = result.Where(d => d.OpenTime <= toTimestamp); + + return result.ToArray(); + } + } + + /// + /// Set the initial kline data snapshot + /// + /// + protected void SetInitialData(IEnumerable data) + { + lock (_lock) + { + _data.Clear(); + + IEnumerable items = data.OrderByDescending(d => d.OpenTime); + if (Limit != null) + items = items.Take(Limit.Value); + if (Period != null) + items = items.Where(e => e.OpenTime >= DateTime.UtcNow.Add(-Period.Value)); + + foreach (var item in items.OrderBy(d => d.OpenTime)) + _data.Add(item.OpenTime, item); + + _snapshotSet = true; + + foreach (var item in _preSnapshotQueue) + { + if (_data.ContainsKey(item.OpenTime)) + continue; + + _data.Add(item.OpenTime, item); + } + + _firstTimestamp = _data.Min(v => v.Key); + ApplyWindow(false); + _logger.KlineTrackerInitialDataSet(SymbolName, _data.Last().Key); + } + } + + /// + /// Add or update a kline + /// + /// + protected void AddOrUpdate(SharedKline item) => AddOrUpdate(new[] { item }); + + /// + /// Add or update klines + /// + /// + protected void AddOrUpdate(IEnumerable items) + { + lock (_lock) + { + if (_restClient != null && _startWithSnapshot && !_snapshotSet) + { + _preSnapshotQueue.AddRange(items); + return; + } + + foreach (var item in items) + { + if (_data.TryGetValue(item.OpenTime, out var existing)) + { + _data.Remove(item.OpenTime); + _data.Add(item.OpenTime, item); + OnUpdated?.Invoke(item); + _logger.KlineTrackerKlineUpdated(SymbolName, _data.Last().Key); + } + else + { + _data.Add(item.OpenTime, item); + OnAdded?.Invoke(item); + _logger.KlineTrackerKlineAdded(SymbolName, _data.Last().Key); + } + } + + _firstTimestamp = _data.Min(x => x.Key); + _changed = true; + + SetSyncStatus(); + ApplyWindow(true); + } + } + + private void ApplyWindow(bool broadcastEvents) + { + if (!_changed && (DateTime.UtcNow - _lastWindowApplied) < TimeSpan.FromSeconds(1)) + return; + + if (Period != null) + { + var compareDate = DateTime.UtcNow.Add(-Period.Value); + for (var i = 0; i < _data.Count; i++) + { + var item = _data.ElementAt(0); + if (item.Key >= compareDate) + break; + + _data.Remove(item.Key); + if (broadcastEvents) + OnRemoved?.Invoke(item.Value); + } + } + + if (Limit != null && _data.Count > Limit.Value) + { + var toRemove = Math.Max(0, _data.Count - Limit.Value); + for (var i = 0; i < toRemove; i++) + { + var item = _data.ElementAt(0); + _data.Remove(item.Key); + if (broadcastEvents) + OnRemoved?.Invoke(item.Value); + } + } + + _lastWindowApplied = DateTime.UtcNow; + _changed = false; + } + + private void HandleConnectionLost() + { + _logger.KlineTrackerConnectionLost(SymbolName); + if (Status != SyncStatus.Disconnected) + { + Status = SyncStatus.Syncing; + _snapshotSet = false; + _firstTimestamp = null; + _preSnapshotQueue.Clear(); + } + } + + private void HandleConnectionClosed() + { + _logger.KlineTrackerConnectionClosed(SymbolName); + Status = SyncStatus.Disconnected; + _ = StopAsync(); + } + + private async void HandleConnectionRestored(TimeSpan _) + { + Status = SyncStatus.Syncing; + var success = false; + while (!success) + { + if (Status != SyncStatus.Syncing) + return; + + var resyncResult = await DoStartAsync().ConfigureAwait(false); + success = resyncResult; + } + + _logger.KlineTrackerConnectionRestored(SymbolName); + SetSyncStatus(); + } + + private void SetSyncStatus() + { + if (Status == SyncStatus.Synced) + return; + + if (Period != null) + { + if (_firstTimestamp <= DateTime.UtcNow - Period.Value) + Status = SyncStatus.Synced; + else + Status = SyncStatus.PartiallySynced; + } + + if (Limit != null) + { + if (_data.Count == Limit.Value) + Status = SyncStatus.Synced; + else + Status = SyncStatus.PartiallySynced; + } + + if (Period == null && Limit == null) + Status = SyncStatus.Synced; + } } diff --git a/CryptoExchange.Net/Trackers/Klines/KlinesCompare.cs b/CryptoExchange.Net/Trackers/Klines/KlinesCompare.cs index 6e1deea..705534a 100644 --- a/CryptoExchange.Net/Trackers/Klines/KlinesCompare.cs +++ b/CryptoExchange.Net/Trackers/Klines/KlinesCompare.cs @@ -1,30 +1,25 @@ -using System; -using System.Collections.Generic; -using System.Text; +namespace CryptoExchange.Net.Trackers.Klines; -namespace CryptoExchange.Net.Trackers.Klines +/// +/// Klines statistics comparison +/// +public record KlinesCompare { /// - /// Klines statistics comparison + /// Number of trades /// - public record KlinesCompare - { - /// - /// Number of trades - /// - public CompareValue? LowPriceDif { get; set; } - /// - /// Number of trades - /// - public CompareValue? HighPriceDif { get; set; } - /// - /// Number of trades - /// - public CompareValue? VolumeDif { get; set; } - /// - /// Number of trades - /// - public CompareValue? AverageVolumeDif { get; set; } + public CompareValue? LowPriceDif { get; set; } + /// + /// Number of trades + /// + public CompareValue? HighPriceDif { get; set; } + /// + /// Number of trades + /// + public CompareValue? VolumeDif { get; set; } + /// + /// Number of trades + /// + public CompareValue? AverageVolumeDif { get; set; } - } } diff --git a/CryptoExchange.Net/Trackers/Klines/KlinesStats.cs b/CryptoExchange.Net/Trackers/Klines/KlinesStats.cs index 2a832f1..027c4fd 100644 --- a/CryptoExchange.Net/Trackers/Klines/KlinesStats.cs +++ b/CryptoExchange.Net/Trackers/Klines/KlinesStats.cs @@ -1,59 +1,56 @@ -using System; -using System.Collections.Generic; -using System.Text; +using System; -namespace CryptoExchange.Net.Trackers.Klines +namespace CryptoExchange.Net.Trackers.Klines; + +/// +/// Klines statistics +/// +public record KlinesStats { /// - /// Klines statistics + /// Number of klines /// - public record KlinesStats - { - /// - /// Number of klines - /// - public int KlineCount { get; set; } - /// - /// The kline open time of the first entry - /// - public DateTime? FirstOpenTime { get; set; } - /// - /// The kline open time of the last entry - /// - public DateTime? LastOpenTime { get; set; } - /// - /// Lowest trade price - /// - public decimal? LowPrice { get; set; } - /// - /// Highest trade price - /// - public decimal? HighPrice { get; set; } - /// - /// Trade volume - /// - public decimal Volume { get; set; } - /// - /// Average volume per kline - /// - public decimal? AverageVolume { get; set; } - /// - /// Whether the data is complete - /// - public bool Complete { get; set; } + public int KlineCount { get; set; } + /// + /// The kline open time of the first entry + /// + public DateTime? FirstOpenTime { get; set; } + /// + /// The kline open time of the last entry + /// + public DateTime? LastOpenTime { get; set; } + /// + /// Lowest trade price + /// + public decimal? LowPrice { get; set; } + /// + /// Highest trade price + /// + public decimal? HighPrice { get; set; } + /// + /// Trade volume + /// + public decimal Volume { get; set; } + /// + /// Average volume per kline + /// + public decimal? AverageVolume { get; set; } + /// + /// Whether the data is complete + /// + public bool Complete { get; set; } - /// - /// Compare 2 stat snapshots to eachother - /// - public KlinesCompare CompareTo(KlinesStats otherStats) + /// + /// Compare 2 stat snapshots to each other + /// + public KlinesCompare CompareTo(KlinesStats otherStats) + { + return new KlinesCompare { - return new KlinesCompare - { - LowPriceDif = new CompareValue(LowPrice, otherStats.LowPrice), - HighPriceDif = new CompareValue(HighPrice, otherStats.HighPrice), - VolumeDif = new CompareValue(Volume, otherStats.Volume), - AverageVolumeDif = new CompareValue(AverageVolume, otherStats.AverageVolume), - }; - } + LowPriceDif = new CompareValue(LowPrice, otherStats.LowPrice), + HighPriceDif = new CompareValue(HighPrice, otherStats.HighPrice), + VolumeDif = new CompareValue(Volume, otherStats.Volume), + AverageVolumeDif = new CompareValue(AverageVolume, otherStats.AverageVolume), + }; } } diff --git a/CryptoExchange.Net/Trackers/Trades/ITradeTracker.cs b/CryptoExchange.Net/Trackers/Trades/ITradeTracker.cs index 4f08aea..fda02f2 100644 --- a/CryptoExchange.Net/Trackers/Trades/ITradeTracker.cs +++ b/CryptoExchange.Net/Trackers/Trades/ITradeTracker.cs @@ -1,100 +1,98 @@ -using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects; using CryptoExchange.Net.SharedApis; using System; -using System.Collections.Generic; using System.Threading.Tasks; -namespace CryptoExchange.Net.Trackers.Trades +namespace CryptoExchange.Net.Trackers.Trades; + +/// +/// A tracker for trades on a symbol +/// +public interface ITradeTracker { /// - /// A tracker for trades on a symbol + /// The total number of trades /// - public interface ITradeTracker - { - /// - /// The total number of trades - /// - int Count { get; } + int Count { get; } - /// - /// Exchange name - /// - string Exchange { get; } + /// + /// Exchange name + /// + string Exchange { get; } - /// - /// Symbol name - /// - string SymbolName { get; } + /// + /// Symbol name + /// + string SymbolName { get; } - /// - /// Symbol - /// - SharedSymbol Symbol { get; } + /// + /// Symbol + /// + SharedSymbol Symbol { get; } - /// - /// The max number of trades tracked - /// - int? Limit { get; } + /// + /// The max number of trades tracked + /// + int? Limit { get; } - /// - /// The max age of the data tracked - /// - TimeSpan? Period { get; } + /// + /// The max age of the data tracked + /// + TimeSpan? Period { get; } - /// - /// From which timestamp the trades are registered - /// - DateTime? SyncedFrom { get; } + /// + /// From which timestamp the trades are registered + /// + DateTime? SyncedFrom { get; } - /// - /// The current synchronization status - /// - SyncStatus Status { get; } + /// + /// The current synchronization status + /// + SyncStatus Status { get; } - /// - /// Get the last trade - /// - SharedTrade? Last { get; } + /// + /// Get the last trade + /// + SharedTrade? Last { get; } - /// - /// Event for when a new trade is added - /// - event Func? OnAdded; - /// - /// Event for when a trade is removed because it's no longer within the period/limit window - /// - event Func? OnRemoved; - /// - /// Event for when the sync status changes - /// - event Func? OnStatusChanged; + /// + /// Event for when a new trade is added + /// + event Func? OnAdded; + /// + /// Event for when a trade is removed because it's no longer within the period/limit window + /// + event Func? OnRemoved; + /// + /// Event for when the sync status changes + /// + event Func? OnStatusChanged; - /// - /// Start synchronization - /// - /// - Task StartAsync(bool startWithSnapshot = true); + /// + /// Start synchronization + /// + /// + Task StartAsync(bool startWithSnapshot = true); - /// - /// Stop synchronization - /// - /// - Task StopAsync(); + /// + /// Stop synchronization + /// + /// + Task StopAsync(); - /// - /// Get the data tracked - /// - /// Start timestamp to get the data from, defaults to tracked data start time - /// End timestamp to get the data until, defaults to current time - /// - SharedTrade[] GetData(DateTime? fromTimestamp = null, DateTime? toTimestamp = null); + /// + /// Get the data tracked + /// + /// Start timestamp to get the data from, defaults to tracked data start time + /// End timestamp to get the data until, defaults to current time + /// + SharedTrade[] GetData(DateTime? fromTimestamp = null, DateTime? toTimestamp = null); - /// - /// Get statistics on the trades - /// - /// Start timestamp to get the data from, defaults to tracked data start time - /// End timestamp to get the data until, defaults to current time - /// - TradesStats GetStats(DateTime? fromTimestamp = null, DateTime? toTimestamp = null); - } + /// + /// Get statistics on the trades + /// + /// Start timestamp to get the data from, defaults to tracked data start time + /// End timestamp to get the data until, defaults to current time + /// + TradesStats GetStats(DateTime? fromTimestamp = null, DateTime? toTimestamp = null); } \ No newline at end of file diff --git a/CryptoExchange.Net/Trackers/Trades/TradeTracker.cs b/CryptoExchange.Net/Trackers/Trades/TradeTracker.cs index 5d50e1e..e6e772c 100644 --- a/CryptoExchange.Net/Trackers/Trades/TradeTracker.cs +++ b/CryptoExchange.Net/Trackers/Trades/TradeTracker.cs @@ -1,4 +1,4 @@ -using CryptoExchange.Net.Logging.Extensions; +using CryptoExchange.Net.Logging.Extensions; using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects.Sockets; using CryptoExchange.Net.SharedApis; @@ -6,486 +6,484 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using System.Threading.Tasks; -namespace CryptoExchange.Net.Trackers.Trades +namespace CryptoExchange.Net.Trackers.Trades; + +/// +public class TradeTracker : ITradeTracker { + private readonly ITradeSocketClient _socketClient; + private readonly IRecentTradeRestClient? _recentRestClient; + private readonly ITradeHistoryRestClient? _historyRestClient; + private SyncStatus _status; + private long _snapshotId; + private bool _startWithSnapshot; + + /// + /// The internal data structure + /// + protected readonly List _data = new List(); + /// + /// The pre-snapshot queue buffering updates received before the snapshot is set and which will be applied after the snapshot was set + /// + protected readonly List _preSnapshotQueue = new List(); + + /// + /// The last time the window was applied + /// + protected DateTime _lastWindowApplied = DateTime.MinValue; + /// + /// Whether or not the data has changed since last window was applied + /// + protected bool _changed; + /// + /// Lock for accessing _data + /// + protected readonly object _lock = new object(); + /// + /// Whether the snapshot has been set + /// + protected bool _snapshotSet; + /// + /// Logger + /// + protected readonly ILogger _logger; + /// + /// Update subscription + /// + protected UpdateSubscription? _updateSubscription; + + /// + /// The timestamp of the first item + /// + protected DateTime? _firstTimestamp; + /// - public class TradeTracker : ITradeTracker + public string Exchange { get; } + + /// + public string SymbolName { get; } + + /// + public SharedSymbol Symbol { get; } + + /// + public int? Limit { get; } + /// + public TimeSpan? Period { get; } + + /// + public SyncStatus Status { - private readonly ITradeSocketClient _socketClient; - private readonly IRecentTradeRestClient? _recentRestClient; - private readonly ITradeHistoryRestClient? _historyRestClient; - private SyncStatus _status; - private long _snapshotId; - private bool _startWithSnapshot; - - /// - /// The internal data structure - /// - protected readonly List _data = new List(); - /// - /// The pre-snapshot queue buffering updates received before the snapshot is set and which will be applied after the snapshot was set - /// - protected readonly List _preSnapshotQueue = new List(); - - /// - /// The last time the window was applied - /// - protected DateTime _lastWindowApplied = DateTime.MinValue; - /// - /// Whether or not the data has changed since last window was applied - /// - protected bool _changed = false; - /// - /// Lock for accessing _data - /// - protected readonly object _lock = new object(); - /// - /// Whether the snapshot has been set - /// - protected bool _snapshotSet; - /// - /// Logger - /// - protected readonly ILogger _logger; - /// - /// Update subscription - /// - protected UpdateSubscription? _updateSubscription; - - /// - /// The timestamp of the first item - /// - protected DateTime? _firstTimestamp; - - /// - public string Exchange { get; } - - /// - public string SymbolName { get; } - - /// - public SharedSymbol Symbol { get; } - - /// - public int? Limit { get; } - /// - public TimeSpan? Period { get; } - - /// - public SyncStatus Status + get => _status; + set { - get => _status; - set - { - if (value == _status) - return; - - var old = _status; - _status = value; - _logger.TradeTrackerStatusChanged(SymbolName, old, value); - OnStatusChanged?.Invoke(old, _status); - } - } - - /// - public int Count - { - get - { - lock (_lock) - { - ApplyWindow(true); - return _data.Count; - } - } - } - - /// - public DateTime? SyncedFrom - { - get - { - if (Period == null) - return _firstTimestamp; - - var max = DateTime.UtcNow - Period.Value; - if (_firstTimestamp > max) - return _firstTimestamp; - - return max; - } - } - - /// - public SharedTrade? Last - { - get - { - lock (_lock) - { - ApplyWindow(true); - return _data.LastOrDefault(); - } - } - } - - /// - public event Func? OnAdded; - /// - public event Func? OnRemoved; - /// - public event Func? OnStatusChanged; - - /// - /// ctor - /// - public TradeTracker( - ILogger? logger, - IRecentTradeRestClient? recentRestClient, - ITradeHistoryRestClient? historyRestClient, - ITradeSocketClient socketClient, - SharedSymbol symbol, - int? limit = null, - TimeSpan? period = null) - { - _logger = logger ?? new NullLogger(); - _recentRestClient = recentRestClient; - _historyRestClient = historyRestClient; - _socketClient = socketClient; - Exchange = socketClient.Exchange; - Symbol = symbol; - SymbolName = socketClient.FormatSymbol(symbol.BaseAsset, symbol.QuoteAsset, symbol.TradingMode, symbol.DeliverTime); - Limit = limit; - Period = period; - } - - private static TradesStats GetStats(IEnumerable trades) - { - if (!trades.Any()) - return new TradesStats(); - - return new TradesStats - { - TradeCount = trades.Count(), - FirstTradeTime = trades.First().Timestamp, - LastTradeTime = trades.Last().Timestamp, - AveragePrice = Math.Round(trades.Select(d => d.Price).DefaultIfEmpty().Average(), 8), - VolumeWeightedAveragePrice = trades.Any() ? Math.Round(trades.Select(d => d.Price * d.Quantity).DefaultIfEmpty().Sum() / trades.Select(d => d.Quantity).DefaultIfEmpty().Sum(), 8) : null, - Volume = Math.Round(trades.Sum(d => d.Quantity), 8), - QuoteVolume = Math.Round(trades.Sum(d => d.Quantity * d.Price), 8), - BuySellRatio = Math.Round(trades.Where(x => x.Side == SharedOrderSide.Buy).Sum(x => x.Quantity) / trades.Sum(x => x.Quantity), 8) - }; - } - - /// - public TradesStats GetStats(DateTime? fromTimestamp = null, DateTime? toTimestamp = null) - { - var compareTime = SyncedFrom?.AddSeconds(-2); - var stats = GetStats(GetData(fromTimestamp, toTimestamp)); - stats.Complete = (fromTimestamp == null || fromTimestamp >= compareTime) && (toTimestamp == null || toTimestamp >= compareTime); - return stats; - } - - /// - public async Task StartAsync(bool startWithSnapshot = true) - { - if (Status != SyncStatus.Disconnected) - throw new InvalidOperationException($"Can't start syncing unless state is {SyncStatus.Disconnected}. Current state: {Status}"); - - _startWithSnapshot = startWithSnapshot; - Status = SyncStatus.Syncing; - _logger.TradeTrackerStarting(SymbolName); - var subResult = await _socketClient.SubscribeToTradeUpdatesAsync(new SubscribeTradeRequest(Symbol), - update => - { - AddData(update.Data); - }).ConfigureAwait(false); - - if (!subResult) - { - _logger.TradeTrackerStartFailed(SymbolName, subResult.Error!.Message ?? subResult.Error!.ErrorDescription!, subResult.Error.Exception); - Status = SyncStatus.Disconnected; - return subResult; - } - - _updateSubscription = subResult.Data; - _updateSubscription.ConnectionLost += HandleConnectionLost; - _updateSubscription.ConnectionClosed += HandleConnectionClosed; - _updateSubscription.ConnectionRestored += HandleConnectionRestored; - - var result = await DoStartAsync().ConfigureAwait(false); - if (!result) - { - _ = subResult.Data.CloseAsync(); - Status = SyncStatus.Disconnected; - return result; - } - - SetSyncStatus(); - _logger.TradeTrackerStarted(SymbolName); - return CallResult.SuccessResult; - } - - /// - public async Task StopAsync() - { - _logger.TradeTrackerStopping(SymbolName); - Status = SyncStatus.Disconnected; - await DoStopAsync().ConfigureAwait(false); - _data.Clear(); - _preSnapshotQueue.Clear(); - _logger.TradeTrackerStopped(SymbolName); - } - - /// - /// The start procedure needed for trade syncing, generally subscribing to an update stream and requesting the snapshot - /// - /// - protected virtual async Task DoStartAsync() - { - if (!_startWithSnapshot) - return CallResult.SuccessResult; - - if (_historyRestClient != null) - { - var startTime = Period == null ? DateTime.UtcNow.AddMinutes(-5) : DateTime.UtcNow.Add(-Period.Value); - var request = new GetTradeHistoryRequest(Symbol, startTime, DateTime.UtcNow); - var data = new List(); - await foreach(var result in ExchangeHelpers.ExecutePages(_historyRestClient.GetTradeHistoryAsync, request).ConfigureAwait(false)) - { - if (!result) - return result; - - if (Limit != null && data.Count > Limit) - break; - - data.AddRange(result.Data); - } - - SetInitialData(data); - } - else if (_recentRestClient != null) - { - int? limit = null; - if (Limit.HasValue) - limit = Math.Min(_recentRestClient.GetRecentTradesOptions.MaxLimit, Limit.Value); - - var snapshot = await _recentRestClient.GetRecentTradesAsync(new GetRecentTradesRequest(Symbol, limit)).ConfigureAwait(false); - if (!snapshot) - { - return snapshot; - } - - SetInitialData(snapshot.Data); - } - - return CallResult.SuccessResult; - } - - /// - /// The stop procedure needed, generally stopping the update stream - /// - /// - protected virtual Task DoStopAsync() => _updateSubscription?.CloseAsync() ?? Task.CompletedTask; - - /// - public SharedTrade[] GetData(DateTime? since = null, DateTime? until = null) - { - lock (_lock) - { - ApplyWindow(true); - - IEnumerable result = _data; - if (since != null) - result = result.Where(d => d.Timestamp >= since); - if (until != null) - result = result.Where(d => d.Timestamp <= until); - - return result.ToArray(); - } - } - - /// - /// Set the initial trade data snapshot - /// - /// - protected void SetInitialData(IEnumerable data) - { - lock (_lock) - { - _data.Clear(); - - IEnumerable items = data.OrderByDescending(d => d.Timestamp); - if (Limit != null) - items = items.Take(Limit.Value); - if (Period != null) - items = items.Where(e => e.Timestamp >= DateTime.UtcNow.Add(-Period.Value)); - - _snapshotId = data.Max(d => d.Timestamp.Ticks); - foreach (var item in items.OrderBy(d => d.Timestamp)) - _data.Add(item); - - _snapshotSet = true; - _changed = true; - - _logger.TradeTrackerInitialDataSet(SymbolName, _data.Count, _snapshotId); - - foreach (var item in _preSnapshotQueue) - { - if (_snapshotId >= item.Timestamp.Ticks) - { - _logger.TradeTrackerPreSnapshotSkip(SymbolName, item.Timestamp.Ticks); - continue; - } - - _logger.TradeTrackerPreSnapshotApplied(SymbolName, item.Timestamp.Ticks); - _data.Add(item); - } - - if (_data.Count != 0) - _firstTimestamp = _data.Min(v => v.Timestamp); - - ApplyWindow(false); - } - } - - /// - /// Add a trade - /// - /// - protected void AddData(SharedTrade item) => AddData(new[] { item }); - - /// - /// Add a list of trades - /// - /// - protected void AddData(IEnumerable items) - { - lock (_lock) - { - if ((_recentRestClient != null || _historyRestClient != null) && _startWithSnapshot && !_snapshotSet) - { - _preSnapshotQueue.AddRange(items); - return; - } - - foreach (var item in items) - { - _logger.TradeTrackerTradeAdded(SymbolName, item.Timestamp.Ticks); - _data.Add(item); - OnAdded?.Invoke(item); - } - - _firstTimestamp = _data.Min(x => x.Timestamp); - _changed = true; - SetSyncStatus(); - ApplyWindow(true); - } - } - - private void ApplyWindow(bool broadcastEvents) - { - if (!_changed && (DateTime.UtcNow - _lastWindowApplied) < TimeSpan.FromSeconds(1)) + if (value == _status) return; - if (Period != null) - { - var compareDate = DateTime.UtcNow.Add(-Period.Value); - for(var i = 0; i < _data.Count; i++) - { - var item = _data[0]; - if (item.Timestamp >= compareDate) - break; - - _data.Remove(item); - if (broadcastEvents) - OnRemoved?.Invoke(item); - } - } - - if (Limit != null && _data.Count > Limit.Value) - { - var toRemove = _data.Count - Limit.Value; - for (var i = 0; i < toRemove; i++) - { - var item = _data[0]; - _data.Remove(item); - if (broadcastEvents) - OnRemoved?.Invoke(item); - } - } - - _lastWindowApplied = DateTime.UtcNow; - _changed = false; - - if (Status == SyncStatus.PartiallySynced) - // Need to check if sync status should be changed even if there may not be any new data - SetSyncStatus(); - } - - private void HandleConnectionLost() - { - _logger.TradeTrackerConnectionLost(SymbolName); - if (Status != SyncStatus.Disconnected) - { - Status = SyncStatus.Syncing; - _snapshotSet = false; - _firstTimestamp = null; - _preSnapshotQueue.Clear(); - } - } - - private void HandleConnectionClosed() - { - _logger.TradeTrackerConnectionClosed(SymbolName); - Status = SyncStatus.Disconnected; - _ = StopAsync(); - } - - private async void HandleConnectionRestored(TimeSpan _) - { - Status = SyncStatus.Syncing; - var success = false; - while (!success) - { - if (Status != SyncStatus.Syncing) - return; - - var resyncResult = await DoStartAsync().ConfigureAwait(false); - success = resyncResult; - } - - _logger.TradeTrackerConnectionRestored(SymbolName); - SetSyncStatus(); - } - - private void SetSyncStatus() - { - if (Status == SyncStatus.Synced) - return; - - if (Period != null) - { - if (_firstTimestamp <= DateTime.UtcNow - Period.Value) - Status = SyncStatus.Synced; - else - Status = SyncStatus.PartiallySynced; - } - - if (Limit != null) - { - if (_data.Count == Limit.Value) - Status = SyncStatus.Synced; - else - Status = SyncStatus.PartiallySynced; - } - - if (Period == null && Limit == null) - Status = SyncStatus.Synced; + var old = _status; + _status = value; + _logger.TradeTrackerStatusChanged(SymbolName, old, value); + OnStatusChanged?.Invoke(old, _status); } } + + /// + public int Count + { + get + { + lock (_lock) + { + ApplyWindow(true); + return _data.Count; + } + } + } + + /// + public DateTime? SyncedFrom + { + get + { + if (Period == null) + return _firstTimestamp; + + var max = DateTime.UtcNow - Period.Value; + if (_firstTimestamp > max) + return _firstTimestamp; + + return max; + } + } + + /// + public SharedTrade? Last + { + get + { + lock (_lock) + { + ApplyWindow(true); + return _data.LastOrDefault(); + } + } + } + + /// + public event Func? OnAdded; + /// + public event Func? OnRemoved; + /// + public event Func? OnStatusChanged; + + /// + /// ctor + /// + public TradeTracker( + ILogger? logger, + IRecentTradeRestClient? recentRestClient, + ITradeHistoryRestClient? historyRestClient, + ITradeSocketClient socketClient, + SharedSymbol symbol, + int? limit = null, + TimeSpan? period = null) + { + _logger = logger ?? new NullLogger(); + _recentRestClient = recentRestClient; + _historyRestClient = historyRestClient; + _socketClient = socketClient; + Exchange = socketClient.Exchange; + Symbol = symbol; + SymbolName = socketClient.FormatSymbol(symbol.BaseAsset, symbol.QuoteAsset, symbol.TradingMode, symbol.DeliverTime); + Limit = limit; + Period = period; + } + + private static TradesStats GetStats(IEnumerable trades) + { + if (!trades.Any()) + return new TradesStats(); + + return new TradesStats + { + TradeCount = trades.Count(), + FirstTradeTime = trades.First().Timestamp, + LastTradeTime = trades.Last().Timestamp, + AveragePrice = Math.Round(trades.Select(d => d.Price).DefaultIfEmpty().Average(), 8), + VolumeWeightedAveragePrice = trades.Any() ? Math.Round(trades.Select(d => d.Price * d.Quantity).DefaultIfEmpty().Sum() / trades.Select(d => d.Quantity).DefaultIfEmpty().Sum(), 8) : null, + Volume = Math.Round(trades.Sum(d => d.Quantity), 8), + QuoteVolume = Math.Round(trades.Sum(d => d.Quantity * d.Price), 8), + BuySellRatio = Math.Round(trades.Where(x => x.Side == SharedOrderSide.Buy).Sum(x => x.Quantity) / trades.Sum(x => x.Quantity), 8) + }; + } + + /// + public TradesStats GetStats(DateTime? fromTimestamp = null, DateTime? toTimestamp = null) + { + var compareTime = SyncedFrom?.AddSeconds(-2); + var stats = GetStats(GetData(fromTimestamp, toTimestamp)); + stats.Complete = (fromTimestamp == null || fromTimestamp >= compareTime) && (toTimestamp == null || toTimestamp >= compareTime); + return stats; + } + + /// + public async Task StartAsync(bool startWithSnapshot = true) + { + if (Status != SyncStatus.Disconnected) + throw new InvalidOperationException($"Can't start syncing unless state is {SyncStatus.Disconnected}. Current state: {Status}"); + + _startWithSnapshot = startWithSnapshot; + Status = SyncStatus.Syncing; + _logger.TradeTrackerStarting(SymbolName); + var subResult = await _socketClient.SubscribeToTradeUpdatesAsync(new SubscribeTradeRequest(Symbol), + update => + { + AddData(update.Data); + }).ConfigureAwait(false); + + if (!subResult) + { + _logger.TradeTrackerStartFailed(SymbolName, subResult.Error!.Message ?? subResult.Error!.ErrorDescription!, subResult.Error.Exception); + Status = SyncStatus.Disconnected; + return subResult; + } + + _updateSubscription = subResult.Data; + _updateSubscription.ConnectionLost += HandleConnectionLost; + _updateSubscription.ConnectionClosed += HandleConnectionClosed; + _updateSubscription.ConnectionRestored += HandleConnectionRestored; + + var result = await DoStartAsync().ConfigureAwait(false); + if (!result) + { + _ = subResult.Data.CloseAsync(); + Status = SyncStatus.Disconnected; + return result; + } + + SetSyncStatus(); + _logger.TradeTrackerStarted(SymbolName); + return CallResult.SuccessResult; + } + + /// + public async Task StopAsync() + { + _logger.TradeTrackerStopping(SymbolName); + Status = SyncStatus.Disconnected; + await DoStopAsync().ConfigureAwait(false); + _data.Clear(); + _preSnapshotQueue.Clear(); + _logger.TradeTrackerStopped(SymbolName); + } + + /// + /// The start procedure needed for trade syncing, generally subscribing to an update stream and requesting the snapshot + /// + /// + protected virtual async Task DoStartAsync() + { + if (!_startWithSnapshot) + return CallResult.SuccessResult; + + if (_historyRestClient != null) + { + var startTime = Period == null ? DateTime.UtcNow.AddMinutes(-5) : DateTime.UtcNow.Add(-Period.Value); + var request = new GetTradeHistoryRequest(Symbol, startTime, DateTime.UtcNow); + var data = new List(); + await foreach(var result in ExchangeHelpers.ExecutePages(_historyRestClient.GetTradeHistoryAsync, request).ConfigureAwait(false)) + { + if (!result) + return result; + + if (Limit != null && data.Count > Limit) + break; + + data.AddRange(result.Data); + } + + SetInitialData(data); + } + else if (_recentRestClient != null) + { + int? limit = null; + if (Limit.HasValue) + limit = Math.Min(_recentRestClient.GetRecentTradesOptions.MaxLimit, Limit.Value); + + var snapshot = await _recentRestClient.GetRecentTradesAsync(new GetRecentTradesRequest(Symbol, limit)).ConfigureAwait(false); + if (!snapshot) + { + return snapshot; + } + + SetInitialData(snapshot.Data); + } + + return CallResult.SuccessResult; + } + + /// + /// The stop procedure needed, generally stopping the update stream + /// + /// + protected virtual Task DoStopAsync() => _updateSubscription?.CloseAsync() ?? Task.CompletedTask; + + /// + public SharedTrade[] GetData(DateTime? fromTimestamp = null, DateTime? toTimestamp = null) + { + lock (_lock) + { + ApplyWindow(true); + + IEnumerable result = _data; + if (fromTimestamp != null) + result = result.Where(d => d.Timestamp >= fromTimestamp); + if (toTimestamp != null) + result = result.Where(d => d.Timestamp <= toTimestamp); + + return result.ToArray(); + } + } + + /// + /// Set the initial trade data snapshot + /// + /// + protected void SetInitialData(IEnumerable data) + { + lock (_lock) + { + _data.Clear(); + + IEnumerable items = data.OrderByDescending(d => d.Timestamp); + if (Limit != null) + items = items.Take(Limit.Value); + if (Period != null) + items = items.Where(e => e.Timestamp >= DateTime.UtcNow.Add(-Period.Value)); + + _snapshotId = data.Max(d => d.Timestamp.Ticks); + foreach (var item in items.OrderBy(d => d.Timestamp)) + _data.Add(item); + + _snapshotSet = true; + _changed = true; + + _logger.TradeTrackerInitialDataSet(SymbolName, _data.Count, _snapshotId); + + foreach (var item in _preSnapshotQueue) + { + if (_snapshotId >= item.Timestamp.Ticks) + { + _logger.TradeTrackerPreSnapshotSkip(SymbolName, item.Timestamp.Ticks); + continue; + } + + _logger.TradeTrackerPreSnapshotApplied(SymbolName, item.Timestamp.Ticks); + _data.Add(item); + } + + if (_data.Count != 0) + _firstTimestamp = _data.Min(v => v.Timestamp); + + ApplyWindow(false); + } + } + + /// + /// Add a trade + /// + /// + protected void AddData(SharedTrade item) => AddData(new[] { item }); + + /// + /// Add a list of trades + /// + /// + protected void AddData(IEnumerable items) + { + lock (_lock) + { + if ((_recentRestClient != null || _historyRestClient != null) && _startWithSnapshot && !_snapshotSet) + { + _preSnapshotQueue.AddRange(items); + return; + } + + foreach (var item in items) + { + _logger.TradeTrackerTradeAdded(SymbolName, item.Timestamp.Ticks); + _data.Add(item); + OnAdded?.Invoke(item); + } + + _firstTimestamp = _data.Min(x => x.Timestamp); + _changed = true; + SetSyncStatus(); + ApplyWindow(true); + } + } + + private void ApplyWindow(bool broadcastEvents) + { + if (!_changed && (DateTime.UtcNow - _lastWindowApplied) < TimeSpan.FromSeconds(1)) + return; + + if (Period != null) + { + var compareDate = DateTime.UtcNow.Add(-Period.Value); + for(var i = 0; i < _data.Count; i++) + { + var item = _data[0]; + if (item.Timestamp >= compareDate) + break; + + _data.Remove(item); + if (broadcastEvents) + OnRemoved?.Invoke(item); + } + } + + if (Limit != null && _data.Count > Limit.Value) + { + var toRemove = _data.Count - Limit.Value; + for (var i = 0; i < toRemove; i++) + { + var item = _data[0]; + _data.Remove(item); + if (broadcastEvents) + OnRemoved?.Invoke(item); + } + } + + _lastWindowApplied = DateTime.UtcNow; + _changed = false; + + if (Status == SyncStatus.PartiallySynced) + // Need to check if sync status should be changed even if there may not be any new data + SetSyncStatus(); + } + + private void HandleConnectionLost() + { + _logger.TradeTrackerConnectionLost(SymbolName); + if (Status != SyncStatus.Disconnected) + { + Status = SyncStatus.Syncing; + _snapshotSet = false; + _firstTimestamp = null; + _preSnapshotQueue.Clear(); + } + } + + private void HandleConnectionClosed() + { + _logger.TradeTrackerConnectionClosed(SymbolName); + Status = SyncStatus.Disconnected; + _ = StopAsync(); + } + + private async void HandleConnectionRestored(TimeSpan _) + { + Status = SyncStatus.Syncing; + var success = false; + while (!success) + { + if (Status != SyncStatus.Syncing) + return; + + var resyncResult = await DoStartAsync().ConfigureAwait(false); + success = resyncResult; + } + + _logger.TradeTrackerConnectionRestored(SymbolName); + SetSyncStatus(); + } + + private void SetSyncStatus() + { + if (Status == SyncStatus.Synced) + return; + + if (Period != null) + { + if (_firstTimestamp <= DateTime.UtcNow - Period.Value) + Status = SyncStatus.Synced; + else + Status = SyncStatus.PartiallySynced; + } + + if (Limit != null) + { + if (_data.Count == Limit.Value) + Status = SyncStatus.Synced; + else + Status = SyncStatus.PartiallySynced; + } + + if (Period == null && Limit == null) + Status = SyncStatus.Synced; + } } diff --git a/CryptoExchange.Net/Trackers/Trades/TradesCompare.cs b/CryptoExchange.Net/Trackers/Trades/TradesCompare.cs index 1d02593..822c677 100644 --- a/CryptoExchange.Net/Trackers/Trades/TradesCompare.cs +++ b/CryptoExchange.Net/Trackers/Trades/TradesCompare.cs @@ -1,37 +1,32 @@ -using System; -using System.Collections.Generic; -using System.Text; +namespace CryptoExchange.Net.Trackers.Trades; -namespace CryptoExchange.Net.Trackers.Trades +/// +/// Trades statistics comparison +/// +public record TradesCompare { /// - /// Trades statistics comparison + /// Number of trades /// - public record TradesCompare - { - /// - /// Number of trades - /// - public CompareValue TradeCountDif { get; set; } = new CompareValue(null, null); - /// - /// Average trade price - /// - public CompareValue? AveragePriceDif { get; set; } - /// - /// Volume weighted average trade price - /// - public CompareValue? VolumeWeightedAveragePriceDif { get; set; } - /// - /// Volume of the trades - /// - public CompareValue VolumeDif { get; set; } = new CompareValue(null, null); - /// - /// Volume of the trades in quote asset - /// - public CompareValue QuoteVolumeDif { get; set; } = new CompareValue(null, null); - /// - /// The volume weighted Buy/Sell ratio. A 0.7 ratio means 70% of the trade volume was a buy. - /// - public CompareValue? BuySellRatioDif { get; set; } - } + public CompareValue TradeCountDif { get; set; } = new CompareValue(null, null); + /// + /// Average trade price + /// + public CompareValue? AveragePriceDif { get; set; } + /// + /// Volume weighted average trade price + /// + public CompareValue? VolumeWeightedAveragePriceDif { get; set; } + /// + /// Volume of the trades + /// + public CompareValue VolumeDif { get; set; } = new CompareValue(null, null); + /// + /// Volume of the trades in quote asset + /// + public CompareValue QuoteVolumeDif { get; set; } = new CompareValue(null, null); + /// + /// The volume weighted Buy/Sell ratio. A 0.7 ratio means 70% of the trade volume was a buy. + /// + public CompareValue? BuySellRatioDif { get; set; } } diff --git a/CryptoExchange.Net/Trackers/Trades/TradesStats.cs b/CryptoExchange.Net/Trackers/Trades/TradesStats.cs index 74107ca..dc86675 100644 --- a/CryptoExchange.Net/Trackers/Trades/TradesStats.cs +++ b/CryptoExchange.Net/Trackers/Trades/TradesStats.cs @@ -1,65 +1,62 @@ -using System; -using System.Collections.Generic; -using System.Text; +using System; -namespace CryptoExchange.Net.Trackers.Trades +namespace CryptoExchange.Net.Trackers.Trades; + +/// +/// Trades statistics +/// +public record TradesStats { /// - /// Trades statistics + /// Number of trades /// - public record TradesStats - { - /// - /// Number of trades - /// - public int TradeCount { get; set; } - /// - /// Timestamp of the last trade - /// - public DateTime? FirstTradeTime { get; set; } - /// - /// Timestamp of the first trade - /// - public DateTime? LastTradeTime { get; set; } - /// - /// Average trade price - /// - public decimal? AveragePrice { get; set; } - /// - /// Volume weighted average trade price - /// - public decimal? VolumeWeightedAveragePrice { get; set; } - /// - /// Volume of the trades - /// - public decimal Volume { get; set; } - /// - /// Volume of the trades in quote asset - /// - public decimal QuoteVolume { get; set; } - /// - /// The volume weighted Buy/Sell ratio. A 0.7 ratio means 70% of the trade volume was a buy. - /// - public decimal? BuySellRatio { get; set; } - /// - /// Whether the data is complete - /// - public bool Complete { get; set; } + public int TradeCount { get; set; } + /// + /// Timestamp of the last trade + /// + public DateTime? FirstTradeTime { get; set; } + /// + /// Timestamp of the first trade + /// + public DateTime? LastTradeTime { get; set; } + /// + /// Average trade price + /// + public decimal? AveragePrice { get; set; } + /// + /// Volume weighted average trade price + /// + public decimal? VolumeWeightedAveragePrice { get; set; } + /// + /// Volume of the trades + /// + public decimal Volume { get; set; } + /// + /// Volume of the trades in quote asset + /// + public decimal QuoteVolume { get; set; } + /// + /// The volume weighted Buy/Sell ratio. A 0.7 ratio means 70% of the trade volume was a buy. + /// + public decimal? BuySellRatio { get; set; } + /// + /// Whether the data is complete + /// + public bool Complete { get; set; } - /// - /// Compare 2 stat snapshots to each other - /// - public TradesCompare CompareTo(TradesStats otherStats) + /// + /// Compare 2 stat snapshots to each other + /// + public TradesCompare CompareTo(TradesStats otherStats) + { + return new TradesCompare { - return new TradesCompare - { - TradeCountDif = new CompareValue(TradeCount, otherStats.TradeCount), - AveragePriceDif = new CompareValue(AveragePrice, otherStats.AveragePrice), - VolumeWeightedAveragePriceDif = new CompareValue(VolumeWeightedAveragePrice, otherStats.VolumeWeightedAveragePrice), - VolumeDif = new CompareValue(Volume, otherStats.Volume), - QuoteVolumeDif = new CompareValue(QuoteVolume, otherStats.QuoteVolume), - BuySellRatioDif = new CompareValue(BuySellRatio, otherStats.BuySellRatio), - }; - } + TradeCountDif = new CompareValue(TradeCount, otherStats.TradeCount), + AveragePriceDif = new CompareValue(AveragePrice, otherStats.AveragePrice), + VolumeWeightedAveragePriceDif = new CompareValue(VolumeWeightedAveragePrice, otherStats.VolumeWeightedAveragePrice), + VolumeDif = new CompareValue(Volume, otherStats.Volume), + QuoteVolumeDif = new CompareValue(QuoteVolume, otherStats.QuoteVolume), + BuySellRatioDif = new CompareValue(BuySellRatio, otherStats.BuySellRatio), + }; } }