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

Compare commits

...

64 Commits

Author SHA1 Message Date
Jkorf 860d753ad6 Update README.md 2026-08-07 09:09:51 +02:00
Jkorf e16c91792c Added LBank reference 2026-08-06 10:29:02 +02:00
Jkorf a3d95da9fa Updated examples 2026-08-06 10:26:39 +02:00
JKorf 907399b878 Added ManualUpdateSubscription and UpdateSubscription additional constructor to allow producing websocket events without actual connection 2026-08-05 21:31:59 +02:00
JKorf 9eab0d967e Merge branch 'master' of https://github.com/JKorf/CryptoExchange.Net 2026-08-05 17:53:51 +02:00
JKorf 0dee68e8ae Fixed timing issue in test 2026-08-05 17:53:43 +02:00
Jkorf d838d3377f Updated logging unhandled websocket message 2026-08-05 13:13:48 +02:00
Jkorf 2141ad9061 Fixed incorrect check test output 2026-07-29 13:05:42 +02:00
Jkorf 9be8798ccf Updated to version 12.4.0 2026-07-28 12:45:36 +02:00
Jkorf 4803ed91cd Updated SharedTrade to use SharedOrderQuantity for quantities 2026-07-28 11:47:59 +02:00
Jkorf 20bddd5c37 Updated SharedFuturesTicker, SharedSpotTicker and SharedKline to use SharedOrderQuantity for volumes. Added AveragePrice property to SharedQuantity model 2026-07-28 09:05:48 +02:00
Jkorf 0e75ddb3d0 Updated json deserialization error for empty response 2026-07-24 15:42:37 +02:00
Jkorf 0e5b46002c Added DebuggerDisplay Result objects 2026-07-24 15:42:11 +02:00
Jkorf 8f7c71f9ce Add Pionex reference 2026-07-24 15:40:56 +02:00
Jkorf 73377fbb87 Updated to version 12.3.0 2026-07-23 13:47:24 +02:00
Jkorf 3a00d6371a Updated some Shared errors 2026-07-23 13:17:08 +02:00
Jkorf caf6d36bcd Extracted ConnectionCanBeUsedFor method in SocketApiClient for easier custom logic implementation 2026-07-23 13:16:59 +02:00
Jkorf e078a373da Remove duplicate warnings from testing output 2026-07-21 16:39:30 +02:00
Jkorf 007743f5a1 Added calculation of AveragePrice on Shared order models if data is available and AveragePrice is not set 2026-07-21 11:35:59 +02:00
Jkorf d06f891cee Updated to version 12.2.0 2026-07-20 14:02:03 +02:00
Jkorf 8dcbb687f5 Fixed warning 2026-07-20 13:45:32 +02:00
Jan Korf fcb36f7ee0 Shared asset and symbol types
Added SpotSymbolCatalog to Shared ISpotSymbolRestClient interface
Added FuturesSymbolCatalog to Shared IFuturesSymbolRestClient interface
Added BaseAssetType, BaseAssetSubType, QuoteAssetType and QuoteAssetSubType to GetSymbolsRequest model
Added DisplayName to SharedSpotSymbol and SharedFuturesSymbol models
Added BaseAssetType, BaseAssetSubType, QuoteAssetType and QuoteAssetSubType to SharedSpotSymbol and SharedFuturesSymbol models
Added IsStableCoin, IsCommodity and IsEquity helper methods to LibraryHelpers
2026-07-20 13:40:58 +02:00
nils2525 3cffd67518 Fixed socket individual subscription target calculation (#282)
* Fixed socket individual subscription target calculation

* Fixed socket selection when the least-loaded connection has reached its individual subscription limit
2026-07-20 09:24:01 +02:00
Jkorf ecd00ea707 Added DebuggerDisplay attributes to Shared models 2026-07-14 09:59:02 +02:00
JKorf 38a7b981ce Updated to version 12.1.1 2026-07-11 15:34:31 +02:00
JKorf c41cc3c4c7 Added timestamp deserialization support for yyyy-MM-dd HH:mm:ss.ffffff+00:00:00 2026-07-11 15:19:02 +02:00
Jkorf 4accc8039b Updated to version 12.1.0 2026-07-09 15:06:09 +02:00
Jkorf 87c86ec0c0 Fixed nullability operator on Parameters.AddCommaSeperated 2026-07-09 08:35:39 +02:00
Jkorf d9850da282 Added ExchangeParameters parameter to KlineTracker, TradeTracker and ITrackerFactory methods 2026-07-08 12:00:26 +02:00
Jkorf c4a8b02054 Updated some testing logic 2026-07-02 14:09:55 +02:00
Jkorf 34b7258496 Updated to version 12.0.2 2026-07-01 13:19:57 +02:00
Jkorf 69099922c9 Updated test validation to output a list of issues instead of throwing on the first 2026-07-01 13:16:06 +02:00
Jkorf 64c1cd5fa8 Updated RestIntegrationTest to output missing properties in a list so the running test can output them as warning instead of error 2026-07-01 09:42:55 +02:00
JKorf 936ac6640b Cleanup 2026-06-30 19:22:54 +02:00
Jkorf 68ad9ae114 Fixed warnings in tests 2026-06-30 15:25:08 +02:00
Jkorf 6238c17471 Added skills hub reference 2026-06-30 15:01:54 +02:00
Jkorf 52e6fbfe47 Updated examples 2026-06-30 12:55:20 +02:00
Jkorf 4129622d71 Added Lighter ref 2026-06-29 16:07:25 +02:00
Jkorf 95e0aefb9f Updated to version 12.0.1 2026-06-29 11:33:33 +02:00
Jkorf dd1cefdc90 Fixed bug in bool converter 2026-06-29 11:20:06 +02:00
Jkorf f4736bcf49 Updated to version 12.0.0 2026-06-29 10:55:02 +02:00
Jan Korf e823114623 CryptoExchange V12 (#281)
* Result types:
  * (Web)CallResult types are replaced by HttpResult, WebSocketResult and QueryResult with the same logic
  * Updated result types to record type
  * Result creation can be done with (Http/WebSocket/Query)Result.Ok(..) and .Fail(..)
  * Removed implicit result type conversion to bool, `if (result)` no longer works, instead use `if (result.Success)`
  * Replaced CallResult.SuccessResult with CallResult.Ok()
  * Fixed result object nullability hinting, for example Data might be null if Success isn't checked for true

* Parameters & serialization:
  * Added support for `enabled` and `disabled` strings to bool converter
  * Removed ParameterCollection type, has been replaced by Parameters type
  * Removed ArraySerialization, OrderParameters and ParameterOrderComparer properties from RestApiClient, moved to ParameterSerializationsSettings
  * Updated RestRequestConfiguration in AuthenticationProvider.ProcessRequest to contain the full RequestDefinition instead of copied fields	

* Clients:
  * Updated Api client constructor logging parameter from ILogger to ILoggerFactory? 
  * Added Api client constructor exchange name parameter
  * Added ToString overrides on base API types
  * Added Exchange property on BaseApiClient
  * Added ApiCredentials property on IRestApiClient and ISocketApiClient interfaces
  * Updated ILogger source from client name to topic specific client name
  * Removed logging from client creation
  * Fixed BaseRestClient SetApiCredentials not marked as virtual

* Rest:
  * Added BaseAddress to RequestDefinition object
  * Updated RestApiClient AuthenticationProvider logic from private to protected and virtual
  * Removed RestApiClient.SendAsync baseAddress parameter removed
  * Removed RestApiClient.SendAsync without type parameter

* WebSocket:
  * Updated MessageRouting definition into CreateForEvent for subscriptions and CreateForQuery for queries
  * Improved Query type safety with CeateForQuery which allows second parameter for specifying the result type
  * Renamed MessageRouter.CreateWithoutHandler to CreateVoid
  * Updated SocketApiClient.GetSocketConnection to check connection uri instead of Tag for finding compatible connections
  * Removed unused UnhandledMessageExpected property SocketApiClient
  * Fixed issue in SocketApiClient.GetSocketConnection causing requests to always wait the full max 10 seconds when there was a reconnecting socket
	
* Shared APIs:
  * Updated Option definitions to always require the exchange name as first parameter
  * Added missing dedicated option types
  * Added Discover method on ISharedClient interface, returning info on supported capabilities and operations
  * Added SharedRequest GetParamValue helper method accepting multiple parameter names
  * Added ResetStaticExchangeParameters method on ExchangeParameters
  * Added Status property to SharedWithdrawal model
  * Added TradingModes property to SharedBalance model
  * Updated ExchangeSymbolCache to support multiple environments and additional key separation
  * Updated Shared ExchangeParameters parameter names to be case insensitive
  * Updated code comments
  * Replaced ExchangeResult with ExchangeCallResult type
  * Removed AsExchangeResult/ExchangeWebResult
  * Removed TradingMode from the response model, only maintained on models where it makes sense
  * Removed IListenKey support, listen keys now rely on internal management with TokenManager

* Rate limiting:
  * Fixed websocket connection attempts counting towards rate limit even when server could not be reached
  * Removed host from rate limit methods, now part of the already provided RequestDefinition
  * Added amount parameter to RateLimit Reset method to allow partially resetting the limit

* Added TokenManager implementation for automatic listenkey/token management
* Added UserClientProvider base class
* Added async streaming on UserDataTracker items with StreamUpdatesAsync
* Added cancellation token support to UserDataTracker starting
* Added Unit type for non-result types
* Added ServerError constructor taking ErrorType and message to make it easier to create
* Added SupportedEnvironments property to PlatformInfo
* Updated SymbolOrderBook DoResyncAsync to return CallResult instead of CallResult<bool> which was redundant
* Various small performance improvements
2026-06-29 10:38:09 +02:00
Jkorf afb84a1bf0 Updated to version 11.2.2 2026-06-08 14:56:14 +02:00
Jkorf 89cbd85875 Fixed timing issue causing websocket connection to possible loop in error state 2026-06-08 11:22:53 +02:00
Jkorf cc25a405c6 Updated examples 2026-06-03 10:13:34 +02:00
Jkorf d64c1171a3 Removed deprecated Protobuf separate project 2026-06-03 09:33:02 +02:00
Jkorf d4aded1ee2 Fixed Shared incorrect pagination for descending klines 2026-06-02 16:40:23 +02:00
Jkorf 271503c426 Updated to version 11.2.1 2026-06-02 11:07:10 +02:00
Jkorf f0ece589f7 Updated some test logic 2026-06-01 16:05:50 +02:00
Jkorf 3a9382ca0f Fixed test rest request checking for no-response requests 2026-05-28 16:19:38 +02:00
Jkorf 1e1a02324a Potential fix for test splitting on newline 2026-05-28 16:13:13 +02:00
Jkorf 95dd050c73 Updated to version 11.2.0 2026-05-26 10:34:26 +02:00
Jkorf 504836924c Fixed test 2026-05-26 10:30:16 +02:00
Jkorf f176fb5db5 Fixed timing issue in SymbolOrderBook stopping 2026-05-26 09:27:04 +02:00
Jkorf 6b575be1ac Improved EnumConverter initialization performance 2026-05-26 09:26:45 +02:00
Jkorf b637d5cdc4 Added RateLimitGroup option 2026-05-26 09:25:53 +02:00
JKorf a46b018c50 Merge branch 'master' of https://github.com/JKorf/CryptoExchange.Net 2026-05-25 12:55:23 +02:00
JKorf 562d1d76c1 Cleanup 2026-05-25 12:55:21 +02:00
Jkorf 7dcb2241c6 Ai docs 2026-05-09 21:56:08 +02:00
Jkorf 8c4cf62d9f Fixed some examples 2026-05-07 14:15:50 +02:00
Jkorf 6e4dbcf7b1 Added AI documentation 2026-05-07 13:28:20 +02:00
Jkorf 7853834286 Added request parameter checking in RestRequestValidator 2026-04-13 11:20:41 +02:00
Jkorf 9ae1263662 Added Weex to examples 2026-04-10 12:57:37 +02:00
Jkorf ee30a6716e Added Weex reference 2026-04-10 11:54:32 +02:00
848 changed files with 9944 additions and 122250 deletions
+83
View File
@@ -0,0 +1,83 @@
---
description: Conventions for cross-exchange code using CryptoExchange.Net SharedApis abstractions. Apply when generating C# code that interacts with multiple cryptocurrency exchanges through a unified interface.
globs:
- "**/*.cs"
- "**/*.csproj"
alwaysApply: false
---
# CryptoExchange.Net Conventions
This codebase uses **CryptoExchange.Net** abstractions for multi-exchange access. Each exchange has its own library (Binance.Net, Bybit.Net, OKX.Net, ...). Use `CryptoExchange.Net.SharedApis` for code that should work across exchanges.
## Multi-exchange pattern
```csharp
using Binance.Net.Clients;
using OKX.Net.Clients;
using CryptoExchange.Net.SharedApis;
ISpotTickerRestClient binance = new BinanceRestClient().SpotApi.SharedClient;
ISpotTickerRestClient okx = new OKXRestClient().UnifiedApi.SharedClient;
var symbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
var ticker = await binance.GetSpotTickerAsync(new GetTickerRequest(symbol));
// ticker.Data.LastPrice — same model regardless of exchange
```
## Symbol normalization
`SharedSymbol(TradingMode.Spot, "BTC", "USDT")` is portable. Each library translates to its native format internally. Don't pass raw strings like `"BTCUSDT"` to shared methods.
## Symbol metadata and catalogs
In 12.2.0, `SharedSpotSymbol` and `SharedFuturesSymbol` include `DisplayName` and base/quote asset classification through `SharedAssetType` (`Crypto`, `Fiat`, `TradFi`) and `SharedAssetSubType` (`StableCoin`, `Equity`, `Commodity`). Pass the matching base/quote filters to `GetSymbolsRequest` when discovery should return only a class of markets.
After calling `GetSpotSymbolsAsync`, `ISpotSymbolRestClient.SpotSymbolCatalog` maps asset and symbol names to shared metadata. `IFuturesSymbolRestClient.FuturesSymbolCatalog` works the same way after `GetFuturesSymbolsAsync`. Treat either property as unavailable before its corresponding request has populated the cache.
When implementing an exchange library, use `LibraryHelpers.IsStableCoin`, `IsCommodity`, and `IsEquity` only as best-effort classifiers and supply exchange-specific additions where needed.
## Shared market-data quantities
In 12.4.0, use `SharedOrderQuantity`-valued `Volumes` on shared spot/futures tickers and klines, and `Quantities` on shared trades. The scalar `Volume`, `QuoteVolume`, and `Quantity` members are obsolete.
## Result pattern
REST methods return `HttpResult<T>` and websocket subscription methods return `WebSocketResult<UpdateSubscription>`. Always check `.Success`. `.Exchange` property identifies which exchange responded — useful for logging.
## Available shared interfaces
- REST tickers/symbols/orderbook/klines/trades, orders (spot/futures, regular/trigger/TP-SL), balances, positions, fees, deposits/withdrawals, transfers
- WebSocket tickers, book tickers, order book, trades, klines, user data
Each exchange documents which it implements. Not every exchange supports every operation.
## Multi-exchange aggregation
Run requests across exchanges concurrently via `Task.WhenAll` — the library is async-safe and concurrent requests are the norm.
```csharp
var clients = new ISpotTickerRestClient[] { binance, okx, bybit };
var tasks = clients.Select(c => c.GetSpotTickerAsync(new GetTickerRequest(symbol)));
var results = await Task.WhenAll(tasks);
```
## Hard rules
- ❌ Never install `CryptoExchange.Net` alone and expect to call exchanges — it's a base library; you need exchange-specific packages
- ❌ Never mix exchange-specific models in cross-exchange code (use `SharedSymbol`, `SharedSpotTicker`, etc.)
- ❌ Never use `.Result` / `.Wait()` — async-only
- ❌ Never iterate sequentially when concurrency is fine — `Task.WhenAll` is your friend
- ❌ Never instantiate clients per-request — reuse via DI
- ✅ Always use `.SharedClient` for cross-exchange code
- ✅ Always check `.Success` before reading `.Data`
- ✅ Always log with `.Exchange` so multi-exchange logs are decipherable
- ✅ Always handle "not supported on this exchange" errors gracefully
## Reference
- `AGENTS.md` in repo root has fuller examples
- `llms.txt` for AI context
- `Examples/ai-friendly/` for compilable examples
- For single-exchange code, see that exchange's library (e.g., Binance.Net `AGENTS.md`)
+62
View File
@@ -0,0 +1,62 @@
# Copilot Instructions for CryptoExchange.Net
This repository is **CryptoExchange.Net** — the base library powering 28+ cryptocurrency exchange wrappers in C#/.NET (Binance.Net, Bybit.Net, OKX.Net, Kraken.Net, Coinbase.Net, etc.).
When generating code in this ecosystem, follow these conventions:
## You don't install CryptoExchange.Net directly
Install the exchange-specific library you need (`Binance.Net`, `JK.OKX.Net`, `Bybit.Net`, ...) or `CryptoClients.Net` for the bundle. CryptoExchange.Net is pulled in as a dependency.
## Multi-exchange code uses SharedApis
For code that must work against multiple exchanges, use `CryptoExchange.Net.SharedApis` interfaces accessed via `.SharedClient` properties on each exchange's API surface:
```csharp
using CryptoExchange.Net.SharedApis;
ISpotTickerRestClient binance = new BinanceRestClient().SpotApi.SharedClient;
ISpotTickerRestClient okx = new OKXRestClient().UnifiedApi.SharedClient;
var symbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
var ticker = await binance.GetSpotTickerAsync(new GetTickerRequest(symbol));
```
Same code works on every exchange that implements the interface. Use `Task.WhenAll` for concurrent multi-exchange calls.
## Shared symbol metadata
CryptoExchange.Net 12.2.0 classifies the base and quote sides of `SharedSpotSymbol` and `SharedFuturesSymbol` with `SharedAssetType` (`Crypto`, `Fiat`, `TradFi`) and optional `SharedAssetSubType` (`StableCoin`, `Equity`, `Commodity`). The models also expose `DisplayName`. Use the corresponding base/quote fields on `GetSymbolsRequest` to filter symbol discovery.
`ISpotSymbolRestClient.SpotSymbolCatalog` is populated by `GetSpotSymbolsAsync`; `IFuturesSymbolRestClient.FuturesSymbolCatalog` is populated by `GetFuturesSymbolsAsync`. Do not assume a catalog is available before that request. For exchange-library implementations, `LibraryHelpers.IsStableCoin`, `IsCommodity`, and `IsEquity` offer best-effort classification and can be extended with exchange-specific values.
## Shared market-data quantities
CryptoExchange.Net 12.4.0 uses `SharedOrderQuantity` for market-data quantities. Prefer `Volumes` on `SharedSpotTicker`, `SharedFuturesTicker`, and `SharedKline`, and `Quantities` on `SharedTrade`; the scalar `Volume`, `QuoteVolume`, and `Quantity` members are obsolete.
## Single-exchange code uses the exchange's own client
For Binance-only code, use `BinanceRestClient` directly (see Binance.Net repo `AGENTS.md`). SharedApis is for portability — use it when you need that.
## Result pattern
REST methods return `HttpResult<T>` and websocket subscription methods return `WebSocketResult<UpdateSubscription>`. Check `.Success` before `.Data`. `.Error` has structured info. `.Exchange` on shared clients identifies which exchange responded.
## Available shared interfaces
REST: tickers, symbols, orderbook, klines, trades, orders (spot/futures, trigger, TP-SL), balances, positions, fees, deposits/withdrawals, transfers.
WebSocket: tickers, book tickers, orderbook, trades, klines, user data.
Each exchange library implements a subset. Check exchange docs for support matrix.
## Avoid
- Installing `CryptoExchange.Net` alone and trying to call exchange APIs (need exchange-specific packages)
- Mixing exchange-native models in cross-exchange code (use Shared* types)
- Synchronous `.Result` / `.Wait()` (use `await`)
- Instantiating clients per-request (use DI, reuse instances)
- Sequential per-exchange calls when parallel is fine (`Task.WhenAll`)
## Reference
For detailed patterns see `AGENTS.md` and `llms.txt` in repo root, `examples/ai-friendly/` for compilable examples.
+191
View File
@@ -0,0 +1,191 @@
---
name: cryptoexchange-net
description: Use CryptoExchange.Net abstractions when generating C#/.NET code that needs to work across MULTIPLE cryptocurrency exchanges (Binance + Bybit + OKX + Kraken + Coinbase + ...) — for arbitrage, best-execution routing, multi-exchange portfolio dashboards, exchange-agnostic trading bots, or comparison tools. Triggers on requests mentioning multi-exchange, cross-exchange, arbitrage, exchange-agnostic, or unified crypto API in C#. Also triggers when the user is implementing a new exchange library following the CryptoExchange.Net pattern.
---
# CryptoExchange.Net Skill
## When to use
CryptoExchange.Net is the **base library** powering 28+ exchange-specific libraries (Binance.Net, Bybit.Net, OKX.Net, Kraken.Net, Coinbase.Net, etc.). You don't install it directly — you install the exchange libraries, which depend on it.
**Three usage modes:**
1. **You target ONE exchange** → use that exchange's library directly (e.g., Binance.Net), see its CLAUDE.md.
2. **You target MULTIPLE exchanges** → install each library you need + use `CryptoExchange.Net.SharedApis` interfaces — write code once, runs against any exchange. **This is the main use case for this skill.**
3. **You want ALL exchanges in one package** → install `CryptoClients.Net`, get `ExchangeRestClient` and `ExchangeSocketClient` with everything bundled.
## Installation
For a multi-exchange project:
```bash
dotnet add package Binance.Net
dotnet add package JK.OKX.Net
dotnet add package Bybit.Net
# ... etc
```
Or the bundle:
```bash
dotnet add package CryptoClients.Net
```
## Core Pattern: Shared Interfaces
Every exchange library exposes `.SharedClient` properties on its API surfaces. These implement the same interfaces from `CryptoExchange.Net.SharedApis`.
```csharp
using Binance.Net.Clients;
using OKX.Net.Clients;
using Bybit.Net.Clients;
using CryptoExchange.Net.SharedApis;
// All three implement ISpotTickerRestClient
ISpotTickerRestClient binance = new BinanceRestClient().SpotApi.SharedClient;
ISpotTickerRestClient okx = new OKXRestClient().UnifiedApi.SharedClient;
ISpotTickerRestClient bybit = new BybitRestClient().V5Api.SharedClient;
// Single agnostic call — works against any of them
var symbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
var ticker = await binance.GetSpotTickerAsync(new GetTickerRequest(symbol));
// ticker.Data.LastPrice, ticker.Data.HighPrice, etc. — same model regardless of exchange
```
## Core Pattern: SharedSymbol
Different exchanges format symbols differently — Binance uses `BTCUSDT`, OKX uses `BTC-USDT`, others may have other formats. `SharedSymbol` normalizes this:
```csharp
var btcusdt = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
// Each exchange library translates SharedSymbol → its native format internally.
// For futures:
var btcusdtPerp = new SharedSymbol(TradingMode.PerpetualLinear, "BTC", "USDT");
```
For exchanges that use exotic asset names, see the AssetAliases configuration.
## Symbol Metadata and Asset Classification
Since CryptoExchange.Net 12.2.0, shared symbol responses describe both sides of a market with `BaseAssetType`, `BaseAssetSubType`, `QuoteAssetType`, and `QuoteAssetSubType`. `SharedAssetType` distinguishes `Crypto`, `Fiat`, and `TradFi`; `SharedAssetSubType` distinguishes `StableCoin`, `Equity`, and `Commodity`. `SharedSpotSymbol` and `SharedFuturesSymbol` also expose `DisplayName`.
The same fields on `GetSymbolsRequest` filter spot or futures symbol discovery:
```csharp
var request = new GetSymbolsRequest(
baseAssetType: SharedAssetType.Crypto,
quoteAssetSubType: SharedAssetSubType.StableCoin);
var result = await symbolClient.GetSpotSymbolsAsync(request);
```
After calling `GetSpotSymbolsAsync` or `GetFuturesSymbolsAsync`, use the client's `SpotSymbolCatalog` or `FuturesSymbolCatalog` to look up normalized asset and symbol metadata by name. The catalog is unavailable until the corresponding symbol request has populated the cache.
For exchange-library implementations, `LibraryHelpers.IsStableCoin`, `IsCommodity`, and `IsEquity` provide best-effort classification of known assets and accept exchange-specific additions. These helpers are heuristics, not an exhaustive source of truth.
## Shared Market-Data Quantities
Since CryptoExchange.Net 12.4.0, shared market-data models use `SharedOrderQuantity` so base-asset, quote-asset, and contract quantities remain explicit. Read `SharedSpotTicker.Volumes`, `SharedFuturesTicker.Volumes`, and `SharedKline.Volumes`; read `SharedTrade.Quantities`. The former scalar `Volume`, `QuoteVolume`, and `Quantity` members are obsolete.
## Available Shared Interfaces
**REST:**
- Market data: `ISpotTickerRestClient`, `IBookTickerRestClient`, `ISpotSymbolRestClient`, `IFuturesSymbolRestClient`, `IOrderBookRestClient`, `IRecentTradeRestClient`, `IKlineRestClient`
- Orders: `ISpotOrderRestClient`, `IFuturesOrderRestClient`, `ISpotOrderClientIdRestClient`, `IFuturesOrderClientIdRestClient`, `ISpotTriggerOrderRestClient`, `IFuturesTriggerOrderRestClient`, `IFuturesTpSlRestClient`
- Account: `IBalanceRestClient`, `IPositionRestClient`, `IFeeRestClient`, `ITransferRestClient`, `IDepositRestClient`, `IWithdrawalRestClient`
**WebSocket:**
- `ITickerSocketClient`, `IBookTickerSocketClient`
- `IOrderBookSocketClient`, `ITradeSocketClient`, `IKlineSocketClient`
- `IUserTradeSocketClient`, `ISpotOrderSocketClient`, `IFuturesOrderSocketClient`, `IPositionSocketClient`, `IBalanceSocketClient`
Each exchange documents which interfaces it implements (some exchanges don't support every operation).
## Core Pattern: Result Handling
Same as exchange-specific libraries: REST calls return `HttpResult<T>` and websocket subscription calls return `WebSocketResult<UpdateSubscription>`, both with `.Success`, `.Data`, and `.Error`. Always check `.Success` first.
```csharp
var result = await sharedClient.GetSpotTickerAsync(new GetTickerRequest(symbol));
if (!result.Success)
{
Console.WriteLine($"[{sharedClient.Exchange}] Error: {result.Error}");
return;
}
Console.WriteLine($"[{sharedClient.Exchange}] {result.Data.LastPrice}");
```
`.Exchange` property on every shared client tells you which exchange you're talking to — useful for logging.
## Core Pattern: Multi-Exchange Aggregation
```csharp
var clients = new ISpotTickerRestClient[]
{
new BinanceRestClient().SpotApi.SharedClient,
new OKXRestClient().UnifiedApi.SharedClient,
new BybitRestClient().V5Api.SharedClient,
};
var symbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
// Fetch concurrently from all exchanges
var tasks = clients.Select(c => c.GetSpotTickerAsync(new GetTickerRequest(symbol))).ToArray();
var results = await Task.WhenAll(tasks);
for (int i = 0; i < clients.Length; i++)
{
if (results[i].Success)
Console.WriteLine($"{clients[i].Exchange}: {results[i].Data!.LastPrice}");
}
```
## Per-Exchange Setup
Each exchange library has its own credentials class and options. See each library's CLAUDE.md for specifics. The pattern is consistent: `XxxRestClient(options => { options.ApiCredentials = new XxxCredentials(...); })`.
## Dependency Injection
Each exchange library has its own `services.AddXxx(...)` extension. They all share the same option-builder pattern. Register only the ones you use:
```csharp
services.AddBinance(restOpts => { /*...*/ }, socketOpts => { /*...*/ });
services.AddOKX(restOpts => { /*...*/ }, socketOpts => { /*...*/ });
// Inject IBinanceRestClient, IOKXRestClient, etc.
```
For one-package access: `services.AddCryptoClients(...)` from `CryptoClients.Net`.
## Common Pitfalls — AVOID
- **Do NOT install `CryptoExchange.Net` and try to call exchange APIs directly** — it's a base abstraction; you need an exchange library.
- **Do NOT try to use one exchange's models with another's client** — use the SharedApis types (`SharedSymbol`, `SharedSpotTicker`, `SharedSpotOrder`, etc.) for cross-exchange code.
- **Do NOT block on async operations** — use `await` throughout. `Task.WhenAll` for parallelism across exchanges.
- **Do NOT assume every exchange supports every operation** — check exchange docs or the library's implementation. Operations may return errors like "not supported on this exchange".
- **Do NOT instantiate clients per-request** — reuse via DI.
- **Do NOT iterate exchanges sequentially when concurrency is fine** — use `Task.WhenAll` for ~Nx speedup.
## Implementing a New Exchange Library
If you're building a NEW exchange wrapper following the CryptoExchange.Net pattern (rare but valuable):
- Inherit from `RestApiClient` and `SocketApiClient` base classes
- Define your own `XxxCredentials` extending `ApiCredentials` (or use `ApiCredentials` directly)
- Implement `AuthenticationProvider` for the exchange's signing scheme
- Implement the relevant `Shared*` interfaces on your API client classes for cross-exchange support
- Follow the same `XxxRestOptions` / `XxxSocketOptions` pattern
See existing libraries (Binance.Net, Bybit.Net) as reference implementations.
## Reference
- Source: https://github.com/JKorf/CryptoExchange.Net
- Documentation: https://cryptoexchange.jkorf.dev/
- SharedApis docs: https://cryptoexchange.jkorf.dev/CryptoExchange.Net/idocs_shared.html
- Bundle (all exchanges): https://github.com/JKorf/CryptoClients.Net
- Demo app: https://github.com/JKorf/CryptoManager.Net
- Discord: https://discord.gg/MSpeEtSY8t
@@ -1,530 +0,0 @@
using CryptoExchange.Net.Converters.MessageParsing;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using ProtoBuf;
using ProtoBuf.Meta;
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Runtime.InteropServices.ComTypes;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Converters.Protobuf
{
/// <summary>
/// System.Text.Json message accessor
/// </summary>
#if NET5_0_OR_GREATER
public abstract class ProtobufMessageAccessor<
[DynamicallyAccessedMembers(
#if NET8_0_OR_GREATER
DynamicallyAccessedMemberTypes.NonPublicConstructors |
DynamicallyAccessedMemberTypes.PublicFields |
DynamicallyAccessedMemberTypes.NonPublicFields |
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
DynamicallyAccessedMemberTypes.PublicProperties |
DynamicallyAccessedMemberTypes.NonPublicProperties |
#endif
DynamicallyAccessedMemberTypes.PublicNestedTypes |
DynamicallyAccessedMemberTypes.NonPublicMethods |
DynamicallyAccessedMemberTypes.PublicMethods
)]
TIntermediateType> : IMessageAccessor
#else
public abstract class ProtobufMessageAccessor<TIntermediateType> : IMessageAccessor
#endif
{
/// <summary>
/// The intermediate deserialization object
/// </summary>
protected TIntermediateType? _intermediateType;
/// <summary>
/// Runtime type model
/// </summary>
protected RuntimeTypeModel _model;
/// <inheritdoc />
public bool IsValid { get; set; }
/// <inheritdoc />
public abstract bool OriginalDataAvailable { get; }
/// <inheritdoc />
public object? Underlying => _intermediateType;
/// <summary>
/// ctor
/// </summary>
public ProtobufMessageAccessor(RuntimeTypeModel model)
{
_model = model;
}
/// <inheritdoc />
public NodeType? GetNodeType()
{
throw new NotImplementedException();
}
/// <inheritdoc />
public NodeType? GetNodeType(MessagePath path)
{
if (_intermediateType == null)
throw new InvalidOperationException("Data not read");
object? value = _intermediateType;
foreach (var step in path)
{
if (value == null)
break;
if (step.Type == 0)
{
// array index
}
else if (step.Type == 1)
{
// property value
#pragma warning disable IL2075 // Type is already annotated
value = value.GetType().GetProperty(step.Property!)?.GetValue(value);
#pragma warning restore
}
else
{
// property name
}
}
if (value == null)
return null;
var valueType = value.GetType();
if (valueType.IsArray)
return NodeType.Array;
if (IsSimple(valueType))
return NodeType.Value;
return NodeType.Object;
}
private static bool IsSimple(Type type)
{
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
{
// nullable type, check if the nested type is simple.
return IsSimple(type.GetGenericArguments()[0]);
}
return type.IsPrimitive
|| type.IsEnum
|| type == typeof(string)
|| type == typeof(decimal);
}
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2075:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
public T? GetValue<T>(MessagePath path)
{
if (_intermediateType == null)
throw new InvalidOperationException("Data not read");
object? value = _intermediateType;
foreach(var step in path)
{
if (value == null)
break;
if (step.Type == 0)
{
// array index
}
else if (step.Type == 1)
{
// property value
#pragma warning disable IL2075 // Type is already annotated
value = value.GetType().GetProperty(step.Property!)?.GetValue(value);
#pragma warning restore
}
else
{
// property name
}
}
return (T?)value;
}
/// <inheritdoc />
public T?[]? GetValues<T>(MessagePath path)
{
throw new NotImplementedException();
}
/// <inheritdoc />
public abstract string GetOriginalString();
/// <inheritdoc />
public abstract void Clear();
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
public abstract CallResult<object> Deserialize(
#if NET5_0_OR_GREATER
[DynamicallyAccessedMembers(
#if NET8_0_OR_GREATER
DynamicallyAccessedMemberTypes.NonPublicConstructors |
DynamicallyAccessedMemberTypes.PublicFields |
DynamicallyAccessedMemberTypes.NonPublicFields |
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
DynamicallyAccessedMemberTypes.PublicProperties |
DynamicallyAccessedMemberTypes.NonPublicProperties |
DynamicallyAccessedMemberTypes.PublicConstructors |
#endif
DynamicallyAccessedMemberTypes.PublicNestedTypes |
DynamicallyAccessedMemberTypes.NonPublicMethods |
DynamicallyAccessedMemberTypes.PublicMethods
)]
#endif
Type type, MessagePath? path = null);
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
public abstract CallResult<T> Deserialize<
#if NET5_0_OR_GREATER
[DynamicallyAccessedMembers(
#if NET8_0_OR_GREATER
DynamicallyAccessedMemberTypes.NonPublicConstructors |
DynamicallyAccessedMemberTypes.PublicFields |
DynamicallyAccessedMemberTypes.NonPublicFields |
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
DynamicallyAccessedMemberTypes.PublicProperties |
DynamicallyAccessedMemberTypes.NonPublicProperties |
DynamicallyAccessedMemberTypes.PublicConstructors |
#endif
DynamicallyAccessedMemberTypes.PublicNestedTypes |
DynamicallyAccessedMemberTypes.NonPublicMethods |
DynamicallyAccessedMemberTypes.PublicMethods
)]
#endif
T>(MessagePath? path = null);
}
/// <summary>
/// System.Text.Json stream message accessor
/// </summary>
public class ProtobufStreamMessageAccessor<
#if NET5_0_OR_GREATER
[DynamicallyAccessedMembers(
#if NET8_0_OR_GREATER
DynamicallyAccessedMemberTypes.NonPublicConstructors |
DynamicallyAccessedMemberTypes.PublicFields |
DynamicallyAccessedMemberTypes.NonPublicFields |
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
DynamicallyAccessedMemberTypes.PublicProperties |
DynamicallyAccessedMemberTypes.NonPublicProperties |
DynamicallyAccessedMemberTypes.PublicConstructors |
#endif
DynamicallyAccessedMemberTypes.PublicNestedTypes |
DynamicallyAccessedMemberTypes.NonPublicMethods |
DynamicallyAccessedMemberTypes.PublicMethods
)]
#endif
TIntermediate> : ProtobufMessageAccessor<TIntermediate>, IStreamMessageAccessor
{
private Stream? _stream;
/// <inheritdoc />
public override bool OriginalDataAvailable => _stream?.CanSeek == true;
/// <summary>
/// ctor
/// </summary>
public ProtobufStreamMessageAccessor(RuntimeTypeModel model) : base(model)
{
}
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
public override CallResult<object> Deserialize(
#if NET5_0_OR_GREATER
[DynamicallyAccessedMembers(
#if NET8_0_OR_GREATER
DynamicallyAccessedMemberTypes.NonPublicConstructors |
DynamicallyAccessedMemberTypes.PublicFields |
DynamicallyAccessedMemberTypes.NonPublicFields |
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
DynamicallyAccessedMemberTypes.PublicProperties |
DynamicallyAccessedMemberTypes.NonPublicProperties |
DynamicallyAccessedMemberTypes.PublicConstructors |
#endif
DynamicallyAccessedMemberTypes.PublicNestedTypes |
DynamicallyAccessedMemberTypes.NonPublicMethods |
DynamicallyAccessedMemberTypes.PublicMethods
)]
#endif
Type type, MessagePath? path = null)
{
try
{
var result = _model.Deserialize(type, _stream);
return new CallResult<object>(result);
}
catch (Exception ex)
{
return new CallResult<object>(new DeserializeError("Protobuf deserialization failed: " + ex.Message, ex));
}
}
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
public override CallResult<T> Deserialize<
#if NET5_0_OR_GREATER
[DynamicallyAccessedMembers(
#if NET8_0_OR_GREATER
DynamicallyAccessedMemberTypes.NonPublicConstructors |
DynamicallyAccessedMemberTypes.PublicFields |
DynamicallyAccessedMemberTypes.NonPublicFields |
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
DynamicallyAccessedMemberTypes.PublicProperties |
DynamicallyAccessedMemberTypes.NonPublicProperties |
DynamicallyAccessedMemberTypes.PublicConstructors |
#endif
DynamicallyAccessedMemberTypes.PublicNestedTypes |
DynamicallyAccessedMemberTypes.NonPublicMethods |
DynamicallyAccessedMemberTypes.PublicMethods
)]
#endif
T>(MessagePath? path = null)
{
try
{
var result = _model.Deserialize<T>(_stream);
return new CallResult<T>(result);
}
catch(Exception ex)
{
return new CallResult<T>(new DeserializeError("Protobuf deserialization failed: " + ex.Message, ex));
}
}
/// <inheritdoc />
public Task<CallResult> Read(Stream stream, bool bufferStream)
{
if (bufferStream && stream is not MemoryStream)
{
// We need to be buffer the stream, and it's not currently a seekable stream, so copy it to a new memory stream
_stream = new MemoryStream();
stream.CopyTo(_stream);
_stream.Position = 0;
}
else if (bufferStream)
{
// We need to buffer the stream, and the current stream is seekable, store as is
_stream = stream;
}
else
{
// We don't need to buffer the stream, so don't bother keeping the reference
}
try
{
_intermediateType = _model.Deserialize<TIntermediate>(_stream);
IsValid = true;
return Task.FromResult(CallResult.SuccessResult);
}
catch (Exception ex)
{
// Not a json message
IsValid = false;
return Task.FromResult(new CallResult(new DeserializeError("Protobuf deserialization failed: " + ex.Message, ex)));
}
}
/// <inheritdoc />
public override string GetOriginalString()
{
if (_stream is null)
throw new NullReferenceException("Stream not initialized");
_stream.Position = 0;
using var textReader = new StreamReader(_stream, Encoding.UTF8, false, 1024, true);
return textReader.ReadToEnd();
}
/// <inheritdoc />
public override void Clear()
{
_stream?.Dispose();
_stream = null;
_intermediateType = default;
}
}
/// <summary>
/// Protobuf byte message accessor
/// </summary>
public class ProtobufByteMessageAccessor<
#if NET5_0_OR_GREATER
[DynamicallyAccessedMembers(
#if NET8_0_OR_GREATER
DynamicallyAccessedMemberTypes.NonPublicConstructors |
DynamicallyAccessedMemberTypes.PublicFields |
DynamicallyAccessedMemberTypes.NonPublicFields |
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
DynamicallyAccessedMemberTypes.PublicProperties |
DynamicallyAccessedMemberTypes.NonPublicProperties |
DynamicallyAccessedMemberTypes.PublicConstructors |
#endif
DynamicallyAccessedMemberTypes.PublicNestedTypes |
DynamicallyAccessedMemberTypes.NonPublicMethods |
DynamicallyAccessedMemberTypes.PublicMethods
)]
#endif
TIntermediate> : ProtobufMessageAccessor<TIntermediate>, IByteMessageAccessor
{
private ReadOnlyMemory<byte> _bytes;
/// <summary>
/// ctor
/// </summary>
public ProtobufByteMessageAccessor(RuntimeTypeModel model) : base(model)
{
}
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
public override CallResult<object> Deserialize(
#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")]
[DynamicallyAccessedMembers(
#if NET8_0_OR_GREATER
DynamicallyAccessedMemberTypes.NonPublicConstructors |
DynamicallyAccessedMemberTypes.PublicFields |
DynamicallyAccessedMemberTypes.NonPublicFields |
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
DynamicallyAccessedMemberTypes.PublicProperties |
DynamicallyAccessedMemberTypes.NonPublicProperties |
DynamicallyAccessedMemberTypes.PublicConstructors |
#endif
DynamicallyAccessedMemberTypes.PublicNestedTypes |
DynamicallyAccessedMemberTypes.NonPublicMethods |
DynamicallyAccessedMemberTypes.PublicMethods
)]
#endif
Type type, MessagePath? path = null)
{
try
{
using var stream = new MemoryStream(_bytes.ToArray());
stream.Position = 0;
var result = _model.Deserialize(type, stream);
return new CallResult<object>(result);
}
catch (Exception ex)
{
return new CallResult<object>(new DeserializeError("Protobuf deserialization failed: " + ex.Message, ex));
}
}
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
#if NET5_0_OR_GREATER
public override CallResult<T> Deserialize<
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[DynamicallyAccessedMembers(
#if NET8_0_OR_GREATER
DynamicallyAccessedMemberTypes.NonPublicConstructors |
DynamicallyAccessedMemberTypes.PublicFields |
DynamicallyAccessedMemberTypes.NonPublicFields |
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
DynamicallyAccessedMemberTypes.PublicProperties |
DynamicallyAccessedMemberTypes.NonPublicProperties |
DynamicallyAccessedMemberTypes.PublicConstructors |
#endif
DynamicallyAccessedMemberTypes.PublicNestedTypes |
DynamicallyAccessedMemberTypes.NonPublicMethods |
DynamicallyAccessedMemberTypes.PublicMethods
)]
T>(MessagePath? path = null)
#else
public override CallResult<T> Deserialize<T>(MessagePath? path = null)
#endif
{
try
{
var result = _model.Deserialize<T>(_bytes);
return new CallResult<T>(result);
}
catch (Exception ex)
{
return new CallResult<T>(new DeserializeError("Protobuf deserialization failed: " + ex.Message, ex));
}
}
/// <inheritdoc />
public CallResult Read(ReadOnlyMemory<byte> data)
{
_bytes = data;
try
{
_intermediateType = _model.Deserialize<TIntermediate>(data);
IsValid = true;
return CallResult.SuccessResult;
}
catch (Exception ex)
{
// Not a json message
IsValid = false;
return new CallResult(new DeserializeError("Protobuf deserialization failed: " + ex.Message, ex));
}
}
/// <inheritdoc />
public override string GetOriginalString() =>
// NetStandard 2.0 doesn't support GetString from a ReadonlySpan<byte>, so use ToArray there instead
#if NETSTANDARD2_0
Encoding.UTF8.GetString(_bytes.ToArray());
#else
Encoding.UTF8.GetString(_bytes.Span);
#endif
/// <inheritdoc />
public override bool OriginalDataAvailable => true;
/// <inheritdoc />
public override void Clear()
{
_bytes = null;
_intermediateType = default;
}
}
}
@@ -1,53 +0,0 @@
using CryptoExchange.Net.Interfaces;
using ProtoBuf.Meta;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Reflection;
namespace CryptoExchange.Net.Converters.Protobuf
{
/// <inheritdoc />
public class ProtobufMessageSerializer : IByteMessageSerializer
{
private RuntimeTypeModel _model;
/// <summary>
/// ctor
/// </summary>
public ProtobufMessageSerializer(RuntimeTypeModel model)
{
_model = model;
}
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
#if NET5_0_OR_GREATER
public byte[] Serialize<
[DynamicallyAccessedMembers(
#if NET8_0_OR_GREATER
DynamicallyAccessedMemberTypes.NonPublicConstructors |
DynamicallyAccessedMemberTypes.PublicFields |
DynamicallyAccessedMemberTypes.NonPublicFields |
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
DynamicallyAccessedMemberTypes.PublicProperties |
DynamicallyAccessedMemberTypes.NonPublicProperties |
DynamicallyAccessedMemberTypes.PublicConstructors |
#endif
DynamicallyAccessedMemberTypes.PublicNestedTypes |
DynamicallyAccessedMemberTypes.NonPublicMethods |
DynamicallyAccessedMemberTypes.PublicMethods
)]
T>(T message)
#else
public byte[] Serialize<T>(T message)
#endif
{
using var memoryStream = new MemoryStream();
_model.Serialize(memoryStream, message);
return memoryStream.ToArray();
}
}
}
@@ -1,47 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1;net8.0;net9.0;net10.0</TargetFrameworks>
</PropertyGroup>
<PropertyGroup>
<PackageId>CryptoExchange.Net.Protobuf</PackageId>
<Authors>JKorf</Authors>
<Description>Protobuf support for CryptoExchange.Net</Description>
<PackageVersion>10.0.1</PackageVersion>
<AssemblyVersion>10.0.1</AssemblyVersion>
<FileVersion>10.0.1</FileVersion>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<PackageTags>CryptoExchange;CryptoExchange.Net</PackageTags>
<RepositoryType>git</RepositoryType>
<RepositoryUrl>https://github.com/JKorf/CryptoExchange.Net.git</RepositoryUrl>
<PackageProjectUrl>https://github.com/JKorf/CryptoExchange.Net/tree/master/CryptoExchange.Net.Protobuf</PackageProjectUrl>
<NeutralLanguage>en</NeutralLanguage>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageIcon>icon.png</PackageIcon>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageReleaseNotes>https://github.com/JKorf/CryptoExchange.Net?tab=readme-ov-file#release-notes</PackageReleaseNotes>
<Nullable>enable</Nullable>
<LangVersion>12.0</LangVersion>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
</PropertyGroup>
<ItemGroup>
<None Include="..\CryptoExchange.Net\Icon\icon.png" Pack="true" PackagePath="\" />
<None Include="README.md" Pack="true" PackagePath="\" />
</ItemGroup>
<PropertyGroup Label="AOT" Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net7.0'))">
<IsAotCompatible>true</IsAotCompatible>
</PropertyGroup>
<PropertyGroup Label="Deterministic Build" Condition="'$(Configuration)' == 'Release'">
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
</PropertyGroup>
<PropertyGroup>
<DocumentationFile>CryptoExchange.Net.Protobuf.xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CryptoExchange.Net" Version="10.0.2" />
<PackageReference Include="protobuf-net" Version="3.2.56" />
</ItemGroup>
</Project>
@@ -1,128 +0,0 @@
<?xml version="1.0"?>
<doc>
<assembly>
<name>CryptoExchange.Net.Protobuf</name>
</assembly>
<members>
<member name="T:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1">
<summary>
System.Text.Json message accessor
</summary>
</member>
<member name="F:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1._intermediateType">
<summary>
The intermediate deserialization object
</summary>
</member>
<member name="F:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1._model">
<summary>
Runtime type model
</summary>
</member>
<member name="P:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.IsValid">
<inheritdoc />
</member>
<member name="P:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.OriginalDataAvailable">
<inheritdoc />
</member>
<member name="P:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.Underlying">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.#ctor(ProtoBuf.Meta.RuntimeTypeModel)">
<summary>
ctor
</summary>
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.GetNodeType">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.GetNodeType(CryptoExchange.Net.Converters.MessageParsing.MessagePath)">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.GetValue``1(CryptoExchange.Net.Converters.MessageParsing.MessagePath)">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.GetValues``1(CryptoExchange.Net.Converters.MessageParsing.MessagePath)">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.GetOriginalString">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.Clear">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.Deserialize(System.Type,System.Nullable{CryptoExchange.Net.Converters.MessageParsing.MessagePath})">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.Deserialize``1(System.Nullable{CryptoExchange.Net.Converters.MessageParsing.MessagePath})">
<inheritdoc />
</member>
<member name="T:CryptoExchange.Net.Converters.Protobuf.ProtobufStreamMessageAccessor`1">
<summary>
System.Text.Json stream message accessor
</summary>
</member>
<member name="P:CryptoExchange.Net.Converters.Protobuf.ProtobufStreamMessageAccessor`1.OriginalDataAvailable">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufStreamMessageAccessor`1.#ctor(ProtoBuf.Meta.RuntimeTypeModel)">
<summary>
ctor
</summary>
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufStreamMessageAccessor`1.Deserialize(System.Type,System.Nullable{CryptoExchange.Net.Converters.MessageParsing.MessagePath})">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufStreamMessageAccessor`1.Deserialize``1(System.Nullable{CryptoExchange.Net.Converters.MessageParsing.MessagePath})">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufStreamMessageAccessor`1.Read(System.IO.Stream,System.Boolean)">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufStreamMessageAccessor`1.GetOriginalString">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufStreamMessageAccessor`1.Clear">
<inheritdoc />
</member>
<member name="T:CryptoExchange.Net.Converters.Protobuf.ProtobufByteMessageAccessor`1">
<summary>
Protobuf byte message accessor
</summary>
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufByteMessageAccessor`1.#ctor(ProtoBuf.Meta.RuntimeTypeModel)">
<summary>
ctor
</summary>
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufByteMessageAccessor`1.Deserialize(System.Type,System.Nullable{CryptoExchange.Net.Converters.MessageParsing.MessagePath})">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufByteMessageAccessor`1.Deserialize``1(System.Nullable{CryptoExchange.Net.Converters.MessageParsing.MessagePath})">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufByteMessageAccessor`1.Read(System.ReadOnlyMemory{System.Byte})">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufByteMessageAccessor`1.GetOriginalString">
<inheritdoc />
</member>
<member name="P:CryptoExchange.Net.Converters.Protobuf.ProtobufByteMessageAccessor`1.OriginalDataAvailable">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufByteMessageAccessor`1.Clear">
<inheritdoc />
</member>
<member name="T:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageSerializer">
<inheritdoc />
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageSerializer.#ctor(ProtoBuf.Meta.RuntimeTypeModel)">
<summary>
ctor
</summary>
</member>
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageSerializer.Serialize``1(``0)">
<inheritdoc />
</member>
</members>
</doc>
-52
View File
@@ -1,52 +0,0 @@
# ![.CryptoExchange.Net](https://github.com/JKorf/CryptoExchange.Net/blob/ffcb7db8ff597c2f14982d68464015a748815580/CryptoExchange.Net/Icon/icon.png) CryptoExchange.Net.Proto
[![.NET](https://img.shields.io/github/actions/workflow/status/JKorf/CryptoExchange.Net/dotnet.yml?style=for-the-badge)](https://github.com/JKorf/CryptoExchange.Net/actions/workflows/dotnet.yml) [![Nuget downloads](https://img.shields.io/nuget/dt/CryptoExchange.Net.Protobuf.svg?style=for-the-badge)](https://www.nuget.org/packages/CryptoExchange.Net.Protobuf) ![License](https://img.shields.io/github/license/JKorf/CryptoExchange.Net?style=for-the-badge)
Protobuf support for CryptoExchange.Net.
## Release notes
* Version 10.0.1 - 16 Dec 2025
* Updated CryptoExchange.Net version to 10.0.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
* Version 10.0.0 - 16 Dec 2025
* Updated CryptoExchange.Net version to 10.0.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
* Version 9.13.0 - 10 Nov 2025
* Updated CryptoExchange.Net version to 9.13.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
* Version 9.12.0 - 03 Nov 2025
* Updated CryptoExchange.Net version to 9.12.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
* Version 9.11.1 - 30 Oct 2025
* Updated CryptoExchange.Net version to 9.11.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
* Version 9.11.0 - 30 Oct 2025
* Updated CryptoExchange.Net version to 9.11.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
* Version 9.10.0 - 15 Oct 2025
* Updated CryptoExchange.Net version to 9.10.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
* Version 9.9.0 - 06 Oct 2025
* Updated CryptoExchange.Net version to 9.9.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
* Version 9.8.0 - 30 Sep 2025
* Updated CryptoExchange.Net version to 9.8.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
* Version 9.7.0 - 01 Sep 2025
* Updated CryptoExchange.Net version to 9.7.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
* Version 9.6.0 - 25 Aug 2025
* Updated CryptoExchange.Net version to 9.6.0
* Version 9.5.0 - 19 Aug 2025
* Updated CryptoExchange.Net version to 9.5.0
* Version 9.4.0 - 04 Aug 2025
* Updated CryptoExchange.Net to version 9.4.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
* Updated protobuf-net package version to 3.2.56
* Version 9.3.0 - 23 Jul 2025
* Updated CryptoExchange.Net to version 9.3.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
* Version 9.2.0 - 14 Jul 2025
* Initial release
@@ -91,28 +91,25 @@ namespace CryptoExchange.Net.UnitTests
var evnt = new AsyncResetEvent(false, true);
var waiters = new List<Task<bool>>();
for(var i = 0; i < 10; i++)
for (var i = 0; i < 10; i++)
{
waiters.Add(evnt.WaitAsync());
}
List<bool>? results = null;
var resultsWaiter = Task.Run(async () =>
{
await Task.WhenAll(waiters);
results = waiters.Select(w => w.Result).ToList();
});
var remaining = waiters.ToList();
for(var i = 1; i <= 10; i++)
for (var i = 0; i < 10; i++)
{
evnt.Set();
await Task.Delay(1); // Wait for the continuation.
Assert.That(10 - i == waiters.Count(w => w.Status != TaskStatus.RanToCompletion));
var completed = await Task.WhenAny(remaining);
Assert.That(await completed, Is.True);
remaining.Remove(completed);
Assert.That(remaining.Count(w => w.IsCompleted), Is.Zero);
}
await resultsWaiter;
Assert.That(10 == results?.Count(r => r));
Assert.That(remaining, Is.Empty);
}
[Test]
+4 -120
View File
@@ -14,157 +14,41 @@ namespace CryptoExchange.Net.UnitTests
[Test]
public void TestBasicErrorCallResult()
{
var result = new CallResult(new ServerError("TestError", ErrorInfo.Unknown));
var result = CallResult.Fail(new ServerError("TestError", ErrorInfo.Unknown));
ClassicAssert.AreSame(result.Error!.ErrorCode, "TestError");
ClassicAssert.IsFalse(result);
ClassicAssert.IsFalse(result.Success);
}
[Test]
public void TestBasicSuccessCallResult()
{
var result = new CallResult(null);
var result = CallResult.Ok();
ClassicAssert.IsNull(result.Error);
Assert.That(result);
Assert.That(result.Success);
}
[Test]
public void TestCallResultError()
{
var result = new CallResult<object>(new ServerError("TestError", ErrorInfo.Unknown));
var result = CallResult.Fail<object>(new ServerError("TestError", ErrorInfo.Unknown));
ClassicAssert.AreSame(result.Error!.ErrorCode, "TestError");
ClassicAssert.IsNull(result.Data);
ClassicAssert.IsFalse(result);
ClassicAssert.IsFalse(result.Success);
}
[Test]
public void TestCallResultSuccess()
{
var result = new CallResult<object>(new object());
var result = CallResult.Ok<object>(new object());
ClassicAssert.IsNull(result.Error);
ClassicAssert.IsNotNull(result.Data);
Assert.That(result);
Assert.That(result.Success);
}
[Test]
public void TestCallResultSuccessAs()
{
var result = new CallResult<TestObjectResult>(new TestObjectResult());
var asResult = result.As<TestObject2>(result.Data.InnerData);
ClassicAssert.IsNull(asResult.Error);
ClassicAssert.IsNotNull(asResult.Data);
Assert.That(asResult.Data is not null);
Assert.That(asResult);
Assert.That(asResult.Success);
}
[Test]
public void TestCallResultErrorAs()
{
var result = new CallResult<TestObjectResult>(new ServerError("TestError", ErrorInfo.Unknown));
var asResult = result.As<TestObject2>(default);
ClassicAssert.IsNotNull(asResult.Error);
ClassicAssert.AreSame(asResult.Error!.ErrorCode, "TestError");
ClassicAssert.IsNull(asResult.Data);
ClassicAssert.IsFalse(asResult);
ClassicAssert.IsFalse(asResult.Success);
}
[Test]
public void TestCallResultErrorAsError()
{
var result = new CallResult<TestObjectResult>(new ServerError("TestError", ErrorInfo.Unknown));
var asResult = result.AsError<TestObject2>(new ServerError("TestError2", ErrorInfo.Unknown));
ClassicAssert.IsNotNull(asResult.Error);
ClassicAssert.AreSame(asResult.Error!.ErrorCode, "TestError2");
ClassicAssert.IsNull(asResult.Data);
ClassicAssert.IsFalse(asResult);
ClassicAssert.IsFalse(asResult.Success);
}
[Test]
public void TestWebCallResultErrorAsError()
{
var result = new WebCallResult<TestObjectResult>(new ServerError("TestError", ErrorInfo.Unknown));
var asResult = result.AsError<TestObject2>(new ServerError("TestError2", ErrorInfo.Unknown));
ClassicAssert.IsNotNull(asResult.Error);
ClassicAssert.AreSame(asResult.Error!.ErrorCode, "TestError2");
ClassicAssert.IsNull(asResult.Data);
ClassicAssert.IsFalse(asResult);
ClassicAssert.IsFalse(asResult.Success);
}
[Test]
public void TestWebCallResultSuccessAsError()
{
var result = new WebCallResult<TestObjectResult>(
System.Net.HttpStatusCode.OK,
HttpVersion.Version11,
new HttpResponseMessage().Headers,
TimeSpan.FromSeconds(1),
null,
"{}",
1,
"https://test.com/api",
null,
HttpMethod.Get,
new HttpRequestMessage().Headers,
ResultDataSource.Server,
new TestObjectResult(),
null);
var asResult = result.AsError<TestObject2>(new ServerError("TestError2", ErrorInfo.Unknown));
ClassicAssert.IsNotNull(asResult.Error);
Assert.That(asResult.Error!.ErrorCode == "TestError2");
Assert.That(asResult.ResponseStatusCode == System.Net.HttpStatusCode.OK);
Assert.That(asResult.ResponseTime == TimeSpan.FromSeconds(1));
Assert.That(asResult.RequestUrl == "https://test.com/api");
Assert.That(asResult.RequestMethod == HttpMethod.Get);
ClassicAssert.IsNull(asResult.Data);
ClassicAssert.IsFalse(asResult);
ClassicAssert.IsFalse(asResult.Success);
}
[Test]
public void TestWebCallResultSuccessAsSuccess()
{
var result = new WebCallResult<TestObjectResult>(
System.Net.HttpStatusCode.OK,
HttpVersion.Version11,
new HttpResponseMessage().Headers,
TimeSpan.FromSeconds(1),
null,
"{}",
1,
"https://test.com/api",
null,
HttpMethod.Get,
new HttpRequestMessage().Headers,
ResultDataSource.Server,
new TestObjectResult(),
null);
var asResult = result.As<TestObject2>(result.Data.InnerData);
ClassicAssert.IsNull(asResult.Error);
Assert.That(asResult.ResponseStatusCode == System.Net.HttpStatusCode.OK);
Assert.That(asResult.ResponseTime == TimeSpan.FromSeconds(1));
Assert.That(asResult.RequestUrl == "https://test.com/api");
Assert.That(asResult.RequestMethod == HttpMethod.Get);
ClassicAssert.IsNotNull(asResult.Data);
Assert.That(asResult);
Assert.That(asResult.Success);
}
}
public class TestObjectResult
@@ -134,7 +134,7 @@ namespace CryptoExchange.Net.UnitTests.ClientTests
client.ApiClient1.SetParameterPosition(httpMethod, pos);
client.ApiClient1.SetNextResponse("{}", System.Net.HttpStatusCode.OK);
var result = await client.ApiClient1.GetResponseAsync<TestObject>(httpMethod, new ParameterCollection
var result = await client.ApiClient1.GetResponseAsync<TestObject>(httpMethod, new Parameters(new ParameterSerializationSettings())
{
{ "TestParam1", "Value1" },
{ "TestParam2", 2 },
@@ -110,7 +110,7 @@ namespace CryptoExchange.Net.UnitTests.ClientTests
var result = await client.ApiClient1.SubscribeToUpdatesAsync<TestObject>(x => {}, false, default);
// act
await client.UnsubscribeAsync(result.Data);
await client.UnsubscribeAsync(result.Data!);
// assert
Assert.That(socket.Connected == false);
@@ -138,6 +138,68 @@ namespace CryptoExchange.Net.UnitTests.ClientTests
Assert.That(socket2.Connected == false);
}
[TestCase()]
public async Task BatchedSubscription_Should_NotExceedIndividualCombineTarget()
{
// arrange
var client = new TestSocketClient(options =>
{
options.SocketSubscriptionsCombineTarget = 10;
options.SocketIndividualSubscriptionCombineTarget = 10;
});
TestHelpers.ConfigureSocketClient(client, "wss://localhost");
// act
await client.ApiClient1.SubscribeToUpdatesAsync<TestObject>(x => { }, false, default, individualSubscriptionCount: 6);
TestHelpers.ConfigureSocketClient(client, "wss://localhost");
await client.ApiClient1.SubscribeToUpdatesAsync<TestObject>(x => { }, false, default, individualSubscriptionCount: 6);
// assert
Assert.That(client.ApiClient1._socketConnections.Count == 2);
Assert.That(client.ApiClient1._socketConnections.Values.All(connection => connection.Subscriptions.Sum(subscription => subscription.IndividualSubscriptionCount) <= 10));
}
[TestCase()]
public async Task BatchedSubscription_FullIndividualConnection_Should_NotPreventEligibleConnectionReuse()
{
// arrange
var client = new TestSocketClient(options =>
{
options.SocketSubscriptionsCombineTarget = 5;
options.SocketIndividualSubscriptionCombineTarget = 10;
});
TestHelpers.ConfigureSocketClient(client, "wss://localhost");
await client.ApiClient1.SubscribeToUpdatesAsync<TestObject>(x => { }, false, default);
await client.ApiClient1.SubscribeToUpdatesAsync<TestObject>(x => { }, false, default);
TestHelpers.ConfigureSocketClient(client, "wss://localhost");
await client.ApiClient1.SubscribeToUpdatesAsync<TestObject>(x => { }, false, default, individualSubscriptionCount: 10);
TestHelpers.ConfigureSocketClient(client, "wss://localhost");
// act
await client.ApiClient1.SubscribeToUpdatesAsync<TestObject>(x => { }, false, default);
// assert
Assert.That(
client.ApiClient1._socketConnections.Count,
Is.EqualTo(2),
"The eligible connection should be reused instead of opening a new connection after selecting a full individual-subscription connection");
var fullConnection = client.ApiClient1._socketConnections.Values
.Single(connection => connection.Subscriptions.Sum(subscription => subscription.IndividualSubscriptionCount) == 10);
Assert.That(
fullConnection.UserSubscriptionCount,
Is.EqualTo(1),
"The full connection should not receive the normal subscription");
var eligibleConnection = client.ApiClient1._socketConnections.Values.Single(connection => connection != fullConnection);
Assert.That(
eligibleConnection.UserSubscriptionCount,
Is.EqualTo(3),
"The existing eligible connection should receive the normal subscription");
}
[TestCase()]
public async Task ErrorResponse_ShouldNot_ConfirmSubscription()
{
@@ -25,6 +25,17 @@ namespace CryptoExchange.Net.UnitTests.ConverterTests
Assert.That(output!.Value == expected);
}
[TestCase(1, true)]
[TestCase(2, true)]
[TestCase(0, false)]
[TestCase(-1, false)]
public void TestBoolConverterInts(int value, bool? expected)
{
var val = $"{value}";
var output = JsonSerializer.Deserialize<STJBoolObject>($"{{ \"Value\": {val} }}", SerializerOptions.WithConverters(new TestSerializerContext()));
Assert.That(output!.Value == expected);
}
[TestCase("1", true)]
[TestCase("true", true)]
[TestCase("yes", true)]
@@ -15,6 +15,7 @@ namespace CryptoExchange.Net.UnitTests.ConverterTests
[TestCase("1620777600000")]
[TestCase("2021-05-12T00:00:00.000Z")]
[TestCase("2021-05-12T00:00:00.000000000Z")]
[TestCase("2021-05-12 00:00:00.000000+00:00:00")]
[TestCase("0.000000", true)]
[TestCase("0", true)]
[TestCase("", true)]
@@ -37,8 +37,8 @@ namespace CryptoExchange.Net.UnitTests
var symbols = CreateTestSymbols();
// act
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
var hasCached = ExchangeSymbolCache.HasCached(topicId);
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
var hasCached = ExchangeSymbolCache.HasCached(topicId, "Env", null);
// assert
Assert.That(hasCached, Is.True);
@@ -52,14 +52,14 @@ namespace CryptoExchange.Net.UnitTests
var symbols = CreateTestSymbols();
// act
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
// assert
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "BTCUSDT"), Is.True);
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "ETHUSDT"), Is.True);
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "BTCEUR"), Is.True);
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "ETHBTC"), Is.True);
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "XRPUSDT"), Is.True);
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, "BTCUSDT"), Is.True);
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, "ETHUSDT"), Is.True);
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, "BTCEUR"), Is.True);
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, "ETHBTC"), Is.True);
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, "XRPUSDT"), Is.True);
}
[Test]
@@ -78,13 +78,13 @@ namespace CryptoExchange.Net.UnitTests
};
// act
ExchangeSymbolCache.UpdateSymbolInfo(topicId, initialSymbols);
ExchangeSymbolCache.UpdateSymbolInfo(topicId, updatedSymbols);
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, initialSymbols);
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, updatedSymbols);
// assert - should still have only the initial symbol since less than 60 minutes passed
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "BTCUSDT"), Is.True);
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, "BTCUSDT"), Is.True);
// The second update should not have been applied
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "ETHUSDT"), Is.False);
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, "ETHUSDT"), Is.False);
}
[Test]
@@ -95,8 +95,8 @@ namespace CryptoExchange.Net.UnitTests
var symbols = Array.Empty<SharedSpotSymbol>();
// act
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
var hasCached = ExchangeSymbolCache.HasCached(topicId);
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
var hasCached = ExchangeSymbolCache.HasCached(topicId, "Env", null);
// assert
Assert.That(hasCached, Is.False);
@@ -109,7 +109,7 @@ namespace CryptoExchange.Net.UnitTests
var nonExistentTopic = "NonExistent_" + Guid.NewGuid();
// act
var result = ExchangeSymbolCache.HasCached(nonExistentTopic);
var result = ExchangeSymbolCache.HasCached(nonExistentTopic, "Env", null);
// assert
Assert.That(result, Is.False);
@@ -121,10 +121,10 @@ namespace CryptoExchange.Net.UnitTests
// arrange
var topicId = "ExchangeWithData";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
// act
var result = ExchangeSymbolCache.HasCached(topicId);
var result = ExchangeSymbolCache.HasCached(topicId, "Env", null);
// assert
Assert.That(result, Is.True);
@@ -135,10 +135,10 @@ namespace CryptoExchange.Net.UnitTests
{
// arrange
var topicId = "ExchangeNoData";
ExchangeSymbolCache.UpdateSymbolInfo(topicId, Array.Empty<SharedSpotSymbol>());
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, Array.Empty<SharedSpotSymbol>());
// act
var result = ExchangeSymbolCache.HasCached(topicId);
var result = ExchangeSymbolCache.HasCached(topicId, "Env", null);
// assert
Assert.That(result, Is.False);
@@ -150,10 +150,10 @@ namespace CryptoExchange.Net.UnitTests
// arrange
var topicId = "ExchangeSupports";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
// act
var result = ExchangeSymbolCache.SupportsSymbol(topicId, "BTCUSDT");
var result = ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, "BTCUSDT");
// assert
Assert.That(result, Is.True);
@@ -165,10 +165,10 @@ namespace CryptoExchange.Net.UnitTests
// arrange
var topicId = "ExchangeNoSupport";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
// act
var result = ExchangeSymbolCache.SupportsSymbol(topicId, "LINKUSDT");
var result = ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, "LINKUSDT");
// assert
Assert.That(result, Is.False);
@@ -181,7 +181,7 @@ namespace CryptoExchange.Net.UnitTests
var nonExistentTopic = "NonExistent_" + Guid.NewGuid();
// act
var result = ExchangeSymbolCache.SupportsSymbol(nonExistentTopic, "BTCUSDT");
var result = ExchangeSymbolCache.SupportsSymbol(nonExistentTopic, "Env", null, "BTCUSDT");
// assert
Assert.That(result, Is.False);
@@ -193,11 +193,11 @@ namespace CryptoExchange.Net.UnitTests
// arrange
var topicId = "ExchangeSharedSymbol";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
var sharedSymbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
// act
var result = ExchangeSymbolCache.SupportsSymbol(topicId, sharedSymbol);
var result = ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, sharedSymbol);
// assert
Assert.That(result, Is.True);
@@ -209,11 +209,11 @@ namespace CryptoExchange.Net.UnitTests
// arrange
var topicId = "ExchangeNoSharedSymbol";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
var sharedSymbol = new SharedSymbol(TradingMode.Spot, "LINK", "USDT");
// act
var result = ExchangeSymbolCache.SupportsSymbol(topicId, sharedSymbol);
var result = ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, sharedSymbol);
// assert
Assert.That(result, Is.False);
@@ -225,11 +225,11 @@ namespace CryptoExchange.Net.UnitTests
// arrange
var topicId = "ExchangeDifferentMode";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
var sharedSymbol = new SharedSymbol(TradingMode.PerpetualLinear, "BTC", "USDT");
// act
var result = ExchangeSymbolCache.SupportsSymbol(topicId, sharedSymbol);
var result = ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, sharedSymbol);
// assert
Assert.That(result, Is.False);
@@ -243,7 +243,7 @@ namespace CryptoExchange.Net.UnitTests
var sharedSymbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
// act
var result = ExchangeSymbolCache.SupportsSymbol(nonExistentTopic, sharedSymbol);
var result = ExchangeSymbolCache.SupportsSymbol(nonExistentTopic, "Env", null, sharedSymbol);
// assert
Assert.That(result, Is.False);
@@ -255,10 +255,10 @@ namespace CryptoExchange.Net.UnitTests
// arrange
var topicId = "ExchangeBaseAsset";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
// act
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "BTC");
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "Env", null, "BTC");
// assert
Assert.That(result, Is.Not.Null);
@@ -273,10 +273,10 @@ namespace CryptoExchange.Net.UnitTests
// arrange
var topicId = "ExchangeCaseInsensitive";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
// act
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "btc");
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "Env", null, "btc");
// assert
Assert.That(result, Is.Not.Null);
@@ -289,10 +289,10 @@ namespace CryptoExchange.Net.UnitTests
// arrange
var topicId = "ExchangeNoBaseAsset";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
// act
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "LINK");
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "Env", null, "LINK");
// assert
Assert.That(result, Is.Not.Null);
@@ -306,7 +306,7 @@ namespace CryptoExchange.Net.UnitTests
var nonExistentTopic = "NonExistent_" + Guid.NewGuid();
// act
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(nonExistentTopic, "BTC");
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(nonExistentTopic, "Env", null, "BTC");
// assert
Assert.That(result, Is.Not.Null);
@@ -319,10 +319,10 @@ namespace CryptoExchange.Net.UnitTests
// arrange
var topicId = "ExchangeParse";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
// act
var result = ExchangeSymbolCache.ParseSymbol(topicId, "BTCUSDT");
var result = ExchangeSymbolCache.ParseSymbol(topicId, "Env", null, "BTCUSDT");
// assert
Assert.That(result, Is.Not.Null);
@@ -338,10 +338,10 @@ namespace CryptoExchange.Net.UnitTests
// arrange
var topicId = "ExchangeNoParse";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
// act
var result = ExchangeSymbolCache.ParseSymbol(topicId, "LINKUSDT");
var result = ExchangeSymbolCache.ParseSymbol(topicId, "Env", null, "LINKUSDT");
// assert
Assert.That(result, Is.Null);
@@ -353,10 +353,10 @@ namespace CryptoExchange.Net.UnitTests
// arrange
var topicId = "ExchangeNullSymbol";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
// act
var result = ExchangeSymbolCache.ParseSymbol(topicId, null);
var result = ExchangeSymbolCache.ParseSymbol(topicId, "Env", null, null);
// assert
Assert.That(result, Is.Null);
@@ -369,7 +369,7 @@ namespace CryptoExchange.Net.UnitTests
var nonExistentTopic = "NonExistent_" + Guid.NewGuid();
// act
var result = ExchangeSymbolCache.ParseSymbol(nonExistentTopic, "BTCUSDT");
var result = ExchangeSymbolCache.ParseSymbol(nonExistentTopic, "Env", null, "BTCUSDT");
// assert
Assert.That(result, Is.Null);
@@ -391,14 +391,14 @@ namespace CryptoExchange.Net.UnitTests
};
// act
ExchangeSymbolCache.UpdateSymbolInfo(topic1, symbols1);
ExchangeSymbolCache.UpdateSymbolInfo(topic2, symbols2);
ExchangeSymbolCache.UpdateSymbolInfo(topic1, "Env", null, symbols1);
ExchangeSymbolCache.UpdateSymbolInfo(topic2, "Env", null, symbols2);
// assert
Assert.That(ExchangeSymbolCache.SupportsSymbol(topic1, "BTCUSDT"), Is.True);
Assert.That(ExchangeSymbolCache.SupportsSymbol(topic1, "ETHUSDT"), Is.False);
Assert.That(ExchangeSymbolCache.SupportsSymbol(topic2, "ETHUSDT"), Is.True);
Assert.That(ExchangeSymbolCache.SupportsSymbol(topic2, "BTCUSDT"), Is.False);
Assert.That(ExchangeSymbolCache.SupportsSymbol(topic1, "Env", null, "BTCUSDT"), Is.True);
Assert.That(ExchangeSymbolCache.SupportsSymbol(topic1, "Env", null, "ETHUSDT"), Is.False);
Assert.That(ExchangeSymbolCache.SupportsSymbol(topic2, "Env", null, "ETHUSDT"), Is.True);
Assert.That(ExchangeSymbolCache.SupportsSymbol(topic2, "Env", null, "BTCUSDT"), Is.False);
}
[Test]
@@ -411,14 +411,14 @@ namespace CryptoExchange.Net.UnitTests
var allSymbols = spotSymbols.Concat(futuresSymbols).ToArray();
// act
ExchangeSymbolCache.UpdateSymbolInfo(topicId, allSymbols);
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, allSymbols);
// assert
var spotSymbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
var futuresSymbol = new SharedSymbol(TradingMode.PerpetualLinear, "BTC", "USDT");
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, spotSymbol), Is.True);
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, futuresSymbol), Is.True);
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, spotSymbol), Is.True);
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, futuresSymbol), Is.True);
}
[Test]
@@ -429,10 +429,10 @@ namespace CryptoExchange.Net.UnitTests
var spotSymbols = CreateTestSymbols();
var futuresSymbols = CreateFuturesSymbols();
var allSymbols = spotSymbols.Concat(futuresSymbols).ToArray();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, allSymbols);
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, allSymbols);
// act
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "BTC");
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "Env", null, "BTC");
// assert
Assert.That(result.Length, Is.GreaterThanOrEqualTo(2));
@@ -451,15 +451,119 @@ namespace CryptoExchange.Net.UnitTests
new SharedSpotSymbol("ETH", "BTC", "ETHBTC", true, TradingMode.Spot),
new SharedSpotSymbol("ETH", "EUR", "ETHEUR", true, TradingMode.Spot)
};
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
// act
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "ETH");
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "Env", null, "ETH");
// assert
Assert.That(result.Length, Is.EqualTo(3));
Assert.That(result.All(x => x.BaseAsset == "ETH"), Is.True);
}
[Test]
public void GetSymbolsForBaseAsset_WithDifferentEnvironments_Should_ReturnNone()
{
// arrange
var topicId = "Topic1";
var spotSymbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Live", null, spotSymbols);
// act
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "Test", null, "BTC");
// assert
Assert.That(result.Length, Is.EqualTo(0));
}
[Test]
public void GetSymbolsForBaseAsset_WithDifferentKey_Should_ReturnNone()
{
// arrange
var topicId = "Topic2";
var spotSymbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Live", "1", spotSymbols);
// act
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "Live", "2", "BTC");
// assert
Assert.That(result.Length, Is.EqualTo(0));
}
[Test]
public void GetSymbolsForBaseAsset_WithSetKey_Should_ReturnNone()
{
// arrange
var topicId = "Topic3";
var spotSymbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Live", null, spotSymbols);
// act
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "Live", "2", "BTC");
// assert
Assert.That(result.Length, Is.EqualTo(0));
}
[Test]
public void GetSymbolsForBaseAsset_WithNotSetKey_Should_ReturnNone()
{
// arrange
var topicId = "Topic4";
var spotSymbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Live", "2", spotSymbols);
// act
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "Live", null, "BTC");
// assert
Assert.That(result.Length, Is.EqualTo(2));
}
[Test]
public void ParseSymbol_WithDifferentKey_Should_ReturnNull()
{
// arrange
var topicId = "Topic5";
var spotSymbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Live", "1", spotSymbols);
// act
var result = ExchangeSymbolCache.ParseSymbol(topicId, "Live", "2", "BTCUSDT");
// assert
Assert.That(result, Is.Null);
}
[Test]
public void ParseSymbol_WithSetKey_Should_ReturnNull()
{
// arrange
var topicId = "Topic6";
var spotSymbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Live", null, spotSymbols);
// act
var result = ExchangeSymbolCache.ParseSymbol(topicId, "Live", "2", "BTCUSDT");
// assert
Assert.That(result, Is.Null);
}
[Test]
public void ParseSymbol_WithNotSetKey_Should_ReturnNull()
{
// arrange
var topicId = "Topic7";
var spotSymbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Live", "1", spotSymbols);
// act
var result = ExchangeSymbolCache.ParseSymbol(topicId, "Live", null, "BTCUSDT");
// assert
Assert.That(result, Is.Not.Null);
}
}
}
@@ -11,15 +11,15 @@ namespace CryptoExchange.Net.UnitTests.Implementations
{
public TestQuery(TestSocketMessage request, bool authenticated) : base(request, authenticated, 1)
{
MessageRouter = MessageRouter.CreateWithoutTopicFilter<TestSocketMessage>(request.Id.ToString(), HandleMessage);
MessageRouter = MessageRouter.CreateForQuery<TestSocketMessage>(request.Id.ToString(), HandleMessage);
}
private CallResult? HandleMessage(SocketConnection connection, DateTime time, string? arg3, TestSocketMessage message)
private CallResult<TestSocketMessage>? HandleMessage(SocketConnection connection, DateTime time, string? arg3, TestSocketMessage message)
{
if (message.Data != "OK")
return new CallResult(new ServerError(ErrorInfo.Unknown with { Message = message.Data }));
return CallResult.Fail<TestSocketMessage>(new ServerError(ErrorInfo.Unknown with { Message = message.Data }));
return CallResult.SuccessResult;
return CallResult.Ok(message);
}
}
}
@@ -3,6 +3,7 @@ using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
using CryptoExchange.Net.Converters.SystemTextJson;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.RateLimiting;
using CryptoExchange.Net.SharedApis;
using CryptoExchange.Net.Testing.Implementations;
using Microsoft.Extensions.Logging;
@@ -19,8 +20,8 @@ namespace CryptoExchange.Net.UnitTests.Implementations
{
protected override IRestMessageHandler MessageHandler { get; } = new TestRestMessageHandler();
public TestRestApiClient(ILogger logger, HttpClient? httpClient, TestRestOptions options)
: base(logger, httpClient, options.Environment.RestClientAddress, options, options.ExchangeOptions)
public TestRestApiClient(ILoggerFactory? loggerFactory, HttpClient? httpClient, TestRestOptions options)
: base(loggerFactory, "Test", httpClient, options.Environment.RestClientAddress, options, options.ExchangeOptions)
{
}
@@ -47,13 +48,14 @@ namespace CryptoExchange.Net.UnitTests.Implementations
RequestFactory = factory;
}
internal async Task<WebCallResult<T>> GetResponseAsync<T>(HttpMethod? httpMethod = null, ParameterCollection? collection = null)
internal async Task<HttpResult<T>> GetResponseAsync<T>(HttpMethod? httpMethod = null, Parameters? collection = null, RateLimitGate? rateLimitGate = null)
{
var definition = new RequestDefinition("/path", httpMethod ?? HttpMethod.Get)
var definition = new RequestDefinition(BaseAddress, "/path", httpMethod ?? HttpMethod.Get)
{
Weight = 0
Weight = rateLimitGate == null ? 0 : 1,
RateLimitGate = rateLimitGate
};
return await SendAsync<T>(BaseAddress, definition, collection ?? new ParameterCollection(), default);
return await SendAsync<T>(definition, collection ?? new Parameters(new ParameterSerializationSettings()), default);
}
internal void SetParameterPosition(HttpMethod httpMethod, HttpMethodParameterPosition pos)
@@ -20,8 +20,8 @@ namespace CryptoExchange.Net.UnitTests.Implementations
{
Initialize(options.Value);
ApiClient1 = AddApiClient(new TestRestApiClient(_logger, httpClient, options.Value));
ApiClient2 = AddApiClient(new TestRestApiClient(_logger, httpClient, options.Value));
ApiClient1 = AddApiClient(new TestRestApiClient(loggerFactory, httpClient, options.Value));
ApiClient2 = AddApiClient(new TestRestApiClient(loggerFactory, httpClient, options.Value));
}
}
}
@@ -1,4 +1,5 @@
using CryptoExchange.Net.UnitTests.ConverterTests;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.UnitTests.ConverterTests;
using CryptoExchange.Net.UnitTests.Implementations;
using System.Collections.Generic;
using System.Text.Json.Serialization;
@@ -11,6 +12,7 @@ namespace CryptoExchange.Net.UnitTests
[JsonSerializable(typeof(IDictionary<string, string>))]
[JsonSerializable(typeof(Dictionary<string, object>))]
[JsonSerializable(typeof(IDictionary<string, object>))]
[JsonSerializable(typeof(Parameters))]
[JsonSerializable(typeof(TestObject))]
[JsonSerializable(typeof(TestSocketMessage))]
@@ -17,13 +17,13 @@ namespace CryptoExchange.Net.UnitTests.Implementations
{
internal class TestSocketApiClient : SocketApiClient<TestEnvironment, TestAuthenticationProvider, TestCredentials>
{
public TestSocketApiClient(ILogger logger, TestSocketOptions options)
: base(logger, options.Environment.SocketClientAddress, options, options.ExchangeOptions)
public TestSocketApiClient(ILoggerFactory? loggerFactory, TestSocketOptions options)
: base(loggerFactory, "Test", options.Environment.SocketClientAddress, options, options.ExchangeOptions)
{
}
public TestSocketApiClient(ILogger logger, HttpClient httpClient, string baseAddress, TestSocketOptions options, SocketApiOptions apiOptions)
: base(logger, baseAddress, options, apiOptions)
public TestSocketApiClient(ILoggerFactory? loggerFactory, HttpClient httpClient, string baseAddress, TestSocketOptions options, SocketApiOptions apiOptions)
: base(loggerFactory, "Test", baseAddress, options, apiOptions)
{
}
@@ -36,9 +36,13 @@ namespace CryptoExchange.Net.UnitTests.Implementations
protected override TestAuthenticationProvider CreateAuthenticationProvider(TestCredentials credentials) =>
new TestAuthenticationProvider(credentials);
public async Task<CallResult<UpdateSubscription>> SubscribeToUpdatesAsync<T>(Action<DataEvent<T>> handler, bool subQuery, CancellationToken ct)
public async Task<WebSocketResult<UpdateSubscription>> SubscribeToUpdatesAsync<T>(Action<DataEvent<T>> handler, bool subQuery, CancellationToken ct, int individualSubscriptionCount = 1)
{
return await base.SubscribeAsync(new TestSubscription<T>(_logger, handler, subQuery, false), ct);
var subscription = new TestSubscription<T>(_logger, handler, subQuery, false)
{
IndividualSubscriptionCount = individualSubscriptionCount
};
return await base.SubscribeAsync(subscription, ct);
}
}
}
@@ -19,8 +19,8 @@ namespace CryptoExchange.Net.UnitTests.Implementations
{
Initialize(options.Value);
ApiClient1 = AddApiClient(new TestSocketApiClient(_logger, options.Value));
ApiClient2 = AddApiClient(new TestSocketApiClient(_logger, options.Value));
ApiClient1 = AddApiClient(new TestSocketApiClient(loggerFactory, options.Value));
ApiClient2 = AddApiClient(new TestSocketApiClient(loggerFactory, options.Value));
}
}
}
@@ -21,7 +21,7 @@ namespace CryptoExchange.Net.UnitTests.Implementations
_handler = handler;
_subQuery = subQuery;
MessageRouter = MessageRouter.CreateWithoutTopicFilter<T>("test", HandleUpdate);
MessageRouter = MessageRouter.CreateForEvent<T>("test", HandleUpdate);
}
protected override Query? GetSubQuery(SocketConnection connection)
@@ -44,7 +44,7 @@ namespace CryptoExchange.Net.UnitTests.Implementations
private CallResult? HandleUpdate(SocketConnection connection, DateTime time, string? originalData, T data)
{
_handler(new DataEvent<T>("Test", data, time, originalData));
return CallResult.SuccessResult;
return CallResult.Ok();
}
}
}
@@ -0,0 +1,136 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Errors;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.Sockets.Default;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace CryptoExchange.Net.UnitTests
{
[TestFixture]
public class ManualUpdateSubscriptionTests
{
[Test]
public void Constructor_Should_CreateSubscribedVirtualSubscription()
{
var controller = new ManualUpdateSubscription(socketId: 12);
Assert.That(controller.Subscription.SocketId, Is.EqualTo(12));
Assert.That(controller.Subscription.Id, Is.GreaterThan(0));
Assert.That(controller.Subscription.SocketStatus, Is.EqualTo(SocketStatus.Connected));
Assert.That(controller.Subscription.SubscriptionStatus, Is.EqualTo(SubscriptionStatus.Subscribed));
Assert.That(controller.Subscription.LastReceiveTime, Is.Null);
}
[Test]
public void StateChanges_Should_BeVisibleOnSubscription()
{
var controller = new ManualUpdateSubscription();
var timestamp = new DateTime(2026, 8, 5, 12, 0, 0, DateTimeKind.Utc);
var statuses = new List<SubscriptionStatus>();
controller.Subscription.SubscriptionStatusChanged += statuses.Add;
controller.SetLastReceiveTime(timestamp);
controller.SetSocketStatus(SocketStatus.Reconnecting);
controller.SetSubscriptionStatus(SubscriptionStatus.Subscribing);
controller.SetSubscriptionStatus(SubscriptionStatus.Subscribed);
Assert.That(controller.Subscription.LastReceiveTime, Is.EqualTo(timestamp));
Assert.That(controller.Subscription.SocketStatus, Is.EqualTo(SocketStatus.Reconnecting));
Assert.That(controller.Subscription.SubscriptionStatus, Is.EqualTo(SubscriptionStatus.Subscribed));
Assert.That(statuses, Is.EqualTo(new[]
{
SubscriptionStatus.Subscribing,
SubscriptionStatus.Subscribed
}));
}
[Test]
public void LifecycleMethods_Should_InvokeSubscriptionEvents()
{
var controller = new ManualUpdateSubscription();
var error = new ServerError("Test error", ErrorInfo.Unknown);
var exception = new InvalidOperationException("Test exception");
var disconnectedPeriod = TimeSpan.FromMinutes(2);
var lost = 0;
var restored = TimeSpan.Zero;
Error? resubscribeError = null;
var paused = 0;
var unpaused = 0;
Exception? receivedException = null;
controller.Subscription.ConnectionLost += () => lost++;
controller.Subscription.ConnectionRestored += x => restored = x;
controller.Subscription.ResubscribingFailed += x => resubscribeError = x;
controller.Subscription.ActivityPaused += () => paused++;
controller.Subscription.ActivityUnpaused += () => unpaused++;
controller.Subscription.Exception += x => receivedException = x;
controller.InvokeConnectionLost();
controller.InvokeConnectionRestored(disconnectedPeriod);
controller.InvokeResubscribingFailed(error);
controller.InvokeActivityPaused();
controller.InvokeActivityUnpaused();
controller.InvokeException(exception);
Assert.That(lost, Is.EqualTo(1));
Assert.That(restored, Is.EqualTo(disconnectedPeriod));
Assert.That(resubscribeError, Is.SameAs(error));
Assert.That(paused, Is.EqualTo(1));
Assert.That(unpaused, Is.EqualTo(1));
Assert.That(receivedException, Is.SameAs(exception));
}
[Test]
public void InvokeConnectionClosed_Should_CloseAndOnlyInvokeOnce()
{
var controller = new ManualUpdateSubscription();
var closed = 0;
controller.Subscription.ConnectionClosed += () => closed++;
controller.InvokeConnectionClosed();
controller.InvokeConnectionClosed();
Assert.That(closed, Is.EqualTo(1));
Assert.That(controller.Subscription.SocketStatus, Is.EqualTo(SocketStatus.Closed));
Assert.That(controller.Subscription.SubscriptionStatus, Is.EqualTo(SubscriptionStatus.Closed));
}
[Test]
public async Task SubscriptionOperations_Should_InvokeCallbacks()
{
var closes = 0;
var reconnects = 0;
var resubscribes = 0;
var controller = new ManualUpdateSubscription(
closeAsync: () =>
{
closes++;
return Task.CompletedTask;
},
reconnectAsync: () =>
{
reconnects++;
return Task.CompletedTask;
},
resubscribeAsync: () =>
{
resubscribes++;
return Task.FromResult(CallResult.Ok());
});
await controller.Subscription.ReconnectAsync();
var resubscribeResult = await controller.Subscription.ResubscribeAsync();
await controller.Subscription.CloseAsync();
await controller.Subscription.CloseAsync();
Assert.That(reconnects, Is.EqualTo(1));
Assert.That(resubscribes, Is.EqualTo(1));
Assert.That(resubscribeResult.Success, Is.True);
Assert.That(closes, Is.EqualTo(1));
Assert.That(controller.Subscription.SubscriptionStatus, Is.EqualTo(SubscriptionStatus.Closed));
}
}
}
@@ -12,319 +12,248 @@ namespace CryptoExchange.Net.UnitTests
[Test]
public void AddingBasicValue_SetValueCorrectly()
{
var parameters = new ParameterCollection();
var parameters = new Parameters(new ParameterSerializationSettings());
parameters.Add("test", "value");
Assert.That(parameters["test"], Is.EqualTo("value"));
}
[Test]
public void AddingBasicNullValue_ThrowsException()
{
var parameters = new ParameterCollection();
Assert.Throws<ArgumentNullException>(() => parameters.Add("test", null!));
}
[Test]
public void AddingOptionalBasicValue_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddOptional("test", "value");
Assert.That(parameters["test"], Is.EqualTo("value"));
}
[Test]
public void AddingOptionalBasicNullValue_DoesntSetValue()
{
var parameters = new ParameterCollection();
parameters.AddOptional("test", null);
var parameters = new Parameters(new ParameterSerializationSettings());
parameters.Add("test", null);
Assert.That(parameters.ContainsKey("test"), Is.False);
}
[Test]
public void AddingDecimalValueAsString_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddString("test", 0.1m);
var parameters = new Parameters(new ParameterSerializationSettings());
parameters.Add("test", 0.1m, DecimalSerialization.String);
Assert.That(parameters["test"], Is.EqualTo("0.1"));
}
[Test]
public void AddingOptionalDecimalValueAsString_SetValueCorrectly()
public void AddingDecimalValueAsString2_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddOptionalString("test", 0.1m);
var parameters = new Parameters(new ParameterSerializationSettings()
{
Decimal = DecimalSerialization.String
});
parameters.Add("test", 0.1m);
Assert.That(parameters["test"], Is.EqualTo("0.1"));
}
[Test]
public void AddingOptionalDecimalNullValueAsString_DoesntSetValue()
{
var parameters = new ParameterCollection();
parameters.AddOptionalString("test", (decimal?)null);
Assert.That(parameters.ContainsKey("test"), Is.False);
}
[Test]
public void AddingIntValueAsString_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddString("test", 1);
Assert.That(parameters["test"], Is.EqualTo("1"));
}
[Test]
public void AddingOptionalIntValueAsString_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddOptionalString("test", 1);
Assert.That(parameters["test"], Is.EqualTo("1"));
}
[Test]
public void AddingOptionalIntNullValueAsString_DoesntSetValue()
{
var parameters = new ParameterCollection();
parameters.AddOptionalString("test", (int?)null);
var parameters = new Parameters(new ParameterSerializationSettings());
parameters.Add("test", (int?)null);
Assert.That(parameters.ContainsKey("test"), Is.False);
}
[Test]
public void AddingLongValueAsString_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddString("test", 1L);
var parameters = new Parameters(new ParameterSerializationSettings());
parameters.Add("test", 1L, IntegerSerialization.String);
Assert.That(parameters["test"], Is.EqualTo("1"));
}
[Test]
public void AddingOptionalLongValueAsString_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddOptionalString("test", 1L);
var parameters = new Parameters(new ParameterSerializationSettings()
{
Integer = IntegerSerialization.String
});
parameters.Add("test", 1L);
Assert.That(parameters["test"], Is.EqualTo("1"));
}
[Test]
public void AddingOptionalLongNullValueAsString_DoesntSetValue()
{
var parameters = new ParameterCollection();
parameters.AddOptionalString("test", (long?)null);
var parameters = new Parameters(new ParameterSerializationSettings());
parameters.Add("test", (long?)null);
Assert.That(parameters.ContainsKey("test"), Is.False);
}
[Test]
public void AddingMillisecondTimestamp_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddMilliseconds("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc));
var parameters = new Parameters(new ParameterSerializationSettings());
parameters.Add("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc), DateTimeSerialization.MillisecondsNumber);
Assert.That(parameters["test"], Is.EqualTo(1735689600000));
}
[Test]
public void AddingOptionalMillisecondTimestamp_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddOptionalMilliseconds("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc));
var parameters = new Parameters(new ParameterSerializationSettings()
{
DateTimes = DateTimeSerialization.MillisecondsNumber
});
parameters.Add("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc));
Assert.That(parameters["test"], Is.EqualTo(1735689600000));
}
[Test]
public void AddingOptionalMillisecondNullValue_DoesntSetValue()
{
var parameters = new ParameterCollection();
parameters.AddOptionalMilliseconds("test", null);
var parameters = new Parameters(new ParameterSerializationSettings());
parameters.Add("test", (DateTime?)null);
Assert.That(parameters.ContainsKey("test"), Is.False);
}
[Test]
public void AddingMillisecondTimestampString_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddMillisecondsString("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc));
var parameters = new Parameters(new ParameterSerializationSettings());
parameters.Add("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc), DateTimeSerialization.MillisecondsString);
Assert.That(parameters["test"], Is.EqualTo("1735689600000"));
}
[Test]
public void AddingOptionalMillisecondTimestampString_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddOptionalMillisecondsString("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc));
var parameters = new Parameters(new ParameterSerializationSettings()
{
DateTimes = DateTimeSerialization.MillisecondsString
});
parameters.Add("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc));
Assert.That(parameters["test"], Is.EqualTo("1735689600000"));
}
[Test]
public void AddingOptionalMillisecondStringNullValue_DoesntSetValue()
{
var parameters = new ParameterCollection();
parameters.AddOptionalMillisecondsString("test", null);
Assert.That(parameters.ContainsKey("test"), Is.False);
}
[Test]
public void AddingSecondTimestamp_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddSeconds("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc));
var parameters = new Parameters(new ParameterSerializationSettings());
parameters.Add("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc), DateTimeSerialization.SecondsNumber);
Assert.That(parameters["test"], Is.EqualTo(1735689600));
}
[Test]
public void AddingOptionalSecondTimestamp_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddOptionalSeconds("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc));
var parameters = new Parameters(new ParameterSerializationSettings()
{
DateTimes = DateTimeSerialization.SecondsNumber
});
parameters.Add("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc));
Assert.That(parameters["test"], Is.EqualTo(1735689600));
}
[Test]
public void AddingSecondNullValue_DoesntSetValue()
{
var parameters = new ParameterCollection();
parameters.AddOptionalSeconds("test", null);
Assert.That(parameters.ContainsKey("test"), Is.False);
}
[Test]
public void AddingSecondTimestampString_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddSecondsString("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc));
var parameters = new Parameters(new ParameterSerializationSettings());
parameters.Add("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc), DateTimeSerialization.SecondsString);
Assert.That(parameters["test"], Is.EqualTo("1735689600"));
}
[Test]
public void AddingOptionalSecondTimestampString_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddOptionalSecondsString("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc));
var parameters = new Parameters(new ParameterSerializationSettings()
{
DateTimes = DateTimeSerialization.SecondsString
});
parameters.Add("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc));
Assert.That(parameters["test"], Is.EqualTo("1735689600"));
}
[Test]
public void AddingSecondStringNullValue_DoesntSetValue()
{
var parameters = new ParameterCollection();
parameters.AddOptionalSecondsString("test", null);
Assert.That(parameters.ContainsKey("test"), Is.False);
}
[Test]
public void AddingEnum_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddEnum("test", TestEnum.Two);
var parameters = new Parameters(new ParameterSerializationSettings());
parameters.Add("test", TestEnum.Two);
Assert.That(parameters["test"], Is.EqualTo("2"));
}
[Test]
public void AddingOptionalEnum_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddOptionalEnum("test", (TestEnum?)TestEnum.Two);
var parameters = new Parameters(new ParameterSerializationSettings());
parameters.Add("test", (TestEnum?)TestEnum.Two);
Assert.That(parameters["test"], Is.EqualTo("2"));
}
[Test]
public void AddingOptionalEnumNullValue_DoesntSetValue()
{
var parameters = new ParameterCollection();
parameters.AddOptionalEnum("test", (TestEnum?)null);
var parameters = new Parameters(new ParameterSerializationSettings());
parameters.Add("test", (TestEnum?)null);
Assert.That(parameters.ContainsKey("test"), Is.False);
}
[Test]
public void AddingEnumAsInt_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddEnumAsInt("test", TestEnum.Two);
var parameters = new Parameters(new ParameterSerializationSettings());
parameters.Add("test", TestEnum.Two, EnumSerialization.Number);
Assert.That(parameters["test"], Is.EqualTo(2));
}
[Test]
public void AddingOptionalEnumAsInt_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddOptionalEnumAsInt("test", (TestEnum?)TestEnum.Two);
var parameters = new Parameters(new ParameterSerializationSettings()
{
Enum = EnumSerialization.Number
});
parameters.Add("test", TestEnum.Two);
Assert.That(parameters["test"], Is.EqualTo(2));
}
[Test]
public void AddingOptionalEnumAsIntNullValue_DoesntSetValue()
{
var parameters = new ParameterCollection();
parameters.AddOptionalEnumAsInt("test", (TestEnum?)null);
Assert.That(parameters.ContainsKey("test"), Is.False);
}
[Test]
public void AddingCommaSeparated_SetValueCorrectly()
{
var parameters = new ParameterCollection();
var parameters = new Parameters(new ParameterSerializationSettings());
parameters.AddCommaSeparated("test", ["1", "2"]);
Assert.That(parameters["test"], Is.EqualTo("1,2"));
}
[Test]
public void AddingOptionalCommaSeparated_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddOptionalCommaSeparated("test", ["1", "2"]);
Assert.That(parameters["test"], Is.EqualTo("1,2"));
}
[Test]
public void AddingOptionalCommaSeparatedNullValue_DoesntSetValue()
{
var parameters = new ParameterCollection();
parameters.AddOptionalCommaSeparated("test", (string[]?)null);
var parameters = new Parameters(new ParameterSerializationSettings());
parameters.AddCommaSeparated("test", (string[]?)null);
Assert.That(parameters.ContainsKey("test"), Is.False);
}
[Test]
public void AddingCommaSeparatedEnum_SetValueCorrectly()
{
var parameters = new ParameterCollection();
var parameters = new Parameters(new ParameterSerializationSettings());
parameters.AddCommaSeparated("test", [TestEnum.Two, TestEnum.One]);
Assert.That(parameters["test"], Is.EqualTo("2,1"));
}
[Test]
public void AddingOptionalCommaSeparatedEnum_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddOptionalCommaSeparated("test", [TestEnum.Two, TestEnum.One]);
Assert.That(parameters["test"], Is.EqualTo("2,1"));
}
[Test]
public void AddingOptionalCommaSeparatedEnumNullValue_DoesntSetValue()
{
var parameters = new ParameterCollection();
parameters.AddOptionalCommaSeparated("test", (TestEnum[]?)null);
Assert.That(parameters.ContainsKey("test"), Is.False);
}
[Test]
public void AddingBoolString_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddBoolString("test", true);
var parameters = new Parameters(new ParameterSerializationSettings());
parameters.Add("test", true, BoolSerialization.String);
Assert.That(parameters["test"], Is.EqualTo("true"));
}
[Test]
public void AddingOptionalBoolString_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddOptionalBoolString("test", true);
var parameters = new Parameters(new ParameterSerializationSettings()
{
Bool = BoolSerialization.String
});
parameters.Add("test", true);
Assert.That(parameters["test"], Is.EqualTo("true"));
}
[Test]
public void AddingOptionalBoolStringNullValue_DoesntSetValue()
{
var parameters = new ParameterCollection();
parameters.AddOptionalBoolString("test", null);
var parameters = new Parameters(new ParameterSerializationSettings());
parameters.Add("test", null);
Assert.That(parameters.ContainsKey("test"), Is.False);
}
}
+78 -42
View File
@@ -3,11 +3,13 @@ using CryptoExchange.Net.RateLimiting;
using CryptoExchange.Net.RateLimiting.Filters;
using CryptoExchange.Net.RateLimiting.Guards;
using CryptoExchange.Net.RateLimiting.Interfaces;
using CryptoExchange.Net.UnitTests.Implementations;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
@@ -27,16 +29,16 @@ namespace CryptoExchange.Net.UnitTests
var triggered = false;
rateLimiter.RateLimitTriggered += (x) => { triggered = true; };
var requestDefinition = new RequestDefinition("/sapi/v1/system/status", HttpMethod.Get);
var requestDefinition = new RequestDefinition("https://test.com", "/sapi/v1/system/status", HttpMethod.Get);
for (var i = 0; i < requests + 1; i++)
{
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "123", 1, RateLimitingBehaviour.Wait, null, default);
Assert.That(i == requests ? triggered : !triggered);
}
triggered = false;
await Task.Delay((int)Math.Round(perSeconds * 1000) + 10);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "123", 1, RateLimitingBehaviour.Wait, null, default);
Assert.That(!triggered);
}
@@ -50,13 +52,13 @@ namespace CryptoExchange.Net.UnitTests
var rateLimiter = new RateLimitGate("Test");
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new PathStartFilter("/sapi/"), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
var requestDefinition = new RequestDefinition(endpoint, HttpMethod.Get);
var requestDefinition = new RequestDefinition("https://test.com", endpoint, HttpMethod.Get);
RateLimitEvent? evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
for (var i = 0; i < 2; i++)
{
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "123", 1, RateLimitingBehaviour.Wait, null, default);
bool expected = i == 1 ? expectLimiting ? evnt?.DelayTime > TimeSpan.Zero : evnt == null : evnt == null;
Assert.That(expected);
}
@@ -71,15 +73,15 @@ namespace CryptoExchange.Net.UnitTests
var rateLimiter = new RateLimitGate("Test");
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerEndpoint, new PathStartFilter("/sapi/"), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get);
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get);
var requestDefinition1 = new RequestDefinition("https://test.com", endpoint1, HttpMethod.Get);
var requestDefinition2 = new RequestDefinition("https://test.com", endpoint2, HttpMethod.Get);
RateLimitEvent? evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "123", 1, RateLimitingBehaviour.Wait, null, default);
Assert.That(evnt == null);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "123", 1, RateLimitingBehaviour.Wait, null, default);
Assert.That(expectLimiting ? evnt != null : evnt == null);
}
@@ -94,16 +96,16 @@ namespace CryptoExchange.Net.UnitTests
bool triggered = false;
rateLimiter.RateLimitTriggered += (x) => { triggered = true; };
var requestDefinition = new RequestDefinition("/sapi/test", HttpMethod.Get);
var requestDefinition = new RequestDefinition("https://test.com", "/sapi/test", HttpMethod.Get);
for (var i = 0; i < requests + 1; i++)
{
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "123", 1, RateLimitingBehaviour.Wait, null, default);
Assert.That(i == requests ? triggered : !triggered);
}
triggered = false;
await Task.Delay((int)Math.Round(perSeconds * 1000) + 10);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "123", 1, RateLimitingBehaviour.Wait, null, default);
Assert.That(!triggered);
}
@@ -115,13 +117,13 @@ namespace CryptoExchange.Net.UnitTests
var rateLimiter = new RateLimitGate("Test");
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerEndpoint, new ExactPathFilter("/sapi/test"), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
var requestDefinition = new RequestDefinition(endpoint, HttpMethod.Get);
var requestDefinition = new RequestDefinition("https://test.com", endpoint, HttpMethod.Get);
RateLimitEvent? evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
for (var i = 0; i < 2; i++)
{
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "123", 1, RateLimitingBehaviour.Wait, null, default);
bool expected = i == 1 ? expectLimited ? evnt?.DelayTime > TimeSpan.Zero : evnt == null : evnt == null;
Assert.That(expected);
}
@@ -135,13 +137,13 @@ namespace CryptoExchange.Net.UnitTests
{
var rateLimiter = new RateLimitGate("Test");
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerEndpoint, new ExactPathsFilter(new[] { "/sapi/test", "/sapi/test2" }), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
var requestDefinition = new RequestDefinition(endpoint, HttpMethod.Get);
var requestDefinition = new RequestDefinition("https://test.com", endpoint, HttpMethod.Get);
RateLimitEvent? evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
for (var i = 0; i < 2; i++)
{
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "123", 1, RateLimitingBehaviour.Wait, null, default);
bool expected = i == 1 ? expectLimited ? evnt?.DelayTime > TimeSpan.Zero : evnt == null : evnt == null;
Assert.That(expected);
}
@@ -158,15 +160,15 @@ namespace CryptoExchange.Net.UnitTests
{
var rateLimiter = new RateLimitGate("Test");
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerApiKey, new AuthenticatedEndpointFilter(true), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Sliding));
var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get) { Authenticated = key1 != null };
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get) { Authenticated = key2 != null };
var requestDefinition1 = new RequestDefinition("https://test.com", endpoint1, HttpMethod.Get) { Authenticated = key1 != null };
var requestDefinition2 = new RequestDefinition("https://test.com", endpoint2, HttpMethod.Get) { Authenticated = key2 != null };
RateLimitEvent? evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", key1, 1, RateLimitingBehaviour.Wait, null, default);
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, key1, 1, RateLimitingBehaviour.Wait, null, default);
Assert.That(evnt == null);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", key2, 1, RateLimitingBehaviour.Wait, null, default);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, key2, 1, RateLimitingBehaviour.Wait, null, default);
Assert.That(expectLimited ? evnt != null : evnt == null);
}
@@ -177,15 +179,15 @@ namespace CryptoExchange.Net.UnitTests
{
var rateLimiter = new RateLimitGate("Test");
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, Array.Empty<IGuardFilter>(), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get);
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get) { Authenticated = true };
var requestDefinition1 = new RequestDefinition("https://test.com", endpoint1, HttpMethod.Get);
var requestDefinition2 = new RequestDefinition("https://test.com", endpoint2, HttpMethod.Get) { Authenticated = true };
RateLimitEvent? evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "123", 1, RateLimitingBehaviour.Wait, null, default);
Assert.That(evnt == null);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", null, 1, RateLimitingBehaviour.Wait, null, default);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, null, 1, RateLimitingBehaviour.Wait, null, default);
Assert.That(expectLimited ? evnt != null : evnt == null);
}
@@ -197,15 +199,15 @@ namespace CryptoExchange.Net.UnitTests
{
var rateLimiter = new RateLimitGate("Test");
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new HostFilter("https://test.com"), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get);
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get) { Authenticated = true };
var requestDefinition1 = new RequestDefinition(host1, endpoint1, HttpMethod.Get);
var requestDefinition2 = new RequestDefinition(host2, endpoint2, HttpMethod.Get) { Authenticated = true };
RateLimitEvent? evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, host1, "123", 1, RateLimitingBehaviour.Wait, null, default);
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "123", 1, RateLimitingBehaviour.Wait, null, default);
Assert.That(evnt == null);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, host2, "123", 1, RateLimitingBehaviour.Wait, null, default);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "123", 1, RateLimitingBehaviour.Wait, null, default);
Assert.That(expectLimited ? evnt != null : evnt == null);
}
@@ -220,9 +222,9 @@ namespace CryptoExchange.Net.UnitTests
RateLimitEvent? evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), host1, "123", 1, RateLimitingBehaviour.Wait, null, default);
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition(host1, "1", HttpMethod.Get), "123", 1, RateLimitingBehaviour.Wait, null, default);
Assert.That(evnt == null);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), host2, "123", 1, RateLimitingBehaviour.Wait, null, default);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition(host2, "1", HttpMethod.Get), "123", 1, RateLimitingBehaviour.Wait, null, default);
Assert.That(expectLimited ? evnt != null : evnt == null);
}
@@ -236,8 +238,8 @@ namespace CryptoExchange.Net.UnitTests
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
var ct = new CancellationTokenSource(TimeSpan.FromSeconds(0.2));
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, ct.Token);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, ct.Token);
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("https://test.com", "1", HttpMethod.Get), "123", 1, RateLimitingBehaviour.Wait, null, ct.Token);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("https://test.com", "1", HttpMethod.Get), "123", 1, RateLimitingBehaviour.Wait, null, ct.Token);
Assert.That(result2.Error, Is.TypeOf<CancellationRequestedError>());
}
@@ -248,16 +250,16 @@ namespace CryptoExchange.Net.UnitTests
var rateLimiter = new RateLimitGate("Test");
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerConnection, new LimitItemTypeFilter(RateLimitItemType.Request), 1, TimeSpan.FromSeconds(10), RateLimitWindowType.Fixed));
var definition = new RequestDefinition("1", HttpMethod.Get) { ConnectionId = 1 };
var definition = new RequestDefinition("https://test.com", "1", HttpMethod.Get) { ConnectionId = 1 };
RateLimitEvent? evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
var ct = new CancellationTokenSource(TimeSpan.FromSeconds(0.2));
// act
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition, "https://test.com", null, 1, RateLimitingBehaviour.Fail, null, ct.Token);
await rateLimiter.ResetAsync(RateLimitItemType.Request, definition, "https://test.com", null, null, default);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition, "https://test.com", null, 1, RateLimitingBehaviour.Fail, null, ct.Token);
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition, null, 1, RateLimitingBehaviour.Fail, null, ct.Token);
await rateLimiter.ResetAsync(RateLimitItemType.Request, definition, null, null, null, default);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition, null, 1, RateLimitingBehaviour.Fail, null, ct.Token);
// assert
Assert.That(evnt, Is.Null);
@@ -270,20 +272,54 @@ namespace CryptoExchange.Net.UnitTests
var rateLimiter = new RateLimitGate("Test");
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerConnection, new LimitItemTypeFilter(RateLimitItemType.Request), 1, TimeSpan.FromSeconds(10), RateLimitWindowType.Fixed));
var definition1 = new RequestDefinition("1", HttpMethod.Get) { ConnectionId = 1 };
var definition2 = new RequestDefinition("2", HttpMethod.Get) { ConnectionId = 2 };
var definition1 = new RequestDefinition("https://test.com", "1", HttpMethod.Get) { ConnectionId = 1 };
var definition2 = new RequestDefinition("https://test.com", "2", HttpMethod.Get) { ConnectionId = 2 };
RateLimitEvent? evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
// act
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition1, "https://test.com", null, 1, RateLimitingBehaviour.Fail, null, default);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition2, "https://test.com", null, 1, RateLimitingBehaviour.Fail, null, default);
await rateLimiter.ResetAsync(RateLimitItemType.Request, definition1, "https://test.com", null, null, default);
var result3 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition2, "https://test.com", null, 1, RateLimitingBehaviour.Fail, null, default);
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition1, null, 1, RateLimitingBehaviour.Fail, null, default);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition2, null, 1, RateLimitingBehaviour.Fail, null, default);
await rateLimiter.ResetAsync(RateLimitItemType.Request, definition1, null, null, null, default);
var result3 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition2, null, 1, RateLimitingBehaviour.Fail, null, default);
// assert
Assert.That(evnt, Is.Not.Null);
}
[TestCase(null, null, true)]
[TestCase("Group1", null, false)]
[TestCase(null, "Group2", false)]
[TestCase("Group1", "Group2", false)]
[TestCase("Group3", "Group3", true)]
public async Task RateLimiterWithDifferentGroups_Should_LimitPerGroup(string? group1, string? group2, bool expectLimited)
{
// arrange
var data = JsonSerializer.Serialize(new TestObject { });
var client1 = new TestRestClient(x =>
{
x.RateLimitGroup = group1;
});
client1.ApiClient1.SetNextResponse(data, System.Net.HttpStatusCode.OK);
var client2 = new TestRestClient(x =>
{
x.RateLimitGroup = group2;
});
client2.ApiClient1.SetNextResponse(data, System.Net.HttpStatusCode.OK);
var rateLimiter = new RateLimitGate("Test");
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new LimitItemTypeFilter(RateLimitItemType.Request), 1, TimeSpan.FromSeconds(2), RateLimitWindowType.Fixed));
RateLimitEvent? evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
// act
var result1 = await client1.ApiClient1.GetResponseAsync<TestObject>(rateLimitGate: rateLimiter);
var result2 = await client2.ApiClient1.GetResponseAsync<TestObject>(rateLimitGate: rateLimiter);
// assert
Assert.That(evnt != null, Is.EqualTo(expectLimited));
}
}
}
@@ -16,9 +16,9 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
// arrange
var routes = new MessageRoute[]
{
MessageRoute<string>.CreateWithoutTopicFilter("type1", (_, _, _, _) => null),
MessageRoute<string>.CreateWithTopicFilter("type1", "topic1", (_, _, _, _) => null),
MessageRoute<int>.CreateWithTopicFilter("type2", "topic2", (_, _, _, _) => null)
MessageRoute.CreateForEvent<string>("type1", (_, _, _, _) => null),
MessageRoute.CreateForEvent<string>("type1", "topic1", (_, _, _, _) => null),
MessageRoute.CreateForEvent<int>("type2", "topic2", (_, _, _, _) => null)
};
var router = new QueryRouter(routes);
@@ -46,10 +46,10 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
var collection = new QueryRouteCollection(typeof(string));
// act
collection.AddRoute(null, MessageRoute<string>.CreateWithoutTopicFilter("type", (_, _, _, _) => null));
collection.AddRoute(null, MessageRoute.CreateForEvent<string>("type", (_, _, _, _) => null));
var beforeMultipleReaders = collection.MultipleReaders;
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) => null, true));
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) => null, true));
var afterMultipleReaders = collection.MultipleReaders;
// assert
@@ -63,12 +63,12 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
// arrange
var calls = new List<string>();
var collection = new QueryRouteCollection(typeof(string));
collection.AddRoute(null, MessageRoute<string>.CreateWithoutTopicFilter("type", (_, _, _, _) =>
collection.AddRoute(null, MessageRoute.CreateForEvent<string>("type", (_, _, _, _) =>
{
calls.Add("no-topic");
return null;
}));
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
{
calls.Add("topic");
return null;
@@ -89,7 +89,7 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
{
// arrange
var collection = new QueryRouteCollection(typeof(string));
collection.AddRoute("other-topic", MessageRoute<string>.CreateWithTopicFilter("type", "other-topic", (_, _, _, _) => null));
collection.AddRoute("other-topic", MessageRoute.CreateForEvent<string>("type", "other-topic", (_, _, _, _) => null));
collection.Build();
// act
@@ -106,12 +106,12 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
// arrange
var calls = new List<string>();
var collection = new QueryRouteCollection(typeof(string));
collection.AddRoute(null, MessageRoute<string>.CreateWithoutTopicFilter("type", (_, _, _, _) =>
collection.AddRoute(null, MessageRoute.CreateForEvent<string>("type", (_, _, _, _) =>
{
calls.Add("no-topic");
return null;
}));
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
{
calls.Add("topic");
return null;
@@ -132,17 +132,17 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
{
// arrange
var calls = new List<string>();
var expectedResult = CallResult.SuccessResult;
var expectedResult = CallResult.Ok();
var collection = new QueryRouteCollection(typeof(string));
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
{
calls.Add("first");
return expectedResult;
}));
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
{
calls.Add("second");
return new CallResult(null);
return CallResult.Ok();
}));
collection.Build();
@@ -160,17 +160,17 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
{
// arrange
var calls = new List<string>();
var expectedResult = CallResult.SuccessResult;
var expectedResult = CallResult.Ok();
var collection = new QueryRouteCollection(typeof(string));
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
{
calls.Add("first");
return expectedResult;
}, true));
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
{
calls.Add("second");
return new CallResult(null);
return CallResult.Ok();
}));
collection.Build();
@@ -188,22 +188,22 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
{
// arrange
var calls = new List<string>();
var expectedResult = CallResult.SuccessResult;
var expectedResult = CallResult.Ok();
var collection = new QueryRouteCollection(typeof(string));
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
{
calls.Add("first");
return null;
}));
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
{
calls.Add("second");
return expectedResult;
}));
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
{
calls.Add("third");
return new CallResult(null);
return CallResult.Ok();
}));
collection.Build();
@@ -221,7 +221,7 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
{
// arrange
var collection = new QueryRouteCollection(typeof(string));
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) => null));
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) => null));
collection.Build();
// act
@@ -17,13 +17,13 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
var processor1 = new TestMessageProcessor(
1,
MessageRouter.Create(
MessageRoute<string>.CreateWithoutTopicFilter("type1", (_, _, _, _) => null),
MessageRoute<string>.CreateWithTopicFilter("type1", "topic1", (_, _, _, _) => null)));
MessageRoute.CreateForEvent<string>("type1", (_, _, _, _) => null),
MessageRoute.CreateForEvent<string>("type1", "topic1", (_, _, _, _) => null)));
var processor2 = new TestMessageProcessor(
2,
MessageRouter.Create(
MessageRoute<int>.CreateWithTopicFilter("type2", "topic2", (_, _, _, _) => null)));
MessageRoute.CreateForEvent<int>("type2", "topic2", (_, _, _, _) => null)));
var table = new RoutingTable();
@@ -57,12 +57,12 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
var processor1 = new TestMessageProcessor(
1,
MessageRouter.Create(
MessageRoute<string>.CreateWithoutTopicFilter("type1", (_, _, _, _) => null)));
MessageRoute.CreateForEvent<string>("type1", (_, _, _, _) => null)));
var processor2 = new TestMessageProcessor(
2,
MessageRouter.Create(
MessageRoute<string>.CreateWithTopicFilter("type1", "topic1", (_, _, _, _) => null)));
MessageRoute.CreateForEvent<string>("type1", "topic1", (_, _, _, _) => null)));
var table = new RoutingTable();
@@ -85,12 +85,12 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
var initialProcessor = new TestMessageProcessor(
1,
MessageRouter.Create(
MessageRoute<string>.CreateWithoutTopicFilter("type1", (_, _, _, _) => null)));
MessageRoute.CreateForEvent<string>("type1", (_, _, _, _) => null)));
var replacementProcessor = new TestMessageProcessor(
2,
MessageRouter.Create(
MessageRoute<int>.CreateWithoutTopicFilter("type2", (_, _, _, _) => null)));
MessageRoute.CreateForEvent<int>("type2", (_, _, _, _) => null)));
var table = new RoutingTable();
table.Update(new IMessageProcessor[] { initialProcessor });
@@ -116,7 +116,7 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
var processor = new TestMessageProcessor(
1,
MessageRouter.Create(
MessageRoute<string>.CreateWithoutTopicFilter("type1", (_, _, _, _) => null)));
MessageRoute.CreateForEvent<string>("type1", (_, _, _, _) => null)));
var table = new RoutingTable();
table.Update(new IMessageProcessor[] { processor });
@@ -156,8 +156,9 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
MessageRouter = messageRouter;
}
#pragma warning disable CS0067 // The event is never used, but it's required by the interface
public event Action? OnMessageRouterUpdated;
#pragma warning restore CS0067
public bool Handle(string typeIdentifier, string? topicFilter, SocketConnection socketConnection, DateTime receiveTime, string? originalData, object result)
{
return true;
@@ -15,9 +15,9 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
// arrange
var routes = new MessageRoute[]
{
MessageRoute<string>.CreateWithoutTopicFilter("type1", (_, _, _, _) => null),
MessageRoute<string>.CreateWithTopicFilter("type1", "topic1", (_, _, _, _) => null),
MessageRoute<int>.CreateWithTopicFilter("type2", "topic2", (_, _, _, _) => null)
MessageRoute.CreateForEvent<string>("type1", (_, _, _, _) => null),
MessageRoute.CreateForEvent<string>("type1", "topic1", (_, _, _, _) => null),
MessageRoute.CreateForEvent<int>("type2", "topic2", (_, _, _, _) => null)
};
var router = new SubscriptionRouter(routes);
@@ -44,12 +44,12 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
// arrange
var calls = new List<string>();
var collection = new SubscriptionRouteCollection(typeof(string));
collection.AddRoute(null, MessageRoute<string>.CreateWithoutTopicFilter("type", (_, _, _, _) =>
collection.AddRoute(null, MessageRoute.CreateForEvent<string>("type", (_, _, _, _) =>
{
calls.Add("no-topic");
return null;
}));
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
{
calls.Add("topic");
return null;
@@ -61,7 +61,7 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
// assert
Assert.That(handled, Is.True);
Assert.That(result, Is.SameAs(CallResult.SuccessResult));
Assert.That(result, Is.SameAs(CallResult.Ok()));
Assert.That(calls, Is.EqualTo(new[] { "no-topic" }));
}
@@ -70,7 +70,7 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
{
// arrange
var collection = new SubscriptionRouteCollection(typeof(string));
collection.AddRoute("other-topic", MessageRoute<string>.CreateWithTopicFilter("type", "other-topic", (_, _, _, _) => null));
collection.AddRoute("other-topic", MessageRoute.CreateForEvent<string>("type", "other-topic", (_, _, _, _) => null));
collection.Build();
// act
@@ -78,7 +78,7 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
// assert
Assert.That(handled, Is.False);
Assert.That(result, Is.SameAs(CallResult.SuccessResult));
Assert.That(result, Is.SameAs(CallResult.Ok()));
}
[Test]
@@ -87,12 +87,12 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
// arrange
var calls = new List<string>();
var collection = new SubscriptionRouteCollection(typeof(string));
collection.AddRoute(null, MessageRoute<string>.CreateWithoutTopicFilter("type", (_, _, _, _) =>
collection.AddRoute(null, MessageRoute.CreateForEvent<string>("type", (_, _, _, _) =>
{
calls.Add("no-topic");
return null;
}));
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
{
calls.Add("topic");
return null;
@@ -104,7 +104,7 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
// assert
Assert.That(handled, Is.True);
Assert.That(result, Is.SameAs(CallResult.SuccessResult));
Assert.That(result, Is.SameAs(CallResult.Ok()));
Assert.That(calls, Is.EqualTo(new[] { "no-topic", "topic" }));
}
@@ -114,12 +114,12 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
// arrange
var calls = new List<string>();
var collection = new SubscriptionRouteCollection(typeof(string));
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
{
calls.Add("first");
return CallResult.SuccessResult;
return CallResult.Ok();
}));
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
{
calls.Add("second");
return null;
@@ -131,7 +131,7 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
// assert
Assert.That(handled, Is.True);
Assert.That(result, Is.SameAs(CallResult.SuccessResult));
Assert.That(result, Is.SameAs(CallResult.Ok()));
Assert.That(calls, Is.EqualTo(new[] { "first", "second" }));
}
@@ -141,7 +141,7 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
// arrange
var calls = new List<string>();
var collection = new SubscriptionRouteCollection(typeof(string));
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
{
calls.Add("topic");
return null;
@@ -153,7 +153,7 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
// assert
Assert.That(handled, Is.False);
Assert.That(result, Is.SameAs(CallResult.SuccessResult));
Assert.That(result, Is.SameAs(CallResult.Ok()));
Assert.That(calls, Is.Empty);
}
}
@@ -25,7 +25,7 @@ namespace CryptoExchange.Net.UnitTests
}
protected override Task<CallResult<bool>> DoResyncAsync(CancellationToken ct)
protected override Task<CallResult> DoResyncAsync(CancellationToken ct)
{
throw new NotImplementedException();
}
@@ -0,0 +1,347 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Errors;
using CryptoExchange.Net.Sockets;
using CryptoExchange.Net.Sockets.Default;
using CryptoExchange.Net.TokenManagement;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using NUnit.Framework;
using System;
using System.Threading.Tasks;
namespace CryptoExchange.Net.UnitTests
{
[TestFixture]
public class TokenManagementTests
{
private static readonly TimeSpan TestMaintenanceInterval = TimeSpan.FromMilliseconds(5);
[Test]
public async Task AcquireWithoutApiKeyReturnsCredentialsError()
{
var starts = 0;
var manager = CreateManager(
(_, _) =>
{
starts++;
return Task.FromResult(CallResult.Ok("token"));
});
var result = await manager.AcquireAsync(new TokenScope("Test", "Test", "Test", ""));
Assert.That(result.Success, Is.False);
Assert.That(result.Error, Is.TypeOf<NoApiCredentialsError>());
Assert.That(starts, Is.EqualTo(0));
}
[Test]
public async Task StartTokenFailureIsReturned()
{
var error = new ServerError(ErrorType.Unknown, "start failed");
var manager = CreateManager((_, _) => Task.FromResult(CallResult.Fail<string>(error)));
var result = await manager.AcquireAsync(CreateScope());
Assert.That(result.Success, Is.False);
Assert.That(result.Error, Is.SameAs(error));
}
[Test]
public async Task ActiveTokenIsSharedWhileLeasedAndStoppedAfterLastRelease()
{
var starts = 0;
var stops = 0;
var manager = CreateManager(
(_, _) => Task.FromResult(CallResult.Ok("token-" + ++starts)),
stopToken: (_, _) =>
{
stops++;
return Task.FromResult(CallResult.Ok());
});
var scope = CreateScope();
var first = await manager.AcquireAsync(scope);
var second = await manager.AcquireAsync(scope);
AssertSuccess(first);
AssertSuccess(second);
Assert.That(second.Data!.Token.Token, Is.EqualTo(first.Data!.Token.Token));
Assert.That(starts, Is.EqualTo(1));
await first.Data!.ReleaseAsync();
Assert.That(stops, Is.EqualTo(0));
await second.Data!.ReleaseAsync();
Assert.That(stops, Is.EqualTo(1));
}
[Test]
public async Task ActiveTokenStartsNewTokenAfterLeaseRelease()
{
var starts = 0;
var stops = 0;
var manager = CreateManager(
(_, _) => Task.FromResult(CallResult.Ok("token-" + ++starts)),
stopToken: (_, _) =>
{
stops++;
return Task.FromResult(CallResult.Ok());
});
var scope = CreateScope();
var first = await manager.AcquireAsync(scope);
AssertSuccess(first);
await first.Data!.ReleaseAsync();
var second = await manager.AcquireAsync(scope);
AssertSuccess(second);
Assert.That(second.Data!.Token.Token, Is.Not.EqualTo(first.Data!.Token.Token));
Assert.That(starts, Is.EqualTo(2));
Assert.That(stops, Is.EqualTo(1));
await second.Data!.ReleaseAsync();
}
[Test]
public async Task ReleasingLeaseTwiceOnlyStopsActiveTokenOnce()
{
var stops = 0;
var manager = CreateManager(
(_, _) => Task.FromResult(CallResult.Ok("token")),
stopToken: (_, _) =>
{
stops++;
return Task.FromResult(CallResult.Ok());
});
var leaseResult = await manager.AcquireAsync(CreateScope());
AssertSuccess(leaseResult);
await leaseResult.Data!.ReleaseAsync();
await leaseResult.Data!.ReleaseAsync();
Assert.That(stops, Is.EqualTo(1));
}
[Test]
public async Task CachedTokenIsReusedAfterLeaseRelease()
{
var starts = 0;
var manager = CreateManager(
(_, _) => Task.FromResult(CallResult.Ok("token-" + ++starts)),
managementType: TokenManagementType.Cached);
var scope = CreateScope();
var first = await manager.AcquireAsync(scope);
AssertSuccess(first);
await first.Data!.ReleaseAsync();
var second = await manager.AcquireAsync(scope);
AssertSuccess(second);
Assert.That(second.Data!.Token.Token, Is.EqualTo(first.Data!.Token.Token));
Assert.That(starts, Is.EqualTo(1));
await second.Data!.ReleaseAsync();
}
[Test]
public async Task CachedTokensAreScopedIndependently()
{
var starts = 0;
var manager = CreateManager(
(_, _) => Task.FromResult(CallResult.Ok("token-" + ++starts)),
managementType: TokenManagementType.Cached);
var firstScope = CreateScope(additionalIdentifier: "one");
var secondScope = CreateScope(additionalIdentifier: "two");
var first = await manager.AcquireAsync(firstScope);
var second = await manager.AcquireAsync(secondScope);
AssertSuccess(first);
AssertSuccess(second);
await first.Data!.ReleaseAsync();
await second.Data!.ReleaseAsync();
var firstAgain = await manager.AcquireAsync(firstScope);
AssertSuccess(firstAgain);
Assert.That(firstAgain.Data!.Token.Token, Is.EqualTo(first.Data!.Token.Token));
Assert.That(second.Data!.Token.Token, Is.Not.EqualTo(first.Data!.Token.Token));
Assert.That(starts, Is.EqualTo(2));
await firstAgain.Data!.ReleaseAsync();
}
[Test]
public async Task ExpiredCachedTokenIsNotReused()
{
var starts = 0;
var manager = CreateManager(
(_, _) => Task.FromResult(CallResult.Ok("token-" + ++starts)),
timeValid: TimeSpan.FromMilliseconds(20),
managementType: TokenManagementType.Cached);
var scope = CreateScope();
var first = await manager.AcquireAsync(scope);
AssertSuccess(first);
await first.Data!.ReleaseAsync();
await Task.Delay(50);
var second = await manager.AcquireAsync(scope);
AssertSuccess(second);
Assert.That(first.Data!.Token.Status, Is.EqualTo(TokenStatus.Expired));
Assert.That(second.Data!.Token.Token, Is.Not.EqualTo(first.Data!.Token.Token));
Assert.That(starts, Is.EqualTo(2));
await second.Data!.ReleaseAsync();
}
[Test]
public async Task CachedTokenDoesNotRunKeepAliveLoop()
{
var keepAlives = 0;
var manager = CreateManager(
(_, _) => Task.FromResult(CallResult.Ok("token")),
refreshInterval: TimeSpan.FromMilliseconds(1),
keepAliveToken: (_, _) =>
{
keepAlives++;
return Task.FromResult(CallResult.Ok());
},
managementType: TokenManagementType.Cached);
var leaseResult = await manager.AcquireAsync(CreateScope());
AssertSuccess(leaseResult);
await Task.Delay(50);
Assert.That(keepAlives, Is.EqualTo(0));
await leaseResult.Data!.ReleaseAsync();
}
[Test]
public async Task ActiveTokenKeepAliveRefreshesValidity()
{
var keepAlives = 0;
var manager = CreateManager(
(_, _) => Task.FromResult(CallResult.Ok("token")),
refreshInterval: TimeSpan.FromMilliseconds(1),
timeValid: TimeSpan.FromSeconds(1),
keepAliveToken: (_, _) =>
{
keepAlives++;
return Task.FromResult(CallResult.Ok());
});
var leaseResult = await manager.AcquireAsync(CreateScope());
AssertSuccess(leaseResult);
var originalValidUntil = leaseResult.Data!.Token.ValidUntil;
await WaitUntilAsync(() => keepAlives > 0);
Assert.That(leaseResult.Data!.Token.ValidUntil, Is.GreaterThan(originalValidUntil));
await leaseResult.Data!.ReleaseAsync();
}
[Test]
public async Task ActiveTokenKeepAliveFailureExpiresTokenWhenValidityPassed()
{
var starts = 0;
var expired = false;
var manager = CreateManager(
(_, _) => Task.FromResult(CallResult.Ok("token-" + ++starts)),
refreshInterval: TimeSpan.FromMilliseconds(1),
timeValid: TimeSpan.FromMilliseconds(25),
keepAliveToken: (_, _) => Task.FromResult(CallResult.Fail(new ServerError(ErrorType.Unknown, "keep alive failed"))));
var leaseResult = await manager.AcquireAsync(CreateScope());
AssertSuccess(leaseResult);
leaseResult.Data!.Token.Expired += _ => expired = true;
await WaitUntilAsync(() => expired);
Assert.That(leaseResult.Data!.Token.Status, Is.EqualTo(TokenStatus.Expired));
var nextLease = await manager.AcquireAsync(CreateScope());
AssertSuccess(nextLease);
Assert.That(nextLease.Data!.Token.Token, Is.Not.EqualTo(leaseResult.Data!.Token.Token));
Assert.That(starts, Is.EqualTo(2));
await leaseResult.Data!.ReleaseAsync();
await nextLease.Data!.ReleaseAsync();
}
[Test]
public async Task AcquireAndReplaceReleasesPreviousSubscriptionLease()
{
var starts = 0;
var stops = 0;
var manager = CreateManager(
(_, _) => Task.FromResult(CallResult.Ok("token-" + ++starts)),
stopToken: (_, _) =>
{
stops++;
return Task.FromResult(CallResult.Ok());
});
var subscription = new TestSubscription();
var first = await manager.AcquireAndReplaceAsync(subscription, CreateScope(additionalIdentifier: "one"));
AssertSuccess(first);
var second = await manager.AcquireAndReplaceAsync(subscription, CreateScope(additionalIdentifier: "two"));
AssertSuccess(second);
Assert.That(subscription.TokenLease, Is.SameAs(second.Data));
Assert.That(second.Data!.Token.Token, Is.Not.EqualTo(first.Data!.Token.Token));
Assert.That(stops, Is.EqualTo(1));
await subscription.TokenLease!.ReleaseAsync();
}
private static TokenManager CreateManager(
Func<TokenScope, System.Threading.CancellationToken, Task<CallResult<string>>> startToken,
TimeSpan? refreshInterval = null,
TimeSpan? timeValid = null,
Func<TokenInfo, System.Threading.CancellationToken, Task<CallResult>>? keepAliveToken = null,
Func<TokenInfo, System.Threading.CancellationToken, Task<CallResult>>? stopToken = null,
TokenManagementType managementType = TokenManagementType.Active)
{
return new TokenManager(
Guid.NewGuid().ToString(),
null,
refreshInterval ?? TimeSpan.FromMinutes(1),
timeValid ?? TimeSpan.FromMinutes(1),
startToken,
keepAliveToken,
stopToken,
managementType,
TestMaintenanceInterval);
}
private static TokenScope CreateScope(string apiKey = "apiKey", string? additionalIdentifier = null)
=> new TokenScope("Test", "Test", "Test", apiKey, additionalIdentifier);
private static void AssertSuccess(CallResult<TokenLease> result)
{
Assert.That(result.Success, Is.True, result.Error?.ToString());
Assert.That(result.Data, Is.Not.Null);
}
private static async Task WaitUntilAsync(Func<bool> condition)
{
var timeout = DateTime.UtcNow.AddSeconds(2);
while (!condition())
{
if (DateTime.UtcNow > timeout)
Assert.Fail("Condition was not met within the timeout");
await Task.Delay(10);
}
}
private sealed class TestSubscription : Subscription
{
public TestSubscription() : base(NullLogger.Instance, true)
{
}
protected override Query? GetSubQuery(SocketConnection connection) => null;
protected override Query? GetUnsubQuery(SocketConnection connection) => null;
}
}
}
-6
View File
@@ -15,8 +15,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleClient", "Examples\C
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharedClients", "Examples\SharedClients\SharedClients.csproj", "{988A87EF-EAEA-4313-A6CF-FA869813D5AB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CryptoExchange.Net.Protobuf", "CryptoExchange.Net.Protobuf\CryptoExchange.Net.Protobuf.csproj", "{CC6A807A-9183-6F41-8EF1-8A70172B0E83}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -43,10 +41,6 @@ Global
{988A87EF-EAEA-4313-A6CF-FA869813D5AB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{988A87EF-EAEA-4313-A6CF-FA869813D5AB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{988A87EF-EAEA-4313-A6CF-FA869813D5AB}.Release|Any CPU.Build.0 = Release|Any CPU
{CC6A807A-9183-6F41-8EF1-8A70172B0E83}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CC6A807A-9183-6F41-8EF1-8A70172B0E83}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CC6A807A-9183-6F41-8EF1-8A70172B0E83}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CC6A807A-9183-6F41-8EF1-8A70172B0E83}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -1,7 +1,7 @@
#if NETSTANDARD2_0
namespace System.Diagnostics.CodeAnalysis
namespace System.Diagnostics.CodeAnalysis
{
using System;
#if NETSTANDARD2_0
/// <summary>
/// Specifies that <see langword="null"/> is allowed as an input even if the
@@ -206,5 +206,26 @@ namespace System.Diagnostics.CodeAnalysis
ReturnValue = returnValue;
}
}
#endif
#if NETSTANDARD2_0 || NETSTANDARD2_1
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, AllowMultiple = true, Inherited = false)]
[ExcludeFromCodeCoverage]
internal sealed class MemberNotNullWhenAttribute : Attribute
{
public MemberNotNullWhenAttribute(bool returnValue, string member)
{
ReturnValue = returnValue;
Members = [member];
}
public MemberNotNullWhenAttribute(bool returnValue, params string[] members)
{
ReturnValue = returnValue;
Members = members;
}
public bool ReturnValue { get; }
public string[] Members { get; }
}
#endif
}
#endif
@@ -439,13 +439,13 @@ namespace CryptoExchange.Net.Authentication
/// <param name="serializer"></param>
/// <param name="parameters"></param>
/// <returns></returns>
protected static string GetSerializedBody(IMessageSerializer serializer, IDictionary<string, object> parameters)
protected static string GetSerializedBody(IMessageSerializer serializer, Parameters? 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);
if (parameters?.BodyValue != null)
return stringSerializer.Serialize(parameters.BodyValue);
else
return stringSerializer.Serialize(parameters);
}
+27 -6
View File
@@ -4,6 +4,7 @@ using CryptoExchange.Net.Objects.Errors;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.SharedApis;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
namespace CryptoExchange.Net.Clients
{
@@ -25,7 +26,7 @@ namespace CryptoExchange.Net.Clients
/// <summary>
/// If we are disposing
/// </summary>
protected bool _disposing;
protected bool _disposed;
/// <summary>
/// Whether a proxy is configured
@@ -47,6 +48,11 @@ namespace CryptoExchange.Net.Clients
}
}
/// <summary>
/// The name of the exchange this client is for
/// </summary>
public string Exchange { get; }
/// <summary>
/// The environment this client communicates to
/// </summary>
@@ -75,20 +81,26 @@ namespace CryptoExchange.Net.Clients
/// <summary>
/// ctor
/// </summary>
/// <param name="logger">Logger</param>
/// <param name="loggerFactory">Logger factory</param>
/// <param name="exchange">The exchange name</param>
/// <param name="outputOriginalData">Should data from this client include the original data in the call result</param>
/// <param name="baseAddress">Base address for this API client</param>
/// <param name="clientOptions">Client options</param>
/// <param name="apiOptions">Api options</param>
protected BaseApiClient(
ILogger logger,
ILoggerFactory? loggerFactory,
string exchange,
bool outputOriginalData,
string baseAddress,
ExchangeOptions clientOptions,
ApiOptions apiOptions)
{
_logger = logger;
var loggerName = ClientName.StartsWith(exchange, StringComparison.OrdinalIgnoreCase)
? exchange + "." + ClientName.Substring(exchange.Length).TrimStart('.')
: exchange + "." + ClientName;
_logger = loggerFactory?.CreateLogger(loggerName) ?? NullLogger.Instance;
Exchange = exchange;
ClientOptions = clientOptions;
ApiOptions = apiOptions;
OutputOriginalData = outputOriginalData;
@@ -113,9 +125,18 @@ namespace CryptoExchange.Net.Clients
/// <summary>
/// Dispose
/// </summary>
public virtual void Dispose()
public void Dispose()
{
_disposing = true;
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Dispose
/// </summary>
protected virtual void Dispose(bool disposing)
{
_disposed = true;
}
}
}
+7 -1
View File
@@ -1,6 +1,7 @@
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Objects.Options;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
using System.Threading;
@@ -89,7 +90,6 @@ namespace CryptoExchange.Net.Clients
throw new ArgumentNullException(nameof(options));
ClientOptions = options;
_logger.Log(LogLevel.Trace, $"Client configuration: {options}, CryptoExchange.Net: v{CryptoExchangeLibVersion}, {Exchange}.Net: v{ExchangeLibVersion}");
}
/// <summary>
@@ -115,6 +115,12 @@ namespace CryptoExchange.Net.Clients
return opts;
}
/// <inheritdoc />
public override string ToString()
{
return $"{GetType().Name}, CryptoExchange.Net: v{CryptoExchangeLibVersion}, {Exchange}.Net: v{ExchangeLibVersion}, configuration: {ClientOptions}";
}
/// <summary>
/// Dispose
/// </summary>
+1 -1
View File
@@ -72,7 +72,7 @@ namespace CryptoExchange.Net.Clients
/// Set the API credentials for this client. All Api clients in this client will use the new credentials, regardless of earlier set options.
/// </summary>
/// <param name="credentials">The credentials to set</param>
public void SetApiCredentials(TApiCredentials credentials)
public virtual void SetApiCredentials(TApiCredentials credentials)
{
foreach (var apiClient in ApiClients)
apiClient.SetApiCredentials(credentials);
+128 -136
View File
@@ -41,11 +41,6 @@ namespace CryptoExchange.Net.Clients
/// </summary>
protected internal RequestBodyFormat RequestBodyFormat = RequestBodyFormat.Json;
/// <summary>
/// How to serialize array parameters when making requests
/// </summary>
protected internal ArrayParametersSerialization ArraySerialization = ArrayParametersSerialization.Array;
/// <summary>
/// What request body should be set when no data is send (only used in combination with postParametersPosition.InBody)
/// </summary>
@@ -56,16 +51,6 @@ namespace CryptoExchange.Net.Clients
/// </summary>
protected Dictionary<string, string> StandardRequestHeaders { get; set; } = [];
/// <summary>
/// Whether parameters need to be ordered
/// </summary>
protected internal bool OrderParameters { get; set; } = true;
/// <summary>
/// Parameter order comparer
/// </summary>
protected IComparer<string> ParameterOrderComparer { get; } = new OrderedStringComparer();
/// <summary>
/// Where to put the parameters for requests with different Http methods
/// </summary>
@@ -108,7 +93,7 @@ namespace CryptoExchange.Net.Clients
/// Get the AuthenticationProvider implementation, or null if no ApiCredentials are set
/// </summary>
public virtual AuthenticationProvider? GetAuthenticationProvider() => null;
/// <summary>
/// Configured environment name
/// </summary>
@@ -117,17 +102,20 @@ namespace CryptoExchange.Net.Clients
/// <summary>
/// ctor
/// </summary>
/// <param name="logger">Logger</param>
/// <param name="loggerFactory">Logger factory</param>
/// <param name="exchangeName">The exchange name</param>
/// <param name="httpClient">HttpClient to use</param>
/// <param name="baseAddress">Base address for this API client</param>
/// <param name="options">The base client options</param>
/// <param name="apiOptions">The Api client options</param>
public RestApiClient(ILogger logger,
public RestApiClient(ILoggerFactory? loggerFactory,
string exchangeName,
HttpClient? httpClient,
string baseAddress,
RestExchangeOptions options,
RestApiOptions apiOptions)
: base(logger,
: base(loggerFactory,
exchangeName,
apiOptions.OutputOriginalData ?? options.OutputOriginalData,
baseAddress,
options,
@@ -144,33 +132,10 @@ namespace CryptoExchange.Net.Clients
/// <returns></returns>
protected abstract IMessageSerializer CreateSerializer();
/// <summary>
/// Send a request to the base address based on the request definition
/// </summary>
/// <param name="baseAddress">Host and schema</param>
/// <param name="definition">Request definition</param>
/// <param name="parameters">Request parameters</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <param name="additionalHeaders">Additional headers for this request</param>
/// <param name="weight">Override the request weight for this request definition, for example when the weight depends on the parameters</param>
/// <returns></returns>
protected virtual async Task<WebCallResult> SendAsync(
string baseAddress,
RequestDefinition definition,
ParameterCollection? parameters,
CancellationToken cancellationToken,
Dictionary<string, string>? additionalHeaders = null,
int? weight = null)
{
var result = await SendAsync<object>(baseAddress, definition, parameters, cancellationToken, additionalHeaders, weight).ConfigureAwait(false);
return result.AsDataless();
}
/// <summary>
/// Send a request to the base address based on the request definition
/// </summary>
/// <typeparam name="T">Response type</typeparam>
/// <param name="baseAddress">Host and schema</param>
/// <param name="definition">Request definition</param>
/// <param name="parameters">Request parameters</param>
/// <param name="cancellationToken">Cancellation token</param>
@@ -179,10 +144,9 @@ namespace CryptoExchange.Net.Clients
/// <param name="weightSingleLimiter">Specify the weight to apply to the individual rate limit guard for this request</param>
/// <param name="rateLimitKeySuffix">An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters.</param>
/// <returns></returns>
protected virtual Task<WebCallResult<T>> SendAsync<T>(
string baseAddress,
protected virtual Task<HttpResult<T>> SendAsync<T>(
RequestDefinition definition,
ParameterCollection? parameters,
Parameters? parameters,
CancellationToken cancellationToken,
Dictionary<string, string>? additionalHeaders = null,
int? weight = null,
@@ -191,7 +155,6 @@ namespace CryptoExchange.Net.Clients
{
var parameterPosition = definition.ParameterPosition ?? ParameterPositions[definition.Method];
return SendAsync<T>(
baseAddress,
definition,
parameterPosition == HttpMethodParameterPosition.InUri ? parameters : null,
parameterPosition == HttpMethodParameterPosition.InBody ? parameters : null,
@@ -206,7 +169,6 @@ namespace CryptoExchange.Net.Clients
/// Send a request to the base address based on the request definition
/// </summary>
/// <typeparam name="T">Response type</typeparam>
/// <param name="baseAddress">Host and schema</param>
/// <param name="definition">Request definition</param>
/// <param name="uriParameters">Request query parameters</param>
/// <param name="bodyParameters">Request body parameters</param>
@@ -216,11 +178,10 @@ namespace CryptoExchange.Net.Clients
/// <param name="weightSingleLimiter">Specify the weight to apply to the individual rate limit guard for this request</param>
/// <param name="rateLimitKeySuffix">An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters.</param>
/// <returns></returns>
protected virtual async Task<WebCallResult<T>> SendAsync<T>(
string baseAddress,
protected virtual async Task<HttpResult<T>> SendAsync<T>(
RequestDefinition definition,
ParameterCollection? uriParameters,
ParameterCollection? bodyParameters,
Parameters? uriParameters,
Parameters? bodyParameters,
CancellationToken cancellationToken,
Dictionary<string, string>? additionalHeaders = null,
int? weight = null,
@@ -231,20 +192,20 @@ namespace CryptoExchange.Net.Clients
if (definition.Authenticated && GetAuthenticationProvider() == null)
{
_logger.RestApiNoApiCredentials(requestId, definition.Path);
return new WebCallResult<T>(new NoApiCredentialsError());
return HttpResult.Fail<T>(Exchange, new NoApiCredentialsError());
}
string? cacheKey = null;
if (ShouldCache(definition))
{
cacheKey = baseAddress + definition + uriParameters?.ToFormData();
cacheKey = definition.FullUrl + definition + uriParameters?.ToFormData();
_logger.CheckingCache(cacheKey);
var cachedValue = _cache.Get(cacheKey, ClientOptions.CachingMaxAge);
if (cachedValue != null)
{
_logger.CacheHit(cacheKey);
var original = (WebCallResult<T>)cachedValue;
return original.Cached();
var original = (HttpResult<T>)cachedValue;
return original with { DataSource = ResultDataSource.Cache };
}
_logger.CacheNotHit(cacheKey);
@@ -258,7 +219,6 @@ namespace CryptoExchange.Net.Clients
await CheckTimeSync(requestId, definition).ConfigureAwait(false);
var error = await RateLimitAsync(
baseAddress,
requestId,
definition,
weight ?? definition.Weight,
@@ -266,11 +226,10 @@ namespace CryptoExchange.Net.Clients
weightSingleLimiter,
rateLimitKeySuffix).ConfigureAwait(false);
if (error != null)
return new WebCallResult<T>(error);
return HttpResult.Fail<T>(Exchange, error);
var request = CreateRequest(
requestId,
baseAddress,
definition,
uriParameters,
bodyParameters,
@@ -284,7 +243,7 @@ namespace CryptoExchange.Net.Clients
if (result.Error is not CancellationRequestedError)
{
var originalData = OutputOriginalData ? result.OriginalData : "[Data only available when OutputOriginal = true]";
if (!result)
if (!result.Success)
{
_logger.RestApiErrorReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), result.Error?.ToString(), originalData, result.Error?.Exception);
}
@@ -316,7 +275,6 @@ namespace CryptoExchange.Net.Clients
/// Check rate limits for the request
/// </summary>
protected virtual async ValueTask<Error?> RateLimitAsync(
string host,
int requestId,
RequestDefinition definition,
int weight,
@@ -338,13 +296,12 @@ namespace CryptoExchange.Net.Clients
requestId,
RateLimitItemType.Request,
definition,
host,
GetAuthenticationProvider()?.Key,
requestWeight,
requestWeight,
ClientOptions.RateLimitingBehaviour,
rateLimitKeySuffix,
rateLimitKeySuffix + ClientOptions.RateLimitGroup,
cancellationToken).ConfigureAwait(false);
if (!limitResult)
if (!limitResult.Success)
return limitResult.Error!;
}
}
@@ -360,17 +317,16 @@ namespace CryptoExchange.Net.Clients
var singleRequestWeight = weightSingleLimiter ?? 1;
var limitResult = await definition.RateLimitGate.ProcessSingleAsync(
_logger,
requestId,
requestId,
definition.LimitGuard,
RateLimitItemType.Request,
definition,
host,
GetAuthenticationProvider()?.Key,
singleRequestWeight,
ClientOptions.RateLimitingBehaviour,
rateLimitKeySuffix,
cancellationToken).ConfigureAwait(false);
if (!limitResult)
if (!limitResult.Success)
return limitResult.Error!;
}
}
@@ -382,7 +338,6 @@ namespace CryptoExchange.Net.Clients
/// Creates a request object
/// </summary>
/// <param name="requestId">Id of the request</param>
/// <param name="baseAddress">Host and schema</param>
/// <param name="definition">Request definition</param>
/// <param name="uriParameters">The query parameters of the request</param>
/// <param name="bodyParameters">The body parameters of the request</param>
@@ -390,19 +345,16 @@ namespace CryptoExchange.Net.Clients
/// <returns></returns>
protected virtual IRequest CreateRequest(
int requestId,
string baseAddress,
RequestDefinition definition,
ParameterCollection? uriParameters,
ParameterCollection? bodyParameters,
Parameters? uriParameters,
Parameters? bodyParameters,
Dictionary<string, string>? additionalHeaders)
{
var requestConfiguration = new RestRequestConfiguration(
definition,
baseAddress,
uriParameters == null ? null : CreateParameterDictionary(uriParameters),
bodyParameters == null ? null : CreateParameterDictionary(bodyParameters),
uriParameters,
bodyParameters,
additionalHeaders,
definition.ArraySerialization ?? ArraySerialization,
definition.ParameterPosition ?? ParameterPositions[definition.Method],
definition.RequestBodyFormat ?? RequestBodyFormat);
@@ -414,20 +366,16 @@ namespace CryptoExchange.Net.Clients
{
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 path = baseAddress.AppendPath(definition.Path);
if (definition.ForcePathEndWithSlash == true && !path.EndsWith("/"))
path += "/";
var uri = new Uri(path + queryString);
var uri = new Uri(definition.FullUrl + queryString);
var request = RequestFactory.Create(ClientOptions.HttpVersion, definition.Method, uri, requestId);
request.Accept = MessageHandler.AcceptHeader;
if (requestConfiguration.Headers != null)
if (requestConfiguration.Headers != null)
{
foreach (var header in requestConfiguration.Headers)
request.AddHeader(header.Key, header.Value);
@@ -436,10 +384,12 @@ namespace CryptoExchange.Net.Clients
foreach (var header in StandardRequestHeaders)
{
// Only add it if it isn't overwritten
requestConfiguration.Headers ??= new Dictionary<string, string>();
if (!requestConfiguration.Headers.ContainsKey(header.Key))
if (requestConfiguration.Headers == null
|| !requestConfiguration.Headers.ContainsKey(header.Key))
{
request.AddHeader(header.Key, header.Value);
}
}
}
if (requestConfiguration.ParameterPosition == HttpMethodParameterPosition.InBody)
{
@@ -451,10 +401,10 @@ namespace CryptoExchange.Net.Clients
}
else
{
if (requestConfiguration.BodyParameters != null && requestConfiguration.BodyParameters.Count != 0)
if (requestConfiguration.BodyParameters != null && !requestConfiguration.BodyParameters.Empty)
WriteParamBody(request, requestConfiguration.BodyParameters, contentType);
else if (OmitContentTypeHeaderWithoutContent != true)
request.SetContent(RequestBodyEmptyContent, RequestBodyContentEncoding, contentType);
request.SetContent(RequestBodyEmptyContent, RequestBodyContentEncoding, contentType);
}
}
@@ -469,7 +419,7 @@ namespace CryptoExchange.Net.Clients
/// <param name="gate">The ratelimit gate used</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns></returns>
protected virtual async Task<WebCallResult<T>> GetResponseAsync2<T>(
protected virtual async Task<HttpResult<T>> GetResponseAsync2<T>(
RequestDefinition requestDefinition,
IRequest request,
IRateLimitGate? gate,
@@ -535,16 +485,16 @@ namespace CryptoExchange.Net.Clients
{
_logger.LogError(ex, "Unhandled exception when parsing error response: {Message}", ex.Message);
var errorResult = new ServerError(ErrorInfo.Unknown with { Message = ex.Message });
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, errorResult);
return FailHttpRequest<T>(request, response, sw.Elapsed, originalData, errorResult);
}
}
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
return FailHttpRequest<T>(request, response, sw.Elapsed, originalData, error);
}
if (typeof(T) == typeof(object))
if (typeof(T) == Unit.Type)
// Success status code and expected empty response, assume it's correct
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, 0, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, null);
return OkHttpRequest<T>(request, response, sw.Elapsed, originalData, default!);
// Data response received, inspect the message and check if it is an error or not
var parsedError = await MessageHandler.CheckForErrorResponse(
@@ -563,7 +513,7 @@ namespace CryptoExchange.Net.Clients
}
// Success status code, but TryParseError determined it was an error response
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, parsedError);
return FailHttpRequest<T>(request, response, sw.Elapsed, originalData, parsedError);
}
if (MessageHandler.RequiresSeekableStream)
@@ -571,45 +521,45 @@ namespace CryptoExchange.Net.Clients
responseStream.Position = 0;
// Try deserialization into the expected type
var (deserializeResult, deserializeError) = await MessageHandler.TryDeserializeAsync<T>(responseStream, cancellationToken).ConfigureAwait(false);
var (deserializeResult, deserializeError) = await MessageHandler.TryDeserializeAsync<T>(responseStream, cancellationToken).ConfigureAwait(false);
if (deserializeError != null)
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult, deserializeError); ;
return FailHttpRequest<T>(request, response, sw.Elapsed, originalData, deserializeError, deserializeResult);
try
{
// Check the deserialized response to see if it's an error or not
var responseError = MessageHandler.CheckDeserializedResponse(response.ResponseHeaders, deserializeResult);
if (responseError != null)
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult, responseError);
return FailHttpRequest<T>(request, response, sw.Elapsed, originalData, responseError, deserializeResult);
}
catch (Exception ex)
{
_logger.LogError(ex, "Unhandled exception when checking deserialized response: {Message}", ex.Message);
var error = new ServerError(ErrorInfo.Unknown with { Message = ex.Message });
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult, error);
return FailHttpRequest<T>(request, response, sw.Elapsed, originalData, error, deserializeResult);
}
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult, null);
return OkHttpRequest<T>(request, response, sw.Elapsed, originalData, deserializeResult!);
}
catch (HttpRequestException requestException)
{
// Request exception, can't reach server for instance
var error = new WebError(requestException.Message, requestException);
return new WebCallResult<T>(null, null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
return FailHttpRequest<T>(request, response, sw.Elapsed, null, error);
}
catch (OperationCanceledException canceledException)
{
if (cancellationToken != default && canceledException.CancellationToken == cancellationToken)
{
// Cancellation token canceled by caller
return new WebCallResult<T>(null, null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new CancellationRequestedError(canceledException));
return FailHttpRequest<T>(request, null, sw.Elapsed, null, new CancellationRequestedError(canceledException));
}
else
{
// Request timed out
var error = new WebError($"Request timed out", exception: canceledException);
error.ErrorType = ErrorType.Timeout;
return new WebCallResult<T>(null, null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
return FailHttpRequest<T>(request, null, sw.Elapsed, null, error);
}
}
catch (ArgumentException argumentException)
@@ -618,7 +568,7 @@ namespace CryptoExchange.Net.Clients
{
// Unsupported HTTP version error .net framework
var error = ArgumentError.Invalid(nameof(RestExchangeOptions.HttpVersion), $"Invalid HTTP version {request.HttpVersion}: " + argumentException.Message);
return new WebCallResult<T>(null, null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
return FailHttpRequest<T>(request, null, sw.Elapsed, null, error);
}
throw;
@@ -629,7 +579,7 @@ namespace CryptoExchange.Net.Clients
{
// Unsupported HTTP version error dotnet code
var error = ArgumentError.Invalid(nameof(RestExchangeOptions.HttpVersion), $"Invalid HTTP version {request.HttpVersion}: " + notSupportedException.Message);
return new WebCallResult<T>(null, null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
return FailHttpRequest<T>(request, null, sw.Elapsed, null, error);
}
throw;
@@ -641,16 +591,55 @@ namespace CryptoExchange.Net.Clients
}
}
private HttpResult<T> OkHttpRequest<T>(IRequest request, IResponse response, TimeSpan elapsed, string? originalData, T result)
{
return HttpResult.Ok(
Exchange,
response.StatusCode,
response.HttpVersion,
response.ResponseHeaders,
elapsed,
response.ContentLength,
originalData,
request.RequestId,
request.Uri.ToString(),
request.Content,
request.Method,
request.GetHeaders(),
ResultDataSource.Server,
result);
}
private HttpResult<T> FailHttpRequest<T>(IRequest request, IResponse? response, TimeSpan elapsed, string? originalData, Error error, T? result = default)
{
return HttpResult.Fail<T>(
Exchange,
response?.StatusCode,
response?.HttpVersion,
response?.ResponseHeaders,
elapsed,
response?.ContentLength,
originalData,
request.RequestId,
request.Uri.ToString(),
request.Content,
request.Method,
request.GetHeaders(),
ResultDataSource.Server,
error,
result);
}
/// <summary>
/// Can be used to indicate that a request should be retried. Defaults to false. Make sure to retry a max number of times (based on the the tries parameter) or the request will retry forever.
/// Note that this is always called; even when the request might be successful
/// </summary>
/// <typeparam name="T">WebCallResult type parameter</typeparam>
/// <typeparam name="T">HttpResult type parameter</typeparam>
/// <param name="gate">The rate limit gate the call used</param>
/// <param name="callResult">The result of the call</param>
/// <param name="tries">The current try number</param>
/// <returns>True if call should retry, false if the call should return</returns>
protected virtual async ValueTask<bool> ShouldRetryRequestAsync<T>(IRateLimitGate? gate, WebCallResult<T> callResult, int tries)
protected virtual async ValueTask<bool> ShouldRetryRequestAsync<T>(IRateLimitGate? gate, HttpResult<T> callResult, int tries)
{
if (tries >= 2)
// Only retry once
@@ -681,7 +670,7 @@ namespace CryptoExchange.Net.Clients
/// <param name="request">The request to set the parameters on</param>
/// <param name="parameters">The parameters to set</param>
/// <param name="contentType">The content type of the data</param>
protected virtual void WriteParamBody(IRequest request, IDictionary<string, object> parameters, string contentType)
protected virtual void WriteParamBody(IRequest request, Parameters parameters, string contentType)
{
if (contentType == Constants.JsonContentHeader)
{
@@ -691,8 +680,13 @@ namespace CryptoExchange.Net.Clients
// 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);
if (parameters.BodyValue != null)
{
if (parameters.BodyValue is string bodyString)
stringData = bodyString;
else
stringData = stringSerializer.Serialize(parameters.BodyValue);
}
else
stringData = stringSerializer.Serialize(parameters);
request.SetContent(stringData, RequestBodyContentEncoding, contentType);
@@ -705,24 +699,11 @@ namespace CryptoExchange.Net.Clients
}
}
/// <summary>
/// Create the parameter IDictionary
/// </summary>
/// <param name="parameters"></param>
/// <returns></returns>
protected internal IDictionary<string, object> CreateParameterDictionary(IDictionary<string, object> parameters)
{
if (!OrderParameters)
return parameters;
return new SortedDictionary<string, object>(parameters, ParameterOrderComparer);
}
/// <summary>
/// Retrieve the server time for the purpose of syncing time between client and server to prevent authentication issues
/// </summary>
/// <returns>Server time</returns>
protected virtual Task<WebCallResult<DateTime>> GetServerTimestampAsync() => throw new NotImplementedException();
protected virtual Task<HttpResult<DateTime>> GetServerTimestampAsync() => throw new NotImplementedException();
private async ValueTask CheckTimeSync(int requestId, RequestDefinition definition)
{
@@ -757,7 +738,7 @@ namespace CryptoExchange.Net.Clients
return;
var localTime = DateTime.UtcNow;
WebCallResult<DateTime> result;
HttpResult<DateTime> result;
try
{
result = await GetServerTimestampAsync().ConfigureAwait(false);
@@ -767,7 +748,7 @@ namespace CryptoExchange.Net.Clients
throw new ArgumentException("AutoTimestamp is not available for this API");
}
if (!result)
if (!result.Success)
{
_logger.LogWarning("Failed to determine time offset between client and server, timestamping might fail");
return;
@@ -778,7 +759,7 @@ namespace CryptoExchange.Net.Clients
// 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)
if (!result.Success)
{
_logger.LogWarning("Failed to determine time offset between client and server, timestamping might fail");
return;
@@ -845,12 +826,14 @@ namespace CryptoExchange.Net.Clients
/// ctor
/// </summary>
protected RestApiClient(
ILogger logger,
ILoggerFactory? loggerFactory,
string exchangeName,
HttpClient? httpClient,
string baseAddress,
RestExchangeOptions options,
RestApiOptions apiOptions) : base(
logger,
loggerFactory,
exchangeName,
httpClient,
baseAddress,
options,
@@ -877,12 +860,14 @@ namespace CryptoExchange.Net.Clients
/// ctor
/// </summary>
protected RestApiClient(
ILogger logger,
ILoggerFactory? loggerFactory,
string exchangeName,
HttpClient? httpClient,
string baseAddress,
RestExchangeOptions<TEnvironment, TApiCredentials> options,
RestApiOptions apiOptions) : base(
logger,
loggerFactory,
exchangeName,
httpClient,
baseAddress,
options,
@@ -912,13 +897,18 @@ namespace CryptoExchange.Net.Clients
where TAuthenticationProvider : AuthenticationProvider<TApiCredentials>
where TEnvironment : TradeEnvironment
{
private bool _authProviderInitialized = false;
private TAuthenticationProvider? _authenticationProvider;
/// <summary>
/// Auth provider initialized field
/// </summary>
protected bool _authProviderInitialized = false;
/// <summary>
/// Auth provider field
/// </summary>
protected TAuthenticationProvider? _authenticationProvider;
/// <summary>
/// The authentication provider for this API client. (null if no credentials are set)
/// </summary>
public TAuthenticationProvider? AuthenticationProvider
public virtual TAuthenticationProvider? AuthenticationProvider
{
get
{
@@ -932,7 +922,7 @@ namespace CryptoExchange.Net.Clients
return _authenticationProvider;
}
internal set => _authenticationProvider = value;
protected internal set => _authenticationProvider = value;
}
/// <inheritdoc />
@@ -942,12 +932,14 @@ namespace CryptoExchange.Net.Clients
/// ctor
/// </summary>
protected RestApiClient(
ILogger logger,
ILoggerFactory? loggerFactory,
string exchangeName,
HttpClient? httpClient,
string baseAddress,
RestExchangeOptions<TEnvironment, TApiCredentials> options,
RestApiOptions apiOptions) : base(
logger,
loggerFactory,
exchangeName,
httpClient,
baseAddress,
options,
+239 -193
View File
@@ -15,6 +15,7 @@ using CryptoExchange.Net.Sockets.Default.Interfaces;
using CryptoExchange.Net.Sockets.HighPerf;
using CryptoExchange.Net.Sockets.HighPerf.Interfaces;
using CryptoExchange.Net.Sockets.Interfaces;
using CryptoExchange.Net.TokenManagement;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Concurrent;
@@ -71,11 +72,6 @@ namespace CryptoExchange.Net.Clients
/// </summary>
protected List<SystemSubscription> systemSubscriptions = new();
/// <summary>
/// If a message is received on the socket which is not handled by a handler this boolean determines whether this logs an error message
/// </summary>
protected internal bool UnhandledMessageExpected { get; set; }
/// <summary>
/// The rate limiters
/// </summary>
@@ -153,21 +149,26 @@ namespace CryptoExchange.Net.Clients
/// Configured environment name
/// </summary>
public abstract string EnvironmentName { get; }
private int _isDisposed;
#endregion
/// <summary>
/// ctor
/// </summary>
/// <param name="logger">log</param>
/// <param name="loggerFactory">Logger factory</param>
/// <param name="exchangeName">Exchange name</param>
/// <param name="options">Client options</param>
/// <param name="baseAddress">Base address for this API client</param>
/// <param name="apiOptions">The Api client options</param>
public SocketApiClient(
ILogger logger,
ILoggerFactory? loggerFactory,
string exchangeName,
string baseAddress,
SocketExchangeOptions options,
SocketApiOptions apiOptions)
: base(logger,
: base(loggerFactory,
exchangeName,
apiOptions.OutputOriginalData ?? options.OutputOriginalData,
baseAddress,
options,
@@ -216,7 +217,7 @@ namespace CryptoExchange.Net.Clients
/// <param name="interval"></param>
/// <param name="queryDelegate"></param>
/// <param name="callback"></param>
protected virtual void RegisterPeriodicQuery(string identifier, TimeSpan interval, Func<ISocketConnection, Query> queryDelegate, Action<SocketConnection, CallResult>? callback)
protected virtual void RegisterPeriodicQuery(string identifier, TimeSpan interval, Func<ISocketConnection, Query> queryDelegate, Action<SocketConnection, WebSocketResult>? callback)
{
PeriodicTaskRegistrations.Add(new PeriodicTaskRegistration
{
@@ -233,7 +234,7 @@ namespace CryptoExchange.Net.Clients
/// <param name="subscription">The subscription</param>
/// <param name="ct">Cancellation token for closing this subscription</param>
/// <returns></returns>
protected virtual Task<CallResult<UpdateSubscription>> SubscribeAsync(Subscription subscription, CancellationToken ct)
protected virtual Task<WebSocketResult<UpdateSubscription>> SubscribeAsync(Subscription subscription, CancellationToken ct)
{
return SubscribeAsync(BaseAddress, subscription, ct);
}
@@ -245,86 +246,102 @@ namespace CryptoExchange.Net.Clients
/// <param name="subscription">The subscription</param>
/// <param name="ct">Cancellation token for closing this subscription</param>
/// <returns></returns>
protected virtual async Task<CallResult<UpdateSubscription>> SubscribeAsync(string url, Subscription subscription, CancellationToken ct)
protected virtual async Task<WebSocketResult<UpdateSubscription>> SubscribeAsync(string url, Subscription subscription, CancellationToken ct)
{
if (_disposing)
return new CallResult<UpdateSubscription>(new InvalidOperationError("Client disposed, can't subscribe"));
if (subscription.Authenticated && GetAuthenticationProvider() == null)
{
_logger.LogWarning("Failed to subscribe, private subscription but no API credentials set");
return new CallResult<UpdateSubscription>(new NoApiCredentialsError());
}
if (subscription.IndividualSubscriptionCount > MaxIndividualSubscriptionsPerConnection)
return new CallResult<UpdateSubscription>(ArgumentError.Invalid("subscriptions", $"Max number of subscriptions in a single call is {MaxIndividualSubscriptionsPerConnection}"));
SocketConnection socketConnection;
var released = false;
// Wait for a semaphore here, so we only connect 1 socket at a time.
// This is necessary for being able to see if connections can be combined
bool successResult = false;
try
{
await semaphoreSlim.WaitAsync(ct).ConfigureAwait(false);
}
catch (OperationCanceledException tce)
{
return new CallResult<UpdateSubscription>(new CancellationRequestedError(tce));
}
if (_disposed)
return WebSocketResult.Fail<UpdateSubscription>(Exchange, new InvalidOperationError("Client disposed, can't subscribe"));
try
{
while (true)
if (subscription.Authenticated && GetAuthenticationProvider() == null)
{
// Get a new or existing socket connection
var socketResult = await GetSocketConnection(url, subscription.Authenticated, false, ct, subscription.Topic, subscription.IndividualSubscriptionCount).ConfigureAwait(false);
if (!socketResult)
return socketResult.As<UpdateSubscription>(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<UpdateSubscription>(connectResult.Error!);
break;
_logger.LogWarning("Failed to subscribe, private subscription but no API credentials set");
return WebSocketResult.Fail<UpdateSubscription>(Exchange, new NoApiCredentialsError());
}
if (subscription.IndividualSubscriptionCount > MaxIndividualSubscriptionsPerConnection)
return WebSocketResult.Fail<UpdateSubscription>(Exchange, ArgumentError.Invalid("subscriptions", $"Max number of subscriptions in a single call is {MaxIndividualSubscriptionsPerConnection}"));
SocketConnection socketConnection;
var released = false;
// Wait for a semaphore here, so we only connect 1 socket at a time.
// This is necessary for being able to see if connections can be combined
try
{
await semaphoreSlim.WaitAsync(ct).ConfigureAwait(false);
}
catch (OperationCanceledException tce)
{
return WebSocketResult.Fail<UpdateSubscription>(Exchange, new CancellationRequestedError(tce));
}
try
{
while (true)
{
// Get a new or existing socket connection
var socketResult = await GetSocketConnection(url, subscription.Authenticated, false, ct, subscription.Topic, subscription.IndividualSubscriptionCount).ConfigureAwait(false);
if (!socketResult.Success)
return WebSocketResult.Fail<UpdateSubscription>(Exchange, socketResult.Error);
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.Success)
return WebSocketResult.Fail<UpdateSubscription>(Exchange, connectResult.Error!);
break;
}
}
finally
{
if (!released)
semaphoreSlim.Release();
}
if (socketConnection.PausedActivity)
{
_logger.HasBeenPausedCantSubscribeAtThisMoment(socketConnection.SocketId);
return WebSocketResult.Fail<UpdateSubscription>(Exchange, new ServerError(new ErrorInfo(ErrorType.WebsocketPaused, "Socket is paused")));
}
var subscribeResult = await socketConnection.TrySubscribeAsync(subscription, true, ct).ConfigureAwait(false);
if (!subscribeResult.Success)
return WebSocketResult.Fail<UpdateSubscription>(Exchange, subscribeResult.Error!);
successResult = true;
_logger.SubscriptionCompletedSuccessfully(socketConnection.SocketId, subscription.Id);
return WebSocketResult.Ok(
Exchange,
socketConnection.SocketId,
subscribeResult.ResponseTime!.Value,
subscribeResult.RequestId!.Value,
subscribeResult.Url,
new UpdateSubscription(socketConnection, subscription));
}
finally
{
if (!released)
semaphoreSlim.Release();
if (!successResult && subscription.TokenLease != null)
_ = subscription.TokenLease.ReleaseAsync();
}
if (socketConnection.PausedActivity)
{
_logger.HasBeenPausedCantSubscribeAtThisMoment(socketConnection.SocketId);
return new CallResult<UpdateSubscription>(new ServerError(new ErrorInfo(ErrorType.WebsocketPaused, "Socket is paused")));
}
var subscribeResult = await socketConnection.TrySubscribeAsync(subscription, true, ct).ConfigureAwait(false);
if (!subscribeResult)
return new CallResult<UpdateSubscription>(subscribeResult.Error!);
_logger.SubscriptionCompletedSuccessfully(socketConnection.SocketId, subscription.Id);
return new CallResult<UpdateSubscription>(new UpdateSubscription(socketConnection, subscription));
}
/// <summary>
@@ -335,14 +352,14 @@ namespace CryptoExchange.Net.Clients
/// <param name="connectionFactory">The factory for creating a socket connection</param>
/// <param name="ct">Cancellation token for closing this subscription</param>
/// <returns></returns>
protected virtual async Task<CallResult<HighPerfUpdateSubscription>> SubscribeHighPerfAsync<TUpdateType>(
protected virtual async Task<WebSocketResult<HighPerfUpdateSubscription>> SubscribeHighPerfAsync<TUpdateType>(
string url,
HighPerfSubscription<TUpdateType> subscription,
IHighPerfConnectionFactory connectionFactory,
CancellationToken ct)
{
if (_disposing)
return new CallResult<HighPerfUpdateSubscription>(new InvalidOperationError("Client disposed, can't subscribe"));
if (_disposed)
return WebSocketResult.Fail<HighPerfUpdateSubscription>(Exchange, new InvalidOperationError("Client disposed, can't subscribe"));
HighPerfSocketConnection<TUpdateType> socketConnection;
var released = false;
@@ -354,7 +371,7 @@ namespace CryptoExchange.Net.Clients
}
catch (OperationCanceledException tce)
{
return new CallResult<HighPerfUpdateSubscription>(new CancellationRequestedError(tce));
return WebSocketResult.Fail<HighPerfUpdateSubscription>(Exchange, new CancellationRequestedError(tce));
}
try
@@ -363,8 +380,8 @@ namespace CryptoExchange.Net.Clients
{
// Get a new or existing socket connection
var socketResult = await GetHighPerfSocketConnection<TUpdateType>(url, connectionFactory, ct).ConfigureAwait(false);
if (!socketResult)
return socketResult.As<HighPerfUpdateSubscription>(null);
if (!socketResult.Success)
return WebSocketResult.Fail<HighPerfUpdateSubscription>(Exchange, socketResult.Error);
socketConnection = socketResult.Data;
@@ -383,11 +400,9 @@ namespace CryptoExchange.Net.Clients
released = true;
}
var needsConnecting = !socketConnection.Connected;
var connectResult = await ConnectIfNeededAsync(socketConnection, false, ct).ConfigureAwait(false);
if (!connectResult)
return new CallResult<HighPerfUpdateSubscription>(connectResult.Error!);
if (!connectResult.Success)
return WebSocketResult.Fail<HighPerfUpdateSubscription>(Exchange, connectResult.Error!);
break;
}
@@ -403,10 +418,10 @@ namespace CryptoExchange.Net.Clients
{
// Send the request and wait for answer
var sendResult = await socketConnection.SendAsync(subRequest).ConfigureAwait(false);
if (!sendResult)
if (!sendResult.Success)
{
await socketConnection.CloseAsync().ConfigureAwait(false);
return new CallResult<HighPerfUpdateSubscription>(sendResult.Error!);
return WebSocketResult.Fail<HighPerfUpdateSubscription>(Exchange, sendResult.Error!);
}
}
@@ -420,7 +435,13 @@ namespace CryptoExchange.Net.Clients
}
_logger.SubscriptionCompletedSuccessfully(socketConnection.SocketId, subscription.Id);
return new CallResult<HighPerfUpdateSubscription>(new HighPerfUpdateSubscription(socketConnection, subscription));
return WebSocketResult.Ok(
Exchange,
socketConnection.SocketId,
default,
default,
socketConnection.ConnectionUri.ToString(),
new HighPerfUpdateSubscription(socketConnection, subscription));
}
/// <summary>
@@ -430,7 +451,7 @@ namespace CryptoExchange.Net.Clients
/// <param name="query">The query</param>
/// <param name="ct">Cancellation token</param>
/// <returns></returns>
protected virtual Task<CallResult<THandlerResponse>> QueryAsync<THandlerResponse>(Query<THandlerResponse> query, CancellationToken ct = default)
protected virtual Task<QueryResult<THandlerResponse>> QueryAsync<THandlerResponse>(Query<THandlerResponse> query, CancellationToken ct = default)
{
return QueryAsync(BaseAddress, query, ct);
}
@@ -443,13 +464,13 @@ namespace CryptoExchange.Net.Clients
/// <param name="query">The query</param>
/// <param name="ct">Cancellation token</param>
/// <returns></returns>
protected virtual async Task<CallResult<THandlerResponse>> QueryAsync<THandlerResponse>(string url, Query<THandlerResponse> query, CancellationToken ct = default)
protected virtual async Task<QueryResult<THandlerResponse>> QueryAsync<THandlerResponse>(string url, Query<THandlerResponse> query, CancellationToken ct = default)
{
if (_disposing)
return new CallResult<THandlerResponse>(new InvalidOperationError("Client disposed, can't query"));
if (_disposed)
return QueryResult.Fail<THandlerResponse>(Exchange, new InvalidOperationError("Client disposed, can't query"));
if (ct.IsCancellationRequested)
return new CallResult<THandlerResponse>(new CancellationRequestedError());
return QueryResult.Fail<THandlerResponse>(Exchange, new CancellationRequestedError());
SocketConnection socketConnection;
var released = false;
@@ -457,8 +478,8 @@ namespace CryptoExchange.Net.Clients
try
{
var socketResult = await GetSocketConnection(url, query.Authenticated, true, ct).ConfigureAwait(false);
if (!socketResult)
return socketResult.As<THandlerResponse>(default);
if (!socketResult.Success)
return QueryResult.Fail<THandlerResponse>(Exchange, socketResult.Error);
socketConnection = socketResult.Data;
@@ -470,8 +491,8 @@ namespace CryptoExchange.Net.Clients
}
var connectResult = await ConnectIfNeededAsync(socketConnection, query.Authenticated, ct).ConfigureAwait(false);
if (!connectResult)
return new CallResult<THandlerResponse>(connectResult.Error!);
if (!connectResult.Success)
return QueryResult.Fail<THandlerResponse>(Exchange, connectResult.Error!);
}
finally
{
@@ -482,11 +503,11 @@ namespace CryptoExchange.Net.Clients
if (socketConnection.PausedActivity)
{
_logger.HasBeenPausedCantSendQueryAtThisMoment(socketConnection.SocketId);
return new CallResult<THandlerResponse>(new ServerError(new ErrorInfo(ErrorType.WebsocketPaused, "Socket is paused")));
return QueryResult.Fail<THandlerResponse>(Exchange, new ServerError(new ErrorInfo(ErrorType.WebsocketPaused, "Socket is paused")));
}
if (ct.IsCancellationRequested)
return new CallResult<THandlerResponse>(new CancellationRequestedError());
return QueryResult.Fail<THandlerResponse>(Exchange, new CancellationRequestedError());
return await socketConnection.SendAndWaitQueryAsync(query, ct).ConfigureAwait(false);
}
@@ -501,23 +522,23 @@ namespace CryptoExchange.Net.Clients
protected virtual async Task<CallResult> ConnectIfNeededAsync(ISocketConnection socket, bool authenticated, CancellationToken ct)
{
if (socket.Connected)
return CallResult.SuccessResult;
return CallResult.Ok();
var connectResult = await ConnectSocketAsync(socket, ct).ConfigureAwait(false);
if (!connectResult)
if (!connectResult.Success)
return connectResult;
if (ClientOptions.DelayAfterConnect != TimeSpan.Zero)
await Task.Delay(ClientOptions.DelayAfterConnect).ConfigureAwait(false);
if (!authenticated || socket.Authenticated)
return CallResult.SuccessResult;
return CallResult.Ok();
if (socket is not SocketConnection sc)
throw new InvalidOperationException("HighPerfSocketConnection not supported for authentication");
var result = await AuthenticateSocketAsync(sc).ConfigureAwait(false);
if (!result)
if (!result.Success)
await socket.CloseAsync().ConfigureAwait(false);
return result;
@@ -531,29 +552,28 @@ namespace CryptoExchange.Net.Clients
public virtual async Task<CallResult> AuthenticateSocketAsync(SocketConnection socket)
{
if (GetAuthenticationProvider() == null)
return new CallResult(new NoApiCredentialsError());
return CallResult.Fail(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)
if (!result.Success)
{
_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)!;
return CallResult.Fail(result.Error)!;
}
_logger.Authenticated(socket.SocketId);
}
socket.Authenticated = true;
return CallResult.SuccessResult;
return CallResult.Ok();
}
/// <summary>
@@ -582,7 +602,7 @@ namespace CryptoExchange.Net.Clients
/// <returns></returns>
protected virtual Task<CallResult<string?>> GetConnectionUrlAsync(string address, bool authentication)
{
return Task.FromResult(new CallResult<string?>(address));
return Task.FromResult(CallResult.Ok<string?>(address));
}
/// <summary>
@@ -602,7 +622,22 @@ namespace CryptoExchange.Net.Clients
/// <returns></returns>
protected internal virtual Task<CallResult> RevitalizeRequestAsync(Subscription subscription)
{
return Task.FromResult(CallResult.SuccessResult);
return Task.FromResult(CallResult.Ok());
}
/// <summary>
/// Whether the connection can be used for a new subscription or query with the provided parameters
/// </summary>
/// <param name="connection">The connection to check</param>
/// <param name="address">The address set by the request</param>
/// <param name="authenticated">Whether the request needs an authenticated connection</param>
/// <param name="topic">Topic of the request</param>
/// <returns>True if connection can be used</returns>
protected virtual bool ConnectionCanBeUsedFor(SocketConnection connection, string address, bool authenticated, string? topic = null)
{
return connection.ConnectionUriString.Equals(address.TrimEnd('/'), StringComparison.Ordinal)
&& connection.ApiClient.ClientName.Equals(ClientName, StringComparison.Ordinal)
&& (AllowTopicsOnTheSameConnection || !connection.Topics.Contains(topic));
}
/// <summary>
@@ -623,24 +658,20 @@ namespace CryptoExchange.Net.Clients
string? topic = null,
int individualSubscriptionCount = 1)
{
var socketQuery = _socketConnections.Where(s => s.Value.Tag.TrimEnd('/') == address.TrimEnd('/')
&& s.Value.ApiClient.GetType() == GetType()
&& (AllowTopicsOnTheSameConnection || !s.Value.Topics.Contains(topic)))
.Select(x => x.Value)
.ToList();
var socketQuery = _socketConnections.Where(s => ConnectionCanBeUsedFor(s.Value, address, authenticated, topic)).Select(x => x.Value); // Don't ToList this so the query is executed again when called
// If all current socket connections are reconnecting or resubscribing wait for that to finish as we can probably use the existing connection
var delayStart = DateTime.UtcNow;
var delayed = false;
while (socketQuery.Count >= 1 && socketQuery.All(x => x.Status == SocketStatus.Reconnecting || x.Status == SocketStatus.Resubscribing))
while (socketQuery.Count() >= 1 && socketQuery.All(x => x.Status == SocketStatus.Reconnecting || x.Status == SocketStatus.Resubscribing))
{
if (DateTime.UtcNow - delayStart > TimeSpan.FromSeconds(10))
{
if (socketQuery.Count >= 1 && socketQuery.All(x => x.Status == SocketStatus.Reconnecting || x.Status == SocketStatus.Resubscribing))
if (socketQuery.Count() >= 1 && socketQuery.All(x => x.Status == SocketStatus.Reconnecting || x.Status == SocketStatus.Resubscribing))
{
// If after this time we still trying to reconnect/reprocess there is some issue in the connection
_logger.TimeoutWaitingForReconnectingSocket();
return new CallResult<SocketConnection>(new CantConnectError());
return CallResult.Fail<SocketConnection>(new CantConnectError());
}
break;
@@ -650,7 +681,7 @@ namespace CryptoExchange.Net.Clients
try { await Task.Delay(50, ct).ConfigureAwait(false); } catch (Exception) { }
if (ct.IsCancellationRequested)
return new CallResult<SocketConnection>(new CancellationRequestedError());
return CallResult.Fail<SocketConnection>(new CancellationRequestedError());
}
if (delayed)
@@ -660,58 +691,42 @@ namespace CryptoExchange.Net.Clients
&& (s.Authenticated == authenticated || !authenticated)
&& s.Connected).ToList();
SocketConnection? connection;
if (!dedicatedRequestConnection)
{
connection = socketQuery.Where(s => !s.DedicatedRequestConnection.IsDedicatedRequestConnection).OrderBy(s => s.UserSubscriptionCount).FirstOrDefault();
}
else
bool maxConnectionsReached = _socketConnections.Count >= (ApiOptions.MaxSocketConnections ?? ClientOptions.MaxSocketConnections);
SocketConnection? connection = null;
if (dedicatedRequestConnection)
{
connection = socketQuery.Where(s => s.DedicatedRequestConnection.IsDedicatedRequestConnection).FirstOrDefault();
if (connection != null && !connection.DedicatedRequestConnection.Authenticated)
// Mark dedicated request connection as authenticated if the request is authenticated
connection.DedicatedRequestConnection.Authenticated = authenticated;
if (connection == null)
// Fall back to an existing connection if there is no dedicated request connection available
connection = socketQuery.OrderBy(s => s.UserSubscriptionCount).FirstOrDefault();
}
bool maxConnectionsReached = _socketConnections.Count >= (ApiOptions.MaxSocketConnections ?? ClientOptions.MaxSocketConnections);
if (connection == null)
// Use an eligible non-dedicated connection for subscriptions, or as fallback when no dedicated request connection is available
connection = socketQuery
.Where(s => !s.DedicatedRequestConnection.IsDedicatedRequestConnection)
.Where(s => IsConnectionEligible(s, individualSubscriptionCount, maxConnectionsReached))
.OrderBy(s => s.UserSubscriptionCount)
.FirstOrDefault();
if (connection != null)
{
bool lessThanBatchSubCombineTarget = connection.UserSubscriptionCount < ClientOptions.SocketSubscriptionsCombineTarget;
bool lessThanIndividualSubCombineTarget = connection.Subscriptions.Sum(x => x.IndividualSubscriptionCount) < ClientOptions.SocketIndividualSubscriptionCombineTarget;
if ((lessThanBatchSubCombineTarget && lessThanIndividualSubCombineTarget)
|| maxConnectionsReached)
{
// Use existing socket if it has less than target connections OR it has the least connections and we can't make new
// If there is a max subscriptions per connection limit also only use existing if the new subscription doesn't go over the limit
if (MaxIndividualSubscriptionsPerConnection == null)
return new CallResult<SocketConnection>(connection);
var currentCount = connection.Subscriptions.Sum(x => x.IndividualSubscriptionCount);
if (currentCount + individualSubscriptionCount <= MaxIndividualSubscriptionsPerConnection)
return new CallResult<SocketConnection>(connection);
}
}
return CallResult.Ok(connection);
if (maxConnectionsReached)
return new CallResult<SocketConnection>(new InvalidOperationError("Max amount of socket connections reached"));
return CallResult.Fail<SocketConnection>(new InvalidOperationError("Max amount of socket connections reached"));
var connectionAddress = await GetConnectionUrlAsync(address, authenticated).ConfigureAwait(false);
if (!connectionAddress)
if (!connectionAddress.Success)
{
_logger.FailedToDetermineConnectionUrl(connectionAddress.Error?.ToString());
return connectionAddress.As<SocketConnection>(null);
_logger.FailedToDetermineConnectionUrl(connectionAddress.Error.ToString());
return CallResult.Fail<SocketConnection>(connectionAddress.Error);
}
if (connectionAddress.Data != address)
_logger.ConnectionAddressSetTo(connectionAddress.Data!);
// Create new socket connection
var socketConnection = new SocketConnection(_logger, SocketFactory, GetWebSocketParameters(connectionAddress.Data!), this, address);
var socketConnection = new SocketConnection(_logger, SocketFactory, GetWebSocketParameters(connectionAddress.Data!), this);
socketConnection.ConnectRateLimitedAsync += HandleConnectRateLimitedAsync;
if (dedicatedRequestConnection)
{
@@ -728,7 +743,7 @@ namespace CryptoExchange.Net.Clients
foreach (var systemSubscription in systemSubscriptions)
socketConnection.AddSubscription(systemSubscription);
return new CallResult<SocketConnection>(socketConnection);
return CallResult.Ok(socketConnection);
}
@@ -745,21 +760,36 @@ namespace CryptoExchange.Net.Clients
CancellationToken ct)
{
var connectionAddress = await GetConnectionUrlAsync(address, false).ConfigureAwait(false);
if (!connectionAddress)
if (!connectionAddress.Success)
{
_logger.FailedToDetermineConnectionUrl(connectionAddress.Error?.ToString());
return connectionAddress.As<HighPerfSocketConnection<TUpdateType>>(null);
_logger.FailedToDetermineConnectionUrl(connectionAddress.Error.ToString());
return CallResult.Fail<HighPerfSocketConnection<TUpdateType>>(connectionAddress.Error);
}
if (connectionAddress.Data != address)
_logger.ConnectionAddressSetTo(connectionAddress.Data!);
// Create new socket connection
var socketConnection = connectionFactory.CreateHighPerfConnection<TUpdateType>(_logger, SocketFactory, GetWebSocketParameters(connectionAddress.Data!), this, address);
var socketConnection = connectionFactory.CreateHighPerfConnection<TUpdateType>(_logger, SocketFactory, GetWebSocketParameters(connectionAddress.Data!), this);
foreach (var ptg in PeriodicTaskRegistrations)
socketConnection.QueryPeriodic(ptg.Identifier, ptg.Interval, (con) => ptg.QueryDelegate(con).Request);
return new CallResult<HighPerfSocketConnection<TUpdateType>>(socketConnection);
return CallResult.Ok(socketConnection);
}
private bool IsConnectionEligible(SocketConnection socketConnection, int individualSubscriptionCount, bool maxConnectionsReached)
{
var currentIndividualSubscriptionCount = socketConnection.Subscriptions.Sum(x => x.IndividualSubscriptionCount);
bool lessThanBatchSubCombineTarget = socketConnection.UserSubscriptionCount < ClientOptions.SocketSubscriptionsCombineTarget;
// Include the incoming batch so batched subscriptions cannot overshoot the configured socket target.
bool lessThanIndividualSubCombineTarget = currentIndividualSubscriptionCount + individualSubscriptionCount <= ClientOptions.SocketIndividualSubscriptionCombineTarget;
if ((!lessThanBatchSubCombineTarget || !lessThanIndividualSubCombineTarget)
&& !maxConnectionsReached)
return false;
return MaxIndividualSubscriptionsPerConnection == null
|| currentIndividualSubscriptionCount + individualSubscriptionCount <= MaxIndividualSubscriptionsPerConnection;
}
/// <summary>
@@ -793,7 +823,7 @@ namespace CryptoExchange.Net.Clients
protected virtual async Task<CallResult> ConnectSocketAsync(ISocketConnection socketConnection, CancellationToken ct)
{
var connectResult = await socketConnection.ConnectAsync(ct).ConfigureAwait(false);
if (connectResult)
if (connectResult.Success)
{
if (socketConnection is SocketConnection sc)
_socketConnections.TryAdd(socketConnection.SocketId, sc);
@@ -877,7 +907,7 @@ namespace CryptoExchange.Net.Clients
_logger.UnsubscribingAll(sum);
var tasks = new List<Task>();
var socketList = _socketConnections.Values;
foreach (var connection in socketList)
{
@@ -916,15 +946,15 @@ namespace CryptoExchange.Net.Clients
foreach (var item in DedicatedConnectionConfigs)
{
var socketResult = await GetSocketConnection(item.SocketAddress, item.Authenticated, true, CancellationToken.None).ConfigureAwait(false);
if (!socketResult)
return socketResult.AsDataless();
if (!socketResult.Success)
return CallResult.Fail(socketResult.Error);
var connectResult = await ConnectIfNeededAsync(socketResult.Data, item.Authenticated, default).ConfigureAwait(false);
if (!connectResult)
return new CallResult(connectResult.Error!);
if (!connectResult.Success)
return CallResult.Fail(connectResult.Error!);
}
return CallResult.SuccessResult;
return CallResult.Ok();
}
/// <summary>
@@ -1006,23 +1036,28 @@ namespace CryptoExchange.Net.Clients
/// <summary>
/// Dispose the client
/// </summary>
public override void Dispose()
protected override void Dispose(bool disposing)
{
_disposing = true;
var tasks = new List<Task>();
if (Interlocked.CompareExchange(ref _isDisposed, 1, 0) == 0)
{
var socketList = _socketConnections.Values.Where(x => x.UserSubscriptionCount > 0 || x.Connected);
if (socketList.Any())
_logger.DisposingSocketClient();
if (!disposing)
return;
foreach (var connection in socketList)
var tasks = new List<Task>();
{
tasks.Add(connection.CloseAsync());
}
}
var socketList = _socketConnections.Values.Where(x => x.UserSubscriptionCount > 0 || x.Connected);
if (socketList.Any())
_logger.DisposingSocketClient();
semaphoreSlim?.Dispose();
base.Dispose();
foreach (var connection in socketList)
{
tasks.Add(connection.CloseAsync());
}
}
semaphoreSlim?.Dispose();
base.Dispose(disposing);
}
}
/// <summary>
@@ -1073,11 +1108,13 @@ namespace CryptoExchange.Net.Clients
/// ctor
/// </summary>
protected SocketApiClient(
ILogger logger,
ILoggerFactory? loggerFactory,
string exchangeName,
string baseAddress,
SocketExchangeOptions<TEnvironment> options,
SocketApiOptions apiOptions) : base(
logger,
loggerFactory,
exchangeName,
baseAddress,
options,
apiOptions)
@@ -1103,11 +1140,13 @@ namespace CryptoExchange.Net.Clients
/// ctor
/// </summary>
protected SocketApiClient(
ILogger logger,
ILoggerFactory? loggerFactory,
string exchangeName,
string baseAddress,
SocketExchangeOptions<TEnvironment, TApiCredentials> options,
SocketApiOptions apiOptions) : base(
logger,
loggerFactory,
exchangeName,
baseAddress,
options,
apiOptions)
@@ -1134,13 +1173,18 @@ namespace CryptoExchange.Net.Clients
where TApiCredentials : ApiCredentials
where TEnvironment : TradeEnvironment
{
private bool _authProviderInitialized = false;
private TAuthenticationProvider? _authenticationProvider;
/// <summary>
/// Auth provider initialized field
/// </summary>
protected bool _authProviderInitialized = false;
/// <summary>
/// Auth provider field
/// </summary>
protected TAuthenticationProvider? _authenticationProvider;
/// <summary>
/// The authentication provider for this API client. (null if no credentials are set)
/// </summary>
public TAuthenticationProvider? AuthenticationProvider
public virtual TAuthenticationProvider? AuthenticationProvider
{
get
{
@@ -1154,7 +1198,7 @@ namespace CryptoExchange.Net.Clients
return _authenticationProvider;
}
internal set => _authenticationProvider = value;
protected internal set => _authenticationProvider = value;
}
/// <inheritdoc />
@@ -1164,11 +1208,13 @@ namespace CryptoExchange.Net.Clients
/// ctor
/// </summary>
protected SocketApiClient(
ILogger logger,
ILoggerFactory? loggerFactory,
string exchangeName,
string baseAddress,
SocketExchangeOptions<TEnvironment, TApiCredentials> options,
SocketApiOptions apiOptions) : base(
logger,
loggerFactory,
exchangeName,
baseAddress,
options,
apiOptions)
@@ -0,0 +1,172 @@
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Interfaces.Clients;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.SharedApis;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Net.Http;
using System.Text;
namespace CryptoExchange.Net.Clients
{
/// <inheritdoc />
public abstract class UserClientProvider<TRestClient, TSocketClient, TRestOptions, TSocketOptions, TCredentials, TEnvironment>
where TRestClient : IRestClient<TCredentials>
where TSocketClient : ISocketClient<TCredentials>
where TRestOptions : RestExchangeOptions<TEnvironment, TCredentials>, new()
where TSocketOptions : SocketExchangeOptions<TEnvironment, TCredentials>, new()
where TCredentials : ApiCredentials
where TEnvironment : TradeEnvironment
{
private ConcurrentDictionary<string, TRestClient> _restClients = new ConcurrentDictionary<string, TRestClient>();
private ConcurrentDictionary<string, TSocketClient> _socketClients = new ConcurrentDictionary<string, TSocketClient>();
private readonly IOptions<TRestOptions> _restOptions;
private readonly IOptions<TSocketOptions> _socketOptions;
private readonly HttpClient _httpClient;
private readonly ILoggerFactory? _loggerFactory;
/// <inheritdoc />
public abstract string ExchangeName { get; }
/// <summary>
/// ctor
/// </summary>
public UserClientProvider(
HttpClient? httpClient,
ILoggerFactory? loggerFactory,
IOptions<TRestOptions> restOptions,
IOptions<TSocketOptions> socketOptions)
{
_httpClient = httpClient ?? new HttpClient();
_httpClient.Timeout = restOptions.Value.RequestTimeout;
_loggerFactory = loggerFactory;
_restOptions = restOptions;
_socketOptions = socketOptions;
}
private IOptions<TRestOptions> SetRestEnvironment(IOptions<TRestOptions> options, TEnvironment? environment)
{
if (environment == null)
return options;
var newRestClientOptions = new TRestOptions();
options.Value.Set(newRestClientOptions);
newRestClientOptions.Environment = environment;
return Options.Create(newRestClientOptions);
}
private IOptions<TSocketOptions> SetSocketEnvironment(IOptions<TSocketOptions> options, TEnvironment? environment)
{
if (environment == null)
return options;
var newSocketClientOptions = new TSocketOptions();
options.Value.Set(newSocketClientOptions);
newSocketClientOptions.Environment = environment;
return Options.Create(newSocketClientOptions);
}
/// <inheritdoc />
public void InitializeUserClient(string userIdentifier, TCredentials credentials, TEnvironment? environment = null)
{
CreateRestClient(userIdentifier, credentials, environment);
CreateSocketClient(userIdentifier, credentials, environment);
}
/// <inheritdoc />
public TRestClient GetRestClient(string userIdentifier, TCredentials? credentials = null, TEnvironment? environment = null)
{
if (!_restClients.TryGetValue(userIdentifier, out var client) || client.Disposed)
client = CreateRestClient(userIdentifier, credentials, environment);
return client;
}
/// <inheritdoc />
public TSocketClient GetSocketClient(string userIdentifier, TCredentials? credentials = null, TEnvironment? environment = null)
{
if (!_socketClients.TryGetValue(userIdentifier, out var client) || client.Disposed)
client = CreateSocketClient(userIdentifier, credentials, environment);
return client;
}
private TRestClient CreateRestClient(string userIdentifier, TCredentials? credentials, TEnvironment? environment)
{
var clientRestOptions = SetRestEnvironment(_restOptions, environment);
var client = ConstructRestClient(_httpClient, _loggerFactory, clientRestOptions);
if (credentials != null)
{
_restClients[userIdentifier] = client;
client.SetApiCredentials(credentials);
}
return client;
}
private TSocketClient CreateSocketClient(string userIdentifier, TCredentials? credentials, TEnvironment? environment)
{
var clientSocketOptions = SetSocketEnvironment(_socketOptions, environment);
var client = ConstructSocketClient(_loggerFactory, clientSocketOptions);
if (credentials != null)
{
_socketClients[userIdentifier] = client;
client.SetApiCredentials(credentials);
}
return client;
}
/// <summary>
/// Constructs a new instance of the rest client
/// </summary>
protected abstract TRestClient ConstructRestClient(
HttpClient client,
ILoggerFactory? loggerFactory,
IOptions<TRestOptions> options);
/// <summary>
/// Constructs a new instance of the socket client
/// </summary>
protected abstract TSocketClient ConstructSocketClient(
ILoggerFactory? loggerFactory,
IOptions<TSocketOptions> options);
/// <inheritdoc />
public void ClearUserClients(string userIdentifier)
{
_restClients.TryRemove(userIdentifier, out var restClient);
_socketClients.TryRemove(userIdentifier, out var socketClient);
restClient?.Dispose();
socketClient?.Dispose();
}
/// <inheritdoc />
public void Clear()
{
foreach (var client in _restClients.Values)
client.Dispose();
_restClients.Clear();
foreach (var client in _socketClients.Values)
client.Dispose();
_socketClients.Clear();
}
/// <summary>
/// Applies the provided options delegate to a new instance of the specified type.
/// </summary>
protected static T ApplyOptionsDelegate<T>(Action<T>? del) where T : new()
{
var opts = new T();
del?.Invoke(opts);
return opts;
}
}
}
@@ -56,14 +56,26 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
if (reader.TokenType == JsonTokenType.False)
return false;
var value = reader.TokenType switch
if (reader.TokenType == JsonTokenType.Number)
{
JsonTokenType.String => reader.GetString(),
JsonTokenType.Number => reader.GetInt16().ToString(),
_ => null
};
var number = reader.GetInt16();
if (number >= 1)
return true;
value = value?.ToLowerInvariant().Trim();
return false;
}
if (reader.TokenType == JsonTokenType.Null)
{
if (typeToConvert == typeof(bool))
LibraryHelpers.StaticLogger?.LogWarning("Received null bool value, but property type is not a nullable bool. Resolver: {Resolver}", options.TypeInfoResolver?.GetType()?.Name);
return default;
}
if (reader.TokenType != JsonTokenType.String)
throw new SerializationException($"Can't convert bool value for token type {reader.TokenType}");
var value = reader.GetString()?.ToLowerInvariant().Trim();
if (string.IsNullOrEmpty(value))
{
if (typeToConvert == typeof(bool))
@@ -73,12 +85,14 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
switch (value)
{
case "enabled":
case "true":
case "yes":
case "y":
case "1":
case "on":
return true;
case "disabled":
case "false":
case "no":
case "n":
@@ -88,7 +102,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
return false;
}
throw new SerializationException($"Can't convert bool value {value}");
throw new SerializationException($"Can't convert bool value, unknown string value: {value}");
}
}
@@ -16,17 +16,19 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
private const long _ticksPerSecond = TimeSpan.TicksPerMillisecond * 1000;
private const decimal _ticksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000m;
private const decimal _ticksPerNanosecond = TimeSpan.TicksPerMillisecond / 1000m / 1000;
private static Type _dateTimeType = typeof(DateTime);
private static Type _nullableDateTimeType = typeof(DateTime?);
/// <inheritdoc />
public override bool CanConvert(Type typeToConvert)
{
return typeToConvert == typeof(DateTime) || typeToConvert == typeof(DateTime?);
return typeToConvert == _dateTimeType || typeToConvert == _nullableDateTimeType;
}
/// <inheritdoc />
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
return typeToConvert == typeof(DateTime) ? new DateTimeConverterInner() : new NullableDateTimeConverterInner();
return typeToConvert == _dateTimeType ? new DateTimeConverterInner() : new NullableDateTimeConverterInner();
}
private class NullableDateTimeConverterInner : JsonConverter<DateTime?>
@@ -68,7 +70,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
{
if (reader.TokenType == JsonTokenType.Null)
{
if (typeToConvert == typeof(DateTime))
if (typeToConvert == _dateTimeType)
LibraryHelpers.StaticLogger?.LogWarning("DateTime value of null, but property is not nullable. Resolver: {Resolver}", options.TypeInfoResolver?.GetType()?.Name);
return default;
}
@@ -76,7 +78,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
if (reader.TokenType is JsonTokenType.Number)
{
var decValue = reader.GetDecimal();
if (decValue == 0 || decValue < 0)
if (decValue <= 0)
return default;
return ParseFromDecimal(decValue);
@@ -86,8 +88,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
var stringValue = reader.GetString();
if (string.IsNullOrWhiteSpace(stringValue)
|| stringValue!.Equals("-1", StringComparison.Ordinal)
|| stringValue!.Equals("0001-01-01T00:00:00Z", StringComparison.OrdinalIgnoreCase)
|| decimal.TryParse(stringValue, out var decVal) && decVal == 0)
|| stringValue!.Equals("0001-01-01T00:00:00Z", StringComparison.OrdinalIgnoreCase))
{
return default;
}
@@ -124,7 +125,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
/// <summary>
/// Parse a string value to datetime
/// </summary>
public static DateTime ParseFromString(string stringValue, string? resolverName)
public static DateTime? ParseFromString(string stringValue, string? resolverName)
{
if (stringValue!.Length == 12 && stringValue.StartsWith("202", StringComparison.OrdinalIgnoreCase))
{
@@ -197,6 +198,9 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc);
}
if (stringValue.EndsWith("+00:00:00"))
return DateTime.Parse(stringValue.Substring(0, stringValue.Length - 9), CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal);
return DateTime.Parse(stringValue, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal);
}
@@ -67,25 +67,14 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
#endif
: JsonConverter<T>, INullableConverterFactory where T : struct, Enum
{
class EnumMapping
{
public T Value { get; set; }
public string StringValue { get; set; }
public EnumMapping(T value, string stringValue)
{
Value = value;
StringValue = stringValue;
}
}
#if NET8_0_OR_GREATER
private static FrozenSet<EnumMapping>? _mappingToEnum = null;
private static FrozenDictionary<string, T>? _mappingToEnum = null;
private static FrozenDictionary<T, string>? _mappingToString = null;
private static bool RunOptimistic => true;
#else
private static List<EnumMapping>? _mappingToEnum = null;
private static Dictionary<string, T>? _mappingToEnum = null;
private static Dictionary<T, string>? _mappingToString = null;
// In NetStandard the `ValueTextEquals` method used is slower than just string comparing
@@ -205,7 +194,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
if (!_unknownValuesWarned.Contains(stringValue))
{
_unknownValuesWarned.Add(stringValue!);
LibraryHelpers.StaticLogger?.LogWarning($"Cannot map enum value. EnumType: {_enumType.FullName}, Value: {stringValue}, Known values: [{string.Join(", ", _mappingToEnum!.Select(m => $"{m.StringValue}: {m.Value}"))}]. If you think {stringValue} should be added please open an issue on the Github repo");
LibraryHelpers.StaticLogger?.LogWarning($"Cannot map enum value. EnumType: {_enumType.FullName}, Value: {stringValue}, Known values: [{string.Join(", ", _mappingToEnum!.Select(m => $"{m.Key}: {m.Value}"))}]. If you think {stringValue} should be added please open an issue on the Github repo");
}
}
@@ -217,7 +206,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
if (!_notOptimalValuesWarned.Contains(stringValue))
{
_notOptimalValuesWarned.Add(stringValue!);
LibraryHelpers.StaticLogger?.LogTrace($"Enum mapping sub-optimal. EnumType: {_enumType.FullName}, Value: {stringValue}, Known values: [{string.Join(", ", _mappingToEnum!.Select(m => $"{m.StringValue}: {m.Value}"))}]");
LibraryHelpers.StaticLogger?.LogTrace($"Enum mapping sub-optimal. EnumType: {_enumType.FullName}, Value: {stringValue}, Known values: [{string.Join(", ", _mappingToEnum!.Select(m => $"{m.Key}: {m.Value}"))}]");
}
}
@@ -252,7 +241,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
optimisticCheckDone = true;
foreach (var item in _mappingToEnum!)
{
if (reader.ValueTextEquals(item.StringValue))
if (reader.ValueTextEquals(item.Key))
return item.Value;
}
@@ -261,43 +250,44 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
private static bool GetValue(string value, bool optimisticCheckDone, out T? result)
{
if (_mappingToEnum != null)
if (_mappingToEnum == null)
throw new InvalidOperationException("Enum mapping not initialized");
T? mapping = null;
// If we tried the optimistic path first we already know its not case match
if (!optimisticCheckDone)
{
EnumMapping? mapping = null;
// If we tried the optimistic path first we already know its not case match
if (!optimisticCheckDone)
// Try match on full equals
foreach (var item in _mappingToEnum)
{
// Try match on full equals
foreach (var item in _mappingToEnum)
if (item.Key.Equals(value, StringComparison.Ordinal))
{
if (item.StringValue.Equals(value, StringComparison.Ordinal))
{
mapping = item;
break;
}
mapping = item.Value;
break;
}
}
// If not found, try matching ignoring case
if (mapping == null)
{
foreach (var item in _mappingToEnum)
{
if (item.StringValue.Equals(value, StringComparison.OrdinalIgnoreCase))
{
mapping = item;
break;
}
}
}
if (mapping != null)
{
result = mapping.Value;
return true;
}
}
// If not found, try matching ignoring case
if (mapping == null)
{
foreach (var item in _mappingToEnum)
{
if (item.Key.Equals(value, StringComparison.OrdinalIgnoreCase))
{
mapping = item.Value;
break;
}
}
}
if (mapping != null)
{
result = mapping;
return true;
}
if (_hasFlagsAttribute)
{
var intValue = int.Parse(value);
@@ -345,26 +335,21 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
private static void CreateMapping()
{
var mappingStringToEnum = new List<EnumMapping>();
var mappingStringToEnum = new Dictionary<string, T>();
var mappingEnumToString = new Dictionary<T, string>();
#pragma warning disable IL2080
var enumMembers = _enumType.GetFields();
var enumMembers = _enumType.GetFields(BindingFlags.Public | BindingFlags.Static);
#pragma warning restore IL2080
foreach (var member in enumMembers)
{
var enumVal = (T)member.GetValue(null)!;
var maps = member.GetCustomAttributes(typeof(MapAttribute), false);
foreach (MapAttribute attribute in maps)
{
foreach (var value in attribute.Values)
{
#if NET8_0_OR_GREATER
var enumVal = Enum.Parse<T>(member.Name);
#else
var enumVal = (T)Enum.Parse(_enumType, member.Name);
#endif
mappingStringToEnum.Add(new EnumMapping(enumVal, value));
mappingStringToEnum.Add(value, enumVal);
if (!mappingEnumToString.ContainsKey(enumVal))
mappingEnumToString.Add(enumVal, value);
}
@@ -372,7 +357,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
}
#if NET8_0_OR_GREATER
_mappingToEnum = mappingStringToEnum.ToFrozenSet();
_mappingToEnum = mappingStringToEnum.ToFrozenDictionary();
_mappingToString = mappingEnumToString.ToFrozenDictionary();
#else
_mappingToEnum = mappingStringToEnum;
@@ -411,33 +396,20 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
if (_mappingToEnum == null)
CreateMapping();
EnumMapping? mapping = null;
// Try match on full equals
foreach(var item in _mappingToEnum!)
{
if (item.StringValue.Equals(value, StringComparison.Ordinal))
{
mapping = item;
break;
}
if (item.Key.Equals(value, StringComparison.Ordinal))
return item.Value;
}
// If not found, try matching ignoring case
if (mapping == null)
foreach (var item in _mappingToEnum)
{
foreach (var item in _mappingToEnum)
{
if (item.StringValue.Equals(value, StringComparison.OrdinalIgnoreCase))
{
mapping = item;
break;
}
}
if (item.Key.Equals(value, StringComparison.OrdinalIgnoreCase))
return item.Value;
}
if (mapping != null)
return mapping.Value;
try
{
#if NET8_0_OR_GREATER
@@ -88,7 +88,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers
stream.Seek(0, SeekOrigin.Begin);
var written = new StreamReader(stream).ReadBlock(dataSnippet, 0, _errorResponseSnippetLimit);
var data = new string(dataSnippet, 0, written);
errorMsg += $": {data}";
errorMsg += $": {(string.IsNullOrEmpty(data) ? "(empty)" : data)}";
if (data.Length == _errorResponseSnippetLimit)
errorMsg += " (truncated)";
}
+3 -3
View File
@@ -6,9 +6,9 @@
<PackageId>CryptoExchange.Net</PackageId>
<Authors>JKorf</Authors>
<Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description>
<PackageVersion>11.1.1</PackageVersion>
<AssemblyVersion>11.1.1</AssemblyVersion>
<FileVersion>11.1.1</FileVersion>
<PackageVersion>12.4.0</PackageVersion>
<AssemblyVersion>12.4.0</AssemblyVersion>
<FileVersion>12.4.0</FileVersion>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange;CryptoExchange.Net</PackageTags>
<RepositoryType>git</RepositoryType>
+9 -9
View File
@@ -311,16 +311,16 @@ namespace CryptoExchange.Net
/// <param name="request">The request parameters</param>
/// <param name="ct">Cancellation token</param>
/// <returns></returns>
public static async IAsyncEnumerable<ExchangeWebResult<T[]>> ExecutePages<T, U>(Func<U, PageRequest?, CancellationToken, Task<ExchangeWebResult<T[]>>> paginatedFunc, U request, [EnumeratorCancellation]CancellationToken ct = default)
public static async IAsyncEnumerable<HttpResult<T[]>> ExecutePages<T, U>(Func<U, PageRequest?, CancellationToken, Task<HttpResult<T[]>>> paginatedFunc, U request, [EnumeratorCancellation]CancellationToken ct = default)
{
var result = new List<T>();
ExchangeWebResult<T[]> batch;
HttpResult<T[]> batch;
PageRequest? nextPageToken = null;
while (true)
{
batch = await paginatedFunc(request, nextPageToken, ct).ConfigureAwait(false);
yield return batch;
if (!batch || ct.IsCancellationRequested)
if (!batch.Success || ct.IsCancellationRequested)
break;
result.AddRange(batch.Data);
@@ -399,8 +399,8 @@ namespace CryptoExchange.Net
/// <param name="asyncHandler">The async update handler</param>
/// <param name="maxQueuedItems">The max number of updates to be queued up. When happens when the queue is full and a new write is attempted can be specified with <see>fullMode</see></param>
/// <param name="fullBehavior">What should happen if the queue contains <see>maxQueuedItems</see> pending updates. If no max is set this setting is ignored</param>
public static async Task<CallResult<UpdateSubscription>> ProcessQueuedAsync<T>(
Func<Action<DataEvent<T>>, Task<CallResult<UpdateSubscription>>> subscribeCall,
public static async Task<WebSocketResult<UpdateSubscription>> ProcessQueuedAsync<T>(
Func<Action<DataEvent<T>>, Task<WebSocketResult<UpdateSubscription>>> subscribeCall,
Func<DataEvent<T>, Task> asyncHandler,
int? maxQueuedItems = null,
QueueFullBehavior? fullBehavior = null)
@@ -408,7 +408,7 @@ namespace CryptoExchange.Net
var processor = new ProcessQueue<DataEvent<T>>(asyncHandler, maxQueuedItems, fullBehavior);
await processor.StartAsync().ConfigureAwait(false);
var result = await subscribeCall(upd => processor.Write(upd)).ConfigureAwait(false);
if (!result)
if (!result.Success)
{
await processor.StopAsync().ConfigureAwait(false);
return result;
@@ -473,7 +473,7 @@ namespace CryptoExchange.Net
}, maxQueuedItems, fullBehavior);
await processor.StartAsync().ConfigureAwait(false);
var result = await subscribeCall(processor).ConfigureAwait(false);
if (!result)
if (!result.Success)
{
await processor.StopAsync().ConfigureAwait(false);
return result;
@@ -499,7 +499,7 @@ namespace CryptoExchange.Net
return null;
// Try parse, only fails for these reasons:
// 1. string is null or empty
// 1. string is null or empty (already covered)
// 2. value is larger or smaller than decimal max/min
// 3. unparsable format
if (decimal.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var decValue))
@@ -516,7 +516,7 @@ namespace CryptoExchange.Net
if (string.Equals("Infinity", value, StringComparison.OrdinalIgnoreCase))
return decimal.MaxValue;
else if(string.Equals("-Infinity", value, StringComparison.OrdinalIgnoreCase))
return decimal.MinValue;
return decimal.MinValue;
if (value!.Length > 27 && decimal.TryParse(value.Substring(0, 27), out var overflowValue))
{
+278 -39
View File
@@ -11,85 +11,92 @@ namespace CryptoExchange.Net
/// </summary>
public static class ExchangeSymbolCache
{
private static ConcurrentDictionary<string, ExchangeInfo> _symbolInfos = new ConcurrentDictionary<string, ExchangeInfo>();
private static ConcurrentDictionary<string, ExchangeKeyedCache> _symbolInfos = new ConcurrentDictionary<string, ExchangeKeyedCache>();
/// <summary>
/// Update the cached symbol data for an exchange
/// </summary>
/// <param name="topicId">Id for the provided data</param>
/// <param name="environment">Trading environment</param>
/// <param name="key">Optional data set key</param>
/// <param name="updateData">Symbol data</param>
public static void UpdateSymbolInfo(string topicId, SharedSpotSymbol[] updateData)
public static void UpdateSymbolInfo(string topicId, string environment, string? key, SharedSpotSymbol[] updateData)
{
if(!_symbolInfos.TryGetValue(topicId, out var exchangeInfo))
var id = topicId + environment;
if(!_symbolInfos.TryGetValue(id, out var exchangeInfo))
{
exchangeInfo = new ExchangeInfo(DateTime.UtcNow, updateData.ToDictionary(x => x.Name, x => x.SharedSymbol));
_symbolInfos.TryAdd(topicId, exchangeInfo);
exchangeInfo = new ExchangeKeyedCache();
_symbolInfos.TryAdd(id, exchangeInfo);
}
if (DateTime.UtcNow - exchangeInfo.UpdateTime < TimeSpan.FromMinutes(60))
var keyedCache = exchangeInfo.Get(key);
if (keyedCache != null && DateTime.UtcNow - keyedCache.UpdateTime < TimeSpan.FromMinutes(60))
return;
_symbolInfos[topicId] = new ExchangeInfo(DateTime.UtcNow, updateData.ToDictionary(x => x.Name, x => x.SharedSymbol));
exchangeInfo.Set(key, new ExchangeInfo(DateTime.UtcNow, updateData.ToDictionary(x => x.Name, x => x)));
}
/// <summary>
/// Whether the specific topic has been cached
/// </summary>
/// <param name="topicId">Id</param>
public static bool HasCached(string topicId)
/// <param name="environment">Trading environment</param>
/// <param name="key">Optional data set key</param>
public static bool HasCached(string topicId, string environment, string? key)
{
if (!_symbolInfos.TryGetValue(topicId, out var exchangeInfo))
var id = topicId + environment;
if (!_symbolInfos.TryGetValue(id, out var exchangeInfo))
return false;
return exchangeInfo.Symbols.Count > 0;
return exchangeInfo.HasCached(key);
}
/// <summary>
/// Whether a specific exchange(topic) support the provided symbol
/// </summary>
/// <param name="topicId">Id for the provided data</param>
/// <param name="environment">Trading environment</param>
/// <param name="key">Optional data set key</param>
/// <param name="symbolName">The symbol name</param>
public static bool SupportsSymbol(string topicId, string symbolName)
public static bool SupportsSymbol(string topicId, string environment, string? key, string symbolName)
{
if (!_symbolInfos.TryGetValue(topicId, out var exchangeInfo))
var id = topicId + environment;
if (!_symbolInfos.TryGetValue(id, out var exchangeInfo))
return false;
if (!exchangeInfo.Symbols.TryGetValue(symbolName, out var symbolInfo))
return false;
return true;
return exchangeInfo.SupportsSymbol(key, symbolName);
}
/// <summary>
/// Whether a specific exchange(topic) support the provided symbol
/// </summary>
/// <param name="topicId">Id for the provided data</param>
/// <param name="environment">Trading environment</param>
/// <param name="key">Optional data set key</param>
/// <param name="symbol">The symbol info</param>
public static bool SupportsSymbol(string topicId, SharedSymbol symbol)
public static bool SupportsSymbol(string topicId, string environment, string? key, SharedSymbol symbol)
{
if (!_symbolInfos.TryGetValue(topicId, out var exchangeInfo))
var id = topicId + environment;
if (!_symbolInfos.TryGetValue(id, out var exchangeInfo))
return false;
return exchangeInfo.Symbols.Any(x =>
x.Value.TradingMode == symbol.TradingMode
&& x.Value.BaseAsset == symbol.BaseAsset
&& x.Value.QuoteAsset == symbol.QuoteAsset);
return exchangeInfo.SupportsSymbol(key, symbol);
}
/// <summary>
/// Get all symbols for a specific base asset
/// </summary>
/// <param name="topicId">Id for the provided data</param>
/// <param name="environment">Trading environment</param>
/// <param name="key">Optional data set key</param>
/// <param name="baseAsset">Base asset name</param>
public static SharedSymbol[] GetSymbolsForBaseAsset(string topicId, string baseAsset)
public static SharedSymbol[] GetSymbolsForBaseAsset(string topicId, string environment, string? key, string baseAsset)
{
if (!_symbolInfos.TryGetValue(topicId, out var exchangeInfo))
var id = topicId + environment;
if (!_symbolInfos.TryGetValue(id, out var exchangeInfo))
return [];
return exchangeInfo.Symbols
.Where(x => x.Value.BaseAsset.Equals(baseAsset, StringComparison.InvariantCultureIgnoreCase))
.Select(x => x.Value)
.ToArray();
return exchangeInfo.GetSymbolsForBaseAsset(key, baseAsset);
}
/// <summary>
@@ -97,29 +104,261 @@ namespace CryptoExchange.Net
/// </summary>
/// <param name="topicId">Id for the provided data</param>
/// <param name="symbolName">Symbol name</param>
public static SharedSymbol? ParseSymbol(string topicId, string? symbolName)
/// <param name="environment">Trade environment</param>
/// <param name="key">Additional data set identification key</param>
public static SharedSymbol? ParseSymbol(string topicId, string environment, string? key, string? symbolName)
{
if (symbolName == null)
return null;
if (!_symbolInfos.TryGetValue(topicId, out var exchangeInfo))
var id = topicId + environment;
if (!_symbolInfos.TryGetValue(id, out var exchangeInfo))
return null;
if (!exchangeInfo.Symbols.TryGetValue(symbolName, out var symbolInfo))
return null;
return new SharedSymbol(symbolInfo.TradingMode, symbolInfo.BaseAsset, symbolInfo.QuoteAsset, symbolName)
{
DeliverTime = symbolInfo.DeliverTime
};
return exchangeInfo.ParseSymbol(key, symbolName);
}
/// <summary>
/// Get a symbol catalog for a specific exchange(topic) and environment. Only available if <see cref="UpdateSymbolInfo(string, string, string?, SharedSpotSymbol[])"/> has been called previously.
/// </summary>
/// <param name="exchange">Exchange name</param>
/// <param name="topicId">Id for the provided data</param>
/// <param name="environmentName">Trade environment</param>
/// <param name="key">Additional data set identification key</param>
public static SharedSymbolCatalog? GetSymbolCatalog(string exchange, string topicId, string environmentName, string? key)
{
var id = topicId + environmentName;
if (!_symbolInfos.TryGetValue(id, out var exchangeInfo))
return null;
return exchangeInfo.GetSymbolCatalog(exchange, key);
}
class ExchangeKeyedCache
{
private ExchangeInfo? _noKeyCache;
private ConcurrentDictionary<string, ExchangeInfo> _keyedCache = new ConcurrentDictionary<string, ExchangeInfo>();
public ExchangeInfo? Get(string? key)
{
if (key == null)
return _noKeyCache;
if (_keyedCache.TryGetValue(key, out var exchangeInfo))
return exchangeInfo;
return null;
}
public void Set(string? key, ExchangeInfo exchangeInfo)
{
if (key == null)
_noKeyCache = exchangeInfo;
else
_keyedCache[key] = exchangeInfo;
}
public bool HasCached(string? key)
{
if (key == null)
{
if (_noKeyCache?.Symbols.Count > 0)
return true;
foreach (var cache in _keyedCache.Values)
{
if (cache.Symbols.Count > 0)
return true;
}
return false;
}
return _keyedCache.TryGetValue(key, out var exchangeInfo) && exchangeInfo.Symbols.Count > 0;
}
public SharedSymbol? ParseSymbol(string? key, string symbolName)
{
SharedSpotSymbol? symbolInfo = null;
if (key == null)
{
if (_noKeyCache != null)
{
if (!_noKeyCache.Symbols.TryGetValue(symbolName, out symbolInfo))
return null;
return new SharedSymbol(symbolInfo.TradingMode, symbolInfo.BaseAsset, symbolInfo.QuoteAsset, symbolName)
{
DeliverTime = (symbolInfo as SharedFuturesSymbol)?.DeliveryTime
};
}
foreach(var cache in _keyedCache.Values)
{
if (cache.Symbols.TryGetValue(symbolName, out symbolInfo))
{
return new SharedSymbol(symbolInfo.TradingMode, symbolInfo.BaseAsset, symbolInfo.QuoteAsset, symbolName)
{
DeliverTime = (symbolInfo as SharedFuturesSymbol)?.DeliveryTime
};
}
}
return null;
}
var hasKeyedSet = _keyedCache.TryGetValue(key, out var exchangeInfo);
if (!hasKeyedSet || exchangeInfo == null)
return null;
if (exchangeInfo.Symbols.TryGetValue(symbolName, out symbolInfo))
{
return new SharedSymbol(symbolInfo.TradingMode, symbolInfo.BaseAsset, symbolInfo.QuoteAsset, symbolName)
{
DeliverTime = (symbolInfo as SharedFuturesSymbol)?.DeliveryTime
};
}
return null;
}
public bool SupportsSymbol(string? key, string symbolName)
{
if (key == null)
{
if (_noKeyCache?.Symbols.ContainsKey(symbolName) == true)
return true;
foreach(var cache in _keyedCache.Values)
{
if (cache.Symbols.ContainsKey(symbolName))
return true;
}
return false;
}
return _keyedCache.TryGetValue(key, out var exchangeInfo) && exchangeInfo.Symbols.ContainsKey(symbolName);
}
public bool SupportsSymbol(string? key, SharedSymbol symbol)
{
if (key == null)
{
if (_noKeyCache?.Symbols.Any(x =>
x.Value.TradingMode == symbol.TradingMode
&& x.Value.BaseAsset == symbol.BaseAsset
&& x.Value.QuoteAsset == symbol.QuoteAsset) == true)
{
return true;
}
foreach (var cache in _keyedCache.Values)
{
if (cache.Symbols.Any(x =>
x.Value.TradingMode == symbol.TradingMode
&& x.Value.BaseAsset == symbol.BaseAsset
&& x.Value.QuoteAsset == symbol.QuoteAsset))
{
return true;
}
}
return false;
}
return _keyedCache.TryGetValue(key, out var exchangeInfo) && exchangeInfo.Symbols.Any(x =>
x.Value.TradingMode == symbol.TradingMode
&& x.Value.BaseAsset == symbol.BaseAsset
&& x.Value.QuoteAsset == symbol.QuoteAsset);
}
public SharedSymbol[] GetSymbolsForBaseAsset(string? key, string baseAsset)
{
if (key == null)
{
if (_noKeyCache != null)
{
return _noKeyCache.Symbols
.Where(x => x.Value.BaseAsset.Equals(baseAsset, StringComparison.InvariantCultureIgnoreCase))
.Select(x => x.Value.SharedSymbol)
.ToArray();
}
var result = new List<SharedSymbol>();
foreach(var cache in _keyedCache.Values)
{
result.AddRange(cache.Symbols
.Where(x => x.Value.BaseAsset.Equals(baseAsset, StringComparison.InvariantCultureIgnoreCase))
.Select(x => x.Value.SharedSymbol));
}
return result.ToArray();
}
var hasKeyedSet = _keyedCache.TryGetValue(key, out var exchangeInfo);
if (!hasKeyedSet || exchangeInfo == null)
return [];
return exchangeInfo.Symbols
.Where(x => x.Value.BaseAsset.Equals(baseAsset, StringComparison.InvariantCultureIgnoreCase))
.Select(x => x.Value.SharedSymbol)
.ToArray();
}
internal SharedSymbolCatalog? GetSymbolCatalog(string exchange, string? key)
{
IEnumerable<SharedSpotSymbol> cachedSymbols;
if (key == null)
{
if (_noKeyCache != null)
cachedSymbols = _noKeyCache.Symbols.Values;
else
cachedSymbols = _keyedCache.Values.SelectMany(x => x.Symbols.Values);
}
else
{
if (!_keyedCache.TryGetValue(key, out var exchangeInfo) || exchangeInfo == null)
return null;
cachedSymbols = exchangeInfo.Symbols.Values;
}
var assets = new Dictionary<string, SharedAssetInfo>();
var symbols = new Dictionary<string, SharedSpotSymbol>();
foreach (var symbol in cachedSymbols)
{
if (!assets.TryGetValue(symbol.BaseAsset, out var baseAssetInfo))
{
baseAssetInfo = new SharedAssetInfo(symbol.BaseAsset, symbol.BaseAssetType, symbol.BaseAssetSubType);
assets.Add(symbol.BaseAsset, baseAssetInfo);
}
if (!assets.TryGetValue(symbol.QuoteAsset, out var quoteAssetInfo))
{
quoteAssetInfo = new SharedAssetInfo(symbol.QuoteAsset, symbol.QuoteAssetType, symbol.QuoteAssetSubType);
assets.Add(symbol.QuoteAsset, quoteAssetInfo);
}
symbols.Add(symbol.Name, symbol);
}
return new SharedSymbolCatalog
{
Exchange = exchange,
Assets = assets,
Symbols = symbols
};
}
}
class ExchangeInfo
{
public DateTime UpdateTime { get; set; }
public Dictionary<string, SharedSymbol> Symbols { get; set; }
public Dictionary<string, SharedSpotSymbol> Symbols { get; set; }
public ExchangeInfo(DateTime updateTime, Dictionary<string, SharedSymbol> symbols)
public ExchangeInfo(DateTime updateTime, Dictionary<string, SharedSpotSymbol> symbols)
{
UpdateTime = updateTime;
Symbols = symbols;
+5 -6
View File
@@ -1,4 +1,5 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.SharedApis;
using Microsoft.Extensions.DependencyInjection;
using System;
@@ -24,7 +25,7 @@ namespace CryptoExchange.Net
/// <param name="parameters"></param>
/// <param name="key"></param>
/// <param name="value"></param>
public static void AddParameter(this Dictionary<string, object> parameters, string key, string value)
public static void AddParameter(this IDictionary<string, object> parameters, string key, string value)
{
parameters.Add(key, value);
}
@@ -35,7 +36,7 @@ namespace CryptoExchange.Net
/// <param name="parameters"></param>
/// <param name="key"></param>
/// <param name="value"></param>
public static void AddParameter(this Dictionary<string, object> parameters, string key, object value)
public static void AddParameter(this IDictionary<string, object> parameters, string key, object value)
{
parameters.Add(key, value);
}
@@ -46,7 +47,7 @@ namespace CryptoExchange.Net
/// <param name="parameters"></param>
/// <param name="key"></param>
/// <param name="value"></param>
public static void AddOptionalParameter(this Dictionary<string, object> parameters, string key, object? value)
public static void AddOptionalParameter(this IDictionary<string, object> parameters, string key, object? value)
{
if (value != null)
parameters.Add(key, value);
@@ -378,8 +379,6 @@ namespace CryptoExchange.Net
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)))
@@ -10,6 +10,10 @@ namespace CryptoExchange.Net.Interfaces.Clients
/// </summary>
public interface IBaseApiClient
{
/// <summary>
/// Exchange name
/// </summary>
string Exchange { get; }
/// <summary>
/// Base address
/// </summary>
@@ -28,6 +28,11 @@ namespace CryptoExchange.Net.Interfaces.Clients
/// </summary>
bool Authenticated { get; }
/// <summary>
/// Configured credentials
/// </summary>
TApiCredentials? ApiCredentials { get; }
/// <summary>
/// Set the API credentials for this API client
/// </summary>
@@ -84,6 +84,12 @@ namespace CryptoExchange.Net.Interfaces.Clients
/// Whether or not API credentials have been configured for this client. Does not check the credentials are actually valid.
/// </summary>
bool Authenticated { get; }
/// <summary>
/// Configured credentials
/// </summary>
TApiCredentials? ApiCredentials { get; }
/// <summary>
/// Set the API credentials for this API client
/// </summary>
@@ -107,7 +107,7 @@ namespace CryptoExchange.Net.Interfaces
/// </summary>
/// <param name="ct">A cancellation token to stop the order book when canceled</param>
/// <returns></returns>
Task<CallResult<bool>> StartAsync(CancellationToken? ct = null);
Task<CallResult> StartAsync(CancellationToken? ct = null);
/// <summary>
/// Stop syncing the order book
@@ -24,8 +24,9 @@ namespace CryptoExchange.Net.Interfaces
/// <param name="interval">Kline interval</param>
/// <param name="limit">The max amount of klines to retain</param>
/// <param name="period">The max period the data should be retained</param>
/// <param name="exchangeParameters">Exchange parameters</param>
/// <returns></returns>
IKlineTracker CreateKlineTracker(SharedSymbol symbol, SharedKlineInterval interval, int? limit = null, TimeSpan? period = null);
IKlineTracker CreateKlineTracker(SharedSymbol symbol, SharedKlineInterval interval, int? limit = null, TimeSpan? period = null, ExchangeParameters? exchangeParameters = null);
/// <summary>
/// Whether the factory supports creating a TradeTracker instance for this symbol
@@ -39,7 +40,8 @@ namespace CryptoExchange.Net.Interfaces
/// <param name="symbol">The symbol</param>
/// <param name="limit">The max amount of trades to retain</param>
/// <param name="period">The max period the data should be retained</param>
/// <param name="exchangeParameters">Exchange parameters</param>
/// <returns></returns>
ITradeTracker CreateTradeTracker(SharedSymbol symbol, int? limit = null, TimeSpan? period = null);
ITradeTracker CreateTradeTracker(SharedSymbol symbol, int? limit = null, TimeSpan? period = null, ExchangeParameters? exchangeParameters = null);
}
}
+119 -1
View File
@@ -4,6 +4,8 @@ using CryptoExchange.Net.Objects.Options;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.IO.Pipelines;
using System.Linq;
using System.Net;
using System.Net.Http;
@@ -14,6 +16,61 @@ namespace CryptoExchange.Net
/// </summary>
public static class LibraryHelpers
{
private static readonly HashSet<string> _stableCoins = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
// USD
"USDT", "USDC", "DAI", "FDUSD", "USDE", "TUSD", "USDP", "PYUSD", "GUSD",
"USDD", "LUSD", "USDJ", "SUSD", "ZUSD", "BUSD", "USTC", "USDX", "USDK",
"CUSD", "USD1", "USD0", "XUSD", "BFUSD", "USDS", "RLUSD", "OUSD", "USDH",
"APXUSD", "USDQ", "USDPT", "FIDD", "AUSD",
// EUR
"EURS", "EURC", "EURI", "EURT", "AGEUR", "CEUR", "AEUR", "EURQ", "EUROP",
// Other
"CNYT", // CNY
"CREAL", "BRL1", // BRL
"XSGD", // SGD
"GYEN", // JPY
"KGST", // KGS
"QCAD", // CAD
"TGBP", // GBP
"AUDX", // AUD
"MXNB", // MXN
};
private static readonly HashSet<string> _commodities = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
// Metals
"XAU", "XAUT", "XAG", "XPT", "XPD", "COPPER", "PAXG", "XNI", "XCU", "XAL", "GOLD", "SILVER",
// Energy
"BZ", "NATGAS", "NGAS", "CL", "XTI", "UKOIL", "USOIL", "BRENTOIL"
};
private static readonly HashSet<string> _stocks = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
// Top stocks, will need to update periodically
"AAAU", "AADR", "AAPL", "ACWI", "ACWX", "AGG", "AMD", "AMLP", "AMZN", "ARKF",
"ARKG", "ARKK", "ARKQ", "ARKW", "AVGO", "BA", "BABA", "BND", "BNDX", "BOTZ",
"CIBR", "COIN", "DIA", "DIVB", "DVY", "EEM", "EFA", "EFAV", "ESGU", "EWG",
"EWJ", "EWT", "EWU", "EWW", "EWY", "EWZ", "FDN", "FEZ", "GLDM", "GOOGL",
"HDV", "HOOD", "HYG", "IAU", "IBB", "ICLN", "IEFA", "IEMG", "IGSB", "IJH",
"IJR", "INTC", "ITOT", "IUSB", "IUSG", "IUSV", "IWM", "IWO", "IWR", "IYR",
"JETS", "JPM", "LIT", "MCHI", "META", "MGK", "MSTR", "MTUM", "MU", "NET",
"NFLX", "NOBL", "NVDA", "OIH", "ORCL", "PAVE", "PBW", "PLTR", "QQQ", "QQQM",
"SCHB", "SCHD", "SCHF", "SCHG", "SCHH", "SCHV", "SCHX", "SKHY", "SPCX", "SPCXD",
"SPLG", "SPY", "SPYG", "SPYV", "SQQQ", "TSLA", "TSM", "TQQQ", "USMV", "VBR",
"VCIT", "VCSH", "VEA", "VEU", "VGIT", "VGK", "VGT", "VHT", "VIG", "VNQ",
"VOO", "VOT", "VTI", "VTV", "VUG", "VXUS", "XBI", "XLC", "XLE", "XLF",
"XLI", "XLK", "XLP", "XLU", "XLV", "XLY", "CSCO", "UBER", "MRVL", "RKLB",
"COHR", "SOXL", "HD", "DIS", "CBRS", "V", "BRKB", "FLNC", "LLY", "COST",
"ARM", "BMNR", "NBIS", "ASML", "AAOI", "GLW", "SHLD", "BE", "QNTX", "IBM",
"AMAT", "NOK", "ASTS", "BBX", "SLX", "SKHYNIX", "SAMSUNG", "HYUNDAI", "NVO",
"IREN", "ONDS", "CRM" , "VRT", "ZEST", "BTW", "HPE", "AXTI", "BX", "CRWD",
"CRDO", "NOW", "ZM", "DKNG", "RIVN", "URNM", "EBAY", "ADBE", "UVXY", "RDW",
"CIEN","PANW", "WIN", "PAYP", "HIMS", "CRWV", "QCOM", "LITE", "DRAM", "ANTHROPIC",
"OPENAI", "USAR", "BILL", "SNDK", "NASDAQ100", "SPX500", "BSB", "CRCL", "STRC",
"MSFT", "WDC"
};
private static ILogger? _staticLogger;
/// <summary>
/// Static logger
@@ -55,7 +112,7 @@ namespace CryptoExchange.Net
{ "Kucoin.SpotKey", "f8ae62cb-2b3d-420c-8c98-e1c17dd4e30a" },
{ "Mexc", "EASYT" },
{ "OKX", "1425d83a94fbBCDE" },
{ "Weex", "WEEX111124" },
{ "Weex", "b-WEEX111124-" },
{ "XT", "4XWeqN10M1fcoI5L" },
};
@@ -105,6 +162,67 @@ namespace CryptoExchange.Net
return _defaultClientReferences.TryGetValue(key, out var id) ? id : throw new KeyNotFoundException($"{exchange} not found in configuration");
}
/// <summary>
/// Check whether an asset is a known stablecoin. Note that this is not definitive, only large known stocks are checked
/// </summary>
/// <param name="asset">Asset name</param>
/// <param name="additionalStableCoins">Additional stablecoin names for the specific exchange</param>
public static bool IsStableCoin(string asset, params HashSet<string> additionalStableCoins)
{
if (string.IsNullOrEmpty(asset))
return false;
return _stableCoins.Contains(asset) || (additionalStableCoins != null && additionalStableCoins.Contains(asset, StringComparer.OrdinalIgnoreCase));
}
/// <summary>
/// Check whether an asset is a known commodity. Note that this is not definitive, only large known stocks are checked
/// </summary>
/// <param name="asset">Asset name</param>
/// <param name="additionalCommodities">Additional commodity names for the specific exchange</param>
public static bool IsCommodity(string asset, params HashSet<string> additionalCommodities)
{
if (string.IsNullOrEmpty(asset))
return false;
return _commodities.Contains(asset) || (additionalCommodities != null && additionalCommodities.Contains(asset, StringComparer.OrdinalIgnoreCase));
}
/// <summary>
/// Check whether an asset is a known stock. Note that this is not definitive, only large known stocks are checked
/// </summary>
/// <param name="asset">Asset name</param>
/// <param name="additionalStocks">Additional stock names for the specific exchange</param>
public static bool IsEquity(string asset, params HashSet<string> additionalStocks)
=> IsEquity(asset, [], additionalStocks);
/// <summary>
/// Check whether an asset is a known stock.
/// </summary>
/// <param name="asset">Asset name</param>
/// <param name="potentialSuffixes">Suffixes to check, for example when `X` is a potential suffix both `TSLA` and `TSLAX` will be checked</param>
/// <param name="additionalStocks">Additional stock names for the specific exchange</param>
public static bool IsEquity(string asset, string[] potentialSuffixes, params HashSet<string> additionalStocks)
{
if (string.IsNullOrEmpty(asset))
return false;
if (_stocks.Contains(asset) || (additionalStocks != null && additionalStocks.Contains(asset, StringComparer.OrdinalIgnoreCase)))
return true;
foreach (var suffix in potentialSuffixes)
{
if (!asset.EndsWith(suffix))
continue;
var suffixAsset = asset.Substring(0, asset.Length - suffix.Length);
if (_stocks.Contains(suffixAsset) || (additionalStocks != null && additionalStocks.Contains(suffixAsset, StringComparer.OrdinalIgnoreCase)))
return true;
}
return false;
}
/// <summary>
/// Create a new HttpMessageHandler instance
/// </summary>
@@ -18,32 +18,32 @@ namespace CryptoExchange.Net.Logging.Extensions
_rateLimitRequestFailed = LoggerMessage.Define<int, string, string, string>(
LogLevel.Warning,
new EventId(6000, "RateLimitRequestFailed"),
"[Req {Id}] Call to {Path} failed because of ratelimit guard {Guard}; {Limit}");
"[Req {Id}] call to {Path} failed because of ratelimit guard {Guard}; {Limit}");
_rateLimitConnectionFailed = LoggerMessage.Define<int, string, string>(
LogLevel.Warning,
new EventId(6001, "RateLimitConnectionFailed"),
"[Sckt {Id}] Connection failed because of ratelimit guard {Guard}; {Limit}");
"[Sckt {Id}] connection failed because of ratelimit guard {Guard}; {Limit}");
_rateLimitDelayingRequest = LoggerMessage.Define<int, string, TimeSpan, string, string>(
LogLevel.Warning,
new EventId(6002, "RateLimitDelayingRequest"),
"[Req {Id}] Delaying call to {Path} by {Delay} because of ratelimit guard {Guard}; {Limit}");
"[Req {Id}] delaying call to {Path} by {Delay} because of ratelimit guard {Guard}; {Limit}");
_rateLimitDelayingConnection = LoggerMessage.Define<int, TimeSpan, string, string>(
LogLevel.Warning,
new EventId(6003, "RateLimitDelayingConnection"),
"[Sckt {Id}] Delaying connection by {Delay} because of ratelimit guard {Guard}; {Limit}");
"[Sckt {Id}] delaying connection by {Delay} because of ratelimit guard {Guard}; {Limit}");
_rateLimitAppliedConnection = LoggerMessage.Define<int, string, string, int>(
LogLevel.Trace,
new EventId(6004, "RateLimitDelayingConnection"),
"[Sckt {Id}] Connection passed ratelimit guard {Guard}; {Limit}, New count: {Current}");
"[Sckt {Id}] connection passed ratelimit guard {Guard}; {Limit}, New count: {Current}");
_rateLimitAppliedRequest = LoggerMessage.Define<int, string, string, string, int>(
LogLevel.Trace,
new EventId(6005, "RateLimitAppliedRequest"),
"[Req {Id}] Call to {Path} passed ratelimit guard {Guard}; {Limit}, New count: {Current}");
"[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)
@@ -28,67 +28,67 @@ namespace CryptoExchange.Net.Logging.Extensions
_restApiErrorReceived = LoggerMessage.Define<int?, int?, long, string?, string?>(
LogLevel.Warning,
new EventId(4000, "RestApiErrorReceived"),
"[Req {RequestId}] {ResponseStatusCode} - Error received in {ResponseTime}ms: {ErrorMessage}, Data: {OriginalData}");
"[Req {RequestId}] {ResponseStatusCode} - error received in {ResponseTime}ms: {ErrorMessage}, Data: {OriginalData}");
_restApiResponseReceived = LoggerMessage.Define<int?, int?, long, string?>(
LogLevel.Debug,
new EventId(4001, "RestApiResponseReceived"),
"[Req {RequestId}] {ResponseStatusCode} - Response received in {ResponseTime}ms: {OriginalData}");
"[Req {RequestId}] {ResponseStatusCode} - response received in {ResponseTime}ms: {OriginalData}");
_restApiFailedToSyncTime = LoggerMessage.Define<int, string>(
LogLevel.Debug,
new EventId(4002, "RestApiFailedToSyncTime"),
"[Req {RequestId}] Failed to sync time, aborting request: {ErrorMessage}");
"[Req {RequestId}] failed to sync time, aborting request: {ErrorMessage}");
_restApiNoApiCredentials = LoggerMessage.Define<int, string>(
LogLevel.Warning,
new EventId(4003, "RestApiNoApiCredentials"),
"[Req {RequestId}] Request {RestApiUri} failed because no ApiCredentials were provided");
"[Req {RequestId}] request {RestApiUri} failed because no ApiCredentials were provided");
_restApiCreatingRequest = LoggerMessage.Define<int, Uri>(
LogLevel.Information,
new EventId(4004, "RestApiCreatingRequest"),
"[Req {RequestId}] Creating request for {RestApiUri}");
"[Req {RequestId}] creating request for {RestApiUri}");
_restApiSendingRequest = LoggerMessage.Define<int, HttpMethod, string, Uri, string>(
LogLevel.Trace,
new EventId(4005, "RestApiSendingRequest"),
"[Req {RequestId}] Sending {Method} {Signed} request to {RestApiUri}{Query}");
"[Req {RequestId}] sending {Method} {Signed} request to {RestApiUri}{Query}");
_restApiRateLimitRetry = LoggerMessage.Define<int, DateTime>(
LogLevel.Warning,
new EventId(4006, "RestApiRateLimitRetry"),
"[Req {RequestId}] Received ratelimit error, retrying after {Timestamp}");
"[Req {RequestId}] received ratelimit error, retrying after {Timestamp}");
_restApiRateLimitPauseUntil = LoggerMessage.Define<int, DateTime>(
LogLevel.Warning,
new EventId(4007, "RestApiRateLimitPauseUntil"),
"[Req {RequestId}] Ratelimit error from server, pausing requests until {Until}");
"[Req {RequestId}] ratelimit error from server, pausing requests until {Until}");
_restApiSendRequest = LoggerMessage.Define<int, RequestDefinition, string?, string, string>(
LogLevel.Debug,
new EventId(4008, "RestApiSendRequest"),
"[Req {RequestId}] Sending {Definition} request with body {Body}, query parameters {Query} and headers {Headers}");
"[Req {RequestId}] sending {Definition} request with body {Body}, query parameters {Query} and headers {Headers}");
_restApiCheckingCache = LoggerMessage.Define<string>(
LogLevel.Trace,
new EventId(4009, "RestApiCheckingCache"),
"Checking cache for key {Key}");
"checking cache for key {Key}");
_restApiCacheHit = LoggerMessage.Define<string>(
LogLevel.Trace,
new EventId(4010, "RestApiCacheHit"),
"Cache hit for key {Key}");
"cache hit for key {Key}");
_restApiCacheNotHit = LoggerMessage.Define<string>(
LogLevel.Trace,
new EventId(4011, "RestApiCacheNotHit"),
"Cache not hit for key {Key}");
"cache not hit for key {Key}");
_restApiCancellationRequested = LoggerMessage.Define<int?>(
LogLevel.Debug,
new EventId(4012, "RestApiCancellationRequested"),
"[Req {RequestId}] Request cancelled by user");
"[Req {RequestId}] request cancelled by user");
}
@@ -61,7 +61,7 @@ namespace CryptoExchange.Net.Logging.Extensions
_attemptingToAuthenticate = LoggerMessage.Define<int>(
LogLevel.Debug,
new EventId(3006, "AttemptingToAuthenticate"),
"[Sckt {SocketId}] Attempting to authenticate");
"[Sckt {SocketId}] attempting to authenticate");
_authenticationFailed = LoggerMessage.Define<int>(
LogLevel.Warning,
@@ -76,12 +76,12 @@ namespace CryptoExchange.Net.Logging.Extensions
_failedToDetermineConnectionUrl = LoggerMessage.Define<string?>(
LogLevel.Warning,
new EventId(3009, "FailedToDetermineConnectionUrl"),
"Failed to determine connection url: {ErrorMessage}");
"failed to determine connection url: {ErrorMessage}");
_connectionAddressSetTo = LoggerMessage.Define<string>(
LogLevel.Debug,
new EventId(3010, "ConnectionAddressSetTo"),
"Connection address set to {ConnectionAddress}");
"connection address set to {ConnectionAddress}");
_socketCreatedForAddress = LoggerMessage.Define<int, string>(
LogLevel.Debug,
@@ -91,37 +91,37 @@ namespace CryptoExchange.Net.Logging.Extensions
_unsubscribingAll = LoggerMessage.Define<int>(
LogLevel.Information,
new EventId(3013, "UnsubscribingAll"),
"Unsubscribing all {SubscriptionCount} subscriptions");
"unsubscribing all {SubscriptionCount} subscriptions");
_disposingSocketClient = LoggerMessage.Define(
LogLevel.Debug,
new EventId(3015, "DisposingSocketClient"),
"Disposing socket client, closing all subscriptions");
"disposing socket client, closing all subscriptions");
_unsubscribingSubscription = LoggerMessage.Define<int, int>(
LogLevel.Information,
new EventId(3016, "UnsubscribingSubscription"),
"[Sckt {SocketId}] Unsubscribing subscription {SubscriptionId}");
"[Sckt {SocketId}] unsubscribing subscription {SubscriptionId}");
_reconnectingAllConnections = LoggerMessage.Define<int>(
LogLevel.Information,
new EventId(3017, "ReconnectingAll"),
"Reconnecting all {ConnectionCount} connections");
"reconnecting all {ConnectionCount} connections");
_addingRetryAfterGuard = LoggerMessage.Define<DateTime>(
LogLevel.Warning,
new EventId(3018, "AddRetryAfterGuard"),
"Adding RetryAfterGuard ({RetryAfter}) because the connection attempt was rate limited");
"adding RetryAfterGuard ({RetryAfter}) because the connection attempt was rate limited");
_timeoutWaitingForReconnectingSocket = LoggerMessage.Define(
LogLevel.Debug,
new EventId(3019, "TimeoutWaitingForReconnectingSocket"),
"Timeout while waiting for existing socket reconnection, failing request");
"timeout while waiting for existing socket reconnection, failing request");
_waitedForReconnectingSocket = LoggerMessage.Define<long>(
LogLevel.Trace,
new EventId(3020, "WaitedForReconnectingSocket"),
"Waited for reconnecting socket for {Timespan}ms");
"waited for reconnecting socket for {Timespan}ms");
}
public static void FailedToAddSubscriptionRetryOnDifferentConnection(this ILogger logger, int socketId)
@@ -60,7 +60,7 @@ namespace CryptoExchange.Net.Logging.Extensions
_unknownExceptionWhileProcessingReconnection = LoggerMessage.Define<int>(
LogLevel.Warning,
new EventId(2003, "UnknownExceptionWhileProcessingReconnection"),
"[Sckt {SocketId}] Unknown exception while processing reconnection, reconnecting again");
"[Sckt {SocketId}] unknown exception while processing reconnection, reconnecting again");
_webSocketErrorCodeAndDetails = LoggerMessage.Define<int, WebSocketError, string?>(
LogLevel.Warning,
@@ -180,7 +180,7 @@ namespace CryptoExchange.Net.Logging.Extensions
_receivedMessageNotMatchedToAnyListener = LoggerMessage.Define<int, string, string, string>(
LogLevel.Warning,
new EventId(2029, "ReceivedMessageNotMatchedToAnyListener"),
"[Sckt {SocketId}] received message not matched to any listener. TypeIdentifier: {TypeIdentifier}, ListenId: {ListenId}, current listeners: [{ListenIds}]");
"[Sckt {SocketId}] received message not matched to any listener. TypeIdentifier: {TypeIdentifier}, TopicFilter: {ListenId}, registered TopicFilters for type: [{TopicFilters}]");
_failedToParse = LoggerMessage.Define<int, string>(
LogLevel.Warning,
@@ -326,9 +326,9 @@ namespace CryptoExchange.Net.Logging.Extensions
_sendingData(logger, socketId, requestId, data, null);
}
public static void ReceivedMessageNotMatchedToAnyListener(this ILogger logger, int socketId, string typeIdentifier, string listenId, string listenIds)
public static void ReceivedMessageNotMatchedToAnyListener(this ILogger logger, int socketId, string typeIdentifier, string topicFilter, string topicFilters)
{
_receivedMessageNotMatchedToAnyListener(logger, socketId, typeIdentifier, listenId, listenIds, null);
_receivedMessageNotMatchedToAnyListener(logger, socketId, typeIdentifier, topicFilter, topicFilters, null);
}
public static void SendingByteData(this ILogger logger, int socketId, int requestId, int length)
-592
View File
@@ -1,592 +0,0 @@
using CryptoExchange.Net.SharedApis;
using System;
using System.Diagnostics.CodeAnalysis;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
namespace CryptoExchange.Net.Objects
{
/// <summary>
/// The result of an operation
/// </summary>
public class CallResult
{
/// <summary>
/// Static success result
/// </summary>
public static CallResult SuccessResult { get; } = new CallResult(null);
/// <summary>
/// An error if the call didn't succeed, will always be filled if Success = false
/// </summary>
public Error? Error { get; internal set; }
/// <summary>
/// Whether the call was successful
/// </summary>
public bool Success => Error == null;
/// <summary>
/// ctor
/// </summary>
/// <param name="error"></param>
public CallResult(Error? error)
{
Error = error;
}
/// <summary>
/// Overwrite bool check so we can use if(callResult) instead of if(callResult.Success)
/// </summary>
/// <param name="obj"></param>
public static implicit operator bool(CallResult obj)
{
return obj?.Success == true;
}
/// <inheritdoc />
public override string ToString()
{
return Success ? $"Success" : $"Error: {Error}";
}
}
/// <summary>
/// The result of an operation
/// </summary>
/// <typeparam name="T"></typeparam>
public class CallResult<T>: CallResult
{
/// <summary>
/// The data returned by the call, only available when Success = true
/// </summary>
public T Data { get; internal set; }
/// <summary>
/// The original data returned by the call, only available when `OutputOriginalData` is set to `true` in the client options
/// </summary>
public string? OriginalData { get; internal set; }
/// <summary>
/// ctor
/// </summary>
/// <param name="data"></param>
/// <param name="originalData"></param>
/// <param name="error"></param>
#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
}
/// <summary>
/// Create a new data result
/// </summary>
/// <param name="data">The data to return</param>
public CallResult(T data) : this(data, null, null) { }
/// <summary>
/// Create a new error result
/// </summary>
/// <param name="error">The error to return</param>
public CallResult(Error error) : this(default, null, error) { }
/// <summary>
/// Create a new error result
/// </summary>
/// <param name="error">The error to return</param>
/// <param name="originalData">The original response data</param>
public CallResult(Error error, string? originalData) : this(default, originalData, error) { }
/// <summary>
/// Overwrite bool check so we can use if(callResult) instead of if(callResult.Success)
/// </summary>
/// <param name="obj"></param>
public static implicit operator bool(CallResult<T> obj)
{
return obj?.Success == true;
}
/// <summary>
/// Whether the call was successful or not. Useful for nullability checking.
/// </summary>
/// <param name="data">The data returned by the call.</param>
/// <param name="error"><see cref="Error"/> on failure.</param>
/// <returns><c>true</c> when <see cref="CallResult{T}"/> succeeded, <c>false</c> otherwise.</returns>
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;
}
}
/// <summary>
/// Copy the WebCallResult to a new data type
/// </summary>
/// <typeparam name="K">The new type</typeparam>
/// <param name="data">The data of the new type</param>
/// <returns></returns>
public CallResult<K> As<K>([AllowNull] K data)
{
return new CallResult<K>(data, OriginalData, Error);
}
/// <summary>
/// Copy as a dataless result
/// </summary>
/// <returns></returns>
public CallResult AsDataless()
{
if (Error != null )
return new CallResult(Error);
return SuccessResult;
}
/// <summary>
/// Copy as a dataless result
/// </summary>
/// <returns></returns>
public CallResult AsDatalessError(Error error)
{
return new CallResult(error);
}
/// <summary>
/// Copy the CallResult to a new data type
/// </summary>
/// <typeparam name="K">The new type</typeparam>
/// <param name="data">The data</param>
/// <param name="error">The error returned</param>
/// <returns></returns>
public CallResult<K> AsErrorWithData<K>(Error error, K data)
{
return new CallResult<K>(data, OriginalData, error);
}
/// <summary>
/// Copy the WebCallResult to a new data type
/// </summary>
/// <typeparam name="K">The new type</typeparam>
/// <param name="error">The error to return</param>
/// <returns></returns>
public CallResult<K> AsError<K>(Error error)
{
return new CallResult<K>(default, OriginalData, error);
}
/// <inheritdoc />
public override string ToString()
{
return Success ? $"Success" : $"Error: {Error}";
}
}
/// <summary>
/// The result of a request
/// </summary>
public class WebCallResult : CallResult
{
/// <summary>
/// The request http method
/// </summary>
public HttpMethod? RequestMethod { get; set; }
/// <summary>
/// HTTP protocol version
/// </summary>
public Version? HttpVersion { get; set; }
/// <summary>
/// The headers sent with the request
/// </summary>
public HttpRequestHeaders? RequestHeaders { get; set; }
/// <summary>
/// The request id
/// </summary>
public int? RequestId { get; set; }
/// <summary>
/// The url which was requested
/// </summary>
public string? RequestUrl { get; set; }
/// <summary>
/// The body of the request
/// </summary>
public string? RequestBody { get; set; }
/// <summary>
/// The original data returned by the call, only available when `OutputOriginalData` is set to `true` in the client options
/// </summary>
public string? OriginalData { get; internal set; }
/// <summary>
/// The status code of the response. Note that a OK status does not always indicate success, check the Success parameter for this.
/// </summary>
public HttpStatusCode? ResponseStatusCode { get; set; }
/// <summary>
/// The response headers
/// </summary>
public HttpResponseHeaders? ResponseHeaders { get; set; }
/// <summary>
/// The time between sending the request and receiving the response
/// </summary>
public TimeSpan? ResponseTime { get; set; }
/// <summary>
/// ctor
/// </summary>
public WebCallResult(
HttpStatusCode? code,
Version? httpVersion,
HttpResponseHeaders? responseHeaders,
TimeSpan? responseTime,
string? originalData,
int? requestId,
string? requestUrl,
string? requestBody,
HttpMethod? requestMethod,
HttpRequestHeaders? requestHeaders,
Error? error) : base(error)
{
ResponseStatusCode = code;
HttpVersion = httpVersion;
ResponseHeaders = responseHeaders;
ResponseTime = responseTime;
RequestId = requestId;
OriginalData = originalData;
RequestUrl = requestUrl;
RequestBody = requestBody;
RequestHeaders = requestHeaders;
RequestMethod = requestMethod;
}
/// <summary>
/// ctor
/// </summary>
/// <param name="error"></param>
public WebCallResult(Error error): base(error) { }
/// <summary>
/// Return the result as an error result
/// </summary>
/// <param name="error">The error returned</param>
/// <returns></returns>
public WebCallResult AsError(Error error)
{
return new WebCallResult(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error);
}
/// <summary>
/// Copy the WebCallResult to a new data type
/// </summary>
/// <typeparam name="K">The new type</typeparam>
/// <param name="data">The data of the new type</param>
/// <returns></returns>
public WebCallResult<K> As<K>([AllowNull] K data)
{
return new WebCallResult<K>(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, 0, null, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, ResultDataSource.Server, data, Error);
}
/// <summary>
/// Copy the WebCallResult to an ExchangeWebResult of a new data type
/// </summary>
/// <typeparam name="K">The new type</typeparam>
/// <param name="exchange">The exchange</param>
/// <param name="tradeMode">Trade mode the result applies to</param>
/// <param name="data">The data</param>
/// <returns></returns>
public ExchangeWebResult<K> AsExchangeResult<K>(string exchange, TradingMode tradeMode, [AllowNull] K data)
{
return new ExchangeWebResult<K>(exchange, tradeMode, this.As<K>(data));
}
/// <summary>
/// Copy the WebCallResult to an ExchangeWebResult of a new data type
/// </summary>
/// <typeparam name="K">The new type</typeparam>
/// <param name="exchange">The exchange</param>
/// <param name="tradeModes">Trade modes the result applies to</param>
/// <param name="data">The data</param>
/// <returns></returns>
public ExchangeWebResult<K> AsExchangeResult<K>(string exchange, TradingMode[]? tradeModes, [AllowNull] K data)
{
return new ExchangeWebResult<K>(exchange, tradeModes, this.As<K>(data));
}
/// <summary>
/// Copy the WebCallResult to a new data type
/// </summary>
/// <typeparam name="K">The new type</typeparam>
/// <param name="error">The error returned</param>
/// <returns></returns>
public WebCallResult<K> AsError<K>(Error error)
{
return new WebCallResult<K>(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, 0, null, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, ResultDataSource.Server, default, error);
}
/// <inheritdoc />
public override string ToString()
{
return (Success ? $"Success" : $"Error: {Error}") + $" in {ResponseTime}";
}
}
/// <summary>
/// The result of a request
/// </summary>
/// <typeparam name="T"></typeparam>
public class WebCallResult<T>: CallResult<T>
{
/// <summary>
/// The request http method
/// </summary>
public HttpMethod? RequestMethod { get; set; }
/// <summary>
/// HTTP protocol version
/// </summary>
public Version? HttpVersion { get; set; }
/// <summary>
/// The headers sent with the request
/// </summary>
public HttpRequestHeaders? RequestHeaders { get; set; }
/// <summary>
/// The request id
/// </summary>
public int? RequestId { get; set; }
/// <summary>
/// The url which was requested
/// </summary>
public string? RequestUrl { get; set; }
/// <summary>
/// The body of the request
/// </summary>
public string? RequestBody { get; set; }
/// <summary>
/// The status code of the response. Note that a OK status does not always indicate success, check the Success parameter for this.
/// </summary>
public HttpStatusCode? ResponseStatusCode { get; set; }
/// <summary>
/// Length in bytes of the response
/// </summary>
public long? ResponseLength { get; set; }
/// <summary>
/// The response headers
/// </summary>
public HttpResponseHeaders? ResponseHeaders { get; set; }
/// <summary>
/// The time between sending the request and receiving the response
/// </summary>
public TimeSpan? ResponseTime { get; set; }
/// <summary>
/// The data source of this result
/// </summary>
public ResultDataSource DataSource { get; set; } = ResultDataSource.Server;
/// <summary>
/// Create a new result
/// </summary>
public WebCallResult(
HttpStatusCode? code,
Version? httpVersion,
HttpResponseHeaders? responseHeaders,
TimeSpan? responseTime,
long? responseLength,
string? originalData,
int? requestId,
string? requestUrl,
string? requestBody,
HttpMethod? requestMethod,
HttpRequestHeaders? requestHeaders,
ResultDataSource dataSource,
[AllowNull] T data,
Error? error) : base(data, originalData, error)
{
HttpVersion = httpVersion;
ResponseStatusCode = code;
ResponseHeaders = responseHeaders;
ResponseTime = responseTime;
ResponseLength = responseLength;
RequestId = requestId;
RequestUrl = requestUrl;
RequestBody = requestBody;
RequestHeaders = requestHeaders;
RequestMethod = requestMethod;
DataSource = dataSource;
}
/// <summary>
/// Copy as a dataless result
/// </summary>
/// <returns></returns>
public new WebCallResult AsDataless()
{
return new WebCallResult(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, Error);
}
/// <summary>
/// Copy as a dataless result
/// </summary>
/// <returns></returns>
public new WebCallResult AsDatalessError(Error error)
{
return new WebCallResult(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error);
}
/// <summary>
/// Create a new error result
/// </summary>
/// <param name="error">The error</param>
public WebCallResult(Error? error) : this(null, null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, default, error) { }
/// <summary>
/// Copy the WebCallResult to a new data type
/// </summary>
/// <typeparam name="K">The new type</typeparam>
/// <param name="data">The data of the new type</param>
/// <returns></returns>
public new WebCallResult<K> As<K>([AllowNull] K data)
{
return new WebCallResult<K>(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, data, Error);
}
/// <summary>
/// Copy the WebCallResult to a new data type
/// </summary>
/// <typeparam name="K">The new type</typeparam>
/// <param name="error">The error returned</param>
/// <returns></returns>
public new WebCallResult<K> AsError<K>(Error error)
{
return new WebCallResult<K>(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, default, error);
}
/// <summary>
/// Copy the WebCallResult to a new data type
/// </summary>
/// <typeparam name="K">The new type</typeparam>
/// <param name="data">The data</param>
/// <param name="error">The error returned</param>
/// <returns></returns>
public new WebCallResult<K> AsErrorWithData<K>(Error error, K data)
{
return new WebCallResult<K>(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, data, error);
}
/// <summary>
/// Copy the WebCallResult to an ExchangeWebResult of a new data type
/// </summary>
/// <param name="exchange">The exchange</param>
/// <param name="tradeMode">Trade mode the result applies to</param>
/// <returns></returns>
public ExchangeWebResult<T> AsExchangeResult(string exchange, TradingMode tradeMode)
{
return new ExchangeWebResult<T>(exchange, tradeMode, this);
}
/// <summary>
/// Copy the WebCallResult to an ExchangeWebResult of a new data type
/// </summary>
/// <param name="exchange">The exchange</param>
/// <param name="tradeModes">Trade modes the result applies to</param>
/// <returns></returns>
public ExchangeWebResult<T> AsExchangeResult(string exchange, TradingMode[] tradeModes)
{
return new ExchangeWebResult<T>(exchange, tradeModes, this);
}
/// <summary>
/// Copy the WebCallResult to an ExchangeWebResult of a new data type
/// </summary>
/// <typeparam name="K">The new type</typeparam>
/// <param name="exchange">The exchange</param>
/// <param name="tradeMode">Trade mode the result applies to</param>
/// <param name="data">Data</param>
/// <param name="nextPageRequest">Next page request</param>
/// <returns></returns>
public ExchangeWebResult<K> AsExchangeResult<K>(string exchange, TradingMode tradeMode, [AllowNull] K data, PageRequest? nextPageRequest = null)
{
return new ExchangeWebResult<K>(exchange, tradeMode, As<K>(data), nextPageRequest);
}
/// <summary>
/// Copy the WebCallResult to an ExchangeWebResult of a new data type
/// </summary>
/// <typeparam name="K">The new type</typeparam>
/// <param name="exchange">The exchange</param>
/// <param name="tradeModes">Trade modes the result applies to</param>
/// <param name="data">Data</param>
/// <param name="nextPageRequest">Next page token</param>
/// <returns></returns>
public ExchangeWebResult<K> AsExchangeResult<K>(string exchange, TradingMode[]? tradeModes, [AllowNull] K data, PageRequest? nextPageRequest = null)
{
return new ExchangeWebResult<K>(exchange, tradeModes, As<K>(data), nextPageRequest);
}
/// <summary>
/// Copy the WebCallResult to an ExchangeWebResult with a specific error
/// </summary>
/// <typeparam name="K">The new type</typeparam>
/// <param name="exchange">The exchange</param>
/// <param name="error">The error returned</param>
/// <returns></returns>
public ExchangeWebResult<K> AsExchangeError<K>(string exchange, Error error)
{
return new ExchangeWebResult<K>(exchange, null, AsError<K>(error));
}
/// <summary>
/// Return a copy of this result with data source set to cache
/// </summary>
/// <returns></returns>
internal WebCallResult<T> Cached()
{
return new WebCallResult<T>(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, ResultDataSource.Cache, Data, Error);
}
/// <inheritdoc />
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();
}
}
}
+8
View File
@@ -149,6 +149,14 @@ namespace CryptoExchange.Net.Objects
/// </summary>
public class ServerError : Error
{
/// <summary>
/// ctor
/// </summary>
public ServerError(ErrorType type, string message, Exception? exception = null)
: base(null, new ErrorInfo(type, message), exception)
{
}
/// <summary>
/// ctor
/// </summary>
@@ -22,6 +22,10 @@ namespace CryptoExchange.Net.Objects.Options
/// Note that this comes at a performance cost
/// </summary>
public bool OutputOriginalData { get; set; } = false;
/// <summary>
/// A group name to use for client side rate limiting. Requests with the same group name will be counted together for rate limiting purposes. If null all requests will be counted together.
/// </summary>
public string? RateLimitGroup { get; set; }
/// <summary>
/// The max time a request is allowed to take
@@ -40,7 +44,7 @@ namespace CryptoExchange.Net.Objects.Options
/// <inheritdoc />
public override string ToString()
{
return $"RequestTimeout: {RequestTimeout}, Proxy: {(Proxy == null ? "-" : "set")}";
return $"Proxy: {(Proxy == null ? "-" : "set")}";
}
}
}
@@ -69,6 +69,7 @@ namespace CryptoExchange.Net.Objects.Options
item.RequestTimeout = RequestTimeout;
item.RateLimiterEnabled = RateLimiterEnabled;
item.RateLimitingBehaviour = RateLimitingBehaviour;
item.RateLimitGroup = RateLimitGroup;
item.CachingEnabled = CachingEnabled;
item.CachingMaxAge = CachingMaxAge;
item.HttpVersion = HttpVersion;
@@ -104,6 +105,12 @@ namespace CryptoExchange.Net.Objects.Options
target.Environment = Environment;
return target;
}
/// <inheritdoc />
public override string ToString()
{
return $"{base.ToString()} | Environment: {Environment.Name}";
}
}
/// <inheritdoc />
@@ -130,7 +137,7 @@ namespace CryptoExchange.Net.Objects.Options
/// <inheritdoc />
public override string ToString()
{
return $"{base.ToString()}, ApiCredentials: {(ApiCredentials == null ? "-" : "set")}";
return $"{base.ToString()} | ApiCredentials: {(ApiCredentials == null ? "-" : "set")}";
}
}
@@ -104,6 +104,7 @@ namespace CryptoExchange.Net.Objects.Options
item.RequestTimeout = RequestTimeout;
item.RateLimitingBehaviour = RateLimitingBehaviour;
item.RateLimiterEnabled = RateLimiterEnabled;
item.RateLimitGroup = RateLimitGroup;
item.ReceiveBufferSize = ReceiveBufferSize;
return item;
}
@@ -131,6 +132,12 @@ namespace CryptoExchange.Net.Objects.Options
target.Environment = Environment;
return target;
}
/// <inheritdoc />
public override string ToString()
{
return $"{base.ToString()} | Environment: {Environment.Name}";
}
}
/// <summary>
@@ -158,7 +165,7 @@ namespace CryptoExchange.Net.Objects.Options
/// <inheritdoc />
public override string ToString()
{
return $"{base.ToString()}, ApiCredentials: {(ApiCredentials == null ? "-" : "set")}";
return $"{base.ToString()} | ApiCredentials: {(ApiCredentials == null ? "-" : "set")}";
}
}
}
@@ -1,343 +0,0 @@
using CryptoExchange.Net.Attributes;
using CryptoExchange.Net.Converters.SystemTextJson;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
namespace CryptoExchange.Net.Objects
{
/// <summary>
/// Parameters collection
/// </summary>
public class ParameterCollection : Dictionary<string, object>
{
/// <inheritdoc />
public new void Add(string key, object value)
{
if (value == null)
throw new ArgumentNullException(key);
base.Add(key, value);
}
/// <summary>
/// Add an optional parameter. Not added if value is null
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
public void AddOptional(string key, object? value)
{
if (value != null)
base.Add(key, value);
}
/// <summary>
/// Add a decimal value as string
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
public void AddString(string key, decimal value)
{
base.Add(key, value.ToString(CultureInfo.InvariantCulture));
}
/// <summary>
/// Add a decimal value as string. Not added if value is null
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
public void AddOptionalString(string key, decimal? value)
{
if (value != null)
base.Add(key, value.Value.ToString(CultureInfo.InvariantCulture));
}
/// <summary>
/// Add a int value as string
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
public void AddString(string key, int value)
{
base.Add(key, value.ToString(CultureInfo.InvariantCulture));
}
/// <summary>
/// Add a int value as string. Not added if value is null
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
public void AddOptionalString(string key, int? value)
{
if (value != null)
base.Add(key, value.Value.ToString(CultureInfo.InvariantCulture));
}
/// <summary>
/// Add a long value as string
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
public void AddString(string key, long value)
{
base.Add(key, value.ToString(CultureInfo.InvariantCulture));
}
/// <summary>
/// Add a long value as string. Not added if value is null
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
public void AddOptionalString(string key, long? value)
{
if (value != null)
base.Add(key, value.Value.ToString(CultureInfo.InvariantCulture));
}
/// <summary>
/// Add a DateTime value as string
/// </summary>
public void AddString(string key, DateTime value)
{
base.Add(key, value.ToString("yyyy-MM-ddTHH:mm:ssZ"));
}
/// <summary>
/// Add a DateTime value as string. Not added if value is null
/// </summary>
public void AddOptionalString(string key, DateTime? value)
{
if (value != null)
base.Add(key, value.Value.ToString("yyyy-MM-ddTHH:mm:ssZ"));
}
/// <summary>
/// Add a datetime value as milliseconds timestamp
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
public void AddMilliseconds(string key, DateTime value)
{
base.Add(key, DateTimeConverter.ConvertToMilliseconds(value));
}
/// <summary>
/// Add a datetime value as milliseconds timestamp. Not added if value is null
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
public void AddOptionalMilliseconds(string key, DateTime? value)
{
if (value != null)
base.Add(key, DateTimeConverter.ConvertToMilliseconds(value));
}
/// <summary>
/// Add a datetime value as milliseconds timestamp
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
public void AddMillisecondsString(string key, DateTime value)
{
base.Add(key, DateTimeConverter.ConvertToMilliseconds(value).Value.ToString(CultureInfo.InvariantCulture));
}
/// <summary>
/// Add a datetime value as milliseconds timestamp. Not added if value is null
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
public void AddOptionalMillisecondsString(string key, DateTime? value)
{
if (value != null)
base.Add(key, DateTimeConverter.ConvertToMilliseconds(value).Value.ToString(CultureInfo.InvariantCulture));
}
/// <summary>
/// Add a datetime value as seconds timestamp
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
public void AddSeconds(string key, DateTime value)
{
base.Add(key, DateTimeConverter.ConvertToSeconds(value));
}
/// <summary>
/// Add a datetime value as seconds timestamp. Not added if value is null
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
public void AddOptionalSeconds(string key, DateTime? value)
{
if (value != null)
base.Add(key, DateTimeConverter.ConvertToSeconds(value));
}
/// <summary>
/// Add a datetime value as string seconds timestamp
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
public void AddSecondsString(string key, DateTime value)
{
base.Add(key, DateTimeConverter.ConvertToSeconds(value).ToString()!);
}
/// <summary>
/// Add a datetime value as string seconds timestamp. Not added if value is null
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
public void AddOptionalSecondsString(string key, DateTime? value)
{
if (value != null)
base.Add(key, DateTimeConverter.ConvertToSeconds(value).ToString()!);
}
/// <summary>
/// Add an enum value as the string value as mapped using the <see cref="MapAttribute" />
/// </summary>
#if NET5_0_OR_GREATER
public void AddEnum<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string key, T value)
#else
public void AddEnum<T>(string key, T value)
#endif
where T : struct, Enum
{
base.Add(key, EnumConverter<T>.GetString(value)!);
}
/// <summary>
/// Add an enum value as the string value as mapped using the <see cref="MapAttribute" />
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
#if NET5_0_OR_GREATER
public void AddEnumAsInt<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string key, T value)
#else
public void AddEnumAsInt<T>(string key, T value)
#endif
where T : struct, Enum
{
var stringVal = EnumConverter<T>.GetString(value)!;
base.Add(key, int.Parse(stringVal)!);
}
/// <summary>
/// Add an enum value as the string value as mapped using the <see cref="MapAttribute" />. Not added if value is null
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
#if NET5_0_OR_GREATER
public void AddOptionalEnum<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string key, T? value)
#else
public void AddOptionalEnum<T>(string key, T? value)
#endif
where T : struct, Enum
{
if (value != null)
base.Add(key, EnumConverter<T>.GetString(value));
}
/// <summary>
/// Add an enum value as the string value as mapped using the <see cref="MapAttribute" />. Not added if value is null
/// </summary>
#if NET5_0_OR_GREATER
public void AddOptionalEnumAsInt<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string key, T? value)
#else
public void AddOptionalEnumAsInt<T>(string key, T? value)
#endif
where T : struct, Enum
{
if (value != null)
{
var stringVal = EnumConverter<T>.GetString(value);
base.Add(key, int.Parse(stringVal));
}
}
/// <summary>
/// Add key as comma separated values
/// </summary>
public void AddCommaSeparated(string key, IEnumerable<string> values)
{
base.Add(key, string.Join(",", values));
}
/// <summary>
/// Add key as comma separated values if there are values provided
/// </summary>
public void AddOptionalCommaSeparated(string key, IEnumerable<string>? values)
{
if (values == null || !values.Any())
return;
base.Add(key, string.Join(",", values));
}
/// <summary>
/// Add key as comma separated values
/// </summary>
#if NET5_0_OR_GREATER
public void AddCommaSeparated<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string key, IEnumerable<T> values)
#else
public void AddCommaSeparated<T>(string key, IEnumerable<T> values)
#endif
where T : struct, Enum
{
base.Add(key, string.Join(",", values.Select(x => EnumConverter.GetString(x))));
}
/// <summary>
/// Add key as comma separated values if there are values provided
/// </summary>
#if NET5_0_OR_GREATER
public void AddOptionalCommaSeparated<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string key, IEnumerable<T>? values)
#else
public void AddOptionalCommaSeparated<T>(string key, IEnumerable<T>? values)
#endif
where T : struct, Enum
{
if (values == null || !values.Any())
return;
base.Add(key, string.Join(",", values.Select(x => EnumConverter.GetString(x))));
}
/// <summary>
/// Add key as boolean lower case value
/// </summary>
public void AddBoolString(string key, bool value)
{
base.Add(key, value.ToString().ToLower());
}
/// <summary>
/// Add key as boolean lower case value if it's not null
/// </summary>
public void AddOptionalBoolString(string key, bool? value)
{
if (value == null)
return;
base.Add(key, value.ToString()!.ToLower());
}
/// <summary>
/// Set the request body. Can be used to specify a simple value or array as the body instead of an object
/// </summary>
/// <param name="body">Body to set</param>
/// <exception cref="InvalidOperationException"></exception>
public void SetBody(object body)
{
if (this.Any())
throw new InvalidOperationException("Can't set body when other parameters already specified");
base.Add(Constants.BodyPlaceHolderKey, body);
}
}
}
@@ -0,0 +1,146 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.Objects
{
/// <summary>
/// Settings for parameter serialization
/// </summary>
public class ParameterSerializationSettings
{
/// <summary>
/// Default serialization settings
/// </summary>
public static ParameterSerializationSettings Default { get; } = new ParameterSerializationSettings();
/// <summary>
/// Whether to sort the parameters
/// </summary>
public bool Sort { get; set; } = true;
/// <summary>
/// The parameter comparer when sorting
/// </summary>
public IComparer<string>? SortComparer { get; set; }
/// <summary>
/// Decimal serialization type
/// </summary>
public DecimalSerialization Decimal { get; set; } = DecimalSerialization.Number;
/// <summary>
/// DateTime serialization type
/// </summary>
public DateTimeSerialization DateTimes { get; set; } = DateTimeSerialization.MillisecondsNumber;
/// <summary>
/// Boolean serialization type
/// </summary>
public BoolSerialization Bool { get; set; } = BoolSerialization.Bool;
/// <summary>
/// Integer serialization type
/// </summary>
public IntegerSerialization Integer { get; set; } = IntegerSerialization.Number;
/// <summary>
/// Enum serialization type
/// </summary>
public EnumSerialization Enum { get; set; } = EnumSerialization.String;
/// <summary>
/// Array serialization type
/// </summary>
public ArrayParametersSerialization Array { get; set; } = ArrayParametersSerialization.Array;
}
/// <summary>
/// Type of decimal value serialization
/// </summary>
public enum DecimalSerialization
{
/// <summary>
/// Decimals should be serialized as numbers
/// </summary>
Number,
/// <summary>
/// Decimals should be strings
/// </summary>
String
}
/// <summary>
/// Type of DateTime value serialization
/// </summary>
public enum DateTimeSerialization
{
/// <summary>
/// DateTimes should be serialized as milliseconds number
/// </summary>
MillisecondsNumber,
/// <summary>
/// DateTimes should be serialized as milliseconds string
/// </summary>
MillisecondsString,
/// <summary>
/// DateTimes should be serialized as seconds number
/// </summary>
SecondsNumber,
/// <summary>
/// DateTimes should be serialized as seconds string
/// </summary>
SecondsString,
/// <summary>
/// DateTimes should be serialized as microseconds number
/// </summary>
MicrosecondsNumber,
/// <summary>
/// DateTimes should be serialized as microseconds string
/// </summary>
MicrosecondsString,
/// <summary>
/// DateTimes should be serialized as ISO 8601 string
/// </summary>
Rfc3339String
}
/// <summary>
/// Type of boolean value serialization
/// </summary>
public enum BoolSerialization
{
/// <summary>
/// Booleans should be serialized as bool values
/// </summary>
Bool,
/// <summary>
/// Booleans should be serialized as strings
/// </summary>
String
}
/// <summary>
/// Type of integer value serialization
/// </summary>
public enum IntegerSerialization
{
/// <summary>
/// Integers should be serialized as integer values
/// </summary>
Number,
/// <summary>
/// Integers should be serialized as strings
/// </summary>
String
}
/// <summary>
/// Type of enum value serialization
/// </summary>
public enum EnumSerialization
{
/// <summary>
/// Enums should be serialized as integer values
/// </summary>
Number,
/// <summary>
/// Enums should be serialized as strings
/// </summary>
String
}
}
+375
View File
@@ -0,0 +1,375 @@
using CryptoExchange.Net.Attributes;
using CryptoExchange.Net.Converters.SystemTextJson;
using CryptoExchange.Net.Interfaces;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
namespace CryptoExchange.Net.Objects
{
/// <summary>
/// Set of parameters
/// </summary>
public class Parameters : IDictionary<string, object>
{
private readonly ParameterSerializationSettings _serializationSettings;
private IDictionary<string, object> _parameters;
private object? _value;
/// <inheritdoc />
public object? BodyValue => _value;
/// <inheritdoc />
public ICollection<string> Keys => _parameters.Keys;
/// <inheritdoc />
public ICollection<object> Values => _parameters.Values;
/// <inheritdoc />
public int Count => _parameters.Count;
/// <inheritdoc />
public bool IsReadOnly => _parameters.IsReadOnly;
/// <summary>
/// Whether any parameters are defined
/// </summary>
public bool Empty => _parameters.Count == 0 && _value == null;
/// <inheritdoc />
public object this[string key] { get => _parameters[key]; set => _parameters[key] = value; }
/// <summary>
/// ctor
/// </summary>
/// <param name="serializationSettings">Serialization settings</param>
public Parameters(ParameterSerializationSettings serializationSettings)
{
_serializationSettings = serializationSettings;
if (_serializationSettings.Sort)
_parameters = new SortedDictionary<string, object>(_serializationSettings.SortComparer);
else
_parameters = new Dictionary<string, object>();
}
/// <summary>
/// ctor
/// </summary>
/// <param name="serializationSettings">Serialization settings</param>
/// <param name="value">Body value</param>
public Parameters(object value, ParameterSerializationSettings serializationSettings)
{
_parameters = new Dictionary<string, object>();
_serializationSettings = serializationSettings;
_value = value;
}
/// <summary>
/// Add a short value if it is not null
/// </summary>
public void Add(string key, short? value, IntegerSerialization? serialization = null)
{
if (value == null)
return;
Add(key, value.Value, serialization);
}
/// <summary>
/// Add a short value
/// </summary>
public void Add(string key, short value, IntegerSerialization? serialization = null)
{
var serializationToUse = serialization ?? _serializationSettings.Integer;
if (serializationToUse == IntegerSerialization.String)
_parameters.Add(key, value.ToString(CultureInfo.InvariantCulture));
else if (serializationToUse == IntegerSerialization.Number)
_parameters.Add(key, value);
else
throw new ArgumentException("Unknown Integer serialization setting");
}
/// <summary>
/// Add an int value if it is not null
/// </summary>
public void Add(string key, int? value, IntegerSerialization? serialization = null)
{
if (value == null)
return;
Add(key, value.Value, serialization);
}
/// <summary>
/// Add an int value
/// </summary>
public void Add(string key, int value, IntegerSerialization? serialization = null)
{
var serializationToUse = serialization ?? _serializationSettings.Integer;
if (serializationToUse == IntegerSerialization.String)
_parameters.Add(key, value.ToString(CultureInfo.InvariantCulture));
else if (serializationToUse == IntegerSerialization.Number)
_parameters.Add(key, value);
else
throw new ArgumentException("Unknown Integer serialization setting");
}
/// <summary>
/// Add a long value if it is not null
/// </summary>
public void Add(string key, long? value, IntegerSerialization? serialization = null)
{
if (value == null)
return;
Add(key, value.Value, serialization);
}
/// <summary>
/// Add a long value
/// </summary>
public void Add(string key, long value, IntegerSerialization? serialization = null)
{
var serializationToUse = serialization ?? _serializationSettings.Integer;
if (serializationToUse == IntegerSerialization.String)
_parameters.Add(key, value.ToString(CultureInfo.InvariantCulture));
else if (serializationToUse == IntegerSerialization.Number)
_parameters.Add(key, value);
else
throw new ArgumentException("Unknown Integer serialization setting");
}
/// <summary>
/// Add a decimal value if it is not null
/// </summary>
public void Add(string key, decimal? value, DecimalSerialization? serialization = null)
{
if (value == null)
return;
Add(key, value.Value, serialization);
}
/// <summary>
/// Add a decimal value
/// </summary>
public void Add(string key, decimal value, DecimalSerialization? serialization = null)
{
var serializationToUse = serialization ?? _serializationSettings.Decimal;
if (serializationToUse == DecimalSerialization.String)
_parameters.Add(key, value.ToString(CultureInfo.InvariantCulture));
else if (serializationToUse == DecimalSerialization.Number)
_parameters.Add(key, value);
else
throw new ArgumentException("Unknown Decimal serialization setting");
}
/// <summary>
/// Add a double value if it is not null
/// </summary>
public void Add(string key, double? value, DecimalSerialization? serialization = null)
{
if (value == null)
return;
Add(key, value.Value, serialization);
}
/// <summary>
/// Add a double value
/// </summary>
public void Add(string key, double value, DecimalSerialization? serialization = null)
{
var serializationToUse = serialization ?? _serializationSettings.Decimal;
if (serializationToUse == DecimalSerialization.String)
_parameters.Add(key, value.ToString(CultureInfo.InvariantCulture));
else if (serializationToUse == DecimalSerialization.Number)
_parameters.Add(key, value);
else
throw new ArgumentException("Unknown Decimal serialization setting");
}
/// <summary>
/// Add a bool value if it is not null
/// </summary>
public void Add(string key, bool? value, BoolSerialization? serialization = null)
{
if (value == null)
return;
Add(key, value.Value, serialization);
}
/// <summary>
/// Add a bool value
/// </summary>
public void Add(string key, bool value, BoolSerialization? serialization = null)
{
var serializationToUse = serialization ?? _serializationSettings.Bool;
if (serializationToUse == BoolSerialization.String)
_parameters.Add(key, value.ToString(CultureInfo.InvariantCulture).ToLowerInvariant());
else if (serializationToUse == BoolSerialization.Bool)
_parameters.Add(key, value);
else
throw new ArgumentException("Unknown Bool serialization setting");
}
/// <summary>
/// Add key as comma separated values if there are values provided
/// </summary>
public void AddCommaSeparated(string key, IEnumerable<string>? values)
{
if (values == null || !values.Any())
return;
_parameters.Add(key, string.Join(",", values));
}
/// <summary>
/// Add key as comma separated values
/// </summary>
#if NET5_0_OR_GREATER
public void AddCommaSeparated<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string key, IEnumerable<T>? values)
#else
public void AddCommaSeparated<T>(string key, IEnumerable<T>? values)
#endif
where T : struct, Enum
{
if (values == null || !values.Any())
return;
_parameters.Add(key, string.Join(",", values.Select(x => EnumConverter.GetString(x))));
}
/// <summary>
/// Add an enum value if it is not null
/// </summary>
public void Add<
#if NET5_0_OR_GREATER
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)]
# endif
T>(string key, T? value, EnumSerialization? serialization = null)
where T : struct, Enum
{
if (value == null)
return;
Add(key, value.Value, serialization);
}
/// <summary>
/// Add a enum value
/// </summary>
public void Add<
#if NET5_0_OR_GREATER
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)]
#endif
T>(string key, T value, EnumSerialization? serialization = null)
where T : struct, Enum
{
var serializationToUse = serialization ?? _serializationSettings.Enum;
if (serializationToUse == EnumSerialization.String)
_parameters.Add(key, EnumConverter<T>.GetString(value));
else if (serializationToUse == EnumSerialization.Number)
_parameters.Add(key, int.Parse(EnumConverter<T>.GetString(value), CultureInfo.InvariantCulture));
else
throw new ArgumentException("Unknown Integer serialization setting");
}
/// <summary>
/// Add a DateTime value if it is not null
/// </summary>
public void Add(string key, DateTime? value, DateTimeSerialization? serialization = null)
{
if (value == null)
return;
Add(key, value.Value, serialization);
}
/// <summary>
/// Add a DateTime value
/// </summary>
public void Add(string key, DateTime value, DateTimeSerialization? serialization = null)
{
var serializationToUse = serialization ?? _serializationSettings.DateTimes;
if (serializationToUse == DateTimeSerialization.MillisecondsNumber)
_parameters.Add(key, DateTimeConverter.ConvertToMilliseconds(value));
else if (serializationToUse == DateTimeSerialization.MillisecondsString)
_parameters.Add(key, DateTimeConverter.ConvertToMilliseconds(value).Value.ToString(CultureInfo.InvariantCulture));
else if (serializationToUse == DateTimeSerialization.SecondsNumber)
_parameters.Add(key, DateTimeConverter.ConvertToSeconds(value));
else if (serializationToUse == DateTimeSerialization.SecondsString)
_parameters.Add(key, DateTimeConverter.ConvertToSeconds(value).Value.ToString(CultureInfo.InvariantCulture));
else if (serializationToUse == DateTimeSerialization.MicrosecondsNumber)
_parameters.Add(key, DateTimeConverter.ConvertToMicroseconds(value));
else if (serializationToUse == DateTimeSerialization.MicrosecondsString)
_parameters.Add(key, DateTimeConverter.ConvertToMicroseconds(value).Value.ToString(CultureInfo.InvariantCulture));
else if (serializationToUse == DateTimeSerialization.Rfc3339String)
_parameters.Add(key, value.ToRfc3339String());
else
throw new ArgumentException("Unknown DateTime serialization setting");
}
/// <summary>
/// Add a string value if it is not null
/// </summary>
public void Add(string key, string? value)
{
if (value == null)
return;
_parameters.Add(key, value);
}
/// <summary>
/// Add an array of values if there are values provided
/// </summary>
public void AddArray<T>(string key, IEnumerable<T>? values)
{
if (values == null || !values.Any())
return;
_parameters.Add(key, values is T[] arr ? arr : values.ToArray());
}
/// <summary>
/// Add a raw object value if it is not null
/// </summary>
public void AddRaw(string key, object? value)
{
if (value == null)
return;
_parameters.Add(key, value);
}
/// <inheritdoc />
public void Add(string key, object value) => _parameters.Add(key, value);
/// <inheritdoc />
public bool ContainsKey(string key) => _parameters.ContainsKey(key);
/// <inheritdoc />
public bool Remove(string key) => _parameters.Remove(key);
/// <inheritdoc />
public bool TryGetValue(string key, out object value) => _parameters.TryGetValue(key, out value!);
/// <inheritdoc />
public void Add(KeyValuePair<string, object> item) => _parameters.Add(item.Key, item.Value);
/// <inheritdoc />
public void Clear() => _parameters.Clear();
/// <inheritdoc />
public bool Contains(KeyValuePair<string, object> item) => _parameters.ContainsKey(item.Key) && _parameters[item.Key] == item.Value;
/// <inheritdoc />
public void CopyTo(KeyValuePair<string, object>[] array, int arrayIndex) => _parameters.CopyTo(array, arrayIndex);
/// <inheritdoc />
public bool Remove(KeyValuePair<string, object> item) => _parameters.Remove(item.Key);
/// <inheritdoc />
public IEnumerator<KeyValuePair<string, object>> GetEnumerator() => _parameters.GetEnumerator();
/// <inheritdoc />
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}
+14 -1
View File
@@ -33,11 +33,23 @@
/// Centralization type
/// </summary>
public CentralizationType CentralizationType { get; }
/// <summary>
/// Supported environments
/// </summary>
public string[] SupportedEnvironments { get; }
/// <summary>
/// ctor
/// </summary>
public PlatformInfo(string id, string displayName, string logo, string url, string[] apiDocsUrl, PlatformType platformType, CentralizationType centralizationType)
public PlatformInfo(
string id,
string displayName,
string logo,
string url,
string[] apiDocsUrl,
PlatformType platformType,
CentralizationType centralizationType,
string[] supportedEnvironments)
{
Id = id;
DisplayName = displayName;
@@ -46,6 +58,7 @@
ApiDocsUrl = apiDocsUrl;
PlatformType = platformType;
CentralizationType = centralizationType;
SupportedEnvironments = supportedEnvironments;
}
}
}
@@ -9,9 +9,14 @@ namespace CryptoExchange.Net.Objects
public class RequestDefinition
{
private string? _stringRep;
private string? _fullUrl;
// Basics
/// <summary>
/// Base address of the request
/// </summary>
public string BaseAddress { get; set; }
/// <summary>
/// Path of the request
/// </summary>
@@ -77,13 +82,31 @@ namespace CryptoExchange.Net.Objects
/// </summary>
public bool? ForcePathEndWithSlash { get; set; }
/// <summary>
/// Full url, host + path
/// </summary>
public string FullUrl
{
get
{
if (_fullUrl != null)
return _fullUrl;
var result = BaseAddress.AppendPath(Path);
if (ForcePathEndWithSlash == true && !result.EndsWith("/"))
result += "/";
_fullUrl = result;
return _fullUrl;
}
}
/// <summary>
/// ctor
/// </summary>
/// <param name="path"></param>
/// <param name="method"></param>
public RequestDefinition(string path, HttpMethod method)
public RequestDefinition(string baseAddress, string path, HttpMethod method)
{
BaseAddress = baseAddress;
Path = path;
Method = method;
@@ -1,5 +1,6 @@
using CryptoExchange.Net.RateLimiting.Interfaces;
using System.Collections.Concurrent;
using System.IO;
using System.Net.Http;
namespace CryptoExchange.Net.Objects
@@ -15,27 +16,30 @@ namespace CryptoExchange.Net.Objects
/// Get a definition if it is already in the cache or create a new definition and add it to the cache
/// </summary>
/// <param name="method">The HttpMethod</param>
/// <param name="baseAddress">The base address/host</param>
/// <param name="path">Endpoint path</param>
/// <param name="authenticated">Endpoint is authenticated</param>
/// <returns></returns>
public RequestDefinition GetOrCreate(HttpMethod method, string path, bool authenticated = false)
=> GetOrCreate(method, path, null, 0, authenticated, null, null, null, null, null);
public RequestDefinition GetOrCreate(HttpMethod method, string baseAddress, string path, bool authenticated = false)
=> GetOrCreate(method, baseAddress, path, null, 0, authenticated, null, null, null, null, null, null, null);
/// <summary>
/// Get a definition if it is already in the cache or create a new definition and add it to the cache
/// </summary>
/// <param name="method">The HttpMethod</param>
/// <param name="baseAddress">The base address/host</param>
/// <param name="path">Endpoint path</param>
/// <param name="rateLimitGate">The rate limit gate</param>
/// <param name="weight">Request weight</param>
/// <param name="authenticated">Endpoint is authenticated</param>
/// <returns></returns>
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);
public RequestDefinition GetOrCreate(HttpMethod method, string baseAddress, string path, IRateLimitGate rateLimitGate, int weight = 1, bool authenticated = false)
=> GetOrCreate(method, baseAddress, path, rateLimitGate, weight, authenticated, null, null, null, null, null, null, null);
/// <summary>
/// Get a definition if it is already in the cache or create a new definition and add it to the cache
/// </summary>
/// <param name="baseAddress">The base address/host</param>
/// <param name="method">The HttpMethod</param>
/// <param name="path">Endpoint path</param>
/// <param name="rateLimitGate">The rate limit gate</param>
@@ -48,9 +52,11 @@ namespace CryptoExchange.Net.Objects
/// <param name="preventCaching">Prevent request caching</param>
/// <param name="tryParseOnNonSuccess">Try parse the response even when status is not success</param>
/// <param name="forcePathEndWithSlash">Force trailing `/`</param>
/// <param name="identifier">Optional request identifier override</param>
/// <returns></returns>
public RequestDefinition GetOrCreate(
HttpMethod method,
string baseAddress,
string path,
IRateLimitGate? rateLimitGate,
int weight,
@@ -61,45 +67,13 @@ namespace CryptoExchange.Net.Objects
ArrayParametersSerialization? arraySerialization = null,
bool? preventCaching = null,
bool? tryParseOnNonSuccess = null,
bool? forcePathEndWithSlash = null)
=> GetOrCreate(method + path, method, path, rateLimitGate, weight, authenticated, limitGuard, requestBodyFormat, parameterPosition, arraySerialization, preventCaching, tryParseOnNonSuccess, forcePathEndWithSlash);
/// <summary>
/// Get a definition if it is already in the cache or create a new definition and add it to the cache
/// </summary>
/// <param name="identifier">Request identifier</param>
/// <param name="method">The HttpMethod</param>
/// <param name="path">Endpoint path</param>
/// <param name="rateLimitGate">The rate limit gate</param>
/// <param name="limitGuard">The rate limit guard for this specific endpoint</param>
/// <param name="weight">Request weight</param>
/// <param name="authenticated">Endpoint is authenticated</param>
/// <param name="requestBodyFormat">Request body format</param>
/// <param name="parameterPosition">Parameter position</param>
/// <param name="arraySerialization">Array serialization type</param>
/// <param name="preventCaching">Prevent request caching</param>
/// <param name="tryParseOnNonSuccess">Try parse the response even when status is not success</param>
/// <param name="forcePathEndWithSlash">Force trailing `/`</param>
/// <returns></returns>
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,
bool? tryParseOnNonSuccess = null,
bool? forcePathEndWithSlash = null)
bool? forcePathEndWithSlash = null,
string? identifier = null)
{
if (!_definitions.TryGetValue(identifier, out var def))
var identifierToUse = identifier ?? $"{path}{method.Method}{baseAddress}";
if (!_definitions.TryGetValue(identifierToUse, out var def))
{
def = new RequestDefinition(path, method)
def = new RequestDefinition(baseAddress, path, method)
{
Authenticated = authenticated,
LimitGuard = limitGuard,
@@ -110,9 +84,9 @@ namespace CryptoExchange.Net.Objects
ParameterPosition = parameterPosition,
PreventCaching = preventCaching ?? false,
TryParseOnNonSuccess = tryParseOnNonSuccess ?? false,
ForcePathEndWithSlash = forcePathEndWithSlash ?? false
ForcePathEndWithSlash = forcePathEndWithSlash ?? false,
};
_definitions.TryAdd(identifier, def);
_definitions.TryAdd(identifierToUse, def);
}
return def;
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using CryptoExchange.Net.Interfaces;
using System.Collections.Generic;
using System.Net.Http;
namespace CryptoExchange.Net.Objects
@@ -12,29 +13,17 @@ namespace CryptoExchange.Net.Objects
private string? _queryString;
/// <summary>
/// Http method
/// The request definition for the request
/// </summary>
public HttpMethod Method { get; set; }
/// <summary>
/// Whether the request needs authentication
/// </summary>
public bool Authenticated { get; set; }
/// <summary>
/// Base address for the request
/// </summary>
public string BaseAddress { get; set; }
/// <summary>
/// The request path
/// </summary>
public string Path { get; set; }
public RequestDefinition RequestDefinition { get; set; }
/// <summary>
/// Query parameters
/// </summary>
public IDictionary<string, object>? QueryParameters { get; set; }
public Parameters? QueryParameters { get; set; }
/// <summary>
/// Body parameters
/// </summary>
public IDictionary<string, object>? BodyParameters { get; set; }
public Parameters? BodyParameters { get; set; }
/// <summary>
/// Request headers
/// </summary>
@@ -57,22 +46,16 @@ namespace CryptoExchange.Net.Objects
/// </summary>
public RestRequestConfiguration(
RequestDefinition requestDefinition,
string baseAddress,
IDictionary<string, object>? queryParams,
IDictionary<string, object>? bodyParams,
Parameters? queryParams,
Parameters? bodyParams,
IDictionary<string, string>? headers,
ArrayParametersSerialization arraySerialization,
HttpMethodParameterPosition parametersPosition,
RequestBodyFormat bodyFormat)
{
Method = requestDefinition.Method;
Authenticated = requestDefinition.Authenticated;
Path = requestDefinition.Path;
BaseAddress = baseAddress;
RequestDefinition = requestDefinition;
QueryParameters = queryParams;
BodyParameters = bodyParams;
Headers = headers;
ArraySerialization = arraySerialization;
ParameterPosition = parametersPosition;
BodyFormat = bodyFormat;
}
@@ -80,15 +63,15 @@ namespace CryptoExchange.Net.Objects
/// <summary>
/// Get the parameter collection based on the ParameterPosition
/// </summary>
public IDictionary<string, object> GetPositionParameters()
public Parameters GetPositionParameters()
{
if (ParameterPosition == HttpMethodParameterPosition.InBody)
{
BodyParameters ??= new Dictionary<string, object>();
BodyParameters ??= new Parameters(ParameterSerializationSettings.Default);
return BodyParameters;
}
QueryParameters ??= new Dictionary<string, object>();
QueryParameters ??= new Parameters(ParameterSerializationSettings.Default);
return QueryParameters;
}
@@ -0,0 +1,121 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Text;
namespace CryptoExchange.Net.Objects;
/// <summary>
/// Call result
/// </summary>
[DebuggerDisplay("{DebugView,nq}")]
public record CallResult : ICallResult
{
private string DebugView => Success ? "Success" : $"Error: {Error}";
private static CallResult _successResult = new CallResult();
/// <inheritdoc />
public Error? Error { get; init; }
/// <inheritdoc />
[MemberNotNullWhen(false, nameof(Error))]
public bool Success => Error == null;
/// <summary>
/// Create an error response
/// </summary>
/// <param name="error">The error</param>
public static CallResult Fail(Error error) => new CallResult { Error = error };
/// <summary>
/// Create a success result
/// </summary>
public static CallResult Ok() => _successResult;
/// <summary>
/// Create a success result
/// </summary>
/// <typeparam name="T">Result type</typeparam>
/// <param name="originalData">The original string data</param>
/// <param name="data">Data type</param>
public static CallResult<T> Ok<T>(T data, string? originalData = null) => new CallResult<T> { Data = data, OriginalData = originalData };
/// <summary>
/// Create an error response
/// </summary>
/// <typeparam name="T">Result type</typeparam>
/// <param name="originalData">The original string data</param>
/// <param name="error">The error</param>
public static CallResult<T> Fail<T>(Error error, string? originalData = null) => new CallResult<T> { Error = error, OriginalData = originalData };
/// <inheritdoc />
public override string ToString()
{
return Success ? $"Success" : $"Error: {Error}";
}
}
/// <inheritdoc />
public record CallResult<T> : CallResult, ICallResult<T>
{
/// <inheritdoc />
public new Error? Error
{
get => base.Error;
init => base.Error = value;
}
/// <inheritdoc />
[MemberNotNullWhen(false, nameof(Error))]
[MemberNotNullWhen(true, nameof(Data))]
public new bool Success => Error == null;
/// <summary>
/// The data returned by the call, only available when Success = true
/// </summary>
public T? Data { get; init; }
/// <summary>
/// The original data returned by the call, only available when `OutputOriginalData` is set to `true` in the client options
/// </summary>
public string? OriginalData { get; init; }
/// <summary>
/// Create an error response
/// </summary>
/// <param name="error">The error</param>
/// <param name="originalData">The original string data</param>
public static CallResult<T> Fail(Error error, string? originalData = null) => new CallResult<T> { Error = error, OriginalData = originalData };
/// <summary>
/// Create a success result
/// </summary>
/// <param name="data">The data</param>
/// <param name="originalData">The original string data</param>
/// <returns></returns>
public static CallResult<T> Ok(T data, string? originalData = null) => new CallResult<T> { Data = data, OriginalData = originalData };
}
/// <summary>
/// Call result for an exchange
/// </summary>
/// <typeparam name="T">Data type</typeparam>
public record ExchangeCallResult<T> : CallResult<T>
{
/// <summary>
/// Exchange name
/// </summary>
public string Exchange { get; set; } = string.Empty;
/// <summary>
/// Create an error response
/// </summary>
/// <param name="exchange">The exchange name</param>
/// <param name="error">The error</param>
/// <param name="originalData">The original string data</param>
public static ExchangeCallResult<T> Fail(string exchange, Error error, string? originalData = null) => new ExchangeCallResult<T> { Exchange = exchange, Error = error };
/// <summary>
/// Create a success result
/// </summary>
/// <param name="exchange">The exchange name</param>
/// <param name="data">The data</param>
/// <param name="originalData">The original string data</param>
/// <returns></returns>
public static ExchangeCallResult<T> Ok(string exchange, T data, string? originalData = null) => new ExchangeCallResult<T> { Exchange = exchange, Data = data };
}
@@ -0,0 +1,314 @@
using CryptoExchange.Net.SharedApis;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
namespace CryptoExchange.Net.Objects;
/// <summary>
/// HTTP call result
/// </summary>
[DebuggerDisplay("{DebugView,nq}")]
public record HttpResult : IHttpResult
{
private string DebugView => $"[Req {RequestId}] " + (Success ? "Success" : $"Error: {Error}");
/// <summary>
/// Create a new success HTTP result
/// </summary>
public static HttpResult<T> Ok<T>(
string exchange,
HttpStatusCode code,
Version version,
HttpResponseHeaders responseHeaders,
TimeSpan elapsed,
long? contentLength,
string? originalData,
int requestId,
string uri,
string? content,
HttpMethod method,
HttpRequestHeaders requestHeaders,
ResultDataSource source,
T data) =>
new HttpResult<T>(exchange, data, null)
{
ResponseStatusCode = code,
HttpVersion = version,
ResponseHeaders = responseHeaders,
ResponseTime = elapsed,
ResponseLength = contentLength,
OriginalData = originalData,
RequestId = requestId,
RequestUrl = uri,
RequestBody = content,
RequestMethod = method,
RequestHeaders = requestHeaders,
DataSource = source,
};
/// <summary>
/// Create a new success HTTP result
/// </summary>
public static HttpResult<T> Ok<T>(IHttpResult result, T data, PageRequest? pageRequest = null) =>
new HttpResult<T>(result.Exchange, data, null)
{
ResponseStatusCode = result.ResponseStatusCode,
HttpVersion = result.HttpVersion,
ResponseHeaders = result.ResponseHeaders,
ResponseTime = result.ResponseTime,
ResponseLength = result.ResponseLength,
OriginalData = result.OriginalData,
RequestId = result.RequestId,
RequestUrl = result.RequestUrl,
RequestBody = result.RequestBody,
RequestMethod = result.RequestMethod,
RequestHeaders = result.RequestHeaders,
DataSource = result.DataSource,
Error = result.Error,
Data = data,
NextPageRequest = pageRequest
};
/// <summary>
/// Create a new error HTTP result
/// </summary>
public static HttpResult<T> Fail<T>(string exchange, Error error) => new HttpResult<T>(exchange, default, error);
/// <summary>
/// Create a new error HTTP result
/// </summary>
public static HttpResult<T> Fail<T>(IHttpResult result, Error? error = null, T? data = default)
=> new HttpResult<T>(result.Exchange, data, error ?? result.Error)
{
ResponseStatusCode = result.ResponseStatusCode,
HttpVersion = result.HttpVersion,
ResponseHeaders = result.ResponseHeaders,
ResponseTime = result.ResponseTime,
ResponseLength = result.ResponseLength,
OriginalData = result.OriginalData,
RequestId = result.RequestId,
RequestUrl = result.RequestUrl,
RequestBody = result.RequestBody,
RequestMethod = result.RequestMethod,
RequestHeaders = result.RequestHeaders,
DataSource = result.DataSource,
};
/// <summary>
/// Create a new error HTTP result
/// </summary>
public static HttpResult<T> Fail<T>(
string exchange,
HttpStatusCode? code,
Version? version,
HttpResponseHeaders? responseHeaders,
TimeSpan elapsed,
long? contentLength,
string? originalData,
int requestId,
string uri,
string? content,
HttpMethod method,
HttpRequestHeaders requestHeaders,
ResultDataSource source,
Error error,
T? result = default) =>
new HttpResult<T>(exchange, result, error)
{
ResponseStatusCode = code,
HttpVersion = version,
ResponseHeaders = responseHeaders,
ResponseTime = elapsed,
ResponseLength = contentLength,
OriginalData = originalData,
RequestId = requestId,
RequestUrl = uri,
RequestBody = content,
RequestMethod = method,
RequestHeaders = requestHeaders,
DataSource = source,
};
/// <summary>
/// Create a new error HTTP result
/// </summary>
public static HttpResult Fail(string exchange, Error error) => new HttpResult() { Exchange = exchange, Error = error };
/// <summary>
/// Create a new error HTTP result
/// </summary>
public static HttpResult Fail(IHttpResult result, Error? error = null)
=> new HttpResult()
{
ResponseStatusCode = result.ResponseStatusCode,
HttpVersion = result.HttpVersion,
ResponseHeaders = result.ResponseHeaders,
ResponseTime = result.ResponseTime,
ResponseLength = result.ResponseLength,
OriginalData = result.OriginalData,
RequestId = result.RequestId,
RequestUrl = result.RequestUrl,
RequestBody = result.RequestBody,
RequestMethod = result.RequestMethod,
RequestHeaders = result.RequestHeaders,
DataSource = result.DataSource,
Exchange = result.Exchange,
Error = error ?? result.Error
};
/// <summary>
/// Create a new success HTTP result
/// </summary>
public static HttpResult Ok(IHttpResult result)
=> new HttpResult()
{
ResponseStatusCode = result.ResponseStatusCode,
HttpVersion = result.HttpVersion,
ResponseHeaders = result.ResponseHeaders,
ResponseTime = result.ResponseTime,
ResponseLength = result.ResponseLength,
OriginalData = result.OriginalData,
RequestId = result.RequestId,
RequestUrl = result.RequestUrl,
RequestBody = result.RequestBody,
RequestMethod = result.RequestMethod,
RequestHeaders = result.RequestHeaders,
DataSource = result.DataSource,
Exchange = result.Exchange,
};
/// <summary>
/// Exchange name
/// </summary>
public string Exchange { get; init; } = string.Empty;
/// <inheritdoc />
public Error? Error { get; internal set; }
/// <inheritdoc />
[MemberNotNullWhen(false, nameof(Error))]
public bool Success => Error == null;
/// <summary>
/// The original data returned by the call, only available when `OutputOriginalData` is set to `true` in the client options
/// </summary>
public string? OriginalData { get; init; }
/// <summary>
/// The request http method
/// </summary>
public HttpMethod? RequestMethod { get; init; }
/// <summary>
/// HTTP protocol version
/// </summary>
public Version? HttpVersion { get; init; }
/// <summary>
/// The headers sent with the request
/// </summary>
public HttpRequestHeaders? RequestHeaders { get; init; }
/// <summary>
/// The request id
/// </summary>
public int? RequestId { get; init; }
/// <summary>
/// The url which was requested
/// </summary>
public string? RequestUrl { get; init; }
/// <summary>
/// The body of the request
/// </summary>
public string? RequestBody { get; init; }
/// <summary>
/// Length in bytes of the response
/// </summary>
public long? ResponseLength { get; init; }
/// <summary>
/// The status code of the response. Note that a OK status does not always indicate success, check the Success parameter for this.
/// </summary>
public HttpStatusCode? ResponseStatusCode { get; init; }
/// <summary>
/// The response headers
/// </summary>
public HttpResponseHeaders? ResponseHeaders { get; init; }
/// <summary>
/// The time between sending the request and receiving the response
/// </summary>
public TimeSpan? ResponseTime { get; init; }
/// <summary>
/// The data source of this result
/// </summary>
public ResultDataSource DataSource { get; init; } = ResultDataSource.Server;
}
/// <inheritdoc />
[DebuggerDisplay("{DebugView,nq}")]
public record HttpResult<T> : HttpResult, IHttpResult<T>
{
private string DebugView
{
get
{
var result = new StringBuilder($"[Req {RequestId}] " + (Success ? "Success" : $"Error: {Error}"));
if (Data != null)
{
result.Append(", ");
var typeName = typeof(T).Name;
if (Data is Array ar)
{
result.Append($"{ar.Length} {typeName.Substring(0, typeName.Length - 2)}");
}
else
{
result.Append(typeName);
}
}
return result.ToString();
}
}
/// <summary>
/// ctor
/// </summary>
public HttpResult(string exchange, T? value, Error? error)
{
Exchange = exchange;
Data = value;
Error = error;
}
/// <inheritdoc />
public new Error? Error
{
get => base.Error;
internal set => base.Error = value;
}
/// <inheritdoc />
[MemberNotNullWhen(false, nameof(Error))]
[MemberNotNullWhen(true, nameof(Data))]
public new bool Success => Error == null;
/// <summary>
/// The data returned by the call, only available when Success = true
/// </summary>
public T? Data { get; init; }
/// <summary>
/// Next page request, only potentially available when using Shared API's
/// </summary>
public PageRequest? NextPageRequest { get; init; }
}
@@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Text;
namespace CryptoExchange.Net.Objects;
/// <summary>
/// Call result
/// </summary>
public interface ICallResult
{
/// <summary>
/// An error if the call didn't succeed, will always be filled if Success = false
/// </summary>
Error? Error { get; }
/// <summary>
/// Whether the call was successful
/// </summary>
[MemberNotNullWhen(false, nameof(Error))]
bool Success { get; }
}
/// <summary>
/// Call result
/// </summary>
/// <typeparam name="T">Result data type</typeparam>
public interface ICallResult<T> : ICallResult
{
/// <inheritdoc />
new Error? Error { get; }
/// <inheritdoc />
[MemberNotNullWhen(false, nameof(Error))]
[MemberNotNullWhen(true, nameof(Data))]
new bool Success { get; }
/// <summary>
/// The result data, only available when Success = true
/// </summary>
T? Data { get; }
}
@@ -0,0 +1,85 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
namespace CryptoExchange.Net.Objects
{
/// <summary>
/// HTTP call result
/// </summary>
public interface IHttpResult : ICallResult
{
/// <summary>
/// Exchange name
/// </summary>
string Exchange { get; init; }
/// <summary>
/// The original data returned by the call, only available when `OutputOriginalData` is set to `true` in the client options
/// </summary>
string? OriginalData { get; init; }
/// <summary>
/// The request http method
/// </summary>
HttpMethod? RequestMethod { get; init; }
/// <summary>
/// HTTP protocol version
/// </summary>
Version? HttpVersion { get; init; }
/// <summary>
/// The headers sent with the request
/// </summary>
HttpRequestHeaders? RequestHeaders { get; init; }
/// <summary>
/// The request id
/// </summary>
int? RequestId { get; init; }
/// <summary>
/// The url which was requested
/// </summary>
string? RequestUrl { get; init; }
/// <summary>
/// The body of the request
/// </summary>
string? RequestBody { get; init; }
/// <summary>
/// Length in bytes of the response
/// </summary>
long? ResponseLength { get; init; }
/// <summary>
/// The status code of the response. Note that a OK status does not always indicate success, check the Success parameter for this.
/// </summary>
HttpStatusCode? ResponseStatusCode { get; init; }
/// <summary>
/// The response headers
/// </summary>
HttpResponseHeaders? ResponseHeaders { get; init; }
/// <summary>
/// The time between sending the request and receiving the response
/// </summary>
TimeSpan? ResponseTime { get; init; }
/// <summary>
/// The data source of this result
/// </summary>
ResultDataSource DataSource { get; init; }
}
/// <summary>
/// HTTP call result
/// </summary>
/// <typeparam name="T">Result data type</typeparam>
public interface IHttpResult<T> : IHttpResult, ICallResult<T>
{
}
}
@@ -0,0 +1,72 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
namespace CryptoExchange.Net.Objects
{
/// <summary>
/// WebSocket call result
/// </summary>
public interface IWebSocketResult : ICallResult
{
/// <summary>
/// Exchange name
/// </summary>
string Exchange { get; init; }
/// <summary>
/// The request id
/// </summary>
public int? RequestId { get; init; }
/// <summary>
/// The url which was requested
/// </summary>
public int? ConnectionId { get; init; }
/// <summary>
/// The websocket url
/// </summary>
public string? Url { get; init; }
/// <summary>
/// The time between sending the request and receiving the response
/// </summary>
public TimeSpan? ResponseTime { get; init; }
}
/// <summary>
/// WebSocket call result
/// </summary>
/// <typeparam name="T">Data result type</typeparam>
public interface IWebSocketResult<T> : IWebSocketResult, ICallResult<T>
{
}
/// <summary>
/// Query result
/// </summary>
public interface IQueryResult : IWebSocketResult
{
/// <summary>
/// The original returned data, only available when OutputOriginalData is set to true in the client options
/// </summary>
public string? OriginalData { get; init; }
/// <summary>
/// The query request body
/// </summary>
public string? RequestBody { get; init; }
}
/// <summary>
/// Query result
/// </summary>
/// <typeparam name="T"></typeparam>
public interface IQueryResult<T> : IQueryResult, IWebSocketResult<T>
{
}
}
@@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.Objects;
/// <summary>
/// Void result
/// </summary>
public readonly struct Unit
{
/// <summary>
/// Void value
/// </summary>
public static readonly Unit Value = default;
/// <summary>
/// Type
/// </summary>
public static Type Type { get; } = typeof(Unit);
}
@@ -0,0 +1,324 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Text;
namespace CryptoExchange.Net.Objects;
/// <summary>
/// WebSocket call result
/// </summary>
[DebuggerDisplay("{DebugView,nq}")]
public record WebSocketResult : IWebSocketResult
{
private string DebugView => $"[Sckt {ConnectionId}] " + (RequestId == null ? "" : $"[Req {RequestId}] ") + (Success ? "Success" : $"Error: {Error}");
/// <summary>
/// ctor
/// </summary>
public WebSocketResult(string exchange, Error? error)
{
Exchange = exchange;
Error = error;
}
/// <summary>
/// Create a new success WebSocket result
/// </summary>
public static WebSocketResult<T> Ok<T>(IWebSocketResult result, T data) =>
new WebSocketResult<T>(result.Exchange, data, null)
{
ConnectionId = result.ConnectionId,
Url = result.Url,
RequestId = result.RequestId,
ResponseTime = result.ResponseTime,
Error = result.Error,
Data = data
};
/// <summary>
/// Create a new success WebSocket result
/// </summary>
public static WebSocketResult<T> Ok<T>(
string exchange,
int connectionId,
TimeSpan elapsed,
int requestId,
string? url,
T data) =>
new WebSocketResult<T>(exchange, data, null)
{
ResponseTime = elapsed,
RequestId = requestId,
ConnectionId = connectionId,
Url = url
};
/// <summary>
/// Create a new success WebSocket result
/// </summary>
public static WebSocketResult Ok(
string exchange,
int connectionId,
TimeSpan elapsed,
int requestId,
string? url) =>
new WebSocketResult(exchange, null)
{
ResponseTime = elapsed,
RequestId = requestId,
ConnectionId = connectionId,
Url = url
};
/// <summary>
/// Create a new error WebSocket result
/// </summary>
public static WebSocketResult Fail(string exchange, Error error) => new WebSocketResult(exchange, error);
/// <summary>
/// Create a new error WebSocket result
/// </summary>
public static WebSocketResult Fail(
string exchange,
int connectionId,
TimeSpan elapsed,
int requestId,
string? url,
Error error) =>
new WebSocketResult(exchange, error)
{
ResponseTime = elapsed,
RequestId = requestId,
ConnectionId = connectionId,
Url = url
};
/// <summary>
/// Create a new error WebSocket result
/// </summary>
public static WebSocketResult<T> Fail<T>(IWebSocketResult result, Error? error = null, T? data = default)
=> new WebSocketResult<T>(result.Exchange, data, error ?? result.Error)
{
ConnectionId = result.ConnectionId,
Url = result.Url,
RequestId = result.RequestId,
ResponseTime = result.ResponseTime,
};
/// <summary>
/// Create a new error WebSocket result
/// </summary>
public static WebSocketResult<T> Fail<T>(string exchange, Error error) => new WebSocketResult<T>(exchange, default, error);
/// <summary>
/// Create a new error WebSocket result
/// </summary>
public static WebSocketResult<T> Fail<T>(
string exchange,
int connectionId,
TimeSpan elapsed,
int requestId,
string? url,
Error error) =>
new WebSocketResult<T>(exchange, default, error)
{
ResponseTime = elapsed,
RequestId = requestId,
ConnectionId = connectionId,
Url = url
};
/// <summary>
/// Exchange name
/// </summary>
public string Exchange { get; init; }
/// <inheritdoc />
public Error? Error { get; init; }
/// <inheritdoc />
[MemberNotNullWhen(false, nameof(Error))]
public bool Success => Error == null;
/// <summary>
/// The request id
/// </summary>
public int? RequestId { get; init; }
/// <summary>
/// The url which was requested
/// </summary>
public int? ConnectionId { get; init; }
/// <summary>
/// The websocket url
/// </summary>
public string? Url { get; init; }
/// <summary>
/// The time between sending the request and receiving the response
/// </summary>
public TimeSpan? ResponseTime { get; init; }
}
/// <inheritdoc />
public record WebSocketResult<T> : WebSocketResult, IWebSocketResult<T>
{
/// <summary>
/// ctor
/// </summary>
public WebSocketResult(string exchange, T? value, Error? error): base(exchange, error)
{
Data = value;
}
/// <inheritdoc />
public new Error? Error
{
get => base.Error;
init => base.Error = value;
}
/// <inheritdoc />
[MemberNotNullWhen(false, nameof(Error))]
[MemberNotNullWhen(true, nameof(Data))]
public new bool Success => Error == null;
/// <summary>
/// The data returned by the call, only available when Success = true
/// </summary>
public T? Data { get; init; }
}
/// <inheritdoc />
public record QueryResult : WebSocketResult
{
/// <summary>
/// ctor
/// </summary>
public QueryResult(string exchange, Error? error) : base(exchange, error)
{
}
/// <summary>
/// Create a new error Query result
/// </summary>
public new static QueryResult Fail(string exchange, Error error) => new QueryResult(exchange, error);
/// <summary>
/// Create a new error WebSocket result
/// </summary>
public static QueryResult Fail(IQueryResult result, Error? error = null)
=> new QueryResult(result.Exchange, error ?? result.Error)
{
ConnectionId = result.ConnectionId,
Url = result.Url,
RequestId = result.RequestId,
ResponseTime = result.ResponseTime,
};
/// <summary>
/// Create a new success query result
/// </summary>
public static QueryResult<T> Ok<T>(
string exchange,
int connectionId,
TimeSpan elapsed,
int requestId,
string? requestBody,
string? url,
string? originalData,
T data) =>
new QueryResult<T>(exchange, data, null)
{
ResponseTime = elapsed,
RequestId = requestId,
RequestBody = requestBody,
ConnectionId = connectionId,
Url = url,
OriginalData = originalData,
};
/// <summary>
/// Create a new success WebSocket result
/// </summary>
public static QueryResult<T> Ok<T>(IQueryResult result, T data) =>
new QueryResult<T>(result.Exchange, data, null)
{
ConnectionId = result.ConnectionId,
Url = result.Url,
RequestId = result.RequestId,
RequestBody = result.RequestBody,
ResponseTime = result.ResponseTime,
Error = result.Error,
OriginalData = result.OriginalData,
Data = data
};
/// <summary>
/// Create a new error WebSocket result
/// </summary>
public static QueryResult<T> Fail<T>(
string exchange,
int connectionId,
TimeSpan elapsed,
int requestId,
string? requestBody,
string? url,
string? originalData,
Error error) =>
new QueryResult<T>(exchange, default, error)
{
ResponseTime = elapsed,
RequestId = requestId,
RequestBody = requestBody,
ConnectionId = connectionId,
OriginalData = originalData,
Url = url
};
/// <summary>
/// Create a new error WebSocket result
/// </summary>
public static QueryResult<T> Fail<T>(IQueryResult result, Error? error = null, T? data = default)
=> new QueryResult<T>(result.Exchange, data, error ?? result.Error)
{
ConnectionId = result.ConnectionId,
Url = result.Url,
RequestId = result.RequestId,
RequestBody = result.RequestBody,
OriginalData = result.OriginalData,
ResponseTime = result.ResponseTime,
};
/// <summary>
/// Create a new error WebSocket result
/// </summary>
public new static QueryResult<T> Fail<T>(string exchange, Error error) => new QueryResult<T>(exchange, default, error);
/// <inheritdoc />
public string? RequestBody { get; init; }
}
/// <inheritdoc />
public record QueryResult<T> : QueryResult, IQueryResult<T>
{
/// <summary>
/// ctor
/// </summary>
public QueryResult(string exchange, T? value, Error? error) : base(exchange, error)
{
Data = value;
}
/// <inheritdoc />
public new Error? Error
{
get => base.Error;
init => base.Error = value;
}
/// <inheritdoc />
[MemberNotNullWhen(false, nameof(Error))]
[MemberNotNullWhen(true, nameof(Data))]
public new bool Success => Error == null;
/// <inheritdoc />
public T? Data { get; set; }
/// <inheritdoc />
public string? OriginalData { get; init; }
}
@@ -0,0 +1,209 @@
using CryptoExchange.Net.Sockets;
using CryptoExchange.Net.Sockets.Default;
using CryptoExchange.Net.Sockets.Default.Routing;
using Microsoft.Extensions.Logging.Abstractions;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Objects.Sockets
{
/// <summary>
/// Controller for an update subscription which isn't backed by a websocket connection. Can be used for testing.
/// </summary>
public class ManualUpdateSubscription
{
private readonly Func<Task> _closeAsync;
private readonly Func<Task> _reconnectAsync;
private readonly Func<Task<CallResult>> _resubscribeAsync;
private readonly ManualSubscription _manualSubscription;
private int _closedEventInvoked;
/// <summary>
/// The update subscription
/// </summary>
public UpdateSubscription Subscription { get; }
/// <summary>
/// The virtual socket id
/// </summary>
public int SocketId { get; }
/// <summary>
/// The last timestamp anything was received by the subscription
/// </summary>
public DateTime? LastReceiveTime { get; private set; }
/// <summary>
/// The current virtual websocket status
/// </summary>
public SocketStatus SocketStatus { get; private set; }
/// <summary>
/// Create a manually controlled update subscription
/// </summary>
/// <param name="socketId">The virtual socket id</param>
/// <param name="closeAsync">Callback when the subscription is closed</param>
/// <param name="reconnectAsync">Callback when a reconnect is requested</param>
/// <param name="resubscribeAsync">Callback when a resubscribe is requested</param>
public ManualUpdateSubscription(
int socketId = 0,
Func<Task>? closeAsync = null,
Func<Task>? reconnectAsync = null,
Func<Task<CallResult>>? resubscribeAsync = null)
{
SocketId = socketId;
SocketStatus = SocketStatus.Connected;
_closeAsync = closeAsync ?? (() => Task.CompletedTask);
_reconnectAsync = reconnectAsync ?? (() => Task.CompletedTask);
_resubscribeAsync = resubscribeAsync ?? (() => Task.FromResult(CallResult.Ok()));
_manualSubscription = new ManualSubscription();
_manualSubscription.Status = SubscriptionStatus.Subscribed;
Subscription = new UpdateSubscription(this, _manualSubscription);
}
/// <summary>
/// Set the last timestamp anything was received by the subscription
/// </summary>
/// <param name="timestamp">The receive timestamp</param>
public void SetLastReceiveTime(DateTime? timestamp)
{
LastReceiveTime = timestamp;
}
/// <summary>
/// Set the virtual websocket status
/// </summary>
/// <param name="status">The status</param>
public void SetSocketStatus(SocketStatus status)
{
SocketStatus = status;
}
/// <summary>
/// Set the subscription status
/// </summary>
/// <param name="status">The status</param>
public void SetSubscriptionStatus(SubscriptionStatus status)
{
_manualSubscription.Status = status;
}
/// <summary>
/// Invoke the connection lost event
/// </summary>
public void InvokeConnectionLost()
{
Subscription.HandleConnectionLostEvent();
}
/// <summary>
/// Invoke the connection restored event
/// </summary>
/// <param name="disconnectedPeriod">The period the connection was disconnected</param>
public void InvokeConnectionRestored(TimeSpan disconnectedPeriod)
{
Subscription.HandleConnectionRestoredEvent(disconnectedPeriod);
}
/// <summary>
/// Invoke the connection closed event
/// </summary>
public void InvokeConnectionClosed()
{
if (Interlocked.Exchange(ref _closedEventInvoked, 1) != 0)
return;
SocketStatus = SocketStatus.Closed;
_manualSubscription.Status = SubscriptionStatus.Closed;
Subscription.HandleConnectionClosedEvent();
}
/// <summary>
/// Invoke the resubscribing failed event
/// </summary>
/// <param name="error">The resubscribe error</param>
public void InvokeResubscribingFailed(Error error)
{
if (error == null)
throw new ArgumentNullException(nameof(error));
Subscription.HandleResubscribeFailedEvent(error);
}
/// <summary>
/// Invoke the activity paused event
/// </summary>
public void InvokeActivityPaused()
{
Subscription.HandlePausedEvent();
}
/// <summary>
/// Invoke the activity unpaused event
/// </summary>
public void InvokeActivityUnpaused()
{
Subscription.HandleUnpausedEvent();
}
/// <summary>
/// Invoke the exception event
/// </summary>
/// <param name="exception">The exception</param>
public void InvokeException(Exception exception)
{
if (exception == null)
throw new ArgumentNullException(nameof(exception));
_manualSubscription.InvokeExceptionHandler(exception);
}
internal async Task CloseAsync()
{
if (_manualSubscription.Status == SubscriptionStatus.Closed
|| _manualSubscription.Status == SubscriptionStatus.Closing)
return;
_manualSubscription.Status = SubscriptionStatus.Closing;
try
{
await _closeAsync().ConfigureAwait(false);
}
finally
{
_manualSubscription.Status = SubscriptionStatus.Closed;
}
}
internal Task ReconnectAsync()
{
return _reconnectAsync();
}
internal Task<CallResult> ResubscribeAsync()
{
return _resubscribeAsync();
}
private class ManualSubscription : Subscription
{
public ManualSubscription()
: base(NullLogger.Instance, false)
{
MessageRouter = MessageRouter.Create();
}
protected override Query? GetSubQuery(SocketConnection connection)
{
return null;
}
protected override Query? GetUnsubQuery(SocketConnection connection)
{
return null;
}
}
}
}
@@ -12,7 +12,8 @@ namespace CryptoExchange.Net.Objects.Sockets
/// </summary>
public class UpdateSubscription
{
private readonly SocketConnection _connection;
private readonly SocketConnection? _connection;
private readonly ManualUpdateSubscription? _manualSubscription;
internal readonly Subscription _subscription;
#if NET9_0_OR_GREATER
@@ -102,7 +103,7 @@ namespace CryptoExchange.Net.Objects.Sockets
/// <summary>
/// The id of the socket
/// </summary>
public int SocketId => _connection.SocketId;
public int SocketId => _connection?.SocketId ?? _manualSubscription!.SocketId;
/// <summary>
/// The id of the subscription
@@ -112,12 +113,12 @@ namespace CryptoExchange.Net.Objects.Sockets
/// <summary>
/// The last timestamp anything was received from the server
/// </summary>
public DateTime? LastReceiveTime => _connection.LastReceiveTime;
public DateTime? LastReceiveTime => _connection?.LastReceiveTime ?? _manualSubscription!.LastReceiveTime;
/// <summary>
/// The current websocket status
/// </summary>
public SocketStatus SocketStatus => _connection.Status;
public SocketStatus SocketStatus => _connection?.Status ?? _manualSubscription!.SocketStatus;
/// <summary>
/// The current subscription status
@@ -143,6 +144,18 @@ namespace CryptoExchange.Net.Objects.Sockets
_subscription.StatusChanged += (x) => SubscriptionStatusChanged?.Invoke(x);
}
/// <summary>
/// ctor
/// </summary>
/// <param name="manualSubscription">The manual subscription for controlling events and data</param>
/// <param name="subscription">The subscription</param>
internal UpdateSubscription(ManualUpdateSubscription manualSubscription, Subscription subscription)
{
_manualSubscription = manualSubscription;
_subscription = subscription;
_subscription.StatusChanged += (x) => SubscriptionStatusChanged?.Invoke(x);
}
private void UnsubscribeConnectionEvents()
{
lock (_eventLock)
@@ -150,22 +163,26 @@ namespace CryptoExchange.Net.Objects.Sockets
if (!_connectionEventsSubscribed)
return;
_connection.ConnectionClosed -= HandleConnectionClosedEvent;
_connection.ConnectionLost -= HandleConnectionLostEvent;
_connection.ConnectionRestored -= HandleConnectionRestoredEvent;
_connection.ResubscribingFailed -= HandleResubscribeFailedEvent;
_connection.ActivityPaused -= HandlePausedEvent;
_connection.ActivityUnpaused -= HandleUnpausedEvent;
if (_connection != null)
{
_connection.ConnectionClosed -= HandleConnectionClosedEvent;
_connection.ConnectionLost -= HandleConnectionLostEvent;
_connection.ConnectionRestored -= HandleConnectionRestoredEvent;
_connection.ResubscribingFailed -= HandleResubscribeFailedEvent;
_connection.ActivityPaused -= HandlePausedEvent;
_connection.ActivityUnpaused -= HandleUnpausedEvent;
}
_connectionEventsSubscribed = false;
}
}
private void HandleConnectionClosedEvent()
internal void HandleConnectionClosedEvent()
{
UnsubscribeConnectionEvents();
// If we're not the subscription closing this connection don't bother emitting
if (!_subscription.IsClosingConnection)
if (_connection != null && !_subscription.IsClosingConnection)
return;
List<Action> handlers;
@@ -176,7 +193,7 @@ namespace CryptoExchange.Net.Objects.Sockets
callback();
}
private void HandleConnectionLostEvent()
internal void HandleConnectionLostEvent()
{
if (!_subscription.Active)
{
@@ -192,7 +209,7 @@ namespace CryptoExchange.Net.Objects.Sockets
callback();
}
private void HandleConnectionRestoredEvent(TimeSpan period)
internal void HandleConnectionRestoredEvent(TimeSpan period)
{
if (!_subscription.Active)
{
@@ -208,7 +225,7 @@ namespace CryptoExchange.Net.Objects.Sockets
callback(period);
}
private void HandleResubscribeFailedEvent(Error error)
internal void HandleResubscribeFailedEvent(Error error)
{
if (!_subscription.Active)
{
@@ -224,7 +241,7 @@ namespace CryptoExchange.Net.Objects.Sockets
callback(error);
}
private void HandlePausedEvent()
internal void HandlePausedEvent()
{
if (!_subscription.Active)
{
@@ -240,7 +257,7 @@ namespace CryptoExchange.Net.Objects.Sockets
callback();
}
private void HandleUnpausedEvent()
internal void HandleUnpausedEvent()
{
if (!_subscription.Active)
{
@@ -262,7 +279,10 @@ namespace CryptoExchange.Net.Objects.Sockets
/// <returns></returns>
public Task CloseAsync()
{
return _connection.CloseAsync(_subscription);
if (_connection != null)
return _connection.CloseAsync(_subscription);
return _manualSubscription!.CloseAsync();
}
/// <summary>
@@ -271,7 +291,10 @@ namespace CryptoExchange.Net.Objects.Sockets
/// <returns></returns>
public Task ReconnectAsync()
{
return _connection.TriggerReconnectAsync();
if (_connection != null)
return _connection.TriggerReconnectAsync();
return _manualSubscription!.ReconnectAsync();
}
/// <summary>
@@ -280,7 +303,13 @@ namespace CryptoExchange.Net.Objects.Sockets
/// <returns></returns>
internal async Task UnsubscribeAsync()
{
await _connection.UnsubscribeAsync(_subscription).ConfigureAwait(false);
if (_connection != null)
{
await _connection.UnsubscribeAsync(_subscription).ConfigureAwait(false);
return;
}
await _manualSubscription!.CloseAsync().ConfigureAwait(false);
}
/// <summary>
@@ -289,7 +318,10 @@ namespace CryptoExchange.Net.Objects.Sockets
/// <returns></returns>
internal async Task<CallResult> ResubscribeAsync()
{
return await _connection.ResubscribeAsync(_subscription).ConfigureAwait(false);
if (_connection != null)
return await _connection.ResubscribeAsync(_subscription).ConfigureAwait(false);
return await _manualSubscription!.ResubscribeAsync().ConfigureAwait(false);
}
}
}
+33 -29
View File
@@ -262,7 +262,7 @@ namespace CryptoExchange.Net.OrderBook
}
/// <inheritdoc/>
public async Task<CallResult<bool>> StartAsync(CancellationToken? ct = null)
public async Task<CallResult> StartAsync(CancellationToken? ct = null)
{
if (Status != OrderBookStatus.Disconnected)
throw new InvalidOperationException($"Can't start book unless state is {OrderBookStatus.Disconnected}. Current state: {Status}");
@@ -286,10 +286,10 @@ namespace CryptoExchange.Net.OrderBook
_processTask = Task.Factory.StartNew(ProcessQueue, TaskCreationOptions.LongRunning);
var startResult = await DoStartAsync(_cts.Token).ConfigureAwait(false);
if (!startResult)
if (!startResult.Success)
{
Status = OrderBookStatus.Disconnected;
return new CallResult<bool>(startResult.Error!);
return CallResult.Fail(startResult.Error!);
}
if (_cts.IsCancellationRequested)
@@ -297,7 +297,7 @@ namespace CryptoExchange.Net.OrderBook
_logger.OrderBookStoppedStarting(Api, Symbol);
await startResult.Data.CloseAsync().ConfigureAwait(false);
Status = OrderBookStatus.Disconnected;
return new CallResult<bool>(new CancellationRequestedError());
return CallResult.Fail(new CancellationRequestedError());
}
_subscription = startResult.Data;
@@ -306,7 +306,7 @@ namespace CryptoExchange.Net.OrderBook
_subscription.ConnectionRestored += HandleConnectionRestored;
Status = OrderBookStatus.Synced;
return new CallResult<bool>(true);
return CallResult.Ok();
}
private void HandleConnectionLost()
@@ -333,17 +333,19 @@ namespace CryptoExchange.Net.OrderBook
{
_logger.OrderBookStopping(Api, Symbol);
_cts?.Cancel();
_queueEvent.Set();
if (_processTask != null)
await _processTask.ConfigureAwait(false);
if (_subscription != null) {
if (_subscription != null)
{
await _subscription.CloseAsync().ConfigureAwait(false);
_subscription.ConnectionLost -= HandleConnectionLost;
_subscription.ConnectionClosed -= HandleConnectionClosed;
_subscription.ConnectionRestored -= HandleConnectionRestored;
}
_queueEvent.Set();
if (_processTask != null)
await _processTask.ConfigureAwait(false);
Status = OrderBookStatus.Disconnected;
_logger.OrderBookStopped(Api, Symbol);
}
@@ -352,7 +354,7 @@ namespace CryptoExchange.Net.OrderBook
public CallResult<decimal> CalculateAverageFillPrice(decimal baseQuantity, OrderBookEntryType type)
{
if (Status != OrderBookStatus.Synced)
return new CallResult<decimal>(new InvalidOperationError($"{nameof(CalculateAverageFillPrice)} is not available when book is not in Synced state"));
return CallResult<decimal>.Fail(new InvalidOperationError($"{nameof(CalculateAverageFillPrice)} is not available when book is not in Synced state"));
var totalCost = 0m;
var totalAmount = 0m;
@@ -365,7 +367,7 @@ namespace CryptoExchange.Net.OrderBook
while (amountLeft > 0)
{
if (step == list.Count)
return new CallResult<decimal>(new InvalidOperationError("Quantity is larger than order in the order book"));
return CallResult<decimal>.Fail(new InvalidOperationError("Quantity is larger than order in the order book"));
var element = list.ElementAt(step);
var stepAmount = Math.Min(element.Value.Quantity, amountLeft);
@@ -376,14 +378,14 @@ namespace CryptoExchange.Net.OrderBook
}
}
return new CallResult<decimal>(Math.Round(totalCost / totalAmount, 8));
return CallResult<decimal>.Ok(Math.Round(totalCost / totalAmount, 8));
}
/// <inheritdoc/>
public CallResult<decimal> CalculateTradableAmount(decimal quoteQuantity, OrderBookEntryType type)
{
if (Status != OrderBookStatus.Synced)
return new CallResult<decimal>(new InvalidOperationError($"{nameof(CalculateTradableAmount)} is not available when book is not in Synced state"));
return CallResult<decimal>.Fail(new InvalidOperationError($"{nameof(CalculateTradableAmount)} is not available when book is not in Synced state"));
var quoteQuantityLeft = quoteQuantity;
var totalBaseQuantity = 0m;
@@ -395,7 +397,7 @@ namespace CryptoExchange.Net.OrderBook
while (quoteQuantityLeft > 0)
{
if (step == list.Count)
return new CallResult<decimal>(new InvalidOperationError("Quantity is larger than order in the order book"));
return CallResult<decimal>.Fail(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);
@@ -405,7 +407,7 @@ namespace CryptoExchange.Net.OrderBook
}
}
return new CallResult<decimal>(Math.Round(totalBaseQuantity, 8));
return CallResult<decimal>.Ok(Math.Round(totalBaseQuantity, 8));
}
/// <summary>
@@ -424,7 +426,7 @@ namespace CryptoExchange.Net.OrderBook
/// Resync the order book
/// </summary>
/// <returns></returns>
protected abstract Task<CallResult<bool>> DoResyncAsync(CancellationToken ct);
protected abstract Task<CallResult> DoResyncAsync(CancellationToken ct);
/// <summary>
/// Implementation for validating a checksum value with the current order book. If checksum validation fails (returns false)
@@ -603,10 +605,9 @@ namespace CryptoExchange.Net.OrderBook
var listToChange = type == OrderBookEntryType.Ask ? _asks : _bids;
if (entry.Quantity == 0)
{
if (!listToChange.ContainsKey(entry.Price))
if (!listToChange.Remove(entry.Price))
return true;
listToChange.Remove(entry.Price);
if (type == OrderBookEntryType.Ask) AskCount--;
else BidCount--;
}
@@ -633,16 +634,16 @@ namespace CryptoExchange.Net.OrderBook
/// <param name="timeout">Max wait time</param>
/// <param name="ct">Cancellation token</param>
/// <returns></returns>
protected async Task<CallResult<bool>> WaitForSetOrderBookAsync(TimeSpan timeout, CancellationToken ct)
protected async Task<CallResult> WaitForSetOrderBookAsync(TimeSpan timeout, CancellationToken ct)
{
var startWait = DateTime.UtcNow;
while (!_bookSet && Status == OrderBookStatus.Syncing)
{
if(ct.IsCancellationRequested)
return new CallResult<bool>(new CancellationRequestedError());
return CallResult.Fail(new CancellationRequestedError());
if (DateTime.UtcNow - startWait > timeout)
return new CallResult<bool>(new ServerError(new ErrorInfo(ErrorType.OrderBookTimeout, "Timeout while waiting for data")));
return CallResult.Fail(new ServerError(new ErrorInfo(ErrorType.OrderBookTimeout, "Timeout while waiting for data")));
try
{
@@ -652,7 +653,7 @@ namespace CryptoExchange.Net.OrderBook
{ }
}
return new CallResult<bool>(true);
return CallResult.Ok();
}
/// <summary>
@@ -668,10 +669,10 @@ namespace CryptoExchange.Net.OrderBook
while (_processBuffer.Count == 0)
{
if (ct.IsCancellationRequested)
return new CallResult(new CancellationRequestedError());
return CallResult.Fail(new CancellationRequestedError());
if (DateTime.UtcNow - startWait > maxWait)
return new CallResult(new ServerError(new ErrorInfo(ErrorType.OrderBookTimeout, "Timeout while waiting for data")));
return CallResult.Fail(new ServerError(new ErrorInfo(ErrorType.OrderBookTimeout, "Timeout while waiting for data")));
try
{
@@ -688,7 +689,7 @@ namespace CryptoExchange.Net.OrderBook
await Task.Delay(minWait.Value - dif).ConfigureAwait(false);
}
return CallResult.SuccessResult;
return CallResult.Ok();
}
/// <summary>
@@ -807,7 +808,7 @@ namespace CryptoExchange.Net.OrderBook
return;
var resyncResult = await DoResyncAsync(_cts!.Token).ConfigureAwait(false);
success = resyncResult;
success = resyncResult.Success;
}
_logger.OrderBookResynced(Api, Symbol);
@@ -833,7 +834,7 @@ namespace CryptoExchange.Net.OrderBook
if (item is OrderBookSnapshot snapshot)
ProcessOrderBookSnapshot(snapshot);
if (item is OrderBookUpdate update)
else if (item is OrderBookUpdate update)
ProcessQueueItem(update);
else if (item is OrderBookChecksum checksum)
ProcessChecksum(checksum);
@@ -961,7 +962,8 @@ namespace CryptoExchange.Net.OrderBook
await _subscription!.UnsubscribeAsync().ConfigureAwait(false);
Reset();
_stopProcessing = false;
if (!await _subscription!.ResubscribeAsync().ConfigureAwait(false))
var resubResult = await _subscription!.ResubscribeAsync().ConfigureAwait(false);
if (!resubResult.Success)
{
// Resubscribing failed, reconnect the socket
_logger.OrderBookResyncFailed(Api, Symbol);
@@ -1053,10 +1055,12 @@ namespace CryptoExchange.Net.OrderBook
private SequenceNumberResult ValidateLiveSequenceNumber(long sequenceNumber)
{
if (sequenceNumber < LastSequenceNumber
if (sequenceNumber < LastSequenceNumber
&& (_firstUpdateAfterSnapshotDone || !_skipSequenceCheckFirstUpdateAfterSnapshotSet))
{
// Update is somehow from before the current state
return SequenceNumberResult.OutOfSync;
}
if (_sequencesAreConsecutive
&& LastSequenceNumber != 0
@@ -20,7 +20,7 @@ namespace CryptoExchange.Net.RateLimiting.Filters
}
/// <inheritdoc />
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey)
public bool Passes(RateLimitItemType type, RequestDefinition definition, string? apiKey)
=> definition.Authenticated == _authenticated;
}
}
@@ -21,7 +21,7 @@ namespace CryptoExchange.Net.RateLimiting.Filters
}
/// <inheritdoc />
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey)
public bool Passes(RateLimitItemType type, RequestDefinition definition, string? apiKey)
=> string.Equals(definition.Path, _path, StringComparison.OrdinalIgnoreCase);
}
}
@@ -21,7 +21,7 @@ namespace CryptoExchange.Net.RateLimiting.Filters
}
/// <inheritdoc />
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey)
public bool Passes(RateLimitItemType type, RequestDefinition definition, string? apiKey)
=> _paths.Contains(definition.Path);
}
}
@@ -20,8 +20,8 @@ namespace CryptoExchange.Net.RateLimiting.Filters
}
/// <inheritdoc />
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey)
=> host.Equals(_host, System.StringComparison.InvariantCulture);
public bool Passes(RateLimitItemType type, RequestDefinition definition, string? apiKey)
=> definition.BaseAddress.Equals(_host, System.StringComparison.InvariantCulture);
}
}
@@ -20,7 +20,7 @@ namespace CryptoExchange.Net.RateLimiting.Filters
}
/// <inheritdoc />
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey)
public bool Passes(RateLimitItemType type, RequestDefinition definition, string? apiKey)
=> type == _type;
}
}
@@ -21,7 +21,7 @@ namespace CryptoExchange.Net.RateLimiting.Filters
}
/// <inheritdoc />
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey)
public bool Passes(RateLimitItemType type, RequestDefinition definition, string? apiKey)
=> definition.Path.TrimStart('/').StartsWith(_path, StringComparison.OrdinalIgnoreCase);
}
}
@@ -14,30 +14,30 @@ namespace CryptoExchange.Net.RateLimiting.Guards
/// <summary>
/// Apply guard per host
/// </summary>
public static Func<RequestDefinition, string, string?, string> PerHost { get; } = new Func<RequestDefinition, string, string?, string>((def, host, key) => host);
public static Func<RequestDefinition, string?, string> PerHost { get; } = new Func<RequestDefinition, string?, string>((def, key) => def.BaseAddress);
/// <summary>
/// Apply guard per endpoint
/// </summary>
public static Func<RequestDefinition, string, string?, string> PerEndpoint { get; } = new Func<RequestDefinition, string, string?, string>((def, host, key) => def.Path + def.Method);
public static Func<RequestDefinition, string?, string> PerEndpoint { get; } = new Func<RequestDefinition, string?, string>((def, key) => def.Path + def.Method);
/// <summary>
/// Apply guard per connection
/// </summary>
public static Func<RequestDefinition, string, string?, string> PerConnection { get; } = new Func<RequestDefinition, string, string?, string>((def, host, key) => def.ConnectionId.ToString()!);
public static Func<RequestDefinition, string?, string> PerConnection { get; } = new Func<RequestDefinition, string?, string>((def, key) => def.ConnectionId.ToString()!);
/// <summary>
/// Apply guard per API key
/// </summary>
public static Func<RequestDefinition, string, string?, string> PerApiKey { get; } = new Func<RequestDefinition, string, string?, string>((def, host, key) => key!);
public static Func<RequestDefinition, string?, string> PerApiKey { get; } = new Func<RequestDefinition, string?, string>((def, key) => key!);
/// <summary>
/// Apply guard per API key per endpoint
/// </summary>
public static Func<RequestDefinition, string, string?, string> PerApiKeyPerEndpoint { get; } = new Func<RequestDefinition, string, string?, string>((def, host, key) => key! + def.Path + def.Method);
public static Func<RequestDefinition, string?, string> PerApiKeyPerEndpoint { get; } = new Func<RequestDefinition, string?, string>((def, key) => key! + def.Path + def.Method);
private readonly IEnumerable<IGuardFilter> _filters;
private readonly Dictionary<string, IWindowTracker> _trackers;
private readonly RateLimitWindowType _windowType;
private readonly double? _decayRate;
private readonly int? _connectionWeight;
private readonly Func<RequestDefinition, string, string?, string> _keySelector;
private readonly Func<RequestDefinition, string?, string> _keySelector;
private readonly SemaphoreSlim? _sharedGuardSemaphore;
/// <inheritdoc />
@@ -71,7 +71,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
/// <param name="decayPerTimeSpan">The decay per timespan if windowType is DecayWindowTracker</param>
/// <param name="connectionWeight">The weight of a new connection</param>
/// <param name="shared">Whether this guard is shared between multiple gates</param>
public RateLimitGuard(Func<RequestDefinition, string, string?, string> keySelector, IGuardFilter filter, int limit, TimeSpan timeSpan, RateLimitWindowType windowType, double? decayPerTimeSpan = null, int? connectionWeight = null, bool shared = false)
public RateLimitGuard(Func<RequestDefinition, string?, string> keySelector, IGuardFilter filter, int limit, TimeSpan timeSpan, RateLimitWindowType windowType, double? decayPerTimeSpan = null, int? connectionWeight = null, bool shared = false)
: this(keySelector, new[] { filter }, limit, timeSpan, windowType, decayPerTimeSpan, connectionWeight, shared)
{
}
@@ -87,7 +87,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
/// <param name="decayPerTimeSpan">The decay per timespan if windowType is DecayWindowTracker</param>
/// <param name="connectionWeight">The weight of a new connection</param>
/// <param name="shared">Whether this guard is shared between multiple gates</param>
public RateLimitGuard(Func<RequestDefinition, string, string?, string> keySelector, IEnumerable<IGuardFilter> filters, int limit, TimeSpan timeSpan, RateLimitWindowType windowType, double? decayPerTimeSpan = null, int? connectionWeight = null, bool shared = false)
public RateLimitGuard(Func<RequestDefinition, string?, string> keySelector, IEnumerable<IGuardFilter> filters, int limit, TimeSpan timeSpan, RateLimitWindowType windowType, double? decayPerTimeSpan = null, int? connectionWeight = null, bool shared = false)
{
_filters = filters;
_trackers = new Dictionary<string, IWindowTracker>();
@@ -104,11 +104,11 @@ namespace CryptoExchange.Net.RateLimiting.Guards
}
/// <inheritdoc />
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix)
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, string? keySuffix)
{
foreach (var filter in _filters)
{
if (!filter.Passes(type, definition, host, apiKey))
if (!filter.Passes(type, definition, apiKey))
return LimitCheck.NotApplicable;
}
@@ -120,7 +120,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
try
{
var key = _keySelector(definition, host, apiKey) + keySuffix;
var key = _keySelector(definition, apiKey) + keySuffix;
if (!_trackers.TryGetValue(key, out var tracker))
{
tracker = CreateTracker();
@@ -141,11 +141,11 @@ namespace CryptoExchange.Net.RateLimiting.Guards
}
/// <inheritdoc />
public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix)
public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, string? keySuffix)
{
foreach (var filter in _filters)
{
if (!filter.Passes(type, definition, host, apiKey))
if (!filter.Passes(type, definition, apiKey))
return RateLimitState.NotApplied;
}
@@ -153,7 +153,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
requestWeight = _connectionWeight ?? requestWeight;
var key = _keySelector(definition, host, apiKey) + keySuffix;
var key = _keySelector(definition, apiKey) + keySuffix;
var tracker = _trackers[key];
if (SharedGuard)
@@ -173,11 +173,11 @@ namespace CryptoExchange.Net.RateLimiting.Guards
}
/// <inheritdoc />
public void Reset(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, string? keySuffix)
public void Reset(RateLimitItemType type, RequestDefinition definition, string? apiKey, string? keySuffix, int? amount)
{
foreach (var filter in _filters)
{
if (!filter.Passes(type, definition, host, apiKey))
if (!filter.Passes(type, definition, apiKey))
return;
}
@@ -186,11 +186,11 @@ namespace CryptoExchange.Net.RateLimiting.Guards
try
{
var key = _keySelector(definition, host, apiKey) + keySuffix;
var key = _keySelector(definition, apiKey) + keySuffix;
if (!_trackers.TryGetValue(key, out var tracker))
return;
tracker.Reset();
tracker.Reset(amount);
}
finally
{
@@ -42,7 +42,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
}
/// <inheritdoc />
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix)
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, string? keySuffix)
{
if (type != Type)
return LimitCheck.NotApplicable;
@@ -55,7 +55,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
}
/// <inheritdoc />
public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix)
public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, string? keySuffix)
{
return RateLimitState.NotApplied;
}
@@ -67,7 +67,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
public void UpdateAfter(DateTime after) => After = after;
/// <inheritdoc />
public void Reset(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, string? keySuffix)
public void Reset(RateLimitItemType type, RequestDefinition definition, string? apiKey, string? keySuffix, int? amount)
{
After = DateTime.UtcNow;
}
@@ -14,19 +14,19 @@ namespace CryptoExchange.Net.RateLimiting.Guards
/// <summary>
/// Default endpoint limit
/// </summary>
public static Func<RequestDefinition, string, string?, string> Default { get; } = new Func<RequestDefinition, string, string?, string>((def, host, key) => def.Path + def.Method);
public static Func<RequestDefinition, string?, string> Default { get; } = new Func<RequestDefinition, string?, string>((def, key) => def.Path + def.Method);
/// <summary>
/// Endpoint limit per API key
/// </summary>
public static Func<RequestDefinition, string, string?, string> PerApiKey { get; } = new Func<RequestDefinition, string, string?, string>((def, host, key) => def.Path + def.Method + key);
public static Func<RequestDefinition, string?, string> PerApiKey { get; } = new Func<RequestDefinition, string?, string>((def, key) => def.Path + def.Method + key);
private readonly Dictionary<string, IWindowTracker> _trackers;
private readonly RateLimitWindowType _windowType;
private readonly double? _decayRate;
private readonly int _limit;
private readonly TimeSpan _period;
private readonly Func<RequestDefinition, string, string?, string> _keySelector;
private readonly Func<RequestDefinition, string?, string> _keySelector;
/// <inheritdoc />
public string Name => "EndpointLimitGuard";
@@ -42,7 +42,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
TimeSpan period,
RateLimitWindowType windowType,
double? decayRate = null,
Func<RequestDefinition, string, string?, string>? keySelector = null)
Func<RequestDefinition, string?, string>? keySelector = null)
{
_limit = limit;
_period = period;
@@ -53,9 +53,9 @@ namespace CryptoExchange.Net.RateLimiting.Guards
}
/// <inheritdoc />
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix)
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, string? keySuffix)
{
var key = _keySelector(definition, host, apiKey) + keySuffix;
var key = _keySelector(definition, apiKey) + keySuffix;
if (!_trackers.TryGetValue(key, out var tracker))
{
tracker = CreateTracker();
@@ -70,9 +70,9 @@ namespace CryptoExchange.Net.RateLimiting.Guards
}
/// <inheritdoc />
public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix)
public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, string? keySuffix)
{
var key = _keySelector(definition, host, apiKey) + keySuffix;
var key = _keySelector(definition, apiKey) + keySuffix;
var tracker = _trackers[key];
tracker.ApplyWeight(requestWeight);
return RateLimitState.Applied(_limit, _period, tracker.Current);
@@ -90,13 +90,13 @@ namespace CryptoExchange.Net.RateLimiting.Guards
}
/// <inheritdoc />
public void Reset(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, string? keySuffix)
public void Reset(RateLimitItemType type, RequestDefinition definition, string? apiKey, string? keySuffix, int? amount)
{
var key = _keySelector(definition, host, apiKey) + keySuffix;
var key = _keySelector(definition, apiKey) + keySuffix;
if (!_trackers.TryGetValue(key, out var tracker))
return;
tracker.Reset();
tracker.Reset(amount);
}
}
}
@@ -12,9 +12,8 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
/// </summary>
/// <param name="type">The type of item</param>
/// <param name="definition">The request definition</param>
/// <param name="host">The host address</param>
/// <param name="apiKey">The API key</param>
/// <returns>True if passed</returns>
bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey);
bool Passes(RateLimitItemType type, RequestDefinition definition, string? apiKey);
}
}
@@ -49,14 +49,13 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
/// <param name="itemId">Id of the item to check</param>
/// <param name="type">The rate limit item type</param>
/// <param name="definition">The request definition</param>
/// <param name="baseAddress">The host address</param>
/// <param name="apiKey">The API key</param>
/// <param name="requestWeight">Request weight</param>
/// <param name="behaviour">Behaviour when rate limit is hit</param>
/// <param name="keySuffix">An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters.</param>
/// <param name="ct">Cancelation token</param>
/// <returns>Error if RateLimitingBehaviour is Fail and rate limit is hit</returns>
ValueTask<CallResult> ProcessAsync(ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string baseAddress, string? apiKey, int requestWeight, RateLimitingBehaviour behaviour, string? keySuffix, CancellationToken ct);
ValueTask<CallResult> ProcessAsync(ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, RateLimitingBehaviour behaviour, string? keySuffix, CancellationToken ct);
/// <summary>
/// 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
@@ -66,30 +65,29 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
/// <param name="guard">The guard</param>
/// <param name="type">The rate limit item type</param>
/// <param name="definition">The request definition</param>
/// <param name="baseAddress">The host address</param>
/// <param name="apiKey">The API key</param>
/// <param name="behaviour">Behaviour when rate limit is hit</param>
/// <param name="requestWeight">The weight to apply to the limit guard</param>
/// <param name="keySuffix">An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters.</param>
/// <param name="ct">Cancelation token</param>
/// <returns>Error if RateLimitingBehaviour is Fail and rate limit is hit</returns>
ValueTask<CallResult> ProcessSingleAsync(ILogger logger, int itemId, IRateLimitGuard guard, RateLimitItemType type, RequestDefinition definition, string baseAddress, string? apiKey, int requestWeight, RateLimitingBehaviour behaviour, string? keySuffix, CancellationToken ct);
ValueTask<CallResult> ProcessSingleAsync(ILogger logger, int itemId, IRateLimitGuard guard, RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, RateLimitingBehaviour behaviour, string? keySuffix, CancellationToken ct);
/// <summary>
/// Reset the limit for the specified parameters
/// </summary>
/// <param name="type">The rate limit item type</param>
/// <param name="definition">The request definition</param>
/// <param name="host">The host address</param>
/// <param name="apiKey">The API key</param>
/// <param name="keySuffix">An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters.</param>
/// <param name="amount">Amount in weight to reset by, or null to set used rate limit to 0</param>
/// <param name="ct">Cancelation token</param>
Task ResetAsync(
RateLimitItemType type,
RequestDefinition definition,
string host,
string? apiKey,
string? keySuffix,
int? amount,
CancellationToken ct);
}
}
@@ -22,33 +22,31 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
/// </summary>
/// <param name="type">The rate limit item type</param>
/// <param name="definition">The request definition</param>
/// <param name="host">The host address</param>
/// <param name="apiKey">The API key</param>
/// <param name="requestWeight">The request weight</param>
/// <param name="keySuffix">An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters.</param>
/// <returns></returns>
LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix);
LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, string? keySuffix);
/// <summary>
/// Apply the request to this guard with the specified weight
/// </summary>
/// <param name="type">The rate limit item type</param>
/// <param name="definition">The request definition</param>
/// <param name="host">The host address</param>
/// <param name="apiKey">The API key</param>
/// <param name="requestWeight">The request weight</param>
/// <param name="keySuffix">An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters.</param>
/// <returns></returns>
RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix);
RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, string? keySuffix);
/// <summary>
/// Reset the limit for the specified parameters
/// </summary>
/// <param name="type">The rate limit item type</param>
/// <param name="definition">The request definition</param>
/// <param name="host">The host address</param>
/// <param name="apiKey">The API key</param>
/// <param name="keySuffix">An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters.</param>
void Reset(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, string? keySuffix);
/// <param name="amount">Amount in weight to reset by, or null to set used rate limit to 0</param>
void Reset(RateLimitItemType type, RequestDefinition definition, string? apiKey, string? keySuffix, int? amount);
}
}
@@ -33,6 +33,6 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
/// <summary>
/// Reset the limit counter for this tracker
/// </summary>
void Reset();
void Reset(int? amount);
}
}
@@ -25,10 +25,6 @@ namespace CryptoExchange.Net.RateLimiting
/// </summary>
public RequestDefinition RequestDefinition { get; set; }
/// <summary>
/// The host the request is for
/// </summary>
public string Host { get; set; } = default!;
/// <summary>
/// The current counter value
/// </summary>
public int Current { get; set; }
@@ -56,13 +52,12 @@ namespace CryptoExchange.Net.RateLimiting
/// <summary>
/// ctor
/// </summary>
public RateLimitEvent(int itemId, string apiLimit, string limitDescription, RequestDefinition definition, string host, int current, int requestWeight, int? limit, TimeSpan? timePeriod, TimeSpan? delayTime, RateLimitingBehaviour behaviour)
public RateLimitEvent(int itemId, string apiLimit, string limitDescription, RequestDefinition definition, 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;
@@ -37,20 +37,20 @@ namespace CryptoExchange.Net.RateLimiting
}
/// <inheritdoc />
public async ValueTask<CallResult> ProcessAsync(ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, RateLimitingBehaviour rateLimitingBehaviour, string? keySuffix, CancellationToken ct)
public async ValueTask<CallResult> ProcessAsync(ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, RateLimitingBehaviour rateLimitingBehaviour, string? keySuffix, CancellationToken ct)
{
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);
return await CheckGuardsAsync(_guards, logger, itemId, type, definition, 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));
return CallResult.Fail(new CancellationRequestedError(tce));
}
finally
{
@@ -67,7 +67,6 @@ namespace CryptoExchange.Net.RateLimiting
IRateLimitGuard guard,
RateLimitItemType type,
RequestDefinition definition,
string host,
string? apiKey,
int requestWeight,
RateLimitingBehaviour rateLimitingBehaviour,
@@ -79,13 +78,13 @@ namespace CryptoExchange.Net.RateLimiting
_waitingCount++;
try
{
return await CheckGuardsAsync(new IRateLimitGuard[] { guard }, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, keySuffix, ct).ConfigureAwait(false);
return await CheckGuardsAsync(new IRateLimitGuard[] { guard }, logger, itemId, type, definition, 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));
return CallResult.Fail(new CancellationRequestedError(tce));
}
finally
{
@@ -95,12 +94,12 @@ namespace CryptoExchange.Net.RateLimiting
}
}
private async ValueTask<CallResult> CheckGuardsAsync(IEnumerable<IRateLimitGuard> guards, ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, RateLimitingBehaviour rateLimitingBehaviour, string? keySuffix, CancellationToken ct)
private async ValueTask<CallResult> CheckGuardsAsync(IEnumerable<IRateLimitGuard> guards, ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, 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);
var result = guard.Check(type, definition, apiKey, requestWeight, keySuffix);
if (result.Delay != TimeSpan.Zero && rateLimitingBehaviour == RateLimitingBehaviour.Fail)
{
// Delay is needed and limit behaviour is to fail the request
@@ -109,8 +108,8 @@ namespace CryptoExchange.Net.RateLimiting
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}"));
RateLimitTriggered?.Invoke(new RateLimitEvent(itemId, _name, guard.Description, definition, result.Current, requestWeight, result.Limit, result.Period, result.Delay, rateLimitingBehaviour));
return CallResult.Fail(new ClientRateLimitError($"Rate limit check failed on guard {guard.Name}; {guard.Description}"));
}
if (result.Delay != TimeSpan.Zero)
@@ -124,17 +123,17 @@ namespace CryptoExchange.Net.RateLimiting
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));
RateLimitTriggered?.Invoke(new RateLimitEvent(itemId, _name, guard.Description, definition, 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);
return await CheckGuardsAsync(guards, logger, itemId, type, definition, 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);
var result = guard.ApplyWeight(type, definition, apiKey, requestWeight, keySuffix);
if (result.IsApplied)
{
RateLimitUpdated?.Invoke(new RateLimitUpdateEvent(itemId, _name, guard.Description, result.Current, result.Limit, result.Period));
@@ -149,7 +148,7 @@ namespace CryptoExchange.Net.RateLimiting
}
}
return CallResult.SuccessResult;
return CallResult.Ok();
}
/// <inheritdoc />
@@ -198,16 +197,16 @@ namespace CryptoExchange.Net.RateLimiting
public async Task ResetAsync(
RateLimitItemType type,
RequestDefinition definition,
string host,
string? apiKey,
string? keySuffix,
int? amount,
CancellationToken ct)
{
await _semaphore.WaitAsync(ct).ConfigureAwait(false);
try
{
foreach (var guard in _guards)
guard.Reset(type, definition, host, apiKey, keySuffix);
guard.Reset(type, definition, apiKey, keySuffix, amount);
}
finally
{
@@ -27,10 +27,17 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
}
/// <inheritdoc />
public void Reset()
public void Reset(int? amount)
{
_currentWeight = 0;
_lastDecrease = DateTime.UtcNow;
if (amount == null)
{
_lastDecrease = DateTime.UtcNow;
_currentWeight = 0;
}
else
{
_currentWeight = Math.Max(0, _currentWeight - amount.Value);
}
}
/// <inheritdoc />
@@ -30,11 +30,27 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
}
/// <inheritdoc />
public void Reset()
public void Reset(int? amount)
{
_entries.Clear();
_currentWeight = 0;
_nextReset = null;
if (amount == null)
{
_entries.Clear();
_currentWeight = 0;
_nextReset = null;
}
else
{
_currentWeight = Math.Max(0, _currentWeight - amount.Value);
var removedWeight = 0;
while (true)
{
if (removedWeight >= amount.Value || _entries.Count == 0)
break;
var lastEntry = _entries.Dequeue();
removedWeight += lastEntry.Weight;
}
}
}
public TimeSpan GetWaitTime(int weight)
@@ -29,10 +29,26 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
}
/// <inheritdoc />
public void Reset()
public void Reset(int? amount)
{
_entries.Clear();
_currentWeight = 0;
if (amount == null)
{
_entries.Clear();
_currentWeight = 0;
}
else
{
_currentWeight = Math.Max(0, _currentWeight - amount.Value);
var removedWeight = 0;
while (true)
{
if (removedWeight >= amount.Value || _entries.Count == 0)
break;
var lastEntry = _entries.Dequeue();
removedWeight += lastEntry.Weight;
}
}
}
/// <inheritdoc />
@@ -29,10 +29,27 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
}
/// <inheritdoc />
public void Reset()
public void Reset(int? amount)
{
_entries.Clear();
_currentWeight = 0;
if (amount == null)
{
_entries.Clear();
_currentWeight = 0;
}
else
{
_currentWeight = Math.Max(0, _currentWeight - amount.Value);
var removedWeight = 0;
while (true)
{
if (removedWeight >= amount.Value || _entries.Count == 0)
break;
var lastEntry = _entries[_entries.Count - 1];
removedWeight += lastEntry.Weight;
_entries.Remove(lastEntry);
}
}
}
/// <inheritdoc />
@@ -0,0 +1,51 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Asset type
/// </summary>
public enum SharedAssetType
{
/// <summary>
/// Unknown or unspecified asset type
/// </summary>
Unspecified,
/// <summary>
/// Cryptocurrency asset type
/// </summary>
Crypto,
/// <summary>
/// Fiat currency asset type
/// </summary>
Fiat,
/// <summary>
/// Traditional finance asset type
/// </summary>
TradFi
}
/// <summary>
/// Asset sub type
/// </summary>
public enum SharedAssetSubType
{
// --- Crypto sub types ---
/// <summary>
/// Stable coin, can be for different fiat currencies
/// </summary>
StableCoin,
// --- TradFi sub types ---
/// <summary>
/// Equity, can be stocks, ETFs, or indices
/// </summary>
Equity,
/// <summary>
/// Commodity, can be oil, gas, metals, etc.
/// </summary>
Commodity
}
}
@@ -22,6 +22,11 @@ namespace CryptoExchange.Net.SharedApis
/// </summary>
bool Authenticated { get; }
/// <summary>
/// Get info on the client and supported features
/// </summary>
SharedClientInfo Discover();
/// <summary>
/// Format a base and quote asset to an exchange accepted symbol
/// </summary>
@@ -33,14 +38,15 @@ namespace CryptoExchange.Net.SharedApis
string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverDate = null);
/// <summary>
/// Set a default exchange parameter. This can be used instead of passing in an ExchangeParameters object which each request.
/// Set a default exchange parameter which will be statically set with each request. This can be used instead of passing it in an ExchangeParameters object with each request.<br />
/// Default exchange parameters can still be overridden by passing the parameter in the ExchangeParameters of a request.
/// </summary>
/// <param name="name">Parameter name</param>
/// <param name="value">Parameter value</param>
void SetDefaultExchangeParameter(string name, object value);
/// <summary>
/// Reset the default exchange parameters, resets parameters for all exchanges
/// Reset previously set default exchange parameters for the exchange.
/// </summary>
void ResetDefaultExchangeParameters();
}

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