mirror of
https://github.com/JKorf/CryptoExchange.Net.git
synced 2026-10-04 10:21:13 +00:00
Compare commits
105 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bda81d8170 | |||
| 666ca76776 | |||
| ae7504c26c | |||
| cecfbcba48 | |||
| 4a4930d658 | |||
| ca245959a2 | |||
| ff43d202d4 | |||
| d1947e643b | |||
| 6fec65cd6a | |||
| 053bcf6ecc | |||
| 6957ba44f5 | |||
| c1af97dd4a | |||
| ecfe3cf392 | |||
| ac1e9cf2ab | |||
| 4987e6be70 | |||
| 9100c0d7f3 | |||
| 5b1ae30e4b | |||
| 36a06e8a9a | |||
| e7eb559074 | |||
| 79e2efec70 | |||
| c8c6d14329 | |||
| b8bd30a09d | |||
| c2b58ab19a | |||
| 90a031c595 | |||
| 860d753ad6 | |||
| e16c91792c | |||
| a3d95da9fa | |||
| 907399b878 | |||
| 9eab0d967e | |||
| 0dee68e8ae | |||
| d838d3377f | |||
| 2141ad9061 | |||
| 9be8798ccf | |||
| 4803ed91cd | |||
| 20bddd5c37 | |||
| 0e75ddb3d0 | |||
| 0e5b46002c | |||
| 8f7c71f9ce | |||
| 73377fbb87 | |||
| 3a00d6371a | |||
| caf6d36bcd | |||
| e078a373da | |||
| 007743f5a1 | |||
| d06f891cee | |||
| 8dcbb687f5 | |||
| fcb36f7ee0 | |||
| 3cffd67518 | |||
| ecd00ea707 | |||
| 38a7b981ce | |||
| c41cc3c4c7 | |||
| 4accc8039b | |||
| 87c86ec0c0 | |||
| d9850da282 | |||
| c4a8b02054 | |||
| 34b7258496 | |||
| 69099922c9 | |||
| 64c1cd5fa8 | |||
| 936ac6640b | |||
| 68ad9ae114 | |||
| 6238c17471 | |||
| 52e6fbfe47 | |||
| 4129622d71 | |||
| 95e0aefb9f | |||
| dd1cefdc90 | |||
| f4736bcf49 | |||
| e823114623 | |||
| afb84a1bf0 | |||
| 89cbd85875 | |||
| cc25a405c6 | |||
| d64c1171a3 | |||
| d4aded1ee2 | |||
| 271503c426 | |||
| f0ece589f7 | |||
| 3a9382ca0f | |||
| 1e1a02324a | |||
| 95dd050c73 | |||
| 504836924c | |||
| f176fb5db5 | |||
| 6b575be1ac | |||
| b637d5cdc4 | |||
| a46b018c50 | |||
| 562d1d76c1 | |||
| 7dcb2241c6 | |||
| 8c4cf62d9f | |||
| 6e4dbcf7b1 | |||
| 7853834286 | |||
| 9ae1263662 | |||
| ee30a6716e | |||
| a4b7b273dc | |||
| c92eeb2ec8 | |||
| 4d4b0576ee | |||
| 9add5e0adc | |||
| 93d92beea6 | |||
| a955ccbc5c | |||
| 4e2dc564dd | |||
| 93034e8af8 | |||
| cdd0bd83ab | |||
| 61d371682c | |||
| 8aae769e54 | |||
| 1b4f6926df | |||
| acca3468f3 | |||
| d6b680d42e | |||
| 24123261e5 | |||
| aab30e05f0 | |||
| 54b15b75d3 |
@@ -0,0 +1,92 @@
|
|||||||
|
---
|
||||||
|
description: Conventions for cross-exchange C# code using CryptoExchange.Net Shared API V2 capabilities.
|
||||||
|
globs:
|
||||||
|
- "**/*.cs"
|
||||||
|
- "**/*.csproj"
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# CryptoExchange.Net conventions
|
||||||
|
|
||||||
|
CryptoExchange.Net is the base dependency for exchange-specific libraries. Install Binance.Net, Bybit.Net, JK.OKX.Net, or another exchange library; use `CryptoClients.Net` when the application needs the bundle.
|
||||||
|
|
||||||
|
## Use Shared API V2
|
||||||
|
|
||||||
|
For new cross-exchange code, use the fine-grained interfaces in `CryptoExchange.Net.SharedApis` and each API surface's `.SharedApi` property:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using CryptoExchange.Net.SharedApis;
|
||||||
|
|
||||||
|
IGetTickerRest binance = new BinanceRestClient().SpotApi.SharedApi;
|
||||||
|
IGetTickerRest okx = new OKXRestClient().UnifiedApi.SharedApi;
|
||||||
|
|
||||||
|
var symbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
|
||||||
|
var result = await binance.GetTickerAsync(new GetTickerRequest(symbol));
|
||||||
|
```
|
||||||
|
|
||||||
|
Use one capability per operation, such as `IGetTickerRest`, `IGetOrderBookRest`, `IPlaceSpotOrderRest`, or `ISubscribeTradesSocket`. Do not use a broad V1 interface for new code. `.SharedClient` remains only for incremental V1 migration.
|
||||||
|
|
||||||
|
## Capability discovery
|
||||||
|
|
||||||
|
When the exchange and API surface are known, assign the typed `.SharedApi` to the required capability. When selection is dynamic, use an exchange-wide `I[Exchange]SharedApiClient` and a strongly typed reference:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var match = sharedClient.GetCapability(
|
||||||
|
SharedCapabilities.Orders.Futures.PlaceOrder.Rest,
|
||||||
|
TradingMode.PerpetualLinear);
|
||||||
|
|
||||||
|
if (match is not null)
|
||||||
|
await match.Capability.PlaceFuturesOrderAsync(request);
|
||||||
|
```
|
||||||
|
|
||||||
|
`GetCapability` returns one preferred match or `null`; `GetCapabilities` returns all matches. Specify `TradingMode` when multiple API surfaces may implement the same capability. A `SharedCapabilities` reference is a lookup key, not proof of exchange support.
|
||||||
|
|
||||||
|
## Symbols and parameters
|
||||||
|
|
||||||
|
Use `SharedSymbol` instead of native strings such as `BTCUSDT`. The exchange library performs formatting.
|
||||||
|
|
||||||
|
Capability presence does not imply support for every request property. For dynamic code, inspect `match.Options.RequestParameterRules`, `ExchangeParameterRules`, and `SupportedTradingModes`. Supply exchange-specific values through the request's `ExchangeParameters`.
|
||||||
|
|
||||||
|
## Transports and results
|
||||||
|
|
||||||
|
- Transport-agnostic capabilities return `IExchangeCallResult<T>`.
|
||||||
|
- `...Rest` capabilities return `HttpResult<T>`.
|
||||||
|
- socket command capabilities return `QueryResult<T>`.
|
||||||
|
- `ISubscribe...Socket` capabilities return `WebSocketResult<UpdateSubscription>`.
|
||||||
|
|
||||||
|
Use `.Rest` or `.Socket` on capability references when the transport matters. Otherwise exchange-wide lookup honors `PreferredTransport`, normally REST. Always check `.Success` before `.Data` and include `.Exchange` in multi-exchange logs.
|
||||||
|
|
||||||
|
## Concurrent aggregation
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var clients = new IGetTickerRest[] { binance, okx, bybit };
|
||||||
|
var tasks = clients.Select(x =>
|
||||||
|
x.GetTickerAsync(new GetTickerRequest(symbol)));
|
||||||
|
var results = await Task.WhenAll(tasks);
|
||||||
|
```
|
||||||
|
|
||||||
|
Reuse clients through dependency injection and run independent exchange calls concurrently.
|
||||||
|
|
||||||
|
## V2 semantics
|
||||||
|
|
||||||
|
- `GetTickerAsync` and `GetAllTickersAsync` return `SharedTicker` for spot and futures.
|
||||||
|
- WebSocket order updates use `SharedSpotOrderUpdate` and `SharedFuturesOrderUpdate`.
|
||||||
|
- `ICloseFullPosition` closes the entire position; use an order capability for partial closes.
|
||||||
|
- Each exchange supports a subset of capabilities and parameter combinations.
|
||||||
|
|
||||||
|
## Hard rules
|
||||||
|
|
||||||
|
- Never install CryptoExchange.Net alone and expect exchange endpoints.
|
||||||
|
- Never pass exchange-native models or symbols through shared services.
|
||||||
|
- Never assume a capability or request field is supported; resolve and inspect it.
|
||||||
|
- Never use `.Result` or `.Wait()`.
|
||||||
|
- Never instantiate exchange clients per request.
|
||||||
|
- Prefer `Task.WhenAll` for independent cross-exchange calls.
|
||||||
|
- Prefer `.SharedApi` and fine-grained V2 interfaces for new code.
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
|
||||||
|
- `AGENTS.md` for fuller examples
|
||||||
|
- `docs/ai-api-map.md` for the V2 interface map
|
||||||
|
- `docs/SHARED_API_V2_MIGRATION.md` for V1 migration details
|
||||||
|
- `llms-full.txt` for expanded AI context
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# Copilot Instructions for CryptoExchange.Net
|
||||||
|
|
||||||
|
CryptoExchange.Net is the base library behind exchange-specific C#/.NET clients such as Binance.Net, Bybit.Net, OKX.Net, Kraken.Net, and Coinbase.Net.
|
||||||
|
|
||||||
|
## Package choice
|
||||||
|
|
||||||
|
Do not install CryptoExchange.Net alone to call an exchange. Install the exchange-specific package, or use `CryptoClients.Net` for the full bundle. For single-exchange code, prefer that library's native client. For portable multi-exchange code, use `CryptoExchange.Net.SharedApis`.
|
||||||
|
|
||||||
|
## Generate Shared API V2 code
|
||||||
|
|
||||||
|
Use fine-grained V2 capabilities through `.SharedApi`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using CryptoExchange.Net.SharedApis;
|
||||||
|
|
||||||
|
IGetTickerRest binance = new BinanceRestClient().SpotApi.SharedApi;
|
||||||
|
IGetTickerRest okx = new OKXRestClient().UnifiedApi.SharedApi;
|
||||||
|
|
||||||
|
var symbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
|
||||||
|
var result = await binance.GetTickerAsync(new GetTickerRequest(symbol));
|
||||||
|
```
|
||||||
|
|
||||||
|
Choose the interface for the operation: for example, `IGetTickerRest`, `IGetOrderBookRest`, `IPlaceSpotOrderRest`, `ICancelFuturesOrderRest`, or `ISubscribeTradesSocket`. V1 broad interfaces remain on `.SharedClient` for migration, but new code should use `.SharedApi`.
|
||||||
|
|
||||||
|
Use `SharedSymbol`; do not hard-code exchange-native symbol formatting in shared code.
|
||||||
|
|
||||||
|
## Runtime capability selection
|
||||||
|
|
||||||
|
When the API surface is known, assign its typed `.SharedApi` directly. When support or the API surface is selected at runtime, use the exchange-wide `I[Exchange]SharedApiClient`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var match = sharedClient.GetCapability(
|
||||||
|
SharedCapabilities.Orders.Futures.PlaceOrder.Rest,
|
||||||
|
TradingMode.PerpetualLinear);
|
||||||
|
|
||||||
|
if (match is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var result = await match.Capability.PlaceFuturesOrderAsync(request);
|
||||||
|
```
|
||||||
|
|
||||||
|
`GetCapability` returns one preferred `SharedCapabilityResolution<T>` or `null`; `GetCapabilities` returns all matching implementations. Include `TradingMode` when an exchange can expose multiple futures surfaces. `SharedCapabilities` entries are lookup references, not guarantees of support.
|
||||||
|
|
||||||
|
Before constructing dynamic requests, inspect `match.Options.RequestParameterRules`, `ExchangeParameterRules`, and `SupportedTradingModes`. A capability may exist while a particular request field is unsupported.
|
||||||
|
|
||||||
|
## Results and transports
|
||||||
|
|
||||||
|
- Transport-agnostic operation: `IExchangeCallResult<T>`
|
||||||
|
- REST capability: `HttpResult<T>`
|
||||||
|
- WebSocket command capability: `QueryResult<T>`
|
||||||
|
- WebSocket subscription capability: `WebSocketResult<UpdateSubscription>`
|
||||||
|
|
||||||
|
Select `.Rest` or `.Socket` when transport-specific behavior matters. Otherwise exchange-wide selection uses `PreferredTransport`, normally REST. Always check `.Success` before `.Data`; use `.Error` and `.Exchange` for diagnostics.
|
||||||
|
|
||||||
|
## Current V2 semantics
|
||||||
|
|
||||||
|
- Ticker operations are `GetTickerAsync` and `GetAllTickersAsync`, returning `SharedTicker` for both spot and futures.
|
||||||
|
- Socket order streams use `SharedSpotOrderUpdate` and `SharedFuturesOrderUpdate`.
|
||||||
|
- `ICloseFullPosition` closes a complete position, not a partial quantity.
|
||||||
|
- Exchange support varies by operation, transport, trading mode, and request parameter.
|
||||||
|
|
||||||
|
## Engineering conventions
|
||||||
|
|
||||||
|
- Reuse clients through dependency injection.
|
||||||
|
- Use `await`; never use `.Result` or `.Wait()`.
|
||||||
|
- Use `Task.WhenAll` for independent requests across exchanges.
|
||||||
|
- Keep exchange-native models out of portable services.
|
||||||
|
- Do not infer feature support from a broad interface or request model.
|
||||||
|
|
||||||
|
See `AGENTS.md`, `docs/ai-api-map.md`, and `docs/SHARED_API_V2_MIGRATION.md` for expanded guidance.
|
||||||
@@ -0,0 +1,227 @@
|
|||||||
|
---
|
||||||
|
name: cryptoexchange-net
|
||||||
|
description: Use CryptoExchange.Net Shared API V2 abstractions when generating C#/.NET code for multiple cryptocurrency exchanges, including arbitrage, routing, portfolio aggregation, exchange-agnostic bots, comparison tools, and new exchange-library implementations.
|
||||||
|
---
|
||||||
|
|
||||||
|
# CryptoExchange.Net Skill
|
||||||
|
|
||||||
|
## When to use
|
||||||
|
|
||||||
|
CryptoExchange.Net is the base library behind exchange-specific libraries such as Binance.Net, Bybit.Net, OKX.Net, Kraken.Net, and Coinbase.Net. Do not install it alone to call an exchange.
|
||||||
|
|
||||||
|
Choose one of these approaches:
|
||||||
|
|
||||||
|
1. One exchange: install and use that exchange's library directly.
|
||||||
|
2. Multiple exchanges: install the required exchange libraries and use `CryptoExchange.Net.SharedApis` V2 capabilities.
|
||||||
|
3. All exchanges in one package: install `CryptoClients.Net` and use its combined clients and shared capability lookup.
|
||||||
|
|
||||||
|
For new cross-exchange code, use Shared API V2. V1 aggregate interfaces remain available through `.SharedClient` for incremental migration.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet add package Binance.Net
|
||||||
|
dotnet add package JK.OKX.Net
|
||||||
|
dotnet add package Bybit.Net
|
||||||
|
```
|
||||||
|
|
||||||
|
Or install the bundle:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet add package CryptoClients.Net
|
||||||
|
```
|
||||||
|
|
||||||
|
## Core pattern: fine-grained capabilities
|
||||||
|
|
||||||
|
Each exchange API surface exposes a typed `.SharedApi` aggregate. Assign it to the capability for the single operation being used:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Binance.Net.Clients;
|
||||||
|
using OKX.Net.Clients;
|
||||||
|
using Bybit.Net.Clients;
|
||||||
|
using CryptoExchange.Net.SharedApis;
|
||||||
|
|
||||||
|
IGetTickerRest binance = new BinanceRestClient().SpotApi.SharedApi;
|
||||||
|
IGetTickerRest okx = new OKXRestClient().UnifiedApi.SharedApi;
|
||||||
|
IGetTickerRest bybit = new BybitRestClient().V5Api.SharedApi;
|
||||||
|
|
||||||
|
var symbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
|
||||||
|
var result = await binance.GetTickerAsync(new GetTickerRequest(symbol));
|
||||||
|
|
||||||
|
if (!result.Success)
|
||||||
|
Console.WriteLine($"[{result.Exchange}] {result.Error}");
|
||||||
|
else
|
||||||
|
Console.WriteLine($"[{result.Exchange}] {result.Data!.LastPrice}");
|
||||||
|
```
|
||||||
|
|
||||||
|
V2 interfaces describe operations, not broad feature groups. Examples include `IGetTickerRest`, `IGetOrderBookRest`, `IPlaceSpotOrderRest`, `ICancelFuturesOrderRest`, and `ISubscribeTradesSocket`. An exchange may implement one operation without implementing adjacent operations.
|
||||||
|
|
||||||
|
## Shared symbols
|
||||||
|
|
||||||
|
Use `SharedSymbol`; never pass exchange-native symbol strings to shared requests:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var spot = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
|
||||||
|
var linearPerpetual = new SharedSymbol(
|
||||||
|
TradingMode.PerpetualLinear,
|
||||||
|
"BTC",
|
||||||
|
"USDT");
|
||||||
|
```
|
||||||
|
|
||||||
|
Each exchange library converts the shared symbol to its native format. For unusual exchange asset names, configure asset aliases.
|
||||||
|
|
||||||
|
## Known surface versus runtime selection
|
||||||
|
|
||||||
|
Use the typed `.SharedApi` surface when the exchange and API are known. This is the simplest and most discoverable approach:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
IPlaceSpotOrderRest orders = restClient.SpotApi.SharedApi;
|
||||||
|
```
|
||||||
|
|
||||||
|
For runtime selection, inject the exchange-wide `I[Exchange]SharedApiClient` registered by `services.Add[Exchange](...)`. Resolve a capability with the strongly typed `SharedCapabilities` catalog:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var match = sharedClient.GetCapability(
|
||||||
|
SharedCapabilities.Orders.Futures.PlaceOrder.Rest,
|
||||||
|
TradingMode.PerpetualLinear);
|
||||||
|
|
||||||
|
if (match is null)
|
||||||
|
return; // This exchange/API/mode does not provide the operation.
|
||||||
|
|
||||||
|
var result = await match.Capability.PlaceFuturesOrderAsync(request);
|
||||||
|
```
|
||||||
|
|
||||||
|
`GetCapability` returns one `SharedCapabilityResolution<T>` containing `Capability` and `Options`, or `null` if no supported implementation matches. `GetCapabilities` returns every match on that exchange-wide client. Always specify `TradingMode` when multiple spot, linear, inverse, or delivery surfaces could match.
|
||||||
|
|
||||||
|
`SharedCapabilities` references identify interface types; they do not guarantee that an exchange implements the operation and do not send a request.
|
||||||
|
|
||||||
|
## Transport selection and result types
|
||||||
|
|
||||||
|
Some commands have three interfaces:
|
||||||
|
|
||||||
|
- Transport-agnostic, such as `IPlaceSpotOrder`, returns `IExchangeCallResult<T>`.
|
||||||
|
- REST-specific, such as `IPlaceSpotOrderRest`, returns `HttpResult<T>`.
|
||||||
|
- socket-specific, such as `IPlaceSpotOrderSocket`, returns `QueryResult<T>`.
|
||||||
|
|
||||||
|
Socket subscription capabilities such as `ISubscribeTickerSocket` return `WebSocketResult<UpdateSubscription>`.
|
||||||
|
|
||||||
|
Use `.Rest` or `.Socket` on a capability reference when transport matters. Without a transport filter, exchange-wide lookup uses `PreferredTransport`, configured through the exchange's `SharedApi.PreferredTransport` option and normally defaulting to REST.
|
||||||
|
|
||||||
|
Always check `.Success` before `.Data`. Use `.Error` for failures and the result or capability `.Exchange` value for multi-exchange logging.
|
||||||
|
|
||||||
|
## Inspect parameter support
|
||||||
|
|
||||||
|
The presence of a capability does not mean every shared request property is accepted by every exchange. Inspect its `CapabilityOptions`:
|
||||||
|
|
||||||
|
- `RequestParameterRules`: whether each shared request field is `Required`, `Optional`, or `NotSupported`.
|
||||||
|
- `ExchangeParameterRules`: required or optional exchange-specific values supplied through the request's `ExchangeParameters`.
|
||||||
|
- `SupportedTradingModes`: the trading modes supported by this implementation.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var match = sharedClient.GetCapability(
|
||||||
|
SharedCapabilities.Orders.Futures.PlaceOrder.Rest,
|
||||||
|
TradingMode.PerpetualLinear);
|
||||||
|
|
||||||
|
var leverageRule = match?.Options.RequestParameterRules
|
||||||
|
.FirstOrDefault(x => x.Name == nameof(PlaceFuturesOrderRequest.Leverage));
|
||||||
|
```
|
||||||
|
|
||||||
|
Do not infer support from the request model alone. Handle a `null` resolution and exchange-specific parameter rules before issuing dynamic trading calls.
|
||||||
|
|
||||||
|
## Common capabilities
|
||||||
|
|
||||||
|
- Market data: `IGetTickerRest`, `IGetAllTickersRest`, `IGetOrderBookRest`, `IGetKlinesRest`, `IGetRecentTradesRest`; ticker, trade, kline, and order-book socket subscriptions.
|
||||||
|
- Trading: fine-grained spot and futures place, edit, cancel, get, open-order, closed-order, and order-update capabilities.
|
||||||
|
- Account: balances, positions, user trades, fees, deposits, withdrawals, and transfers.
|
||||||
|
- Futures data: funding, open interest, leverage, mark price, and index price capabilities.
|
||||||
|
|
||||||
|
Check the typed exchange Shared API surface or use capability lookup instead of assuming universal support.
|
||||||
|
|
||||||
|
## Multi-exchange aggregation
|
||||||
|
|
||||||
|
Run independent exchange requests concurrently:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var clients = new IGetTickerRest[] { binance, okx, bybit };
|
||||||
|
var symbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
|
||||||
|
|
||||||
|
var tasks = clients.Select(client =>
|
||||||
|
client.GetTickerAsync(new GetTickerRequest(symbol)));
|
||||||
|
var results = await Task.WhenAll(tasks);
|
||||||
|
|
||||||
|
foreach (var result in results.Where(x => x.Success))
|
||||||
|
Console.WriteLine($"{result.Exchange}: {result.Data!.LastPrice}");
|
||||||
|
```
|
||||||
|
|
||||||
|
Reuse clients through dependency injection; do not instantiate them per request.
|
||||||
|
|
||||||
|
## Dependency injection
|
||||||
|
|
||||||
|
Each exchange library provides `services.Add[Exchange](...)`. Registration includes its native clients, V1 shared interfaces, V2 operation capabilities, and exchange-wide shared client.
|
||||||
|
|
||||||
|
Inject a capability directly only when the container has one intended implementation:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public sealed class TickerService(IGetTickerRest ticker)
|
||||||
|
{
|
||||||
|
public Task<HttpResult<SharedTicker>> GetAsync(
|
||||||
|
SharedSymbol symbol,
|
||||||
|
CancellationToken ct = default)
|
||||||
|
=> ticker.GetTickerAsync(new GetTickerRequest(symbol), ct);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If multiple exchanges or multiple API surfaces register the same capability, inject the exchange-specific shared client and select its typed API property or call `GetCapability`. Plain single-service resolution does not express which implementation you want.
|
||||||
|
|
||||||
|
For the bundle, use `services.AddCryptoClients(...)`. CryptoClients.Net provides `IExchangeSharedApiClient` for capability lookup across exchanges.
|
||||||
|
|
||||||
|
## V1 migration
|
||||||
|
|
||||||
|
V1 `.SharedClient` facades and broad interfaces such as `ISpotTickerRestClient` remain available. Migrate one operation at a time:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// V1
|
||||||
|
await restClient.SpotApi.SharedClient.GetSpotTickerAsync(request);
|
||||||
|
|
||||||
|
// V2
|
||||||
|
await restClient.SpotApi.SharedApi.GetTickerAsync(request);
|
||||||
|
```
|
||||||
|
|
||||||
|
Important semantic changes:
|
||||||
|
|
||||||
|
- V2 ticker methods are `GetTickerAsync` and `GetAllTickersAsync` and return `SharedTicker` for both spot and futures.
|
||||||
|
- WebSocket order updates use `SharedSpotOrderUpdate` and `SharedFuturesOrderUpdate`; REST order retrieval keeps the ordinary order models.
|
||||||
|
- `ICloseFullPosition` closes the complete position. Use an order capability for partial closes where supported.
|
||||||
|
- Transport-agnostic operations return `IExchangeCallResult<T>`; select the REST or socket interface when code needs a transport-specific result.
|
||||||
|
|
||||||
|
See `docs/SHARED_API_V2_MIGRATION.md` for detailed mappings.
|
||||||
|
|
||||||
|
## Common pitfalls
|
||||||
|
|
||||||
|
- Do not install `CryptoExchange.Net` alone and expect exchange endpoints.
|
||||||
|
- Do not use `.SharedClient` for new V2 code; use `.SharedApi` and fine-grained capabilities.
|
||||||
|
- Do not mix exchange-native request or response models into cross-exchange services.
|
||||||
|
- Do not assume capability presence or request-parameter support; resolve and inspect it.
|
||||||
|
- Do not rely on preferred transport when REST or socket semantics matter.
|
||||||
|
- Do not block with `.Result` or `.Wait()`; use async calls throughout.
|
||||||
|
- Do not query exchanges sequentially when requests are independent.
|
||||||
|
|
||||||
|
## Implementing a new exchange library
|
||||||
|
|
||||||
|
- Derive API clients from `RestApiClient` and `SocketApiClient`.
|
||||||
|
- Follow the exchange credentials and options patterns used by existing libraries.
|
||||||
|
- Implement the relevant fine-grained V2 capability interfaces on typed Shared API classes.
|
||||||
|
- Publish accurate `CapabilityOptions`, including supported trading modes and request/exchange parameter rules.
|
||||||
|
- Register REST/socket Shared APIs and the exchange-wide shared client with the library's DI extension.
|
||||||
|
- Retain V1 facades only where compatibility with existing consumers is required.
|
||||||
|
|
||||||
|
Use maintained exchange libraries such as Binance.Net and Bybit.Net as implementation references.
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
|
||||||
|
- Source: https://github.com/JKorf/CryptoExchange.Net
|
||||||
|
- Documentation: https://cryptoexchange.jkorf.dev/
|
||||||
|
- Shared API migration: https://github.com/JKorf/CryptoExchange.Net/blob/master/docs/SHARED_API_V2_MIGRATION.md
|
||||||
|
- Bundle: 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>
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
#  CryptoExchange.Net.Proto
|
|
||||||
|
|
||||||
[](https://github.com/JKorf/CryptoExchange.Net/actions/workflows/dotnet.yml) [](https://www.nuget.org/packages/CryptoExchange.Net.Protobuf) 
|
|
||||||
|
|
||||||
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 evnt = new AsyncResetEvent(false, true);
|
||||||
|
|
||||||
var waiters = new List<Task<bool>>();
|
var waiters = new List<Task<bool>>();
|
||||||
for(var i = 0; i < 10; i++)
|
for (var i = 0; i < 10; i++)
|
||||||
{
|
{
|
||||||
waiters.Add(evnt.WaitAsync());
|
waiters.Add(evnt.WaitAsync());
|
||||||
}
|
}
|
||||||
|
|
||||||
List<bool> results = null;
|
var remaining = waiters.ToList();
|
||||||
var resultsWaiter = Task.Run(async () =>
|
|
||||||
{
|
|
||||||
await Task.WhenAll(waiters);
|
|
||||||
results = waiters.Select(w => w.Result).ToList();
|
|
||||||
});
|
|
||||||
|
|
||||||
for(var i = 1; i <= 10; i++)
|
for (var i = 0; i < 10; i++)
|
||||||
{
|
{
|
||||||
evnt.Set();
|
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(remaining, Is.Empty);
|
||||||
|
|
||||||
Assert.That(10 == results.Count(r => r));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
using NUnit.Framework;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
using CryptoExchange.Net;
|
||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.Converters.SystemTextJson;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.UnitTests
|
||||||
|
{
|
||||||
|
internal class BodySerializationTests
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void ToFormData_SerializesBasicValuesCorrectly()
|
||||||
|
{
|
||||||
|
var parameters = new Dictionary<string, object>()
|
||||||
|
{
|
||||||
|
{ "a", "1" },
|
||||||
|
{ "b", 2 },
|
||||||
|
{ "c", true }
|
||||||
|
};
|
||||||
|
|
||||||
|
var parameterString = parameters.ToFormData();
|
||||||
|
|
||||||
|
Assert.That(parameterString, Is.EqualTo("a=1&b=2&c=True"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void JsonSerializer_SerializesBasicValuesCorrectly()
|
||||||
|
{
|
||||||
|
var serializer = new SystemTextJsonMessageSerializer(SerializerOptions.WithConverters(new TestSerializerContext()));
|
||||||
|
var parameters = new Dictionary<string, object>()
|
||||||
|
{
|
||||||
|
{ "a", "1" },
|
||||||
|
{ "b", 2 },
|
||||||
|
{ "c", true }
|
||||||
|
};
|
||||||
|
|
||||||
|
var parameterString = serializer.Serialize(parameters);
|
||||||
|
Assert.That(parameterString, Is.EqualTo("{\"a\":\"1\",\"b\":2,\"c\":true}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,6 @@ using CryptoExchange.Net.Objects.Errors;
|
|||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using NUnit.Framework.Legacy;
|
using NUnit.Framework.Legacy;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
|
|
||||||
@@ -15,157 +14,41 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestBasicErrorCallResult()
|
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.AreSame(result.Error!.ErrorCode, "TestError");
|
||||||
ClassicAssert.IsFalse(result);
|
|
||||||
ClassicAssert.IsFalse(result.Success);
|
ClassicAssert.IsFalse(result.Success);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestBasicSuccessCallResult()
|
public void TestBasicSuccessCallResult()
|
||||||
{
|
{
|
||||||
var result = new CallResult(null);
|
var result = CallResult.Ok();
|
||||||
|
|
||||||
ClassicAssert.IsNull(result.Error);
|
ClassicAssert.IsNull(result.Error);
|
||||||
Assert.That(result);
|
|
||||||
Assert.That(result.Success);
|
Assert.That(result.Success);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestCallResultError()
|
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.AreSame(result.Error!.ErrorCode, "TestError");
|
||||||
ClassicAssert.IsNull(result.Data);
|
ClassicAssert.IsNull(result.Data);
|
||||||
ClassicAssert.IsFalse(result);
|
|
||||||
ClassicAssert.IsFalse(result.Success);
|
ClassicAssert.IsFalse(result.Success);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestCallResultSuccess()
|
public void TestCallResultSuccess()
|
||||||
{
|
{
|
||||||
var result = new CallResult<object>(new object());
|
var result = CallResult.Ok<object>(new object());
|
||||||
|
|
||||||
ClassicAssert.IsNull(result.Error);
|
ClassicAssert.IsNull(result.Error);
|
||||||
ClassicAssert.IsNotNull(result.Data);
|
ClassicAssert.IsNotNull(result.Data);
|
||||||
Assert.That(result);
|
|
||||||
Assert.That(result.Success);
|
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
|
public class TestObjectResult
|
||||||
|
|||||||
+11
-12
@@ -1,23 +1,22 @@
|
|||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using NUnit.Framework.Legacy;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests
|
namespace CryptoExchange.Net.UnitTests.ClientTests
|
||||||
{
|
{
|
||||||
[TestFixture()]
|
[TestFixture()]
|
||||||
public class BaseClientTests
|
public class BaseClientTests
|
||||||
{
|
{
|
||||||
[TestCase]
|
//[TestCase]
|
||||||
public void DeserializingValidJson_Should_GiveSuccessfulResult()
|
//public void DeserializingValidJson_Should_GiveSuccessfulResult()
|
||||||
{
|
//{
|
||||||
// arrange
|
// // arrange
|
||||||
var client = new TestBaseClient();
|
// var client = new TestBaseClient();
|
||||||
|
|
||||||
// act
|
// // act
|
||||||
var result = client.SubClient.Deserialize<object>("{\"testProperty\": 123}");
|
// var result = client.SubClient.Deserialize<object>("{\"testProperty\": 123}");
|
||||||
|
|
||||||
// assert
|
// // assert
|
||||||
Assert.That(result.Success);
|
// Assert.That(result.Success);
|
||||||
}
|
//}
|
||||||
|
|
||||||
[TestCase("https://api.test.com/api", new[] { "path1", "path2" }, "https://api.test.com/api/path1/path2")]
|
[TestCase("https://api.test.com/api", new[] { "path1", "path2" }, "https://api.test.com/api/path1/path2")]
|
||||||
[TestCase("https://api.test.com/api", new[] { "path1", "/path2" }, "https://api.test.com/api/path1/path2")]
|
[TestCase("https://api.test.com/api", new[] { "path1", "/path2" }, "https://api.test.com/api/path1/path2")]
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using System;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Threading;
|
||||||
|
using NUnit.Framework.Legacy;
|
||||||
|
using CryptoExchange.Net.RateLimiting;
|
||||||
|
using CryptoExchange.Net.RateLimiting.Guards;
|
||||||
|
using CryptoExchange.Net.RateLimiting.Filters;
|
||||||
|
using CryptoExchange.Net.RateLimiting.Interfaces;
|
||||||
|
using System.Text.Json;
|
||||||
|
using CryptoExchange.Net.UnitTests.Implementations;
|
||||||
|
using CryptoExchange.Net.Testing;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.UnitTests.ClientTests
|
||||||
|
{
|
||||||
|
[TestFixture()]
|
||||||
|
public class RestClientTests
|
||||||
|
{
|
||||||
|
[TestCase]
|
||||||
|
public async Task RequestingData_Should_ResultInData()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var client = new TestRestClient();
|
||||||
|
var expected = new TestObject() { DecimalData = 1.23M, IntData = 10, StringData = "Some data" };
|
||||||
|
var strData = JsonSerializer.Serialize(expected, new JsonSerializerOptions { TypeInfoResolver = new TestSerializerContext() });
|
||||||
|
client.ApiClient1.SetNextResponse(strData, System.Net.HttpStatusCode.OK);
|
||||||
|
|
||||||
|
// act
|
||||||
|
var result = await client.ApiClient1.GetResponseAsync<TestObject>();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(result.Success);
|
||||||
|
Assert.That(TestHelpers.AreEqual(expected, result.Data));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase]
|
||||||
|
public async Task ReceivingInvalidData_Should_ResultInError()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var client = new TestRestClient();
|
||||||
|
client.ApiClient1.SetNextResponse("{\"property\": 123", System.Net.HttpStatusCode.OK);
|
||||||
|
|
||||||
|
// act
|
||||||
|
var result = await client.ApiClient1.GetResponseAsync<TestObject>();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
ClassicAssert.IsFalse(result.Success);
|
||||||
|
Assert.That(result.Error != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase]
|
||||||
|
public async Task ReceivingErrorCode_Should_ResultInError()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var client = new TestRestClient();
|
||||||
|
client.ApiClient1.SetNextResponse("Invalid request", System.Net.HttpStatusCode.BadRequest);
|
||||||
|
|
||||||
|
// act
|
||||||
|
var result = await client.ApiClient1.GetResponseAsync<TestObject>();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
ClassicAssert.IsFalse(result.Success);
|
||||||
|
Assert.That(result.Error != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase]
|
||||||
|
public async Task ReceivingErrorAndNotParsingError_Should_ResultInFlatError()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var client = new TestRestClient();
|
||||||
|
client.ApiClient1.SetNextResponse("{\"errorMessage\": \"Invalid request\", \"errorCode\": 123}", System.Net.HttpStatusCode.BadRequest);
|
||||||
|
|
||||||
|
// act
|
||||||
|
var result = await client.ApiClient1.GetResponseAsync<TestObject>();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
ClassicAssert.IsFalse(result.Success);
|
||||||
|
Assert.That(result.Error != null);
|
||||||
|
Assert.That(result.Error is ServerError);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase]
|
||||||
|
public async Task ReceivingErrorAndNotParsingErrorAndInvalidJson_Should_ContainData()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var client = new TestRestClient();
|
||||||
|
var response = "<html>...</html>";
|
||||||
|
client.ApiClient1.SetNextResponse(response, System.Net.HttpStatusCode.BadRequest);
|
||||||
|
|
||||||
|
// act
|
||||||
|
var result = await client.ApiClient1.GetResponseAsync<TestObject>();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
ClassicAssert.IsFalse(result.Success);
|
||||||
|
Assert.That(result.Error != null);
|
||||||
|
Assert.That(result.Error is DeserializeError);
|
||||||
|
Assert.That(result.Error!.Message!.Contains(response));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase]
|
||||||
|
public async Task ReceivingErrorAndParsingError_Should_ResultInParsedError()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var client = new TestRestClient();
|
||||||
|
client.ApiClient1.SetNextResponse("{\"errorMessage\": \"Invalid request\", \"errorCode\": 123}", System.Net.HttpStatusCode.BadRequest);
|
||||||
|
|
||||||
|
// act
|
||||||
|
var result = await client.ApiClient1.GetResponseAsync<TestObject>();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
ClassicAssert.IsFalse(result.Success);
|
||||||
|
Assert.That(result.Error != null);
|
||||||
|
Assert.That(result.Error is ServerError);
|
||||||
|
Assert.That(result.Error!.ErrorCode == "123");
|
||||||
|
Assert.That(result.Error.Message == "Invalid request");
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase("GET", HttpMethodParameterPosition.InUri)] // No need to test InBody for GET since thats not valid
|
||||||
|
[TestCase("POST", HttpMethodParameterPosition.InBody)]
|
||||||
|
[TestCase("POST", HttpMethodParameterPosition.InUri)]
|
||||||
|
[TestCase("DELETE", HttpMethodParameterPosition.InBody)]
|
||||||
|
[TestCase("DELETE", HttpMethodParameterPosition.InUri)]
|
||||||
|
[TestCase("PUT", HttpMethodParameterPosition.InUri)]
|
||||||
|
[TestCase("PUT", HttpMethodParameterPosition.InBody)]
|
||||||
|
public async Task Setting_Should_ResultInOptionsSet(string method, HttpMethodParameterPosition pos)
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
// act
|
||||||
|
var client = new TestRestClient();
|
||||||
|
|
||||||
|
var httpMethod = new HttpMethod(method);
|
||||||
|
client.ApiClient1.SetParameterPosition(httpMethod, pos);
|
||||||
|
client.ApiClient1.SetNextResponse("{}", System.Net.HttpStatusCode.OK);
|
||||||
|
|
||||||
|
var result = await client.ApiClient1.GetResponseAsync<TestObject>(httpMethod, new Parameters(new ParameterSerializationSettings())
|
||||||
|
{
|
||||||
|
{ "TestParam1", "Value1" },
|
||||||
|
{ "TestParam2", 2 },
|
||||||
|
});
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(result.RequestMethod == new HttpMethod(method));
|
||||||
|
Assert.That(result.RequestBody?.Contains("TestParam1") == true == (pos == HttpMethodParameterPosition.InBody));
|
||||||
|
Assert.That((result.RequestUrl?.ToString().Contains("TestParam1")) == (pos == HttpMethodParameterPosition.InUri));
|
||||||
|
Assert.That(result.RequestBody?.Contains("TestParam2") == true == (pos == HttpMethodParameterPosition.InBody));
|
||||||
|
Assert.That((result.RequestUrl?.ToString().Contains("TestParam2")) == (pos == HttpMethodParameterPosition.InUri));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,239 @@
|
|||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.Testing;
|
||||||
|
using CryptoExchange.Net.UnitTests.Implementations;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.UnitTests.ClientTests
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class SocketClientTests
|
||||||
|
{
|
||||||
|
[TestCase]
|
||||||
|
public void SettingOptions_Should_ResultInOptionsSet()
|
||||||
|
{
|
||||||
|
//arrange
|
||||||
|
//act
|
||||||
|
var client = new TestSocketClient(options =>
|
||||||
|
{
|
||||||
|
options.ExchangeOptions.MaxSocketConnections = 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
//assert
|
||||||
|
Assert.That(1 == client.ApiClient1.ApiOptions.MaxSocketConnections);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(true)]
|
||||||
|
[TestCase(false)]
|
||||||
|
public async Task ConnectSocket_Should_ReturnConnectionResult(bool canConnect)
|
||||||
|
{
|
||||||
|
//arrange
|
||||||
|
var client = new TestSocketClient();
|
||||||
|
var socket = TestHelpers.ConfigureSocketClient(client, "wss://localhost");
|
||||||
|
socket.CanConnect = canConnect;
|
||||||
|
|
||||||
|
//act
|
||||||
|
var connectResult = await client.ApiClient1.SubscribeToUpdatesAsync<TestObject>(x => { }, false, default);
|
||||||
|
|
||||||
|
//assert
|
||||||
|
Assert.That(connectResult.Success == canConnect);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase]
|
||||||
|
public async Task SocketMessages_Should_BeProcessedInDataHandlers()
|
||||||
|
{
|
||||||
|
var client = new TestSocketClient();
|
||||||
|
var socket = TestHelpers.ConfigureSocketClient(client, "wss://localhost");
|
||||||
|
|
||||||
|
var expected = new TestObject() { DecimalData = 1.23M, IntData = 10, StringData = "Some data" };
|
||||||
|
var strData = JsonSerializer.Serialize(expected, new JsonSerializerOptions { TypeInfoResolver = new TestSerializerContext() });
|
||||||
|
|
||||||
|
TestObject? received = null;
|
||||||
|
var resetEvent = new AsyncResetEvent(false);
|
||||||
|
|
||||||
|
await client.ApiClient1.SubscribeToUpdatesAsync<TestObject>(x =>
|
||||||
|
{
|
||||||
|
received = x.Data;
|
||||||
|
resetEvent.Set();
|
||||||
|
}, false, default);
|
||||||
|
|
||||||
|
socket.InvokeMessage(strData);
|
||||||
|
await resetEvent.WaitAsync(TimeSpan.FromSeconds(1));
|
||||||
|
|
||||||
|
Assert.That(received != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(false)]
|
||||||
|
[TestCase(true)]
|
||||||
|
public async Task SocketMessages_Should_ContainOriginalDataIfEnabled(bool enabled)
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var client = new TestSocketClient(options =>
|
||||||
|
{
|
||||||
|
options.ReconnectInterval = TimeSpan.Zero;
|
||||||
|
options.ExchangeOptions.OutputOriginalData = enabled;
|
||||||
|
});
|
||||||
|
var socket = TestHelpers.ConfigureSocketClient(client, "wss://localhost");
|
||||||
|
var expected = new TestObject() { DecimalData = 1.23M, IntData = 10, StringData = "Some data" };
|
||||||
|
var strData = JsonSerializer.Serialize(expected, new JsonSerializerOptions { TypeInfoResolver = new TestSerializerContext() });
|
||||||
|
|
||||||
|
string? originalData = null;
|
||||||
|
var resetEvent = new AsyncResetEvent(false);
|
||||||
|
|
||||||
|
await client.ApiClient1.SubscribeToUpdatesAsync<TestObject>(x =>
|
||||||
|
{
|
||||||
|
originalData = x.OriginalData;
|
||||||
|
resetEvent.Set();
|
||||||
|
}, false, default);
|
||||||
|
|
||||||
|
socket.InvokeMessage(strData);
|
||||||
|
await resetEvent.WaitAsync(TimeSpan.FromSeconds(1));
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(originalData == (enabled ? strData : null));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase()]
|
||||||
|
public async Task UnsubscribingStream_Should_CloseTheSocket()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var client = new TestSocketClient(options =>
|
||||||
|
{
|
||||||
|
options.ReconnectInterval = TimeSpan.Zero;
|
||||||
|
});
|
||||||
|
var socket = TestHelpers.ConfigureSocketClient(client, "wss://localhost");
|
||||||
|
|
||||||
|
var result = await client.ApiClient1.SubscribeToUpdatesAsync<TestObject>(x => {}, false, default);
|
||||||
|
|
||||||
|
// act
|
||||||
|
await client.UnsubscribeAsync(result.Data!);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(socket.Connected == false);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase()]
|
||||||
|
public async Task UnsubscribingAll_Should_CloseAllSockets()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var client = new TestSocketClient(options =>
|
||||||
|
{
|
||||||
|
options.ReconnectInterval = TimeSpan.Zero;
|
||||||
|
});
|
||||||
|
var socket = TestHelpers.ConfigureSocketClient(client, "wss://localhost");
|
||||||
|
var result = await client.ApiClient1.SubscribeToUpdatesAsync<TestObject>(x => { }, false, default);
|
||||||
|
|
||||||
|
var socket2 = TestHelpers.ConfigureSocketClient(client, "wss://localhost");
|
||||||
|
var result2 = await client.ApiClient1.SubscribeToUpdatesAsync<TestObject>(x => { }, false, default);
|
||||||
|
|
||||||
|
// act
|
||||||
|
await client.UnsubscribeAllAsync();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(socket.Connected == false);
|
||||||
|
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()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var client = new TestSocketClient(opt =>
|
||||||
|
{
|
||||||
|
opt.OutputOriginalData = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
var socket = TestHelpers.ConfigureSocketClient(client, "wss://localhost");
|
||||||
|
var subTask = client.ApiClient1.SubscribeToUpdatesAsync<TestObject>(x => { }, true, default);
|
||||||
|
|
||||||
|
socket.InvokeMessage(JsonSerializer.Serialize(new TestSocketMessage { Id = 1, Data = "ErrorWithSub" }));
|
||||||
|
|
||||||
|
var result = await subTask;
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(result.Success == false);
|
||||||
|
Assert.That(result.Error!.Message!.Contains("ErrorWithSub"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase()]
|
||||||
|
public async Task SuccessResponse_Should_ConfirmSubscription()
|
||||||
|
{
|
||||||
|
var client = new TestSocketClient();
|
||||||
|
var socket = TestHelpers.ConfigureSocketClient(client, "wss://localhost");
|
||||||
|
var subTask = client.ApiClient1.SubscribeToUpdatesAsync<TestObject>(x => { }, true, default);
|
||||||
|
|
||||||
|
socket.InvokeMessage(JsonSerializer.Serialize(new TestSocketMessage { Id = 1, Data = "OK" }));
|
||||||
|
|
||||||
|
var result = await subTask;
|
||||||
|
|
||||||
|
var subscription = client.ApiClient1._socketConnections.Single().Value.Subscriptions.Single();
|
||||||
|
Assert.That(subscription.Status == SubscriptionStatus.Subscribed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
using CryptoExchange.Net.Attributes;
|
||||||
|
using CryptoExchange.Net.Converters;
|
||||||
|
using CryptoExchange.Net.Converters.SystemTextJson;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.UnitTests.ConverterTests
|
||||||
|
{
|
||||||
|
public class ArrayConverterTests
|
||||||
|
{
|
||||||
|
[Test()]
|
||||||
|
public void TestArrayConverter()
|
||||||
|
{
|
||||||
|
var data = new Test()
|
||||||
|
{
|
||||||
|
Prop1 = 2,
|
||||||
|
Prop2 = null,
|
||||||
|
Prop3 = "123",
|
||||||
|
Prop3Again = "123",
|
||||||
|
Prop4 = null,
|
||||||
|
Prop5 = new Test2
|
||||||
|
{
|
||||||
|
Prop21 = 3,
|
||||||
|
Prop22 = "456"
|
||||||
|
},
|
||||||
|
Prop6 = new Test3
|
||||||
|
{
|
||||||
|
Prop31 = 4,
|
||||||
|
Prop32 = "789"
|
||||||
|
},
|
||||||
|
Prop7 = TestEnum.Two,
|
||||||
|
TestInternal = new Test
|
||||||
|
{
|
||||||
|
Prop1 = 10
|
||||||
|
},
|
||||||
|
Prop8 = new Test3
|
||||||
|
{
|
||||||
|
Prop31 = 5,
|
||||||
|
Prop32 = "101"
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
var options = new JsonSerializerOptions()
|
||||||
|
{
|
||||||
|
TypeInfoResolver = new TestSerializerContext()
|
||||||
|
};
|
||||||
|
var serialized = JsonSerializer.Serialize(data);
|
||||||
|
var deserialized = JsonSerializer.Deserialize<Test>(serialized);
|
||||||
|
|
||||||
|
Assert.That(deserialized!.Prop1, Is.EqualTo(2));
|
||||||
|
Assert.That(deserialized.Prop2, Is.Null);
|
||||||
|
Assert.That(deserialized.Prop3, Is.EqualTo("123"));
|
||||||
|
Assert.That(deserialized.Prop3Again, Is.EqualTo("123"));
|
||||||
|
Assert.That(deserialized.Prop4, Is.Null);
|
||||||
|
Assert.That(deserialized.Prop5!.Prop21, Is.EqualTo(3));
|
||||||
|
Assert.That(deserialized.Prop5!.Prop22, Is.EqualTo("456"));
|
||||||
|
Assert.That(deserialized.Prop6!.Prop31, Is.EqualTo(4));
|
||||||
|
Assert.That(deserialized.Prop6.Prop32, Is.EqualTo("789"));
|
||||||
|
Assert.That(deserialized.Prop7, Is.EqualTo(TestEnum.Two));
|
||||||
|
Assert.That(deserialized.TestInternal!.Prop1, Is.EqualTo(10));
|
||||||
|
Assert.That(deserialized.Prop8!.Prop31, Is.EqualTo(5));
|
||||||
|
Assert.That(deserialized.Prop8.Prop32, Is.EqualTo("101"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase("[\"4.803E-5\",\"9.623e-4\",\"0.0010025\"]", 0.00004803, 0.0009623, 0.0010025)]
|
||||||
|
[TestCase("[\"81251.5\",\"1E+2\",\"-2.5E-3\"]", 81251.5, 100, -0.0025)]
|
||||||
|
[TestCase("[4.803E-5,9.623e-4,0.0010025]", 0.00004803, 0.0009623, 0.0010025)]
|
||||||
|
public void TestArrayConverterDecimalInScientificNotation(string json, decimal expected1, decimal expected2, decimal expected3)
|
||||||
|
{
|
||||||
|
// Some exchanges send small prices as strings in scientific notation, for example the
|
||||||
|
// Kucoin futures kline stream: "candles":["1789830000","4.803E-5","4.807E-5",...]
|
||||||
|
var deserialized = JsonSerializer.Deserialize<TestDecimal>(json);
|
||||||
|
|
||||||
|
Assert.That(deserialized!.Prop1, Is.EqualTo(expected1));
|
||||||
|
Assert.That(deserialized.Prop2, Is.EqualTo(expected2));
|
||||||
|
Assert.That(deserialized.Prop3, Is.EqualTo(expected3));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonConverter(typeof(ArrayConverter<TestDecimal>))]
|
||||||
|
public record TestDecimal
|
||||||
|
{
|
||||||
|
[ArrayProperty(0)]
|
||||||
|
public decimal Prop1 { get; set; }
|
||||||
|
[ArrayProperty(1)]
|
||||||
|
public decimal? Prop2 { get; set; }
|
||||||
|
[ArrayProperty(2)]
|
||||||
|
public decimal Prop3 { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonConverter(typeof(ArrayConverter<Test>))]
|
||||||
|
public record Test
|
||||||
|
{
|
||||||
|
[ArrayProperty(0)]
|
||||||
|
public int Prop1 { get; set; }
|
||||||
|
[ArrayProperty(1)]
|
||||||
|
public int? Prop2 { get; set; }
|
||||||
|
[ArrayProperty(2)]
|
||||||
|
public string? Prop3 { get; set; }
|
||||||
|
[ArrayProperty(2)]
|
||||||
|
public string? Prop3Again { get; set; }
|
||||||
|
[ArrayProperty(3)]
|
||||||
|
public string? Prop4 { get; set; }
|
||||||
|
[ArrayProperty(4)]
|
||||||
|
public Test2? Prop5 { get; set; }
|
||||||
|
[ArrayProperty(5)]
|
||||||
|
public Test3? Prop6 { get; set; }
|
||||||
|
[ArrayProperty(6), JsonConverter(typeof(EnumConverter<TestEnum>))]
|
||||||
|
public TestEnum? Prop7 { get; set; }
|
||||||
|
[ArrayProperty(7)]
|
||||||
|
public Test? TestInternal { get; set; }
|
||||||
|
[ArrayProperty(8), JsonConversion]
|
||||||
|
public Test3? Prop8 { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonConverter(typeof(ArrayConverter<Test2>))]
|
||||||
|
public record Test2
|
||||||
|
{
|
||||||
|
[ArrayProperty(0)]
|
||||||
|
public int Prop21 { get; set; }
|
||||||
|
[ArrayProperty(1)]
|
||||||
|
public string? Prop22 { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public record Test3
|
||||||
|
{
|
||||||
|
[JsonPropertyName("prop31")]
|
||||||
|
public int Prop31 { get; set; }
|
||||||
|
[JsonPropertyName("prop32")]
|
||||||
|
public string? Prop32 { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
using CryptoExchange.Net.Converters.SystemTextJson;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.UnitTests.ConverterTests
|
||||||
|
{
|
||||||
|
public class BoolConverterTests
|
||||||
|
{
|
||||||
|
[TestCase("1", true)]
|
||||||
|
[TestCase("true", true)]
|
||||||
|
[TestCase("yes", true)]
|
||||||
|
[TestCase("y", true)]
|
||||||
|
[TestCase("on", true)]
|
||||||
|
[TestCase("-1", false)]
|
||||||
|
[TestCase("0", false)]
|
||||||
|
[TestCase("n", false)]
|
||||||
|
[TestCase("no", false)]
|
||||||
|
[TestCase("false", false)]
|
||||||
|
[TestCase("off", false)]
|
||||||
|
[TestCase("", null)]
|
||||||
|
public void TestBoolConverter(string value, bool? expected)
|
||||||
|
{
|
||||||
|
var val = value == null ? "null" : $"\"{value}\"";
|
||||||
|
var output = JsonSerializer.Deserialize<STJBoolObject>($"{{ \"Value\": {val} }}", SerializerOptions.WithConverters(new TestSerializerContext()));
|
||||||
|
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)]
|
||||||
|
[TestCase("y", true)]
|
||||||
|
[TestCase("on", true)]
|
||||||
|
[TestCase("-1", false)]
|
||||||
|
[TestCase("0", false)]
|
||||||
|
[TestCase("n", false)]
|
||||||
|
[TestCase("no", false)]
|
||||||
|
[TestCase("false", false)]
|
||||||
|
[TestCase("off", false)]
|
||||||
|
[TestCase("", false)]
|
||||||
|
public void TestBoolConverterNotNullable(string value, bool expected)
|
||||||
|
{
|
||||||
|
var val = value == null ? "null" : $"\"{value}\"";
|
||||||
|
var output = JsonSerializer.Deserialize<NotNullableSTJBoolObject>($"{{ \"Value\": {val} }}", SerializerOptions.WithConverters(new TestSerializerContext()));
|
||||||
|
Assert.That(output!.Value == expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class STJBoolObject
|
||||||
|
{
|
||||||
|
public bool? Value { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class NotNullableSTJBoolObject
|
||||||
|
{
|
||||||
|
public bool Value { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
using CryptoExchange.Net.Converters.SystemTextJson;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using System;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.UnitTests.ConverterTests
|
||||||
|
{
|
||||||
|
public class DateTimeConverterTests
|
||||||
|
{
|
||||||
|
[TestCase("2021-05-12")]
|
||||||
|
[TestCase("20210512")]
|
||||||
|
[TestCase("210512")]
|
||||||
|
[TestCase("1620777600.000")]
|
||||||
|
[TestCase("1620777600000")]
|
||||||
|
[TestCase("2021-05-12T00:00:00.000Z")]
|
||||||
|
[TestCase("2021-05-12T00:00:00.000000000Z")]
|
||||||
|
[TestCase("2021-05-12 00:00:00.000000+00:00:00")]
|
||||||
|
[TestCase("0.000000", true)]
|
||||||
|
[TestCase("0", true)]
|
||||||
|
[TestCase("", true)]
|
||||||
|
[TestCase(" ", true)]
|
||||||
|
public void TestDateTimeConverterString(string input, bool expectNull = false)
|
||||||
|
{
|
||||||
|
var output = JsonSerializer.Deserialize<STJTimeObject>($"{{ \"time\": \"{input}\" }}");
|
||||||
|
Assert.That(output!.Time == (expectNull ? null : new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(1620777600.000)]
|
||||||
|
[TestCase(1620777600000d)]
|
||||||
|
public void TestDateTimeConverterDouble(double input)
|
||||||
|
{
|
||||||
|
var output = JsonSerializer.Deserialize<STJTimeObject>($"{{ \"time\": {input} }}");
|
||||||
|
Assert.That(output!.Time == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(1620777600)]
|
||||||
|
[TestCase(1620777600000)]
|
||||||
|
[TestCase(1620777600000000)]
|
||||||
|
[TestCase(1620777600000000000)]
|
||||||
|
[TestCase(0, true)]
|
||||||
|
public void TestDateTimeConverterLong(long input, bool expectNull = false)
|
||||||
|
{
|
||||||
|
var output = JsonSerializer.Deserialize<STJTimeObject>($"{{ \"time\": {input} }}");
|
||||||
|
Assert.That(output!.Time == (expectNull ? null : new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(1620777600)]
|
||||||
|
[TestCase(1620777600.000)]
|
||||||
|
public void TestDateTimeConverterFromSeconds(double input)
|
||||||
|
{
|
||||||
|
var output = DateTimeConverter.ConvertFromSeconds(input);
|
||||||
|
Assert.That(output == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestDateTimeConverterToSeconds()
|
||||||
|
{
|
||||||
|
var output = DateTimeConverter.ConvertToSeconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||||
|
Assert.That(output == 1620777600);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(1620777600000)]
|
||||||
|
[TestCase(1620777600000.000)]
|
||||||
|
public void TestDateTimeConverterFromMilliseconds(double input)
|
||||||
|
{
|
||||||
|
var output = DateTimeConverter.ConvertFromMilliseconds(input);
|
||||||
|
Assert.That(output == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestDateTimeConverterToMilliseconds()
|
||||||
|
{
|
||||||
|
var output = DateTimeConverter.ConvertToMilliseconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||||
|
Assert.That(output == 1620777600000);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(1620777600000000)]
|
||||||
|
public void TestDateTimeConverterFromMicroseconds(long input)
|
||||||
|
{
|
||||||
|
var output = DateTimeConverter.ConvertFromMicroseconds(input);
|
||||||
|
Assert.That(output == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestDateTimeConverterToMicroseconds()
|
||||||
|
{
|
||||||
|
var output = DateTimeConverter.ConvertToMicroseconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||||
|
Assert.That(output == 1620777600000000);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(1620777600000000000)]
|
||||||
|
public void TestDateTimeConverterFromNanoseconds(long input)
|
||||||
|
{
|
||||||
|
var output = DateTimeConverter.ConvertFromNanoseconds(input);
|
||||||
|
Assert.That(output == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestDateTimeConverterToNanoseconds()
|
||||||
|
{
|
||||||
|
var output = DateTimeConverter.ConvertToNanoseconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||||
|
Assert.That(output == 1620777600000000000);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase()]
|
||||||
|
public void TestDateTimeConverterNull()
|
||||||
|
{
|
||||||
|
var output = JsonSerializer.Deserialize<STJTimeObject>($"{{ \"time\": null }}");
|
||||||
|
Assert.That(output!.Time == null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class STJTimeObject
|
||||||
|
{
|
||||||
|
[JsonConverter(typeof(DateTimeConverter))]
|
||||||
|
[JsonPropertyName("time")]
|
||||||
|
public DateTime? Time { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
using CryptoExchange.Net.Converters.SystemTextJson;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.UnitTests.ConverterTests
|
||||||
|
{
|
||||||
|
public class DecimalConverterTests
|
||||||
|
{
|
||||||
|
[TestCase("1", 1)]
|
||||||
|
[TestCase("1.1", 1.1)]
|
||||||
|
[TestCase("-1.1", -1.1)]
|
||||||
|
[TestCase(null, null)]
|
||||||
|
[TestCase("", null)]
|
||||||
|
[TestCase("null", null)]
|
||||||
|
[TestCase("nan", null)]
|
||||||
|
[TestCase("1E+2", 100)]
|
||||||
|
[TestCase("1E-2", 0.01)]
|
||||||
|
[TestCase("Infinity", 999)] // 999 is workaround for not being able to specify decimal.MinValue
|
||||||
|
[TestCase("-Infinity", -999)] // -999 is workaround for not being able to specify decimal.MaxValue
|
||||||
|
[TestCase("80228162514264337593543950335", 999)] // 999 is workaround for not being able to specify decimal.MaxValue
|
||||||
|
[TestCase("-80228162514264337593543950335", -999)] // -999 is workaround for not being able to specify decimal.MaxValue
|
||||||
|
public void TestDecimalConverterString(string value, decimal? expected)
|
||||||
|
{
|
||||||
|
var result = JsonSerializer.Deserialize<STJDecimalObject>("{ \"test\": \"" + value + "\"}");
|
||||||
|
Assert.That(result!.Test, Is.EqualTo(expected == -999 ? decimal.MinValue : expected == 999 ? decimal.MaxValue : expected));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase("1", 1)]
|
||||||
|
[TestCase("1.1", 1.1)]
|
||||||
|
[TestCase("-1.1", -1.1)]
|
||||||
|
[TestCase("null", null)]
|
||||||
|
[TestCase("1E+2", 100)]
|
||||||
|
[TestCase("1E-2", 0.01)]
|
||||||
|
[TestCase("80228162514264337593543950335", -999)] // -999 is workaround for not being able to specify decimal.MaxValue
|
||||||
|
public void TestDecimalConverterNumber(string value, decimal? expected)
|
||||||
|
{
|
||||||
|
var result = JsonSerializer.Deserialize<STJDecimalObject>("{ \"test\": " + value + "}");
|
||||||
|
Assert.That(result!.Test, Is.EqualTo(expected == -999 ? decimal.MaxValue : expected));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class STJDecimalObject
|
||||||
|
{
|
||||||
|
[JsonConverter(typeof(DecimalConverter))]
|
||||||
|
[JsonPropertyName("test")]
|
||||||
|
public decimal? Test { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
using CryptoExchange.Net.Attributes;
|
||||||
|
using CryptoExchange.Net.Converters.SystemTextJson;
|
||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.Testing;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.UnitTests.ConverterTests
|
||||||
|
{
|
||||||
|
public class EnumConverterTests
|
||||||
|
{
|
||||||
|
[TestCase(TestEnum.One, "1")]
|
||||||
|
[TestCase(TestEnum.Two, "2")]
|
||||||
|
[TestCase(TestEnum.Three, "three")]
|
||||||
|
[TestCase(TestEnum.Four, "Four")]
|
||||||
|
[TestCase(null, null)]
|
||||||
|
public void TestEnumConverterNullableGetStringTests(TestEnum? value, string expected)
|
||||||
|
{
|
||||||
|
var output = EnumConverter.GetString(value);
|
||||||
|
Assert.That(output == expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(TestEnum.One, "1")]
|
||||||
|
[TestCase(TestEnum.Two, "2")]
|
||||||
|
[TestCase(TestEnum.Three, "three")]
|
||||||
|
[TestCase(TestEnum.Four, "Four")]
|
||||||
|
public void TestEnumConverterGetStringTests(TestEnum value, string expected)
|
||||||
|
{
|
||||||
|
var output = EnumConverter.GetString(value);
|
||||||
|
Assert.That(output == expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase("1", TestEnum.One)]
|
||||||
|
[TestCase("2", TestEnum.Two)]
|
||||||
|
[TestCase("3", TestEnum.Three)]
|
||||||
|
[TestCase("three", TestEnum.Three)]
|
||||||
|
[TestCase("Four", TestEnum.Four)]
|
||||||
|
[TestCase("four", TestEnum.Four)]
|
||||||
|
[TestCase("Four1", null)]
|
||||||
|
[TestCase(null, null)]
|
||||||
|
public void TestEnumConverterNullableDeserializeTests(string value, TestEnum? expected)
|
||||||
|
{
|
||||||
|
var val = value == null ? "null" : $"\"{value}\"";
|
||||||
|
var output = JsonSerializer.Deserialize<STJEnumObject>($"{{ \"Value\": {val} }}", SerializerOptions.WithConverters(new TestSerializerContext()));
|
||||||
|
Assert.That(output!.Value == expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase("1", TestEnum.One)]
|
||||||
|
[TestCase("2", TestEnum.Two)]
|
||||||
|
[TestCase("3", TestEnum.Three)]
|
||||||
|
[TestCase("three", TestEnum.Three)]
|
||||||
|
[TestCase("Four", TestEnum.Four)]
|
||||||
|
[TestCase("four", TestEnum.Four)]
|
||||||
|
[TestCase("Four1", (TestEnum)(-9))]
|
||||||
|
[TestCase(null, (TestEnum)(-9))]
|
||||||
|
public void TestEnumConverterNotNullableDeserializeTests(string value, TestEnum expected)
|
||||||
|
{
|
||||||
|
var val = value == null ? "null" : $"\"{value}\"";
|
||||||
|
var output = JsonSerializer.Deserialize<NotNullableSTJEnumObject>($"{{ \"Value\": {val} }}");
|
||||||
|
Assert.That(output!.Value == expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestEnumConverterMapsUndefinedValueCorrectlyIfDefaultIsDefined()
|
||||||
|
{
|
||||||
|
var output = JsonSerializer.Deserialize<TestEnum2>($"\"TestUndefined\"");
|
||||||
|
Assert.That((int)output == -99);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase("1", TestEnum.One)]
|
||||||
|
[TestCase("2", TestEnum.Two)]
|
||||||
|
[TestCase("3", TestEnum.Three)]
|
||||||
|
[TestCase("three", TestEnum.Three)]
|
||||||
|
[TestCase("Four", TestEnum.Four)]
|
||||||
|
[TestCase("four", TestEnum.Four)]
|
||||||
|
[TestCase("Four1", null)]
|
||||||
|
[TestCase(null, null)]
|
||||||
|
public void TestEnumConverterParseStringTests(string value, TestEnum? expected)
|
||||||
|
{
|
||||||
|
var result = EnumConverter.ParseString<TestEnum>(value);
|
||||||
|
Assert.That(result == expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestEnumConverterParseNullOnNonNullableOnlyLogsOnce()
|
||||||
|
{
|
||||||
|
LibraryHelpers.StaticLogger = new TraceLogger();
|
||||||
|
var listener = new EnumValueTraceListener();
|
||||||
|
Trace.Listeners.Add(listener);
|
||||||
|
EnumConverter<TestEnum>.Reset();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Assert.Throws<Exception>(() =>
|
||||||
|
{
|
||||||
|
var result = JsonSerializer.Deserialize<NotNullableSTJEnumObject>("{\"Value\": null}", SerializerOptions.WithConverters(new TestSerializerContext()));
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.DoesNotThrow(() =>
|
||||||
|
{
|
||||||
|
var result2 = JsonSerializer.Deserialize<NotNullableSTJEnumObject>("{\"Value\": null}", SerializerOptions.WithConverters(new TestSerializerContext()));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Trace.Listeners.Remove(listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public class STJEnumObject
|
||||||
|
{
|
||||||
|
public TestEnum? Value { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class NotNullableSTJEnumObject
|
||||||
|
{
|
||||||
|
public TestEnum Value { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonConverter(typeof(EnumConverter<TestEnum>))]
|
||||||
|
public enum TestEnum
|
||||||
|
{
|
||||||
|
[Map("1")]
|
||||||
|
One,
|
||||||
|
[Map("2")]
|
||||||
|
Two,
|
||||||
|
[Map("three", "3")]
|
||||||
|
Three,
|
||||||
|
Four
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonConverter(typeof(EnumConverter<TestEnum2>))]
|
||||||
|
public enum TestEnum2
|
||||||
|
{
|
||||||
|
[Map("-9")]
|
||||||
|
Minus9 = -9,
|
||||||
|
[Map("1")]
|
||||||
|
One,
|
||||||
|
[Map("2")]
|
||||||
|
Two,
|
||||||
|
[Map("three", "3")]
|
||||||
|
Three,
|
||||||
|
Four
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
using CryptoExchange.Net.Converters.SystemTextJson;
|
||||||
|
using CryptoExchange.Net.SharedApis;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using System;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.UnitTests.ConverterTests
|
||||||
|
{
|
||||||
|
[TestFixture()]
|
||||||
|
public class SharedModelConversionTests
|
||||||
|
{
|
||||||
|
[TestCase(TradingMode.Spot, "ETH", "USDT", null)]
|
||||||
|
[TestCase(TradingMode.PerpetualLinear, "ETH", "USDT", null)]
|
||||||
|
[TestCase(TradingMode.DeliveryLinear, "ETH", "USDT", 1748432430)]
|
||||||
|
public void TestSharedSymbolConversion(TradingMode tradingMode, string baseAsset, string quoteAsset, int? deliverTime)
|
||||||
|
{
|
||||||
|
DateTime? time = deliverTime == null ? null : DateTimeConverter.ParseFromDouble(deliverTime.Value);
|
||||||
|
var symbol = new SharedSymbol(tradingMode, baseAsset, quoteAsset, time);
|
||||||
|
|
||||||
|
var serialized = JsonSerializer.Serialize(symbol);
|
||||||
|
var restored = JsonSerializer.Deserialize<SharedSymbol>(serialized);
|
||||||
|
|
||||||
|
Assert.That(restored!.TradingMode, Is.EqualTo(symbol.TradingMode));
|
||||||
|
Assert.That(restored.BaseAsset, Is.EqualTo(symbol.BaseAsset));
|
||||||
|
Assert.That(restored.QuoteAsset, Is.EqualTo(symbol.QuoteAsset));
|
||||||
|
Assert.That(restored.DeliverTime, Is.EqualTo(symbol.DeliverTime));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(0.1, null, null)]
|
||||||
|
[TestCase(0.1, 0.1, null)]
|
||||||
|
[TestCase(0.1, 0.1, 0.1)]
|
||||||
|
[TestCase(null, 0.1, null)]
|
||||||
|
[TestCase(null, 0.1, 0.1)]
|
||||||
|
public void TestSharedQuantityConversion(double? baseQuantity, double? quoteQuantity, double? contractQuantity)
|
||||||
|
{
|
||||||
|
var symbol = new SharedOrderQuantity((decimal?)baseQuantity, (decimal?)quoteQuantity, (decimal?)contractQuantity);
|
||||||
|
|
||||||
|
var serialized = JsonSerializer.Serialize(symbol);
|
||||||
|
var restored = JsonSerializer.Deserialize<SharedOrderQuantity>(serialized);
|
||||||
|
|
||||||
|
Assert.That(restored!.QuantityInBaseAsset, Is.EqualTo(symbol.QuantityInBaseAsset));
|
||||||
|
Assert.That(restored.QuantityInQuoteAsset, Is.EqualTo(symbol.QuantityInQuoteAsset));
|
||||||
|
Assert.That(restored.QuantityInContracts, Is.EqualTo(symbol.QuantityInContracts));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -37,8 +37,8 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
var symbols = CreateTestSymbols();
|
var symbols = CreateTestSymbols();
|
||||||
|
|
||||||
// act
|
// act
|
||||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
|
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
|
||||||
var hasCached = ExchangeSymbolCache.HasCached(topicId);
|
var hasCached = ExchangeSymbolCache.HasCached(topicId, "Env", null);
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
Assert.That(hasCached, Is.True);
|
Assert.That(hasCached, Is.True);
|
||||||
@@ -52,14 +52,14 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
var symbols = CreateTestSymbols();
|
var symbols = CreateTestSymbols();
|
||||||
|
|
||||||
// act
|
// act
|
||||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
|
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "BTCUSDT"), Is.True);
|
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, "BTCUSDT"), Is.True);
|
||||||
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "ETHUSDT"), Is.True);
|
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, "ETHUSDT"), Is.True);
|
||||||
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "BTCEUR"), Is.True);
|
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, "BTCEUR"), Is.True);
|
||||||
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "ETHBTC"), Is.True);
|
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, "ETHBTC"), Is.True);
|
||||||
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "XRPUSDT"), Is.True);
|
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, "XRPUSDT"), Is.True);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -78,13 +78,13 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
};
|
};
|
||||||
|
|
||||||
// act
|
// act
|
||||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, initialSymbols);
|
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, initialSymbols);
|
||||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, updatedSymbols);
|
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, updatedSymbols);
|
||||||
|
|
||||||
// assert - should still have only the initial symbol since less than 60 minutes passed
|
// 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
|
// 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]
|
[Test]
|
||||||
@@ -95,8 +95,8 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
var symbols = Array.Empty<SharedSpotSymbol>();
|
var symbols = Array.Empty<SharedSpotSymbol>();
|
||||||
|
|
||||||
// act
|
// act
|
||||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
|
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
|
||||||
var hasCached = ExchangeSymbolCache.HasCached(topicId);
|
var hasCached = ExchangeSymbolCache.HasCached(topicId, "Env", null);
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
Assert.That(hasCached, Is.False);
|
Assert.That(hasCached, Is.False);
|
||||||
@@ -109,7 +109,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
var nonExistentTopic = "NonExistent_" + Guid.NewGuid();
|
var nonExistentTopic = "NonExistent_" + Guid.NewGuid();
|
||||||
|
|
||||||
// act
|
// act
|
||||||
var result = ExchangeSymbolCache.HasCached(nonExistentTopic);
|
var result = ExchangeSymbolCache.HasCached(nonExistentTopic, "Env", null);
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
Assert.That(result, Is.False);
|
Assert.That(result, Is.False);
|
||||||
@@ -121,10 +121,10 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
// arrange
|
// arrange
|
||||||
var topicId = "ExchangeWithData";
|
var topicId = "ExchangeWithData";
|
||||||
var symbols = CreateTestSymbols();
|
var symbols = CreateTestSymbols();
|
||||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
|
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
|
||||||
|
|
||||||
// act
|
// act
|
||||||
var result = ExchangeSymbolCache.HasCached(topicId);
|
var result = ExchangeSymbolCache.HasCached(topicId, "Env", null);
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
Assert.That(result, Is.True);
|
Assert.That(result, Is.True);
|
||||||
@@ -135,10 +135,10 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
{
|
{
|
||||||
// arrange
|
// arrange
|
||||||
var topicId = "ExchangeNoData";
|
var topicId = "ExchangeNoData";
|
||||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, Array.Empty<SharedSpotSymbol>());
|
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, Array.Empty<SharedSpotSymbol>());
|
||||||
|
|
||||||
// act
|
// act
|
||||||
var result = ExchangeSymbolCache.HasCached(topicId);
|
var result = ExchangeSymbolCache.HasCached(topicId, "Env", null);
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
Assert.That(result, Is.False);
|
Assert.That(result, Is.False);
|
||||||
@@ -150,10 +150,10 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
// arrange
|
// arrange
|
||||||
var topicId = "ExchangeSupports";
|
var topicId = "ExchangeSupports";
|
||||||
var symbols = CreateTestSymbols();
|
var symbols = CreateTestSymbols();
|
||||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
|
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
|
||||||
|
|
||||||
// act
|
// act
|
||||||
var result = ExchangeSymbolCache.SupportsSymbol(topicId, "BTCUSDT");
|
var result = ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, "BTCUSDT");
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
Assert.That(result, Is.True);
|
Assert.That(result, Is.True);
|
||||||
@@ -165,10 +165,10 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
// arrange
|
// arrange
|
||||||
var topicId = "ExchangeNoSupport";
|
var topicId = "ExchangeNoSupport";
|
||||||
var symbols = CreateTestSymbols();
|
var symbols = CreateTestSymbols();
|
||||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
|
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
|
||||||
|
|
||||||
// act
|
// act
|
||||||
var result = ExchangeSymbolCache.SupportsSymbol(topicId, "LINKUSDT");
|
var result = ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, "LINKUSDT");
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
Assert.That(result, Is.False);
|
Assert.That(result, Is.False);
|
||||||
@@ -181,7 +181,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
var nonExistentTopic = "NonExistent_" + Guid.NewGuid();
|
var nonExistentTopic = "NonExistent_" + Guid.NewGuid();
|
||||||
|
|
||||||
// act
|
// act
|
||||||
var result = ExchangeSymbolCache.SupportsSymbol(nonExistentTopic, "BTCUSDT");
|
var result = ExchangeSymbolCache.SupportsSymbol(nonExistentTopic, "Env", null, "BTCUSDT");
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
Assert.That(result, Is.False);
|
Assert.That(result, Is.False);
|
||||||
@@ -193,11 +193,11 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
// arrange
|
// arrange
|
||||||
var topicId = "ExchangeSharedSymbol";
|
var topicId = "ExchangeSharedSymbol";
|
||||||
var symbols = CreateTestSymbols();
|
var symbols = CreateTestSymbols();
|
||||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
|
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
|
||||||
var sharedSymbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
|
var sharedSymbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
|
||||||
|
|
||||||
// act
|
// act
|
||||||
var result = ExchangeSymbolCache.SupportsSymbol(topicId, sharedSymbol);
|
var result = ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, sharedSymbol);
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
Assert.That(result, Is.True);
|
Assert.That(result, Is.True);
|
||||||
@@ -209,11 +209,11 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
// arrange
|
// arrange
|
||||||
var topicId = "ExchangeNoSharedSymbol";
|
var topicId = "ExchangeNoSharedSymbol";
|
||||||
var symbols = CreateTestSymbols();
|
var symbols = CreateTestSymbols();
|
||||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
|
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
|
||||||
var sharedSymbol = new SharedSymbol(TradingMode.Spot, "LINK", "USDT");
|
var sharedSymbol = new SharedSymbol(TradingMode.Spot, "LINK", "USDT");
|
||||||
|
|
||||||
// act
|
// act
|
||||||
var result = ExchangeSymbolCache.SupportsSymbol(topicId, sharedSymbol);
|
var result = ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, sharedSymbol);
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
Assert.That(result, Is.False);
|
Assert.That(result, Is.False);
|
||||||
@@ -225,11 +225,11 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
// arrange
|
// arrange
|
||||||
var topicId = "ExchangeDifferentMode";
|
var topicId = "ExchangeDifferentMode";
|
||||||
var symbols = CreateTestSymbols();
|
var symbols = CreateTestSymbols();
|
||||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
|
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
|
||||||
var sharedSymbol = new SharedSymbol(TradingMode.PerpetualLinear, "BTC", "USDT");
|
var sharedSymbol = new SharedSymbol(TradingMode.PerpetualLinear, "BTC", "USDT");
|
||||||
|
|
||||||
// act
|
// act
|
||||||
var result = ExchangeSymbolCache.SupportsSymbol(topicId, sharedSymbol);
|
var result = ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, sharedSymbol);
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
Assert.That(result, Is.False);
|
Assert.That(result, Is.False);
|
||||||
@@ -243,7 +243,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
var sharedSymbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
|
var sharedSymbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
|
||||||
|
|
||||||
// act
|
// act
|
||||||
var result = ExchangeSymbolCache.SupportsSymbol(nonExistentTopic, sharedSymbol);
|
var result = ExchangeSymbolCache.SupportsSymbol(nonExistentTopic, "Env", null, sharedSymbol);
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
Assert.That(result, Is.False);
|
Assert.That(result, Is.False);
|
||||||
@@ -255,10 +255,10 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
// arrange
|
// arrange
|
||||||
var topicId = "ExchangeBaseAsset";
|
var topicId = "ExchangeBaseAsset";
|
||||||
var symbols = CreateTestSymbols();
|
var symbols = CreateTestSymbols();
|
||||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
|
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
|
||||||
|
|
||||||
// act
|
// act
|
||||||
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "BTC");
|
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "Env", null, "BTC");
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
Assert.That(result, Is.Not.Null);
|
Assert.That(result, Is.Not.Null);
|
||||||
@@ -273,10 +273,10 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
// arrange
|
// arrange
|
||||||
var topicId = "ExchangeCaseInsensitive";
|
var topicId = "ExchangeCaseInsensitive";
|
||||||
var symbols = CreateTestSymbols();
|
var symbols = CreateTestSymbols();
|
||||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
|
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
|
||||||
|
|
||||||
// act
|
// act
|
||||||
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "btc");
|
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "Env", null, "btc");
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
Assert.That(result, Is.Not.Null);
|
Assert.That(result, Is.Not.Null);
|
||||||
@@ -289,10 +289,10 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
// arrange
|
// arrange
|
||||||
var topicId = "ExchangeNoBaseAsset";
|
var topicId = "ExchangeNoBaseAsset";
|
||||||
var symbols = CreateTestSymbols();
|
var symbols = CreateTestSymbols();
|
||||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
|
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
|
||||||
|
|
||||||
// act
|
// act
|
||||||
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "LINK");
|
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "Env", null, "LINK");
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
Assert.That(result, Is.Not.Null);
|
Assert.That(result, Is.Not.Null);
|
||||||
@@ -306,7 +306,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
var nonExistentTopic = "NonExistent_" + Guid.NewGuid();
|
var nonExistentTopic = "NonExistent_" + Guid.NewGuid();
|
||||||
|
|
||||||
// act
|
// act
|
||||||
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(nonExistentTopic, "BTC");
|
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(nonExistentTopic, "Env", null, "BTC");
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
Assert.That(result, Is.Not.Null);
|
Assert.That(result, Is.Not.Null);
|
||||||
@@ -319,14 +319,14 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
// arrange
|
// arrange
|
||||||
var topicId = "ExchangeParse";
|
var topicId = "ExchangeParse";
|
||||||
var symbols = CreateTestSymbols();
|
var symbols = CreateTestSymbols();
|
||||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
|
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
|
||||||
|
|
||||||
// act
|
// act
|
||||||
var result = ExchangeSymbolCache.ParseSymbol(topicId, "BTCUSDT");
|
var result = ExchangeSymbolCache.ParseSymbol(topicId, "Env", null, "BTCUSDT");
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
Assert.That(result, Is.Not.Null);
|
Assert.That(result, Is.Not.Null);
|
||||||
Assert.That(result.BaseAsset, Is.EqualTo("BTC"));
|
Assert.That(result!.BaseAsset, Is.EqualTo("BTC"));
|
||||||
Assert.That(result.QuoteAsset, Is.EqualTo("USDT"));
|
Assert.That(result.QuoteAsset, Is.EqualTo("USDT"));
|
||||||
Assert.That(result.TradingMode, Is.EqualTo(TradingMode.Spot));
|
Assert.That(result.TradingMode, Is.EqualTo(TradingMode.Spot));
|
||||||
Assert.That(result.SymbolName, Is.EqualTo("BTCUSDT"));
|
Assert.That(result.SymbolName, Is.EqualTo("BTCUSDT"));
|
||||||
@@ -338,10 +338,10 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
// arrange
|
// arrange
|
||||||
var topicId = "ExchangeNoParse";
|
var topicId = "ExchangeNoParse";
|
||||||
var symbols = CreateTestSymbols();
|
var symbols = CreateTestSymbols();
|
||||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
|
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
|
||||||
|
|
||||||
// act
|
// act
|
||||||
var result = ExchangeSymbolCache.ParseSymbol(topicId, "LINKUSDT");
|
var result = ExchangeSymbolCache.ParseSymbol(topicId, "Env", null, "LINKUSDT");
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
Assert.That(result, Is.Null);
|
Assert.That(result, Is.Null);
|
||||||
@@ -353,10 +353,10 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
// arrange
|
// arrange
|
||||||
var topicId = "ExchangeNullSymbol";
|
var topicId = "ExchangeNullSymbol";
|
||||||
var symbols = CreateTestSymbols();
|
var symbols = CreateTestSymbols();
|
||||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
|
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
|
||||||
|
|
||||||
// act
|
// act
|
||||||
var result = ExchangeSymbolCache.ParseSymbol(topicId, null);
|
var result = ExchangeSymbolCache.ParseSymbol(topicId, "Env", null, null);
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
Assert.That(result, Is.Null);
|
Assert.That(result, Is.Null);
|
||||||
@@ -369,7 +369,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
var nonExistentTopic = "NonExistent_" + Guid.NewGuid();
|
var nonExistentTopic = "NonExistent_" + Guid.NewGuid();
|
||||||
|
|
||||||
// act
|
// act
|
||||||
var result = ExchangeSymbolCache.ParseSymbol(nonExistentTopic, "BTCUSDT");
|
var result = ExchangeSymbolCache.ParseSymbol(nonExistentTopic, "Env", null, "BTCUSDT");
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
Assert.That(result, Is.Null);
|
Assert.That(result, Is.Null);
|
||||||
@@ -391,14 +391,14 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
};
|
};
|
||||||
|
|
||||||
// act
|
// act
|
||||||
ExchangeSymbolCache.UpdateSymbolInfo(topic1, symbols1);
|
ExchangeSymbolCache.UpdateSymbolInfo(topic1, "Env", null, symbols1);
|
||||||
ExchangeSymbolCache.UpdateSymbolInfo(topic2, symbols2);
|
ExchangeSymbolCache.UpdateSymbolInfo(topic2, "Env", null, symbols2);
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
Assert.That(ExchangeSymbolCache.SupportsSymbol(topic1, "BTCUSDT"), Is.True);
|
Assert.That(ExchangeSymbolCache.SupportsSymbol(topic1, "Env", null, "BTCUSDT"), Is.True);
|
||||||
Assert.That(ExchangeSymbolCache.SupportsSymbol(topic1, "ETHUSDT"), Is.False);
|
Assert.That(ExchangeSymbolCache.SupportsSymbol(topic1, "Env", null, "ETHUSDT"), Is.False);
|
||||||
Assert.That(ExchangeSymbolCache.SupportsSymbol(topic2, "ETHUSDT"), Is.True);
|
Assert.That(ExchangeSymbolCache.SupportsSymbol(topic2, "Env", null, "ETHUSDT"), Is.True);
|
||||||
Assert.That(ExchangeSymbolCache.SupportsSymbol(topic2, "BTCUSDT"), Is.False);
|
Assert.That(ExchangeSymbolCache.SupportsSymbol(topic2, "Env", null, "BTCUSDT"), Is.False);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -411,14 +411,14 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
var allSymbols = spotSymbols.Concat(futuresSymbols).ToArray();
|
var allSymbols = spotSymbols.Concat(futuresSymbols).ToArray();
|
||||||
|
|
||||||
// act
|
// act
|
||||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, allSymbols);
|
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, allSymbols);
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
var spotSymbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
|
var spotSymbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
|
||||||
var futuresSymbol = new SharedSymbol(TradingMode.PerpetualLinear, "BTC", "USDT");
|
var futuresSymbol = new SharedSymbol(TradingMode.PerpetualLinear, "BTC", "USDT");
|
||||||
|
|
||||||
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, spotSymbol), Is.True);
|
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, spotSymbol), Is.True);
|
||||||
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, futuresSymbol), Is.True);
|
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, futuresSymbol), Is.True);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -429,10 +429,10 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
var spotSymbols = CreateTestSymbols();
|
var spotSymbols = CreateTestSymbols();
|
||||||
var futuresSymbols = CreateFuturesSymbols();
|
var futuresSymbols = CreateFuturesSymbols();
|
||||||
var allSymbols = spotSymbols.Concat(futuresSymbols).ToArray();
|
var allSymbols = spotSymbols.Concat(futuresSymbols).ToArray();
|
||||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, allSymbols);
|
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, allSymbols);
|
||||||
|
|
||||||
// act
|
// act
|
||||||
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "BTC");
|
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "Env", null, "BTC");
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
Assert.That(result.Length, Is.GreaterThanOrEqualTo(2));
|
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", "BTC", "ETHBTC", true, TradingMode.Spot),
|
||||||
new SharedSpotSymbol("ETH", "EUR", "ETHEUR", true, TradingMode.Spot)
|
new SharedSpotSymbol("ETH", "EUR", "ETHEUR", true, TradingMode.Spot)
|
||||||
};
|
};
|
||||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
|
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
|
||||||
|
|
||||||
// act
|
// act
|
||||||
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "ETH");
|
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "Env", null, "ETH");
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
Assert.That(result.Length, Is.EqualTo(3));
|
Assert.That(result.Length, Is.EqualTo(3));
|
||||||
Assert.That(result.All(x => x.BaseAsset == "ETH"), Is.True);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using CryptoExchange.Net.Authentication;
|
||||||
|
using CryptoExchange.Net.Clients;
|
||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.UnitTests.Implementations
|
||||||
|
{
|
||||||
|
internal class TestAuthenticationProvider : AuthenticationProvider<TestCredentials, TestCredentials>
|
||||||
|
{
|
||||||
|
public TestAuthenticationProvider(TestCredentials credentials) : base(credentials, credentials)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void ProcessRequest(RestApiClient apiClient, RestRequestConfiguration requestConfig)
|
||||||
|
{
|
||||||
|
requestConfig.Headers ??= new Dictionary<string, string>();
|
||||||
|
requestConfig.Headers["Authorization"] = Credential.Key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using CryptoExchange.Net.Authentication;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.UnitTests.Implementations
|
||||||
|
{
|
||||||
|
internal class TestCredentials : HMACCredential
|
||||||
|
{
|
||||||
|
public TestCredentials() { }
|
||||||
|
|
||||||
|
public TestCredentials(string key, string secret) : base(key, secret)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public TestCredentials(HMACCredential credential) : base(credential.Key, credential.Secret)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public TestCredentials WithHMAC(string key, string secret)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(Key)) throw new InvalidOperationException("Credentials already set");
|
||||||
|
|
||||||
|
Key = key;
|
||||||
|
Secret = secret;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override ApiCredentials Copy() => new TestCredentials(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.UnitTests.Implementations
|
||||||
|
{
|
||||||
|
internal class TestEnvironment : TradeEnvironment
|
||||||
|
{
|
||||||
|
public string RestClientAddress { get; }
|
||||||
|
public string SocketClientAddress { get; }
|
||||||
|
|
||||||
|
internal TestEnvironment(
|
||||||
|
string name,
|
||||||
|
string restAddress,
|
||||||
|
string streamAddress) :
|
||||||
|
base(name)
|
||||||
|
{
|
||||||
|
RestClientAddress = restAddress;
|
||||||
|
SocketClientAddress = streamAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
|
||||||
|
public TestEnvironment() : base(TradeEnvironmentNames.Live)
|
||||||
|
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
|
||||||
|
{ }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the environment by name
|
||||||
|
/// </summary>
|
||||||
|
public static TestEnvironment? GetEnvironmentByName(string? name)
|
||||||
|
=> name switch
|
||||||
|
{
|
||||||
|
TradeEnvironmentNames.Live => Live,
|
||||||
|
"" => Live,
|
||||||
|
null => Live,
|
||||||
|
_ => default
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Available environment names
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string[] All => [Live.Name];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Live environment
|
||||||
|
/// </summary>
|
||||||
|
public static TestEnvironment Live { get; }
|
||||||
|
= new TestEnvironment(TradeEnvironmentNames.Live,
|
||||||
|
"https://localhost",
|
||||||
|
"wss://localhost");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a custom environment
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name"></param>
|
||||||
|
/// <param name="spotRestAddress"></param>
|
||||||
|
/// <param name="spotSocketStreamsAddress"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static TestEnvironment CreateCustom(
|
||||||
|
string name,
|
||||||
|
string spotRestAddress,
|
||||||
|
string spotSocketStreamsAddress)
|
||||||
|
=> new TestEnvironment(name, spotRestAddress, spotSocketStreamsAddress);
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -1,11 +1,11 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
namespace CryptoExchange.Net.UnitTests.Implementations
|
||||||
{
|
{
|
||||||
public class TestObject
|
public class TestObject
|
||||||
{
|
{
|
||||||
[JsonPropertyName("other")]
|
[JsonPropertyName("other")]
|
||||||
public string StringData { get; set; }
|
public string StringData { get; set; } = string.Empty;
|
||||||
[JsonPropertyName("intData")]
|
[JsonPropertyName("intData")]
|
||||||
public int IntData { get; set; }
|
public int IntData { get; set; }
|
||||||
[JsonPropertyName("decimalData")]
|
[JsonPropertyName("decimalData")]
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.Objects.Errors;
|
||||||
|
using CryptoExchange.Net.Sockets;
|
||||||
|
using CryptoExchange.Net.Sockets.Default;
|
||||||
|
using CryptoExchange.Net.Sockets.Default.Routing;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.UnitTests.Implementations
|
||||||
|
{
|
||||||
|
internal class TestQuery : Query<TestSocketMessage>
|
||||||
|
{
|
||||||
|
public TestQuery(TestSocketMessage request, bool authenticated) : base(request, authenticated, 1)
|
||||||
|
{
|
||||||
|
MessageRouter = MessageRouter.CreateForQuery<TestSocketMessage>(request.Id.ToString(), HandleMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
private CallResult<TestSocketMessage>? HandleMessage(SocketConnection connection, DateTime time, string? arg3, TestSocketMessage message)
|
||||||
|
{
|
||||||
|
if (message.Data != "OK")
|
||||||
|
return CallResult.Fail<TestSocketMessage>(new ServerError(ErrorInfo.Unknown with { Message = message.Data }));
|
||||||
|
|
||||||
|
return CallResult.Ok(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
using CryptoExchange.Net.Clients;
|
||||||
|
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
|
||||||
|
using CryptoExchange.Net.Converters.SystemTextJson;
|
||||||
|
using CryptoExchange.Net.Interfaces;
|
||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.RateLimiting;
|
||||||
|
using CryptoExchange.Net.SharedApis;
|
||||||
|
using CryptoExchange.Net.Testing.Implementations;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.UnitTests.Implementations
|
||||||
|
{
|
||||||
|
internal class TestRestApiClient : RestApiClient<TestEnvironment, TestAuthenticationProvider, TestCredentials>
|
||||||
|
{
|
||||||
|
protected override IRestMessageHandler MessageHandler { get; } = new TestRestMessageHandler();
|
||||||
|
|
||||||
|
public TestRestApiClient(ILoggerFactory? loggerFactory, HttpClient? httpClient, TestRestOptions options)
|
||||||
|
: base(loggerFactory, "Test", httpClient, options.Environment.RestClientAddress, options, options.ExchangeOptions)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverDate = null) =>
|
||||||
|
baseAsset + quoteAsset;
|
||||||
|
|
||||||
|
protected override TestAuthenticationProvider CreateAuthenticationProvider(TestCredentials credentials) =>
|
||||||
|
new TestAuthenticationProvider(credentials);
|
||||||
|
|
||||||
|
protected override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(SerializerOptions.WithConverters(new TestSerializerContext()));
|
||||||
|
|
||||||
|
internal void SetNextResponse(string data, HttpStatusCode code)
|
||||||
|
{
|
||||||
|
var expectedBytes = Encoding.UTF8.GetBytes(data);
|
||||||
|
var responseStream = new MemoryStream();
|
||||||
|
responseStream.Write(expectedBytes, 0, expectedBytes.Length);
|
||||||
|
responseStream.Seek(0, SeekOrigin.Begin);
|
||||||
|
|
||||||
|
var response = new TestResponse(code, responseStream);
|
||||||
|
var request = new TestRequest(response);
|
||||||
|
|
||||||
|
var factory = new TestRequestFactory(request);
|
||||||
|
RequestFactory = factory;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal async Task<HttpResult<T>> GetResponseAsync<T>(HttpMethod? httpMethod = null, Parameters? collection = null, RateLimitGate? rateLimitGate = null)
|
||||||
|
{
|
||||||
|
var definition = new RequestDefinition(BaseAddress, "/path", httpMethod ?? HttpMethod.Get)
|
||||||
|
{
|
||||||
|
Weight = rateLimitGate == null ? 0 : 1,
|
||||||
|
RateLimitGate = rateLimitGate
|
||||||
|
};
|
||||||
|
return await SendAsync<T>(definition, collection ?? new Parameters(new ParameterSerializationSettings()), default);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void SetParameterPosition(HttpMethod httpMethod, HttpMethodParameterPosition pos)
|
||||||
|
{
|
||||||
|
ParameterPositions[httpMethod] = pos;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
using CryptoExchange.Net.Clients;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using System;
|
||||||
|
using System.Net.Http;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.UnitTests.Implementations
|
||||||
|
{
|
||||||
|
internal class TestRestClient : BaseRestClient<TestEnvironment, TestCredentials>
|
||||||
|
{
|
||||||
|
public TestRestApiClient ApiClient1 { get; set; }
|
||||||
|
public TestRestApiClient ApiClient2 { get; set; }
|
||||||
|
|
||||||
|
public TestRestClient(Action<TestRestOptions>? optionsDelegate = null)
|
||||||
|
: this(null, null, Options.Create(ApplyOptionsDelegate(optionsDelegate)))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public TestRestClient(HttpClient? httpClient, ILoggerFactory? loggerFactory, IOptions<TestRestOptions> options) : base(loggerFactory, "Test")
|
||||||
|
{
|
||||||
|
Initialize(options.Value);
|
||||||
|
|
||||||
|
ApiClient1 = AddApiClient(new TestRestApiClient(loggerFactory, httpClient, options.Value));
|
||||||
|
ApiClient2 = AddApiClient(new TestRestApiClient(loggerFactory, httpClient, options.Value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
using CryptoExchange.Net.Converters.SystemTextJson;
|
||||||
|
using CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers;
|
||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.Objects.Errors;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.UnitTests.Implementations
|
||||||
|
{
|
||||||
|
internal class TestRestMessageHandler : JsonRestMessageHandler
|
||||||
|
{
|
||||||
|
public override JsonSerializerOptions Options { get; } = SerializerOptions.WithConverters(new TestSerializerContext());
|
||||||
|
|
||||||
|
public override async ValueTask<Error> ParseErrorResponse(int httpStatusCode, HttpResponseHeaders responseHeaders, Stream responseStream)
|
||||||
|
{
|
||||||
|
var (jsonError, jsonDocument) = await GetJsonDocument(responseStream).ConfigureAwait(false);
|
||||||
|
if (jsonError != null)
|
||||||
|
return jsonError;
|
||||||
|
|
||||||
|
int? code = jsonDocument!.RootElement.TryGetProperty("errorCode", out var codeProp) ? codeProp.GetInt32() : null;
|
||||||
|
var msg = jsonDocument.RootElement.TryGetProperty("errorMessage", out var msgProp) ? msgProp.GetString() : null;
|
||||||
|
if (msg == null)
|
||||||
|
return new ServerError(ErrorInfo.Unknown);
|
||||||
|
|
||||||
|
if (code == null)
|
||||||
|
return new ServerError(ErrorInfo.Unknown with { Message = msg });
|
||||||
|
|
||||||
|
return new ServerError(code.Value, new ErrorInfo(ErrorType.Unknown, false, "Error") with { Message = msg });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
using CryptoExchange.Net.Objects.Options;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.UnitTests.Implementations
|
||||||
|
{
|
||||||
|
internal class TestRestOptions : RestExchangeOptions<TestEnvironment, TestCredentials>
|
||||||
|
{
|
||||||
|
internal static TestRestOptions Default { get; set; } = new TestRestOptions()
|
||||||
|
{
|
||||||
|
Environment = TestEnvironment.Live,
|
||||||
|
AutoTimestamp = true
|
||||||
|
};
|
||||||
|
|
||||||
|
public TestRestOptions()
|
||||||
|
{
|
||||||
|
Default?.Set(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public RestApiOptions ExchangeOptions { get; private set; } = new RestApiOptions();
|
||||||
|
|
||||||
|
internal TestRestOptions Set(TestRestOptions targetOptions)
|
||||||
|
{
|
||||||
|
targetOptions = base.Set<TestRestOptions>(targetOptions);
|
||||||
|
targetOptions.ExchangeOptions = ExchangeOptions.Set(targetOptions.ExchangeOptions);
|
||||||
|
return targetOptions;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.UnitTests.ConverterTests;
|
||||||
|
using CryptoExchange.Net.UnitTests.Implementations;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.UnitTests
|
||||||
|
{
|
||||||
|
[JsonSerializable(typeof(string))]
|
||||||
|
[JsonSerializable(typeof(int))]
|
||||||
|
[JsonSerializable(typeof(Dictionary<string, string>))]
|
||||||
|
[JsonSerializable(typeof(IDictionary<string, string>))]
|
||||||
|
[JsonSerializable(typeof(Dictionary<string, object>))]
|
||||||
|
[JsonSerializable(typeof(IDictionary<string, object>))]
|
||||||
|
[JsonSerializable(typeof(Parameters))]
|
||||||
|
[JsonSerializable(typeof(TestObject))]
|
||||||
|
|
||||||
|
[JsonSerializable(typeof(TestSocketMessage))]
|
||||||
|
[JsonSerializable(typeof(Test))]
|
||||||
|
[JsonSerializable(typeof(Test2))]
|
||||||
|
[JsonSerializable(typeof(Test3))]
|
||||||
|
[JsonSerializable(typeof(NotNullableSTJBoolObject))]
|
||||||
|
[JsonSerializable(typeof(STJBoolObject))]
|
||||||
|
[JsonSerializable(typeof(NotNullableSTJEnumObject))]
|
||||||
|
[JsonSerializable(typeof(STJEnumObject))]
|
||||||
|
[JsonSerializable(typeof(STJDecimalObject))]
|
||||||
|
[JsonSerializable(typeof(STJTimeObject))]
|
||||||
|
internal partial class TestSerializerContext : JsonSerializerContext
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
using CryptoExchange.Net.Clients;
|
||||||
|
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
|
||||||
|
using CryptoExchange.Net.Converters.SystemTextJson;
|
||||||
|
using CryptoExchange.Net.Interfaces;
|
||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.Objects.Options;
|
||||||
|
using CryptoExchange.Net.Objects.Sockets;
|
||||||
|
using CryptoExchange.Net.SharedApis;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.WebSockets;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.UnitTests.Implementations
|
||||||
|
{
|
||||||
|
internal class TestSocketApiClient : SocketApiClient<TestEnvironment, TestAuthenticationProvider, TestCredentials>
|
||||||
|
{
|
||||||
|
public TestSocketApiClient(ILoggerFactory? loggerFactory, TestSocketOptions options)
|
||||||
|
: base(loggerFactory, "Test", options.Environment.SocketClientAddress, options, options.ExchangeOptions)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public TestSocketApiClient(ILoggerFactory? loggerFactory, HttpClient httpClient, string baseAddress, TestSocketOptions options, SocketApiOptions apiOptions)
|
||||||
|
: base(loggerFactory, "Test", baseAddress, options, apiOptions)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override ISocketMessageHandler CreateMessageConverter(WebSocketMessageType messageType) => new TestSocketMessageHandler();
|
||||||
|
protected internal override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(SerializerOptions.WithConverters(new TestSerializerContext()));
|
||||||
|
|
||||||
|
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverDate = null) =>
|
||||||
|
baseAsset + quoteAsset;
|
||||||
|
|
||||||
|
protected override TestAuthenticationProvider CreateAuthenticationProvider(TestCredentials credentials) =>
|
||||||
|
new TestAuthenticationProvider(credentials);
|
||||||
|
|
||||||
|
public async Task<WebSocketResult<UpdateSubscription>> SubscribeToUpdatesAsync<T>(Action<DataEvent<T>> handler, bool subQuery, CancellationToken ct, int individualSubscriptionCount = 1)
|
||||||
|
{
|
||||||
|
var subscription = new TestSubscription<T>(_logger, handler, subQuery, false)
|
||||||
|
{
|
||||||
|
IndividualSubscriptionCount = individualSubscriptionCount
|
||||||
|
};
|
||||||
|
return await base.SubscribeAsync(subscription, ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
using CryptoExchange.Net.Clients;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.UnitTests.Implementations
|
||||||
|
{
|
||||||
|
internal class TestSocketClient : BaseSocketClient<TestEnvironment, TestCredentials>
|
||||||
|
{
|
||||||
|
public TestSocketApiClient ApiClient1 { get; set; }
|
||||||
|
public TestSocketApiClient ApiClient2 { get; set; }
|
||||||
|
|
||||||
|
public TestSocketClient(Action<TestSocketOptions>? optionsDelegate = null)
|
||||||
|
: this(null, Options.Create(ApplyOptionsDelegate(optionsDelegate)))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public TestSocketClient(ILoggerFactory? loggerFactory, IOptions<TestSocketOptions> options) : base(loggerFactory, "Test")
|
||||||
|
{
|
||||||
|
Initialize(options.Value);
|
||||||
|
|
||||||
|
ApiClient1 = AddApiClient(new TestSocketApiClient(loggerFactory, options.Value));
|
||||||
|
ApiClient2 = AddApiClient(new TestSocketApiClient(loggerFactory, options.Value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.UnitTests.Implementations
|
||||||
|
{
|
||||||
|
internal record TestSocketMessage
|
||||||
|
{
|
||||||
|
[JsonPropertyName("id")]
|
||||||
|
public int Id { get; set; }
|
||||||
|
[JsonPropertyName("data")]
|
||||||
|
public string Data { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
|
||||||
|
using CryptoExchange.Net.Converters.SystemTextJson;
|
||||||
|
using CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.UnitTests.Implementations
|
||||||
|
{
|
||||||
|
internal class TestSocketMessageHandler : JsonSocketMessageHandler
|
||||||
|
{
|
||||||
|
public override JsonSerializerOptions Options { get; } = SerializerOptions.WithConverters(new TestSerializerContext());
|
||||||
|
|
||||||
|
public TestSocketMessageHandler()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override MessageTypeDefinition[] TypeEvaluators { get; } = [
|
||||||
|
|
||||||
|
new MessageTypeDefinition {
|
||||||
|
ForceIfFound = true,
|
||||||
|
Fields = [
|
||||||
|
new PropertyFieldReference("id")
|
||||||
|
],
|
||||||
|
TypeIdentifierCallback = (doc) => doc.FieldValue("id")!
|
||||||
|
},
|
||||||
|
|
||||||
|
new MessageTypeDefinition {
|
||||||
|
Fields = [
|
||||||
|
],
|
||||||
|
StaticIdentifier = "test"
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
using CryptoExchange.Net.Objects.Options;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.UnitTests.Implementations
|
||||||
|
{
|
||||||
|
internal class TestSocketOptions : SocketExchangeOptions<TestEnvironment, TestCredentials>
|
||||||
|
{
|
||||||
|
internal static TestSocketOptions Default { get; set; } = new TestSocketOptions()
|
||||||
|
{
|
||||||
|
Environment = TestEnvironment.Live,
|
||||||
|
AutoTimestamp = true
|
||||||
|
};
|
||||||
|
|
||||||
|
public TestSocketOptions()
|
||||||
|
{
|
||||||
|
Default?.Set(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SocketApiOptions ExchangeOptions { get; private set; } = new SocketApiOptions();
|
||||||
|
|
||||||
|
internal TestSocketOptions Set(TestSocketOptions targetOptions)
|
||||||
|
{
|
||||||
|
targetOptions = base.Set<TestSocketOptions>(targetOptions);
|
||||||
|
targetOptions.ExchangeOptions = ExchangeOptions.Set(targetOptions.ExchangeOptions);
|
||||||
|
return targetOptions;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.Objects.Sockets;
|
||||||
|
using CryptoExchange.Net.Sockets;
|
||||||
|
using CryptoExchange.Net.Sockets.Default;
|
||||||
|
using CryptoExchange.Net.Sockets.Default.Routing;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Reflection.Metadata;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.UnitTests.Implementations
|
||||||
|
{
|
||||||
|
internal class TestSubscription<T> : Subscription
|
||||||
|
{
|
||||||
|
private readonly Action<DataEvent<T>> _handler;
|
||||||
|
private bool _subQuery;
|
||||||
|
|
||||||
|
public TestSubscription(ILogger logger, Action<DataEvent<T>> handler, bool subQuery, bool authenticated) : base(logger, authenticated, true)
|
||||||
|
{
|
||||||
|
_handler = handler;
|
||||||
|
_subQuery = subQuery;
|
||||||
|
|
||||||
|
MessageRouter = MessageRouter.CreateForEvent<T>("test", HandleUpdate);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Query? GetSubQuery(SocketConnection connection)
|
||||||
|
{
|
||||||
|
if (!_subQuery)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return new TestQuery(new TestSocketMessage { Id = 1, Data = "Sub" }, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Query? GetUnsubQuery(SocketConnection connection)
|
||||||
|
{
|
||||||
|
if (!_subQuery)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return new TestQuery(new TestSocketMessage { Id = 2, Data = "Unsub" }, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private CallResult? HandleUpdate(SocketConnection connection, DateTime time, string? originalData, T data)
|
||||||
|
{
|
||||||
|
_handler(new DataEvent<T>("Test", data, time, originalData));
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
using CryptoExchange.Net.Authentication;
|
using CryptoExchange.Net.Authentication;
|
||||||
|
using CryptoExchange.Net.Objects;
|
||||||
using CryptoExchange.Net.Objects.Options;
|
using CryptoExchange.Net.Objects.Options;
|
||||||
using CryptoExchange.Net.UnitTests.TestImplementations;
|
using CryptoExchange.Net.UnitTests.Implementations;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
@@ -10,9 +11,9 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
public class OptionsTests
|
public class OptionsTests
|
||||||
{
|
{
|
||||||
[TearDown]
|
[TearDown]
|
||||||
public void Init()
|
public void TearDown()
|
||||||
{
|
{
|
||||||
TestClientOptions.Default = new TestClientOptions
|
TestRestOptions.Default = new TestRestOptions
|
||||||
{
|
{
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -30,9 +31,9 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
// assert
|
// assert
|
||||||
Assert.Throws(typeof(ArgumentException),
|
Assert.Throws(typeof(ArgumentException),
|
||||||
() => {
|
() => {
|
||||||
var opts = new RestExchangeOptions<TestEnvironment, HMACCredential>()
|
var opts = new TestRestOptions()
|
||||||
{
|
{
|
||||||
ApiCredentials = new HMACCredential(key, secret)
|
ApiCredentials = new TestCredentials(key, secret)
|
||||||
};
|
};
|
||||||
opts.ApiCredentials.Validate();
|
opts.ApiCredentials.Validate();
|
||||||
});
|
});
|
||||||
@@ -42,53 +43,110 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
public void TestBasicOptionsAreSet()
|
public void TestBasicOptionsAreSet()
|
||||||
{
|
{
|
||||||
// arrange, act
|
// arrange, act
|
||||||
var options = new TestClientOptions
|
var options = new TestRestOptions
|
||||||
{
|
{
|
||||||
ApiCredentials = new HMACCredential("123", "456"),
|
ApiCredentials = new TestCredentials("123", "456"),
|
||||||
ReceiveWindow = TimeSpan.FromSeconds(10)
|
RequestTimeout = TimeSpan.FromSeconds(10)
|
||||||
};
|
};
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
Assert.That(options.ReceiveWindow == TimeSpan.FromSeconds(10));
|
Assert.That(options.RequestTimeout == TimeSpan.FromSeconds(10));
|
||||||
Assert.That(options.ApiCredentials.Key == "123");
|
Assert.That(options.ApiCredentials.Key == "123");
|
||||||
Assert.That(options.ApiCredentials.Secret == "456");
|
Assert.That(options.ApiCredentials.Secret == "456");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
[Test]
|
||||||
|
public void TestSetOptionsRest()
|
||||||
public class TestClientOptions: RestExchangeOptions<TestEnvironment, HMACCredential>
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Default options for the futures client
|
|
||||||
/// </summary>
|
|
||||||
public static TestClientOptions Default { get; set; } = new TestClientOptions()
|
|
||||||
{
|
{
|
||||||
Environment = new TestEnvironment("test", "https://test.com")
|
var client = new TestRestClient();
|
||||||
};
|
client.SetOptions(new UpdateOptions
|
||||||
|
{
|
||||||
|
RequestTimeout = TimeSpan.FromSeconds(2),
|
||||||
|
Proxy = new ApiProxy("http://testproxy", 1234)
|
||||||
|
});
|
||||||
|
|
||||||
/// <summary>
|
Assert.That(client.ApiClient1.ClientOptions.Proxy, Is.Not.Null);
|
||||||
/// ctor
|
Assert.That(client.ApiClient1.ClientOptions.Proxy!.Host, Is.EqualTo("http://testproxy"));
|
||||||
/// </summary>
|
Assert.That(client.ApiClient1.ClientOptions.Proxy.Port, Is.EqualTo(1234));
|
||||||
public TestClientOptions()
|
Assert.That(client.ApiClient1.ClientOptions.RequestTimeout, Is.EqualTo(TimeSpan.FromSeconds(2)));
|
||||||
{
|
|
||||||
Default?.Set(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
[Test]
|
||||||
/// The default receive window for requests
|
public void TestSetOptionsRestWithCredentials()
|
||||||
/// </summary>
|
|
||||||
public TimeSpan ReceiveWindow { get; set; } = TimeSpan.FromSeconds(5);
|
|
||||||
|
|
||||||
public RestApiOptions Api1Options { get; private set; } = new RestApiOptions();
|
|
||||||
|
|
||||||
public RestApiOptions Api2Options { get; set; } = new RestApiOptions();
|
|
||||||
|
|
||||||
internal TestClientOptions Set(TestClientOptions targetOptions)
|
|
||||||
{
|
{
|
||||||
targetOptions = base.Set<TestClientOptions>(targetOptions);
|
var client = new TestRestClient();
|
||||||
targetOptions.Api1Options = Api1Options.Set(targetOptions.Api1Options);
|
client.SetOptions(new UpdateOptions<TestCredentials>
|
||||||
targetOptions.Api2Options = Api2Options.Set(targetOptions.Api2Options);
|
{
|
||||||
return targetOptions;
|
ApiCredentials = new TestCredentials("123", "456"),
|
||||||
|
RequestTimeout = TimeSpan.FromSeconds(2),
|
||||||
|
Proxy = new ApiProxy("http://testproxy", 1234)
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.That(client.ApiClient1.ApiCredentials, Is.Not.Null);
|
||||||
|
Assert.That(client.ApiClient1.ApiCredentials!.Key, Is.EqualTo("123"));
|
||||||
|
Assert.That(client.ApiClient1.ClientOptions.Proxy, Is.Not.Null);
|
||||||
|
Assert.That(client.ApiClient1.ClientOptions.Proxy!.Host, Is.EqualTo("http://testproxy"));
|
||||||
|
Assert.That(client.ApiClient1.ClientOptions.Proxy.Port, Is.EqualTo(1234));
|
||||||
|
Assert.That(client.ApiClient1.ClientOptions.RequestTimeout, Is.EqualTo(TimeSpan.FromSeconds(2)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestWhenUpdatingSettingsExistingClientsAreNotAffected()
|
||||||
|
{
|
||||||
|
TestRestOptions.Default = new TestRestOptions
|
||||||
|
{
|
||||||
|
ApiCredentials = new TestCredentials("111", "222"),
|
||||||
|
RequestTimeout = TimeSpan.FromSeconds(1),
|
||||||
|
};
|
||||||
|
|
||||||
|
var client1 = new TestRestClient();
|
||||||
|
|
||||||
|
Assert.That(client1.ClientOptions.RequestTimeout, Is.EqualTo(TimeSpan.FromSeconds(1)));
|
||||||
|
Assert.That(client1.ClientOptions.ApiCredentials!.Key, Is.EqualTo("111"));
|
||||||
|
|
||||||
|
TestRestOptions.Default.ApiCredentials = new TestCredentials("333", "444");
|
||||||
|
TestRestOptions.Default.RequestTimeout = TimeSpan.FromSeconds(2);
|
||||||
|
|
||||||
|
var client2 = new TestRestClient();
|
||||||
|
|
||||||
|
Assert.That(client2.ClientOptions.RequestTimeout, Is.EqualTo(TimeSpan.FromSeconds(2)));
|
||||||
|
Assert.That(client2.ClientOptions.ApiCredentials!.Key, Is.EqualTo("333"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//public class TestClientOptions: RestExchangeOptions<TestEnvironment, HMACCredential>
|
||||||
|
//{
|
||||||
|
// /// <summary>
|
||||||
|
// /// Default options for the futures client
|
||||||
|
// /// </summary>
|
||||||
|
// public static TestClientOptions Default { get; set; } = new TestClientOptions()
|
||||||
|
// {
|
||||||
|
// Environment = new TestEnvironment("test", "https://test.com")
|
||||||
|
// };
|
||||||
|
|
||||||
|
// /// <summary>
|
||||||
|
// /// ctor
|
||||||
|
// /// </summary>
|
||||||
|
// public TestClientOptions()
|
||||||
|
// {
|
||||||
|
// Default?.Set(this);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /// <summary>
|
||||||
|
// /// The default receive window for requests
|
||||||
|
// /// </summary>
|
||||||
|
// public TimeSpan ReceiveWindow { get; set; } = TimeSpan.FromSeconds(5);
|
||||||
|
|
||||||
|
// public RestApiOptions Api1Options { get; private set; } = new RestApiOptions();
|
||||||
|
|
||||||
|
// public RestApiOptions Api2Options { get; set; } = new RestApiOptions();
|
||||||
|
|
||||||
|
// internal TestClientOptions Set(TestClientOptions targetOptions)
|
||||||
|
// {
|
||||||
|
// targetOptions = base.Set<TestClientOptions>(targetOptions);
|
||||||
|
// targetOptions.Api1Options = Api1Options.Set(targetOptions.Api1Options);
|
||||||
|
// targetOptions.Api2Options = Api2Options.Set(targetOptions.Api2Options);
|
||||||
|
// return targetOptions;
|
||||||
|
// }
|
||||||
|
//}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,260 @@
|
|||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.UnitTests.ConverterTests;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.UnitTests
|
||||||
|
{
|
||||||
|
internal class ParameterCollectionTests
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void AddingBasicValue_SetValueCorrectly()
|
||||||
|
{
|
||||||
|
var parameters = new Parameters(new ParameterSerializationSettings());
|
||||||
|
parameters.Add("test", "value");
|
||||||
|
Assert.That(parameters["test"], Is.EqualTo("value"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void AddingOptionalBasicNullValue_DoesntSetValue()
|
||||||
|
{
|
||||||
|
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 Parameters(new ParameterSerializationSettings());
|
||||||
|
parameters.Add("test", 0.1m, DecimalSerialization.String);
|
||||||
|
Assert.That(parameters["test"], Is.EqualTo("0.1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void AddingDecimalValueAsString2_SetValueCorrectly()
|
||||||
|
{
|
||||||
|
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 AddingOptionalIntNullValueAsString_DoesntSetValue()
|
||||||
|
{
|
||||||
|
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 Parameters(new ParameterSerializationSettings());
|
||||||
|
parameters.Add("test", 1L, IntegerSerialization.String);
|
||||||
|
Assert.That(parameters["test"], Is.EqualTo("1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void AddingOptionalLongValueAsString_SetValueCorrectly()
|
||||||
|
{
|
||||||
|
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 Parameters(new ParameterSerializationSettings());
|
||||||
|
parameters.Add("test", (long?)null);
|
||||||
|
Assert.That(parameters.ContainsKey("test"), Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void AddingMillisecondTimestamp_SetValueCorrectly()
|
||||||
|
{
|
||||||
|
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 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 Parameters(new ParameterSerializationSettings());
|
||||||
|
parameters.Add("test", (DateTime?)null);
|
||||||
|
Assert.That(parameters.ContainsKey("test"), Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void AddingMillisecondTimestampString_SetValueCorrectly()
|
||||||
|
{
|
||||||
|
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 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 AddingSecondTimestamp_SetValueCorrectly()
|
||||||
|
{
|
||||||
|
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 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 AddingSecondTimestampString_SetValueCorrectly()
|
||||||
|
{
|
||||||
|
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 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 AddingEnum_SetValueCorrectly()
|
||||||
|
{
|
||||||
|
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 Parameters(new ParameterSerializationSettings());
|
||||||
|
parameters.Add("test", (TestEnum?)TestEnum.Two);
|
||||||
|
Assert.That(parameters["test"], Is.EqualTo("2"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void AddingOptionalEnumNullValue_DoesntSetValue()
|
||||||
|
{
|
||||||
|
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 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 Parameters(new ParameterSerializationSettings()
|
||||||
|
{
|
||||||
|
Enum = EnumSerialization.Number
|
||||||
|
});
|
||||||
|
parameters.Add("test", TestEnum.Two);
|
||||||
|
Assert.That(parameters["test"], Is.EqualTo(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void AddingCommaSeparated_SetValueCorrectly()
|
||||||
|
{
|
||||||
|
var parameters = new Parameters(new ParameterSerializationSettings());
|
||||||
|
parameters.AddCommaSeparated("test", ["1", "2"]);
|
||||||
|
Assert.That(parameters["test"], Is.EqualTo("1,2"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void AddingOptionalCommaSeparatedNullValue_DoesntSetValue()
|
||||||
|
{
|
||||||
|
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 Parameters(new ParameterSerializationSettings());
|
||||||
|
parameters.AddCommaSeparated("test", [TestEnum.Two, TestEnum.One]);
|
||||||
|
Assert.That(parameters["test"], Is.EqualTo("2,1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void AddingBoolString_SetValueCorrectly()
|
||||||
|
{
|
||||||
|
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 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 Parameters(new ParameterSerializationSettings());
|
||||||
|
parameters.Add("test", null);
|
||||||
|
Assert.That(parameters.ContainsKey("test"), Is.False);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+203
-210
@@ -1,178 +1,24 @@
|
|||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
using CryptoExchange.Net.UnitTests.TestImplementations;
|
using CryptoExchange.Net.RateLimiting;
|
||||||
|
using CryptoExchange.Net.RateLimiting.Filters;
|
||||||
|
using CryptoExchange.Net.RateLimiting.Guards;
|
||||||
|
using CryptoExchange.Net.RateLimiting.Interfaces;
|
||||||
|
using CryptoExchange.Net.RateLimiting.Trackers;
|
||||||
|
using CryptoExchange.Net.UnitTests.Implementations;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Threading.Tasks;
|
using System.Text;
|
||||||
using System.Threading;
|
|
||||||
using NUnit.Framework.Legacy;
|
|
||||||
using CryptoExchange.Net.RateLimiting;
|
|
||||||
using CryptoExchange.Net.RateLimiting.Guards;
|
|
||||||
using CryptoExchange.Net.RateLimiting.Filters;
|
|
||||||
using CryptoExchange.Net.RateLimiting.Interfaces;
|
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests
|
namespace CryptoExchange.Net.UnitTests
|
||||||
{
|
{
|
||||||
[TestFixture()]
|
[TestFixture()]
|
||||||
public class RestClientTests
|
public class RateLimitTests
|
||||||
{
|
{
|
||||||
[TestCase]
|
|
||||||
public void RequestingData_Should_ResultInData()
|
|
||||||
{
|
|
||||||
// arrange
|
|
||||||
var client = new TestRestClient();
|
|
||||||
var expected = new TestObject() { DecimalData = 1.23M, IntData = 10, StringData = "Some data" };
|
|
||||||
client.SetResponse(JsonSerializer.Serialize(expected, new JsonSerializerOptions { TypeInfoResolver = new TestSerializerContext() }), out _);
|
|
||||||
|
|
||||||
// act
|
|
||||||
var result = client.Api1.Request<TestObject>().Result;
|
|
||||||
|
|
||||||
// assert
|
|
||||||
Assert.That(result.Success);
|
|
||||||
Assert.That(TestHelpers.AreEqual(expected, result.Data));
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase]
|
|
||||||
public void ReceivingInvalidData_Should_ResultInError()
|
|
||||||
{
|
|
||||||
// arrange
|
|
||||||
var client = new TestRestClient();
|
|
||||||
client.SetResponse("{\"property\": 123", out _);
|
|
||||||
|
|
||||||
// act
|
|
||||||
var result = client.Api1.Request<TestObject>().Result;
|
|
||||||
|
|
||||||
// assert
|
|
||||||
ClassicAssert.IsFalse(result.Success);
|
|
||||||
Assert.That(result.Error != null);
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase]
|
|
||||||
public async Task ReceivingErrorCode_Should_ResultInError()
|
|
||||||
{
|
|
||||||
// arrange
|
|
||||||
var client = new TestRestClient();
|
|
||||||
client.SetErrorWithoutResponse(System.Net.HttpStatusCode.BadRequest, "Invalid request");
|
|
||||||
|
|
||||||
// act
|
|
||||||
var result = await client.Api1.Request<TestObject>();
|
|
||||||
|
|
||||||
// assert
|
|
||||||
ClassicAssert.IsFalse(result.Success);
|
|
||||||
Assert.That(result.Error != null);
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase]
|
|
||||||
public async Task ReceivingErrorAndNotParsingError_Should_ResultInFlatError()
|
|
||||||
{
|
|
||||||
// arrange
|
|
||||||
var client = new TestRestClient();
|
|
||||||
client.SetErrorWithResponse("{\"errorMessage\": \"Invalid request\", \"errorCode\": 123}", System.Net.HttpStatusCode.BadRequest);
|
|
||||||
|
|
||||||
// act
|
|
||||||
var result = await client.Api1.Request<TestObject>();
|
|
||||||
|
|
||||||
// assert
|
|
||||||
ClassicAssert.IsFalse(result.Success);
|
|
||||||
Assert.That(result.Error != null);
|
|
||||||
Assert.That(result.Error is ServerError);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
[TestCase]
|
|
||||||
public async Task ReceivingErrorAndNotParsingErrorAndInvalidJson_Should_ContainData()
|
|
||||||
{
|
|
||||||
// arrange
|
|
||||||
var client = new TestRestClient();
|
|
||||||
var response = "<html>...</html>";
|
|
||||||
client.SetErrorWithResponse(response, System.Net.HttpStatusCode.BadRequest);
|
|
||||||
|
|
||||||
// act
|
|
||||||
var result = await client.Api1.Request<TestObject>();
|
|
||||||
|
|
||||||
// assert
|
|
||||||
ClassicAssert.IsFalse(result.Success);
|
|
||||||
Assert.That(result.Error != null);
|
|
||||||
Assert.That(result.Error is DeserializeError);
|
|
||||||
Assert.That(result.Error.Message.Contains(response));
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase]
|
|
||||||
public async Task ReceivingErrorAndParsingError_Should_ResultInParsedError()
|
|
||||||
{
|
|
||||||
// arrange
|
|
||||||
var client = new ParseErrorTestRestClient();
|
|
||||||
client.SetErrorWithResponse("{\"errorMessage\": \"Invalid request\", \"errorCode\": 123}", System.Net.HttpStatusCode.BadRequest);
|
|
||||||
|
|
||||||
// act
|
|
||||||
var result = await client.Api2.Request<TestObject>();
|
|
||||||
|
|
||||||
// assert
|
|
||||||
ClassicAssert.IsFalse(result.Success);
|
|
||||||
Assert.That(result.Error != null);
|
|
||||||
Assert.That(result.Error is ServerError);
|
|
||||||
Assert.That(result.Error.ErrorCode == "123");
|
|
||||||
Assert.That(result.Error.Message == "Invalid request");
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase]
|
|
||||||
public void SettingOptions_Should_ResultInOptionsSet()
|
|
||||||
{
|
|
||||||
// arrange
|
|
||||||
// act
|
|
||||||
var options = new TestClientOptions();
|
|
||||||
options.Api1Options.TimestampRecalculationInterval = TimeSpan.FromMinutes(10);
|
|
||||||
options.Api1Options.OutputOriginalData = true;
|
|
||||||
options.RequestTimeout = TimeSpan.FromMinutes(1);
|
|
||||||
var client = new TestBaseClient(options);
|
|
||||||
|
|
||||||
// assert
|
|
||||||
Assert.That(((TestClientOptions)client.ClientOptions).Api1Options.TimestampRecalculationInterval == TimeSpan.FromMinutes(10));
|
|
||||||
Assert.That(((TestClientOptions)client.ClientOptions).Api1Options.OutputOriginalData == true);
|
|
||||||
Assert.That(((TestClientOptions)client.ClientOptions).RequestTimeout == TimeSpan.FromMinutes(1));
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase("GET", HttpMethodParameterPosition.InUri)] // No need to test InBody for GET since thats not valid
|
|
||||||
[TestCase("POST", HttpMethodParameterPosition.InBody)]
|
|
||||||
[TestCase("POST", HttpMethodParameterPosition.InUri)]
|
|
||||||
[TestCase("DELETE", HttpMethodParameterPosition.InBody)]
|
|
||||||
[TestCase("DELETE", HttpMethodParameterPosition.InUri)]
|
|
||||||
[TestCase("PUT", HttpMethodParameterPosition.InUri)]
|
|
||||||
[TestCase("PUT", HttpMethodParameterPosition.InBody)]
|
|
||||||
public async Task Setting_Should_ResultInOptionsSet(string method, HttpMethodParameterPosition pos)
|
|
||||||
{
|
|
||||||
// arrange
|
|
||||||
// act
|
|
||||||
var client = new TestRestClient();
|
|
||||||
|
|
||||||
client.Api1.SetParameterPosition(new HttpMethod(method), pos);
|
|
||||||
|
|
||||||
client.SetResponse("{}", out var request);
|
|
||||||
|
|
||||||
await client.Api1.RequestWithParams<TestObject>(new HttpMethod(method), new ParameterCollection
|
|
||||||
{
|
|
||||||
{ "TestParam1", "Value1" },
|
|
||||||
{ "TestParam2", 2 },
|
|
||||||
},
|
|
||||||
new Dictionary<string, string>
|
|
||||||
{
|
|
||||||
{ "TestHeader", "123" }
|
|
||||||
});
|
|
||||||
|
|
||||||
// assert
|
|
||||||
Assert.That(request.Method == new HttpMethod(method));
|
|
||||||
Assert.That((request.Content?.Contains("TestParam1") == true) == (pos == HttpMethodParameterPosition.InBody));
|
|
||||||
Assert.That((request.Uri.ToString().Contains("TestParam1")) == (pos == HttpMethodParameterPosition.InUri));
|
|
||||||
Assert.That((request.Content?.Contains("TestParam2") == true) == (pos == HttpMethodParameterPosition.InBody));
|
|
||||||
Assert.That((request.Uri.ToString().Contains("TestParam2")) == (pos == HttpMethodParameterPosition.InUri));
|
|
||||||
Assert.That(request.GetHeaders().First().Key == "TestHeader");
|
|
||||||
Assert.That(request.GetHeaders().First().Value.Contains("123"));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
[TestCase(1, 0.1)]
|
[TestCase(1, 0.1)]
|
||||||
[TestCase(2, 0.1)]
|
[TestCase(2, 0.1)]
|
||||||
[TestCase(5, 1)]
|
[TestCase(5, 1)]
|
||||||
@@ -184,16 +30,16 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
|
|
||||||
var triggered = false;
|
var triggered = false;
|
||||||
rateLimiter.RateLimitTriggered += (x) => { triggered = true; };
|
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++)
|
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, 1, default);
|
||||||
Assert.That(i == requests? triggered : !triggered);
|
Assert.That(i == requests ? triggered : !triggered);
|
||||||
}
|
}
|
||||||
triggered = false;
|
triggered = false;
|
||||||
await Task.Delay((int)Math.Round(perSeconds * 1000) + 10);
|
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, 1, default);
|
||||||
Assert.That(!triggered);
|
Assert.That(!triggered);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,14 +53,14 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
var rateLimiter = new RateLimitGate("Test");
|
var rateLimiter = new RateLimitGate("Test");
|
||||||
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new PathStartFilter("/sapi/"), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
|
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;
|
RateLimitEvent? evnt = null;
|
||||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||||
for (var i = 0; i < 2; i++)
|
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, 1, default);
|
||||||
bool expected = i == 1 ? (expectLimiting ? evnt.DelayTime > TimeSpan.Zero : evnt == null) : evnt == null;
|
bool expected = i == 1 ? expectLimiting ? evnt?.DelayTime > TimeSpan.Zero : evnt == null : evnt == null;
|
||||||
Assert.That(expected);
|
Assert.That(expected);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -228,15 +74,15 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
var rateLimiter = new RateLimitGate("Test");
|
var rateLimiter = new RateLimitGate("Test");
|
||||||
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerEndpoint, new PathStartFilter("/sapi/"), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
|
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerEndpoint, new PathStartFilter("/sapi/"), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
|
||||||
|
|
||||||
var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get);
|
var requestDefinition1 = new RequestDefinition("https://test.com", endpoint1, HttpMethod.Get);
|
||||||
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get);
|
var requestDefinition2 = new RequestDefinition("https://test.com", endpoint2, HttpMethod.Get);
|
||||||
|
|
||||||
RateLimitEvent evnt = null;
|
RateLimitEvent? evnt = null;
|
||||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
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, 1, default);
|
||||||
Assert.That(evnt == null);
|
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, 1, default);
|
||||||
Assert.That(expectLimiting ? evnt != null : evnt == null);
|
Assert.That(expectLimiting ? evnt != null : evnt == null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,16 +97,16 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
|
|
||||||
bool triggered = false;
|
bool triggered = false;
|
||||||
rateLimiter.RateLimitTriggered += (x) => { triggered = true; };
|
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++)
|
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, 1, default);
|
||||||
Assert.That(i == requests ? triggered : !triggered);
|
Assert.That(i == requests ? triggered : !triggered);
|
||||||
}
|
}
|
||||||
triggered = false;
|
triggered = false;
|
||||||
await Task.Delay((int)Math.Round(perSeconds * 1000) + 10);
|
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, 1, default);
|
||||||
Assert.That(!triggered);
|
Assert.That(!triggered);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,15 +117,15 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
{
|
{
|
||||||
var rateLimiter = new RateLimitGate("Test");
|
var rateLimiter = new RateLimitGate("Test");
|
||||||
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerEndpoint, new ExactPathFilter("/sapi/test"), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
|
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerEndpoint, new ExactPathFilter("/sapi/test"), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
|
||||||
|
|
||||||
var requestDefinition = new RequestDefinition(endpoint, HttpMethod.Get);
|
|
||||||
|
|
||||||
RateLimitEvent evnt = null;
|
var requestDefinition = new RequestDefinition("https://test.com", endpoint, HttpMethod.Get);
|
||||||
|
|
||||||
|
RateLimitEvent? evnt = null;
|
||||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||||
for (var i = 0; i < 2; i++)
|
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, 1, default);
|
||||||
bool expected = i == 1 ? (expectLimited ? evnt.DelayTime > TimeSpan.Zero : evnt == null) : evnt == null;
|
bool expected = i == 1 ? expectLimited ? evnt?.DelayTime > TimeSpan.Zero : evnt == null : evnt == null;
|
||||||
Assert.That(expected);
|
Assert.That(expected);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -292,14 +138,14 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
{
|
{
|
||||||
var rateLimiter = new RateLimitGate("Test");
|
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));
|
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;
|
RateLimitEvent? evnt = null;
|
||||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||||
for (var i = 0; i < 2; i++)
|
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, 1, default);
|
||||||
bool expected = i == 1 ? (expectLimited ? evnt.DelayTime > TimeSpan.Zero : evnt == null) : evnt == null;
|
bool expected = i == 1 ? expectLimited ? evnt?.DelayTime > TimeSpan.Zero : evnt == null : evnt == null;
|
||||||
Assert.That(expected);
|
Assert.That(expected);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -315,15 +161,15 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
{
|
{
|
||||||
var rateLimiter = new RateLimitGate("Test");
|
var rateLimiter = new RateLimitGate("Test");
|
||||||
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerApiKey, new AuthenticatedEndpointFilter(true), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Sliding));
|
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 requestDefinition1 = new RequestDefinition("https://test.com", endpoint1, HttpMethod.Get) { Authenticated = key1 != null };
|
||||||
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get) { Authenticated = key2 != null };
|
var requestDefinition2 = new RequestDefinition("https://test.com", endpoint2, HttpMethod.Get) { Authenticated = key2 != null };
|
||||||
|
|
||||||
RateLimitEvent evnt = null;
|
RateLimitEvent? evnt = null;
|
||||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
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, 1, default);
|
||||||
Assert.That(evnt == null);
|
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, 1, default);
|
||||||
Assert.That(expectLimited ? evnt != null : evnt == null);
|
Assert.That(expectLimited ? evnt != null : evnt == null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -334,15 +180,15 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
{
|
{
|
||||||
var rateLimiter = new RateLimitGate("Test");
|
var rateLimiter = new RateLimitGate("Test");
|
||||||
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, Array.Empty<IGuardFilter>(), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
|
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, Array.Empty<IGuardFilter>(), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
|
||||||
var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get);
|
var requestDefinition1 = new RequestDefinition("https://test.com", endpoint1, HttpMethod.Get);
|
||||||
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get) { Authenticated = true };
|
var requestDefinition2 = new RequestDefinition("https://test.com", endpoint2, HttpMethod.Get) { Authenticated = true };
|
||||||
|
|
||||||
RateLimitEvent evnt = null;
|
RateLimitEvent? evnt = null;
|
||||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
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, 1, default);
|
||||||
Assert.That(evnt == null);
|
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, 1, default);
|
||||||
Assert.That(expectLimited ? evnt != null : evnt == null);
|
Assert.That(expectLimited ? evnt != null : evnt == null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -354,15 +200,15 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
{
|
{
|
||||||
var rateLimiter = new RateLimitGate("Test");
|
var rateLimiter = new RateLimitGate("Test");
|
||||||
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new HostFilter("https://test.com"), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
|
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 requestDefinition1 = new RequestDefinition(host1, endpoint1, HttpMethod.Get);
|
||||||
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get) { Authenticated = true };
|
var requestDefinition2 = new RequestDefinition(host2, endpoint2, HttpMethod.Get) { Authenticated = true };
|
||||||
|
|
||||||
RateLimitEvent evnt = null;
|
RateLimitEvent? evnt = null;
|
||||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
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, 1, default);
|
||||||
Assert.That(evnt == null);
|
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, 1, default);
|
||||||
Assert.That(expectLimited ? evnt != null : evnt == null);
|
Assert.That(expectLimited ? evnt != null : evnt == null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -374,12 +220,12 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
var rateLimiter = new RateLimitGate("Test");
|
var rateLimiter = new RateLimitGate("Test");
|
||||||
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new LimitItemTypeFilter(RateLimitItemType.Connection), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
|
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new LimitItemTypeFilter(RateLimitItemType.Connection), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
|
||||||
|
|
||||||
RateLimitEvent evnt = null;
|
RateLimitEvent? evnt = null;
|
||||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
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, 1, default);
|
||||||
Assert.That(evnt == null);
|
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, 1, default);
|
||||||
Assert.That(expectLimited ? evnt != null : evnt == null);
|
Assert.That(expectLimited ? evnt != null : evnt == null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -389,13 +235,160 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
var rateLimiter = new RateLimitGate("Test");
|
var rateLimiter = new RateLimitGate("Test");
|
||||||
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new LimitItemTypeFilter(RateLimitItemType.Connection), 1, TimeSpan.FromSeconds(10), RateLimitWindowType.Fixed));
|
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new LimitItemTypeFilter(RateLimitItemType.Connection), 1, TimeSpan.FromSeconds(10), RateLimitWindowType.Fixed));
|
||||||
|
|
||||||
RateLimitEvent evnt = null;
|
RateLimitEvent? evnt = null;
|
||||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||||
var ct = new CancellationTokenSource(TimeSpan.FromSeconds(0.2));
|
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 result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("https://test.com", "1", HttpMethod.Get), "123", 1, RateLimitingBehaviour.Wait, null, 1, 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 result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("https://test.com", "1", HttpMethod.Get), "123", 1, RateLimitingBehaviour.Wait, null, 1, ct.Token);
|
||||||
Assert.That(result2.Error, Is.TypeOf<CancellationRequestedError>());
|
Assert.That(result2.Error, Is.TypeOf<CancellationRequestedError>());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task RateLimiterReset_Should_AllowNextRequestForSameDefinition()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
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("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, null, 1, RateLimitingBehaviour.Fail, null, 1, 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, 1, ct.Token);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(evnt, Is.Null);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task RateLimiterReset_Should_NotAllowNextRequestForDifferentDefinition()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
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("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, null, 1, RateLimitingBehaviour.Fail, null, 1, default);
|
||||||
|
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition2, null, 1, RateLimitingBehaviour.Fail, null, 1, 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, 1, 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));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(RateLimitWindowType.Fixed)]
|
||||||
|
[TestCase(RateLimitWindowType.FixedAfterFirst)]
|
||||||
|
[TestCase(RateLimitWindowType.Sliding)]
|
||||||
|
[TestCase(RateLimitWindowType.Decay)]
|
||||||
|
public async Task LowerThreshold_ReservesCapacityForFullThreshold(RateLimitWindowType windowType)
|
||||||
|
{
|
||||||
|
var gate = new RateLimitGate("Test");
|
||||||
|
gate.AddGuard(new RateLimitGuard(
|
||||||
|
RateLimitGuard.PerHost,
|
||||||
|
new LimitItemTypeFilter(RateLimitItemType.Request),
|
||||||
|
10,
|
||||||
|
TimeSpan.FromHours(1),
|
||||||
|
windowType,
|
||||||
|
decayPerTimeSpan: 1));
|
||||||
|
|
||||||
|
var definition = new RequestDefinition(
|
||||||
|
"https://test.com", "/ticker", HttpMethod.Get);
|
||||||
|
var logger = new TraceLogger();
|
||||||
|
|
||||||
|
for (var i = 0; i < 8; i++)
|
||||||
|
{
|
||||||
|
var result = await gate.ProcessAsync(
|
||||||
|
logger, i, RateLimitItemType.Request, definition, null, 1,
|
||||||
|
RateLimitingBehaviour.Fail, null, 0.8, default);
|
||||||
|
Assert.That(result.Success, Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
var marketData = await gate.ProcessAsync(
|
||||||
|
logger, 9, RateLimitItemType.Request, definition, null, 1,
|
||||||
|
RateLimitingBehaviour.Fail, null, 0.8, default);
|
||||||
|
Assert.That(marketData.Error, Is.TypeOf<ClientRateLimitError>());
|
||||||
|
|
||||||
|
var order = await gate.ProcessAsync(
|
||||||
|
logger, 10, RateLimitItemType.Request, definition, null, 2,
|
||||||
|
RateLimitingBehaviour.Fail, null, 1.0, default);
|
||||||
|
Assert.That(order.Success, Is.True);
|
||||||
|
|
||||||
|
var overHardLimit = await gate.ProcessAsync(
|
||||||
|
logger, 11, RateLimitItemType.Request, definition, null, 1,
|
||||||
|
RateLimitingBehaviour.Fail, null, 1.0, default);
|
||||||
|
Assert.That(overHardLimit.Error, Is.TypeOf<ClientRateLimitError>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(0.1, 10)]
|
||||||
|
[TestCase(1, 50)]
|
||||||
|
[TestCase(5, 250)]
|
||||||
|
[TestCase(60, 250)]
|
||||||
|
public void RateLimitSafetyMargin_DefaultIsProportionalAndCapped(double periodSeconds, int expectedMarginMilliseconds)
|
||||||
|
{
|
||||||
|
var margin = WindowTrackerHelpers.GetDefaultSafetyMargin(TimeSpan.FromSeconds(periodSeconds));
|
||||||
|
|
||||||
|
Assert.That(margin, Is.EqualTo(TimeSpan.FromMilliseconds(expectedMarginMilliseconds)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void RateLimitGuard_ExplicitSafetyMarginIsUsed()
|
||||||
|
{
|
||||||
|
var guard = new RateLimitGuard(
|
||||||
|
RateLimitGuard.PerHost,
|
||||||
|
new LimitItemTypeFilter(RateLimitItemType.Request),
|
||||||
|
1,
|
||||||
|
TimeSpan.FromSeconds(1),
|
||||||
|
RateLimitWindowType.Sliding,
|
||||||
|
safetyMargin: TimeSpan.Zero);
|
||||||
|
|
||||||
|
Assert.That(guard.SafetyMargin, Is.EqualTo(TimeSpan.Zero));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,281 @@
|
|||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.UnitTests
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class RequestCoalescerTests
|
||||||
|
{
|
||||||
|
private static readonly TimeSpan _testTimeout = TimeSpan.FromSeconds(5);
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task ExecuteAsync_WithIdenticalRequests_ShouldExecuteRequestOnce()
|
||||||
|
{
|
||||||
|
var coalescer = CreateCoalescer();
|
||||||
|
var key = CreateKey();
|
||||||
|
var requestCompletion = CreateCompletionSource<string>();
|
||||||
|
var requestStarted = CreateCompletionSource<bool>();
|
||||||
|
var requestCount = 0;
|
||||||
|
|
||||||
|
async Task<string> ExecuteRequest(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
Interlocked.Increment(ref requestCount);
|
||||||
|
requestStarted.TrySetResult(true);
|
||||||
|
return await requestCompletion.Task;
|
||||||
|
}
|
||||||
|
|
||||||
|
var first = coalescer.ExecuteAsync(key, ExecuteRequest, () => "canceled", CancellationToken.None);
|
||||||
|
await requestStarted.Task.WaitAsync(_testTimeout);
|
||||||
|
var second = coalescer.ExecuteAsync(key, ExecuteRequest, () => "canceled", CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.That(requestCount, Is.EqualTo(1));
|
||||||
|
|
||||||
|
requestCompletion.SetResult("result");
|
||||||
|
|
||||||
|
Assert.That(await first.WaitAsync(_testTimeout), Is.EqualTo("result"));
|
||||||
|
Assert.That(await second.WaitAsync(_testTimeout), Is.EqualTo("result"));
|
||||||
|
Assert.That(requestCount, Is.EqualTo(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task ExecuteAsync_WithDifferentKeys_ShouldExecuteEachRequest()
|
||||||
|
{
|
||||||
|
var coalescer = CreateCoalescer();
|
||||||
|
var firstCompletion = CreateCompletionSource<string>();
|
||||||
|
var secondCompletion = CreateCompletionSource<string>();
|
||||||
|
var firstStarted = CreateCompletionSource<bool>();
|
||||||
|
var secondStarted = CreateCompletionSource<bool>();
|
||||||
|
var requestCount = 0;
|
||||||
|
|
||||||
|
var first = coalescer.ExecuteAsync(
|
||||||
|
CreateKey(parameters: "symbol=ETH"),
|
||||||
|
async cancellationToken =>
|
||||||
|
{
|
||||||
|
Interlocked.Increment(ref requestCount);
|
||||||
|
firstStarted.TrySetResult(true);
|
||||||
|
return await firstCompletion.Task;
|
||||||
|
},
|
||||||
|
() => "canceled",
|
||||||
|
CancellationToken.None);
|
||||||
|
|
||||||
|
var second = coalescer.ExecuteAsync(
|
||||||
|
CreateKey(parameters: "symbol=BTC"),
|
||||||
|
async cancellationToken =>
|
||||||
|
{
|
||||||
|
Interlocked.Increment(ref requestCount);
|
||||||
|
secondStarted.TrySetResult(true);
|
||||||
|
return await secondCompletion.Task;
|
||||||
|
},
|
||||||
|
() => "canceled",
|
||||||
|
CancellationToken.None);
|
||||||
|
|
||||||
|
await Task.WhenAll(firstStarted.Task, secondStarted.Task).WaitAsync(_testTimeout);
|
||||||
|
Assert.That(requestCount, Is.EqualTo(2));
|
||||||
|
|
||||||
|
firstCompletion.SetResult("first");
|
||||||
|
secondCompletion.SetResult("second");
|
||||||
|
|
||||||
|
Assert.That(await first.WaitAsync(_testTimeout), Is.EqualTo("first"));
|
||||||
|
Assert.That(await second.WaitAsync(_testTimeout), Is.EqualTo("second"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task ExecuteAsync_WhenOneListenerCancels_ShouldKeepSharedRequestRunning()
|
||||||
|
{
|
||||||
|
var coalescer = CreateCoalescer();
|
||||||
|
var requestCompletion = CreateCompletionSource<string>();
|
||||||
|
var requestStarted = CreateCompletionSource<CancellationToken>();
|
||||||
|
var requestCount = 0;
|
||||||
|
using var firstCancellationSource = new CancellationTokenSource();
|
||||||
|
|
||||||
|
async Task<string> ExecuteRequest(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
Interlocked.Increment(ref requestCount);
|
||||||
|
requestStarted.TrySetResult(cancellationToken);
|
||||||
|
return await requestCompletion.Task;
|
||||||
|
}
|
||||||
|
|
||||||
|
var first = coalescer.ExecuteAsync(CreateKey(), ExecuteRequest, () => "canceled", firstCancellationSource.Token);
|
||||||
|
var requestCancellationToken = await requestStarted.Task.WaitAsync(_testTimeout);
|
||||||
|
var second = coalescer.ExecuteAsync(CreateKey(), ExecuteRequest, () => "canceled", CancellationToken.None);
|
||||||
|
|
||||||
|
firstCancellationSource.Cancel();
|
||||||
|
|
||||||
|
Assert.That(await first.WaitAsync(_testTimeout), Is.EqualTo("canceled"));
|
||||||
|
Assert.That(requestCancellationToken.IsCancellationRequested, Is.False);
|
||||||
|
Assert.That(requestCount, Is.EqualTo(1));
|
||||||
|
|
||||||
|
requestCompletion.SetResult("result");
|
||||||
|
|
||||||
|
Assert.That(await second.WaitAsync(_testTimeout), Is.EqualTo("result"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task ExecuteAsync_WhenAllListenersCancel_ShouldCancelSharedRequest()
|
||||||
|
{
|
||||||
|
var coalescer = CreateCoalescer();
|
||||||
|
var requestStarted = CreateCompletionSource<bool>();
|
||||||
|
var requestCanceled = CreateCompletionSource<bool>();
|
||||||
|
using var firstCancellationSource = new CancellationTokenSource();
|
||||||
|
using var secondCancellationSource = new CancellationTokenSource();
|
||||||
|
|
||||||
|
async Task<string> ExecuteRequest(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
using (cancellationToken.Register(() => requestCanceled.TrySetResult(true)))
|
||||||
|
{
|
||||||
|
requestStarted.TrySetResult(true);
|
||||||
|
await requestCanceled.Task;
|
||||||
|
return "request canceled";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var first = coalescer.ExecuteAsync(CreateKey(), ExecuteRequest, () => "listener canceled", firstCancellationSource.Token);
|
||||||
|
await requestStarted.Task.WaitAsync(_testTimeout);
|
||||||
|
var second = coalescer.ExecuteAsync(CreateKey(), ExecuteRequest, () => "listener canceled", secondCancellationSource.Token);
|
||||||
|
|
||||||
|
firstCancellationSource.Cancel();
|
||||||
|
Assert.That(await first.WaitAsync(_testTimeout), Is.EqualTo("listener canceled"));
|
||||||
|
Assert.That(requestCanceled.Task.IsCompleted, Is.False);
|
||||||
|
|
||||||
|
secondCancellationSource.Cancel();
|
||||||
|
|
||||||
|
Assert.That(await second.WaitAsync(_testTimeout), Is.EqualTo("listener canceled"));
|
||||||
|
Assert.That(await requestCanceled.Task.WaitAsync(_testTimeout), Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task ExecuteAsync_WithAlreadyCanceledListener_ShouldNotStartRequest()
|
||||||
|
{
|
||||||
|
var coalescer = CreateCoalescer();
|
||||||
|
var requestCount = 0;
|
||||||
|
using var cancellationSource = new CancellationTokenSource();
|
||||||
|
cancellationSource.Cancel();
|
||||||
|
|
||||||
|
var result = await coalescer.ExecuteAsync(
|
||||||
|
CreateKey(),
|
||||||
|
cancellationToken =>
|
||||||
|
{
|
||||||
|
Interlocked.Increment(ref requestCount);
|
||||||
|
return Task.FromResult("result");
|
||||||
|
},
|
||||||
|
() => "canceled",
|
||||||
|
cancellationSource.Token);
|
||||||
|
|
||||||
|
Assert.That(result, Is.EqualTo("canceled"));
|
||||||
|
Assert.That(requestCount, Is.Zero);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task ExecuteAsync_AfterRequestCompletes_ShouldStartNewRequest()
|
||||||
|
{
|
||||||
|
var coalescer = CreateCoalescer();
|
||||||
|
var requestCount = 0;
|
||||||
|
|
||||||
|
Task<int> ExecuteRequest(CancellationToken cancellationToken)
|
||||||
|
=> Task.FromResult(Interlocked.Increment(ref requestCount));
|
||||||
|
|
||||||
|
var first = await coalescer.ExecuteAsync(CreateKey(typeof(int)), ExecuteRequest, () => -1, CancellationToken.None);
|
||||||
|
var second = await coalescer.ExecuteAsync(CreateKey(typeof(int)), ExecuteRequest, () => -1, CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.That(first, Is.EqualTo(1));
|
||||||
|
Assert.That(second, Is.EqualTo(2));
|
||||||
|
Assert.That(requestCount, Is.EqualTo(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ExecuteAsync_AfterRequestFails_ShouldStartNewRequest()
|
||||||
|
{
|
||||||
|
var coalescer = CreateCoalescer();
|
||||||
|
var requestCount = 0;
|
||||||
|
|
||||||
|
Task<string> ExecuteRequest(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (Interlocked.Increment(ref requestCount) == 1)
|
||||||
|
return Task.FromException<string>(new InvalidOperationException("Request failed"));
|
||||||
|
|
||||||
|
return Task.FromResult("result");
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.ThrowsAsync<InvalidOperationException>(async () =>
|
||||||
|
await coalescer.ExecuteAsync(CreateKey(), ExecuteRequest, () => "canceled", CancellationToken.None));
|
||||||
|
|
||||||
|
Assert.That(
|
||||||
|
coalescer.ExecuteAsync(CreateKey(), ExecuteRequest, () => "canceled", CancellationToken.None).GetAwaiter().GetResult(),
|
||||||
|
Is.EqualTo("result"));
|
||||||
|
Assert.That(requestCount, Is.EqualTo(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task ExecuteAsync_WhenAbandonedRequestCompletes_ShouldNotRemoveReplacementRequest()
|
||||||
|
{
|
||||||
|
var coalescer = CreateCoalescer();
|
||||||
|
var key = CreateKey();
|
||||||
|
var oldRequestCompletion = CreateCompletionSource<string>();
|
||||||
|
var oldRequestStarted = CreateCompletionSource<bool>();
|
||||||
|
var replacementCompletion = CreateCompletionSource<string>();
|
||||||
|
var replacementStarted = CreateCompletionSource<bool>();
|
||||||
|
var requestCount = 0;
|
||||||
|
using var cancellationSource = new CancellationTokenSource();
|
||||||
|
|
||||||
|
async Task<string> ExecuteOldRequest(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
Interlocked.Increment(ref requestCount);
|
||||||
|
oldRequestStarted.TrySetResult(true);
|
||||||
|
return await oldRequestCompletion.Task;
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task<string> ExecuteReplacementRequest(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
Interlocked.Increment(ref requestCount);
|
||||||
|
replacementStarted.TrySetResult(true);
|
||||||
|
return await replacementCompletion.Task;
|
||||||
|
}
|
||||||
|
|
||||||
|
var abandonedListener = coalescer.ExecuteAsync(key, ExecuteOldRequest, () => "canceled", cancellationSource.Token);
|
||||||
|
await oldRequestStarted.Task.WaitAsync(_testTimeout);
|
||||||
|
var oldPhysicalRequest = GetPhysicalRequest(coalescer, key);
|
||||||
|
|
||||||
|
cancellationSource.Cancel();
|
||||||
|
Assert.That(await abandonedListener.WaitAsync(_testTimeout), Is.EqualTo("canceled"));
|
||||||
|
|
||||||
|
var replacementListener = coalescer.ExecuteAsync(key, ExecuteReplacementRequest, () => "canceled", CancellationToken.None);
|
||||||
|
await replacementStarted.Task.WaitAsync(_testTimeout);
|
||||||
|
|
||||||
|
oldRequestCompletion.SetResult("old result");
|
||||||
|
await oldPhysicalRequest.WaitAsync(_testTimeout);
|
||||||
|
|
||||||
|
var joinedReplacementListener = coalescer.ExecuteAsync(key, ExecuteReplacementRequest, () => "canceled", CancellationToken.None);
|
||||||
|
Assert.That(requestCount, Is.EqualTo(2));
|
||||||
|
|
||||||
|
replacementCompletion.SetResult("replacement result");
|
||||||
|
|
||||||
|
Assert.That(await replacementListener.WaitAsync(_testTimeout), Is.EqualTo("replacement result"));
|
||||||
|
Assert.That(await joinedReplacementListener.WaitAsync(_testTimeout), Is.EqualTo("replacement result"));
|
||||||
|
Assert.That(requestCount, Is.EqualTo(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RequestCoalescer CreateCoalescer()
|
||||||
|
=> new RequestCoalescer(NullLogger.Instance);
|
||||||
|
|
||||||
|
private static RequestCoalescingKey CreateKey(Type? responseType = null, string? parameters = null)
|
||||||
|
=> new RequestCoalescingKey("GET", "https://localhost/test", parameters, responseType ?? typeof(string));
|
||||||
|
|
||||||
|
private static TaskCompletionSource<T> CreateCompletionSource<T>()
|
||||||
|
=> new TaskCompletionSource<T>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
|
||||||
|
private static Task GetPhysicalRequest(RequestCoalescer coalescer, RequestCoalescingKey key)
|
||||||
|
{
|
||||||
|
var requestsField = typeof(RequestCoalescer).GetField("_requests", BindingFlags.Instance | BindingFlags.NonPublic)!;
|
||||||
|
var requests = (IDictionary)requestsField.GetValue(coalescer)!;
|
||||||
|
var entry = requests[key]!;
|
||||||
|
var requestProperty = entry.GetType().GetProperty("Request")!;
|
||||||
|
return ((Lazy<Task<object>>)requestProperty.GetValue(entry)!).Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,355 @@
|
|||||||
|
using CryptoExchange.Net.Interfaces.Clients;
|
||||||
|
using CryptoExchange.Net.RateLimiting;
|
||||||
|
using CryptoExchange.Net.SharedApis;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.UnitTests
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
internal class SharedApiClientTests
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void GetCapability_ShouldUseCapabilityTradingModes()
|
||||||
|
{
|
||||||
|
var sharedApi = new TestSharedApi(
|
||||||
|
[TradingMode.Spot, TradingMode.PerpetualLinear],
|
||||||
|
[TradingMode.Spot]);
|
||||||
|
var client = new TestSharedApiClient(sharedApi);
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(client.GetCapability<ITestCapability>(TradingMode.Spot)?.Capability, Is.SameAs(sharedApi));
|
||||||
|
Assert.That(client.GetCapability<ITestCapability>(TradingMode.PerpetualLinear), Is.Null);
|
||||||
|
Assert.That(client.GetCapabilities<ITestCapability>(TradingMode.Spot), Has.Count.EqualTo(1));
|
||||||
|
Assert.That(client.GetCapabilities<ITestCapability>(TradingMode.PerpetualLinear), Is.Empty);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void CapabilityTradingModes_ShouldDefaultToSharedApiTradingModes()
|
||||||
|
{
|
||||||
|
var sharedApi = new TestSharedApi(
|
||||||
|
[TradingMode.Spot, TradingMode.PerpetualLinear],
|
||||||
|
null);
|
||||||
|
var client = new TestSharedApiClient(sharedApi);
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(client.GetCapability<ITestCapability>(TradingMode.Spot)?.Capability, Is.SameAs(sharedApi));
|
||||||
|
Assert.That(client.GetCapability<ITestCapability>(TradingMode.PerpetualLinear)?.Capability, Is.SameAs(sharedApi));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void GetCapabilities_WithReference_ShouldReturnMatchingCapabilities()
|
||||||
|
{
|
||||||
|
var sharedApi = new TestSharedApi(
|
||||||
|
[TradingMode.Spot],
|
||||||
|
[TradingMode.Spot]);
|
||||||
|
var client = new TestSharedApiClient(sharedApi);
|
||||||
|
|
||||||
|
var result = client.GetCapabilities(
|
||||||
|
new SharedCapabilityReference<ITestCapability>(),
|
||||||
|
TradingMode.Spot);
|
||||||
|
|
||||||
|
Assert.That(result, Has.Count.EqualTo(1));
|
||||||
|
Assert.That(result[0].Capability, Is.SameAs(sharedApi));
|
||||||
|
Assert.That(result[0].Options, Is.SameAs(sharedApi.CapabilityOptions));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Discover_ShouldReturnAllSharedApiInformation()
|
||||||
|
{
|
||||||
|
var restApi = new TestRestSharedApi();
|
||||||
|
var socketApi = new TestSocketSharedApi();
|
||||||
|
var client = new TestDiSharedApiClient(
|
||||||
|
restApi,
|
||||||
|
socketApi);
|
||||||
|
|
||||||
|
var result = client.Discover();
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(result.PreferredTransport, Is.EqualTo(SharedTransport.Socket));
|
||||||
|
Assert.That(result.SharedApis, Has.Length.EqualTo(2));
|
||||||
|
Assert.That(result.SharedApis[0].Transport, Is.EqualTo(SharedTransport.Rest));
|
||||||
|
Assert.That(result.SharedApis[1].Transport, Is.EqualTo(SharedTransport.Socket));
|
||||||
|
Assert.That(result.SharedApis[0].SupportedTradingModes, Is.EqualTo(new[] { TradingMode.Spot }));
|
||||||
|
Assert.That(result.SharedApis[0].Capabilities, Has.Length.EqualTo(1));
|
||||||
|
Assert.That(result.SharedApis[0].Authenticated, Is.False);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void RegisterSharedApiClient_ShouldResolvePreferredTransport()
|
||||||
|
{
|
||||||
|
var restApi = new TestRestSharedApi();
|
||||||
|
var socketApi = new TestSocketSharedApi();
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
|
||||||
|
services.AddSingleton(restApi);
|
||||||
|
services.AddSingleton(socketApi);
|
||||||
|
|
||||||
|
services.RegisterSharedApiClient<
|
||||||
|
ITestSharedApiClient,
|
||||||
|
TestDiSharedApiClient>(sharedApis => sharedApis
|
||||||
|
.Add(client => client.Rest)
|
||||||
|
.Add(client => client.Socket));
|
||||||
|
|
||||||
|
using var provider = services.BuildServiceProvider();
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(
|
||||||
|
provider.GetRequiredService<ITestSharedApiClient>(),
|
||||||
|
Is.Not.Null);
|
||||||
|
|
||||||
|
Assert.That(
|
||||||
|
provider.GetRequiredService<ISharedApiClientBase>(),
|
||||||
|
Is.InstanceOf<TestDiSharedApiClient>());
|
||||||
|
|
||||||
|
Assert.That(
|
||||||
|
provider.GetRequiredService<ITestCapability>(),
|
||||||
|
Is.SameAs(socketApi));
|
||||||
|
|
||||||
|
Assert.That(
|
||||||
|
provider.GetServices<ITestCapability>().ToArray(),
|
||||||
|
Is.EqualTo(new[] { socketApi }));
|
||||||
|
|
||||||
|
Assert.That(
|
||||||
|
provider.GetRequiredService<ITestRestCapability>(),
|
||||||
|
Is.SameAs(restApi));
|
||||||
|
|
||||||
|
Assert.That(
|
||||||
|
provider.GetRequiredService<ITestSocketCapability>(),
|
||||||
|
Is.SameAs(socketApi));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void CapabilityTradingModes_ShouldIntersectApiApplicableAndOverrideModes()
|
||||||
|
{
|
||||||
|
var sharedApi = new TestSharedApi(
|
||||||
|
[TradingMode.Spot, TradingMode.PerpetualLinear, TradingMode.DeliveryLinear],
|
||||||
|
[TradingMode.Spot, TradingMode.PerpetualLinear],
|
||||||
|
[TradingMode.PerpetualLinear, TradingMode.DeliveryLinear]);
|
||||||
|
|
||||||
|
Assert.That(
|
||||||
|
sharedApi.CapabilityOptions.SupportedTradingModes,
|
||||||
|
Is.EqualTo(new[] { TradingMode.PerpetualLinear }));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void CapabilityOptions_ShouldApplyIntrinsicTradingModes()
|
||||||
|
{
|
||||||
|
var apiModes = new[]
|
||||||
|
{
|
||||||
|
TradingMode.Spot,
|
||||||
|
TradingMode.PerpetualLinear,
|
||||||
|
TradingMode.DeliveryLinear,
|
||||||
|
TradingMode.PerpetualInverse,
|
||||||
|
TradingMode.DeliveryInverse
|
||||||
|
};
|
||||||
|
var spotOptions = new PlaceSpotOrderOptions("TestExchange");
|
||||||
|
var futuresOptions = new GetPositionsOptions("TestExchange", false);
|
||||||
|
var perpetualOptions = new GetFundingRateHistoryOptions(
|
||||||
|
"TestExchange",
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
100,
|
||||||
|
false);
|
||||||
|
var unrestrictedOptions = new GetTickerOptions("TestExchange");
|
||||||
|
|
||||||
|
_ = new TestOptionsHost(apiModes, spotOptions);
|
||||||
|
_ = new TestOptionsHost(apiModes, futuresOptions);
|
||||||
|
_ = new TestOptionsHost(apiModes, perpetualOptions);
|
||||||
|
_ = new TestOptionsHost(apiModes, unrestrictedOptions);
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(spotOptions.SupportedTradingModes, Is.EqualTo(new[] { TradingMode.Spot }));
|
||||||
|
Assert.That(futuresOptions.SupportedTradingModes, Is.EqualTo(new[]
|
||||||
|
{
|
||||||
|
TradingMode.PerpetualLinear,
|
||||||
|
TradingMode.DeliveryLinear,
|
||||||
|
TradingMode.PerpetualInverse,
|
||||||
|
TradingMode.DeliveryInverse
|
||||||
|
}));
|
||||||
|
Assert.That(perpetualOptions.SupportedTradingModes, Is.EqualTo(new[]
|
||||||
|
{
|
||||||
|
TradingMode.PerpetualLinear,
|
||||||
|
TradingMode.PerpetualInverse
|
||||||
|
}));
|
||||||
|
Assert.That(unrestrictedOptions.SupportedTradingModes, Is.EqualTo(apiModes));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private interface ITestCapability : ISharedApiCapability
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TestApiClient : IBaseApiClient
|
||||||
|
{
|
||||||
|
public string Exchange => "TestExchange";
|
||||||
|
public string BaseAddress => "https://test.invalid";
|
||||||
|
|
||||||
|
public string FormatSymbol(
|
||||||
|
string baseAsset,
|
||||||
|
string quoteAsset,
|
||||||
|
TradingMode tradingMode,
|
||||||
|
DateTime? deliverDate = null)
|
||||||
|
=> $"{baseAsset}{quoteAsset}";
|
||||||
|
|
||||||
|
public Task<TResult> WithRateLimitAdmissionAsync<TResult>(
|
||||||
|
RateLimitAdmission admission,
|
||||||
|
Func<Task<TResult>> operation)
|
||||||
|
=> operation();
|
||||||
|
}
|
||||||
|
|
||||||
|
private interface ITestRestCapability : ITestCapability, ISharedRest
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
private interface ITestSocketCapability : ITestCapability, ISharedSocket
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
private interface ITestRestSharedApi : ITestRestCapability
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
private interface ITestSocketSharedApi : ITestSocketCapability
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
private interface ITestSharedApiClient : ISharedApiClientBase
|
||||||
|
{
|
||||||
|
ITestRestSharedApi Rest { get; }
|
||||||
|
ITestSocketSharedApi Socket { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TestCapabilityOptions : CapabilityOptions<SharedRequest, ITestCapability>
|
||||||
|
{
|
||||||
|
public override string Description => "Test capability";
|
||||||
|
|
||||||
|
public TestCapabilityOptions(TradingMode[]? applicableTradingModes)
|
||||||
|
: base("TestExchange", false, "TestOperation", [], applicableTradingModes)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TestSharedApi : SharedApiBase, ITestCapability
|
||||||
|
{
|
||||||
|
public TestCapabilityOptions CapabilityOptions { get; }
|
||||||
|
|
||||||
|
public TestSharedApi(TradingMode[] apiTradingModes, TradingMode[]? capabilityTradingModes)
|
||||||
|
: this(apiTradingModes, capabilityTradingModes, null)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public TestSharedApi(
|
||||||
|
TradingMode[] apiTradingModes,
|
||||||
|
TradingMode[]? capabilityTradingModes,
|
||||||
|
TradingMode[]? applicableTradingModes)
|
||||||
|
: base(
|
||||||
|
SharedTransport.Rest,
|
||||||
|
new TestApiClient(),
|
||||||
|
apiTradingModes,
|
||||||
|
() => false,
|
||||||
|
(baseAsset, quoteAsset, tradingMode, deliverDate) => $"{baseAsset}{quoteAsset}")
|
||||||
|
{
|
||||||
|
var options = new TestCapabilityOptions(applicableTradingModes)
|
||||||
|
{
|
||||||
|
SupportedTradingModeOverrides = capabilityTradingModes
|
||||||
|
};
|
||||||
|
|
||||||
|
SetCapabilities(options);
|
||||||
|
CapabilityOptions = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override SharedClientInfo Discover() => new();
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TestOptionsHost : SharedApiBase
|
||||||
|
{
|
||||||
|
public TestOptionsHost(TradingMode[] apiTradingModes, CapabilityOptions capabilityOptions)
|
||||||
|
: base(
|
||||||
|
SharedTransport.Rest,
|
||||||
|
new TestApiClient(),
|
||||||
|
apiTradingModes,
|
||||||
|
() => false,
|
||||||
|
(baseAsset, quoteAsset, tradingMode, deliverDate) => $"{baseAsset}{quoteAsset}")
|
||||||
|
{
|
||||||
|
SetCapabilities(capabilityOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override SharedClientInfo Discover() => new();
|
||||||
|
}
|
||||||
|
|
||||||
|
private abstract class TestTransportSharedApi : SharedApiBase
|
||||||
|
{
|
||||||
|
protected TestTransportSharedApi(SharedTransport transport)
|
||||||
|
: base(
|
||||||
|
transport,
|
||||||
|
new TestApiClient(),
|
||||||
|
[TradingMode.Spot],
|
||||||
|
() => false,
|
||||||
|
(baseAsset, quoteAsset, tradingMode, deliverDate) => $"{baseAsset}{quoteAsset}")
|
||||||
|
{
|
||||||
|
SetCapabilities(new TestCapabilityOptions([TradingMode.Spot]));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override SharedClientInfo Discover() => new()
|
||||||
|
{
|
||||||
|
Exchange = this.Exchange,
|
||||||
|
TypeName = GetType().Name,
|
||||||
|
SupportedTradingModes = this.SupportedTradingModes,
|
||||||
|
Transport = this.Transport,
|
||||||
|
Authenticated = this.Authenticated,
|
||||||
|
Capabilities = ((ISharedApi)this).Capabilities.ToArray()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TestRestSharedApi : TestTransportSharedApi, ITestRestSharedApi
|
||||||
|
{
|
||||||
|
public TestRestSharedApi() : base(SharedTransport.Rest)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TestSocketSharedApi : TestTransportSharedApi, ITestSocketSharedApi
|
||||||
|
{
|
||||||
|
public TestSocketSharedApi() : base(SharedTransport.Socket)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TestDiSharedApiClient : SharedApiClientBase, ITestSharedApiClient
|
||||||
|
{
|
||||||
|
public ITestRestSharedApi Rest { get; }
|
||||||
|
public ITestSocketSharedApi Socket { get; }
|
||||||
|
|
||||||
|
public TestDiSharedApiClient(
|
||||||
|
TestRestSharedApi rest,
|
||||||
|
TestSocketSharedApi socket)
|
||||||
|
: base(SharedTransport.Socket, rest, socket)
|
||||||
|
{
|
||||||
|
Rest = rest;
|
||||||
|
Socket = socket;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TestSharedApiClient : SharedApiClientBase
|
||||||
|
{
|
||||||
|
public TestSharedApiClient(params ISharedApiCapability[] sharedApis)
|
||||||
|
: base(SharedTransport.Rest, sharedApis)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
using CryptoExchange.Net.SharedApis;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.UnitTests
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
internal class SharedCapabilitiesTests
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void AllSharedApiCapabilities_ShouldBeListedInSharedCapabilities()
|
||||||
|
{
|
||||||
|
var markerTypes = new[]
|
||||||
|
{
|
||||||
|
typeof(ISharedApiCapability),
|
||||||
|
typeof(ISharedRest),
|
||||||
|
typeof(ISharedSocket),
|
||||||
|
typeof(ISharedSubscription)
|
||||||
|
};
|
||||||
|
|
||||||
|
var expectedCapabilities = typeof(ISharedApiCapability).Assembly
|
||||||
|
.GetTypes()
|
||||||
|
.Where(x => x.IsInterface
|
||||||
|
&& typeof(ISharedApiCapability).IsAssignableFrom(x)
|
||||||
|
&& !markerTypes.Contains(x))
|
||||||
|
.OrderBy(x => x.FullName)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
var listedCapabilities = GetTypeAndNestedTypes(typeof(SharedCapabilities))
|
||||||
|
.SelectMany(x => x.GetProperties(
|
||||||
|
BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly))
|
||||||
|
.SelectMany(x => x.PropertyType.GetGenericArguments())
|
||||||
|
.Where(x => x.IsInterface
|
||||||
|
&& typeof(ISharedApiCapability).IsAssignableFrom(x))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
var missingCapabilities = expectedCapabilities
|
||||||
|
.Except(listedCapabilities)
|
||||||
|
.Select(x => x.Name)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
var unknownCapabilities = listedCapabilities
|
||||||
|
.Except(expectedCapabilities)
|
||||||
|
.Select(x => x.Name)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
var duplicateCapabilities = listedCapabilities
|
||||||
|
.GroupBy(x => x)
|
||||||
|
.Where(x => x.Count() > 1)
|
||||||
|
.Select(x => x.Key.Name)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(
|
||||||
|
missingCapabilities,
|
||||||
|
Is.Empty,
|
||||||
|
$"Capabilities missing from SharedCapabilities: " +
|
||||||
|
string.Join(", ", missingCapabilities));
|
||||||
|
|
||||||
|
Assert.That(
|
||||||
|
unknownCapabilities,
|
||||||
|
Is.Empty,
|
||||||
|
$"Unknown capabilities listed in SharedCapabilities: " +
|
||||||
|
string.Join(", ", unknownCapabilities));
|
||||||
|
|
||||||
|
Assert.That(
|
||||||
|
duplicateCapabilities,
|
||||||
|
Is.Empty,
|
||||||
|
$"Capabilities listed multiple times in SharedCapabilities: " +
|
||||||
|
string.Join(", ", duplicateCapabilities));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<Type> GetTypeAndNestedTypes(Type type)
|
||||||
|
{
|
||||||
|
yield return type;
|
||||||
|
|
||||||
|
foreach (var nestedType in type.GetNestedTypes(BindingFlags.Public))
|
||||||
|
{
|
||||||
|
foreach (var result in GetTypeAndNestedTypes(nestedType))
|
||||||
|
yield return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
using NUnit.Framework;
|
||||||
|
using CryptoExchange.Net.SharedApis;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.UnitTests
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
internal class SharedParametersTests
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void AllSharedAPIOptions_ShouldListAllDefaultParametersInRequestParameterRules()
|
||||||
|
{
|
||||||
|
var failures = new List<string>();
|
||||||
|
|
||||||
|
var optionsTypes = typeof(CapabilityOptions).Assembly
|
||||||
|
.GetTypes()
|
||||||
|
.Where(x => x.IsClass
|
||||||
|
&& !x.IsAbstract
|
||||||
|
&& typeof(CapabilityOptions).IsAssignableFrom(x))
|
||||||
|
.OrderBy(x => x.FullName)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
foreach (var optionsType in optionsTypes)
|
||||||
|
{
|
||||||
|
var genericOptionsType = GetGenericOptionsType(optionsType);
|
||||||
|
if (genericOptionsType == null)
|
||||||
|
{
|
||||||
|
if (optionsType == typeof(ClosePositionOptions))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
failures.Add($"{optionsType.Name}: unable to determine request type");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var requestType = genericOptionsType.GetGenericArguments()[0];
|
||||||
|
var defaultRulesField = GetDefaultParameterRulesField(optionsType);
|
||||||
|
if (defaultRulesField == null)
|
||||||
|
{
|
||||||
|
failures.Add($"{optionsType.Name}: no _defaultParameterRules field found");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var rules = (RequestParameterDescription[]?)defaultRulesField.GetValue(null);
|
||||||
|
if (rules == null)
|
||||||
|
{
|
||||||
|
failures.Add($"{optionsType.Name}: _defaultParameterRules is null");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var constructorParameterNames = requestType
|
||||||
|
.GetConstructors(BindingFlags.Instance | BindingFlags.Public)
|
||||||
|
.SelectMany(x => x.GetParameters())
|
||||||
|
.Where(x => !string.Equals(
|
||||||
|
x.Name,
|
||||||
|
"exchangeParameters",
|
||||||
|
StringComparison.OrdinalIgnoreCase))
|
||||||
|
.Select(x => x.Name!)
|
||||||
|
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
var parameterProperties = requestType
|
||||||
|
.GetProperties(BindingFlags.Instance | BindingFlags.Public)
|
||||||
|
.Where(x => x.DeclaringType == requestType
|
||||||
|
|| constructorParameterNames.Contains(x.Name))
|
||||||
|
.Where(x => x.Name != nameof(SharedRequest.ExchangeParameters))
|
||||||
|
.Select(x => x.Name)
|
||||||
|
.OrderBy(x => x)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
var ruleNames = rules
|
||||||
|
.Select(x => x.Name)
|
||||||
|
.ToHashSet(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
foreach (var parameterProperty in parameterProperties)
|
||||||
|
{
|
||||||
|
if (!ruleNames.Contains(parameterProperty))
|
||||||
|
{
|
||||||
|
failures.Add(
|
||||||
|
$"{optionsType.Name}: request property " +
|
||||||
|
$"{requestType.Name}.{parameterProperty} has no default request parameter rule");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.That(
|
||||||
|
failures,
|
||||||
|
Is.Empty,
|
||||||
|
$"Missing default request parameter rules:{Environment.NewLine}" +
|
||||||
|
string.Join(Environment.NewLine, failures));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void RequiredExchangeParameter_ShouldAcceptNameOrAnyAlias()
|
||||||
|
{
|
||||||
|
const string exchange = "TestExchange";
|
||||||
|
var options = new GetTickerOptions(exchange)
|
||||||
|
{
|
||||||
|
ExchangeParameterRules =
|
||||||
|
[
|
||||||
|
ExchangeParameterRule.Required(
|
||||||
|
"Parameter",
|
||||||
|
"Test parameter",
|
||||||
|
1,
|
||||||
|
"Alias1",
|
||||||
|
"Alias2")
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
var missingResult = options.ValidateRequest(null, null, [TradingMode.Spot]);
|
||||||
|
var nameResult = options.ValidateRequest(
|
||||||
|
new ExchangeParameters(new ExchangeParameter(exchange, "Parameter", 1)),
|
||||||
|
null,
|
||||||
|
[TradingMode.Spot]);
|
||||||
|
var firstAliasResult = options.ValidateRequest(
|
||||||
|
new ExchangeParameters(new ExchangeParameter(exchange, "Alias1", 1)),
|
||||||
|
null,
|
||||||
|
[TradingMode.Spot]);
|
||||||
|
var secondAliasResult = options.ValidateRequest(
|
||||||
|
new ExchangeParameters(new ExchangeParameter(exchange, "Alias2", 1)),
|
||||||
|
null,
|
||||||
|
[TradingMode.Spot]);
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(missingResult, Is.Not.Null);
|
||||||
|
Assert.That(nameResult, Is.Null);
|
||||||
|
Assert.That(firstAliasResult, Is.Null);
|
||||||
|
Assert.That(secondAliasResult, Is.Null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void RequiredExchangeParameterWithoutAliases_ShouldBeRequired()
|
||||||
|
{
|
||||||
|
const string exchange = "TestExchange";
|
||||||
|
var options = new GetTickerOptions(exchange)
|
||||||
|
{
|
||||||
|
ExchangeParameterRules =
|
||||||
|
[
|
||||||
|
ExchangeParameterRule.Required(
|
||||||
|
"Parameter",
|
||||||
|
"Test parameter",
|
||||||
|
1)
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = options.ValidateRequest(null, null, [TradingMode.Spot]);
|
||||||
|
|
||||||
|
Assert.That(result, Is.Not.Null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Type? GetGenericOptionsType(Type optionsType)
|
||||||
|
{
|
||||||
|
for (var type = optionsType; type != null; type = type.BaseType)
|
||||||
|
{
|
||||||
|
if (type.IsGenericType
|
||||||
|
&& type.GetGenericTypeDefinition() == typeof(CapabilityOptions<,>))
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static FieldInfo? GetDefaultParameterRulesField(Type optionsType)
|
||||||
|
{
|
||||||
|
for (var type = optionsType; type != null; type = type.BaseType)
|
||||||
|
{
|
||||||
|
var field = type.GetField(
|
||||||
|
"_defaultParameterRules",
|
||||||
|
BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.DeclaredOnly);
|
||||||
|
if (field != null)
|
||||||
|
return field;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
using CryptoExchange.Net.SharedApis;
|
using CryptoExchange.Net.SharedApis;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests
|
namespace CryptoExchange.Net.UnitTests
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,234 +0,0 @@
|
|||||||
//using CryptoExchange.Net.Objects;
|
|
||||||
//using CryptoExchange.Net.Objects.Sockets;
|
|
||||||
//using CryptoExchange.Net.Sockets;
|
|
||||||
//using CryptoExchange.Net.Testing.Implementations;
|
|
||||||
//using CryptoExchange.Net.UnitTests.TestImplementations;
|
|
||||||
//using CryptoExchange.Net.UnitTests.TestImplementations.Sockets;
|
|
||||||
//using Microsoft.Extensions.Logging;
|
|
||||||
//using Moq;
|
|
||||||
//using NUnit.Framework;
|
|
||||||
//using NUnit.Framework.Legacy;
|
|
||||||
//using System;
|
|
||||||
//using System.Collections.Generic;
|
|
||||||
//using System.Net.Sockets;
|
|
||||||
//using System.Text.Json;
|
|
||||||
//using System.Threading;
|
|
||||||
//using System.Threading.Tasks;
|
|
||||||
|
|
||||||
//namespace CryptoExchange.Net.UnitTests
|
|
||||||
//{
|
|
||||||
// [TestFixture]
|
|
||||||
// public class SocketClientTests
|
|
||||||
// {
|
|
||||||
// [TestCase]
|
|
||||||
// public void SettingOptions_Should_ResultInOptionsSet()
|
|
||||||
// {
|
|
||||||
// //arrange
|
|
||||||
// //act
|
|
||||||
// var client = new TestSocketClient(options =>
|
|
||||||
// {
|
|
||||||
// options.SubOptions.ApiCredentials = new Authentication.ApiCredentials("1", "2");
|
|
||||||
// options.SubOptions.MaxSocketConnections = 1;
|
|
||||||
// });
|
|
||||||
|
|
||||||
// //assert
|
|
||||||
// ClassicAssert.NotNull(client.SubClient.ApiOptions.ApiCredentials);
|
|
||||||
// Assert.That(1 == client.SubClient.ApiOptions.MaxSocketConnections);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// [TestCase(true)]
|
|
||||||
// [TestCase(false)]
|
|
||||||
// public void ConnectSocket_Should_ReturnConnectionResult(bool canConnect)
|
|
||||||
// {
|
|
||||||
// //arrange
|
|
||||||
// var client = new TestSocketClient();
|
|
||||||
// var socket = client.CreateSocket();
|
|
||||||
// socket.CanConnect = canConnect;
|
|
||||||
|
|
||||||
// //act
|
|
||||||
// var connectResult = client.SubClient.ConnectSocketSub(
|
|
||||||
// new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, ""));
|
|
||||||
|
|
||||||
// //assert
|
|
||||||
// Assert.That(connectResult.Success == canConnect);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// [TestCase]
|
|
||||||
// public void SocketMessages_Should_BeProcessedInDataHandlers()
|
|
||||||
// {
|
|
||||||
// // arrange
|
|
||||||
// var client = new TestSocketClient(options => {
|
|
||||||
// options.ReconnectInterval = TimeSpan.Zero;
|
|
||||||
// });
|
|
||||||
// var socket = client.CreateSocket();
|
|
||||||
// socket.CanConnect = true;
|
|
||||||
// var sub = new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, "");
|
|
||||||
// var rstEvent = new ManualResetEvent(false);
|
|
||||||
// Dictionary<string, string> result = null;
|
|
||||||
|
|
||||||
// client.SubClient.ConnectSocketSub(sub);
|
|
||||||
|
|
||||||
// var subObj = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) =>
|
|
||||||
// {
|
|
||||||
// result = messageEvent.Data;
|
|
||||||
// rstEvent.Set();
|
|
||||||
// });
|
|
||||||
// sub.AddSubscription(subObj);
|
|
||||||
|
|
||||||
// // act
|
|
||||||
// socket.InvokeMessage("{\"property\": \"123\", \"action\": \"update\", \"topic\": \"topic\"}");
|
|
||||||
// rstEvent.WaitOne(1000);
|
|
||||||
|
|
||||||
// // assert
|
|
||||||
// Assert.That(result["property"] == "123");
|
|
||||||
// }
|
|
||||||
|
|
||||||
// [TestCase(false)]
|
|
||||||
// [TestCase(true)]
|
|
||||||
// public void SocketMessages_Should_ContainOriginalDataIfEnabled(bool enabled)
|
|
||||||
// {
|
|
||||||
// // arrange
|
|
||||||
// var client = new TestSocketClient(options =>
|
|
||||||
// {
|
|
||||||
// options.ReconnectInterval = TimeSpan.Zero;
|
|
||||||
// options.SubOptions.OutputOriginalData = enabled;
|
|
||||||
// });
|
|
||||||
// var socket = client.CreateSocket();
|
|
||||||
// socket.CanConnect = true;
|
|
||||||
// var sub = new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, "");
|
|
||||||
// var rstEvent = new ManualResetEvent(false);
|
|
||||||
// string original = null;
|
|
||||||
|
|
||||||
// client.SubClient.ConnectSocketSub(sub);
|
|
||||||
// var subObj = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) =>
|
|
||||||
// {
|
|
||||||
// original = messageEvent.OriginalData;
|
|
||||||
// rstEvent.Set();
|
|
||||||
// });
|
|
||||||
// sub.AddSubscription(subObj);
|
|
||||||
// var msgToSend = JsonSerializer.Serialize(new { topic = "topic", action = "update", property = "123" });
|
|
||||||
|
|
||||||
// // act
|
|
||||||
// socket.InvokeMessage(msgToSend);
|
|
||||||
// rstEvent.WaitOne(1000);
|
|
||||||
|
|
||||||
// // assert
|
|
||||||
// Assert.That(original == (enabled ? msgToSend : null));
|
|
||||||
// }
|
|
||||||
|
|
||||||
// [TestCase()]
|
|
||||||
// public void UnsubscribingStream_Should_CloseTheSocket()
|
|
||||||
// {
|
|
||||||
// // arrange
|
|
||||||
// var client = new TestSocketClient(options =>
|
|
||||||
// {
|
|
||||||
// options.ReconnectInterval = TimeSpan.Zero;
|
|
||||||
// });
|
|
||||||
// var socket = client.CreateSocket();
|
|
||||||
// socket.CanConnect = true;
|
|
||||||
// var sub = new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, "");
|
|
||||||
// client.SubClient.ConnectSocketSub(sub);
|
|
||||||
|
|
||||||
// var subscription = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) => { });
|
|
||||||
// var ups = new UpdateSubscription(sub, subscription);
|
|
||||||
// sub.AddSubscription(subscription);
|
|
||||||
|
|
||||||
// // act
|
|
||||||
// client.UnsubscribeAsync(ups).Wait();
|
|
||||||
|
|
||||||
// // assert
|
|
||||||
// Assert.That(socket.Connected == false);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// [TestCase()]
|
|
||||||
// public void UnsubscribingAll_Should_CloseAllSockets()
|
|
||||||
// {
|
|
||||||
// // arrange
|
|
||||||
// var client = new TestSocketClient(options => { options.ReconnectInterval = TimeSpan.Zero; });
|
|
||||||
// var socket1 = client.CreateSocket();
|
|
||||||
// var socket2 = client.CreateSocket();
|
|
||||||
// socket1.CanConnect = true;
|
|
||||||
// socket2.CanConnect = true;
|
|
||||||
// var sub1 = new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket1), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, "");
|
|
||||||
// var sub2 = new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket2), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, "");
|
|
||||||
// client.SubClient.ConnectSocketSub(sub1);
|
|
||||||
// client.SubClient.ConnectSocketSub(sub2);
|
|
||||||
// var subscription1 = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) => { });
|
|
||||||
// var subscription2 = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) => { });
|
|
||||||
|
|
||||||
// sub1.AddSubscription(subscription1);
|
|
||||||
// sub2.AddSubscription(subscription2);
|
|
||||||
// var ups1 = new UpdateSubscription(sub1, subscription1);
|
|
||||||
// var ups2 = new UpdateSubscription(sub2, subscription2);
|
|
||||||
|
|
||||||
// // act
|
|
||||||
// client.UnsubscribeAllAsync().Wait();
|
|
||||||
|
|
||||||
// // assert
|
|
||||||
// Assert.That(socket1.Connected == false);
|
|
||||||
// Assert.That(socket2.Connected == false);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// [TestCase()]
|
|
||||||
// public void FailingToConnectSocket_Should_ReturnError()
|
|
||||||
// {
|
|
||||||
// // arrange
|
|
||||||
// var client = new TestSocketClient(options => { options.ReconnectInterval = TimeSpan.Zero; });
|
|
||||||
// var socket = client.CreateSocket();
|
|
||||||
// socket.CanConnect = false;
|
|
||||||
// var sub1 = new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, "");
|
|
||||||
|
|
||||||
// // act
|
|
||||||
// var connectResult = client.SubClient.ConnectSocketSub(sub1);
|
|
||||||
|
|
||||||
// // assert
|
|
||||||
// ClassicAssert.IsFalse(connectResult.Success);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// [TestCase()]
|
|
||||||
// public async Task ErrorResponse_ShouldNot_ConfirmSubscription()
|
|
||||||
// {
|
|
||||||
// // arrange
|
|
||||||
// var channel = "trade_btcusd";
|
|
||||||
// var client = new TestSocketClient(opt =>
|
|
||||||
// {
|
|
||||||
// opt.OutputOriginalData = true;
|
|
||||||
// opt.SocketSubscriptionsCombineTarget = 1;
|
|
||||||
// });
|
|
||||||
// var socket = client.CreateSocket();
|
|
||||||
// socket.CanConnect = true;
|
|
||||||
// client.SubClient.ConnectSocketSub(new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, ""));
|
|
||||||
|
|
||||||
// // act
|
|
||||||
// var sub = client.SubClient.SubscribeToSomethingAsync(channel, onUpdate => {}, ct: default);
|
|
||||||
// socket.InvokeMessage(JsonSerializer.Serialize(new { channel, action = "subscribe", status = "error" }));
|
|
||||||
// await sub;
|
|
||||||
|
|
||||||
// // assert
|
|
||||||
// ClassicAssert.IsTrue(client.SubClient.TestSubscription.Status != SubscriptionStatus.Subscribed);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// [TestCase()]
|
|
||||||
// public async Task SuccessResponse_Should_ConfirmSubscription()
|
|
||||||
// {
|
|
||||||
// // arrange
|
|
||||||
// var channel = "trade_btcusd";
|
|
||||||
// var client = new TestSocketClient(opt =>
|
|
||||||
// {
|
|
||||||
// opt.OutputOriginalData = true;
|
|
||||||
// opt.SocketSubscriptionsCombineTarget = 1;
|
|
||||||
// });
|
|
||||||
// var socket = client.CreateSocket();
|
|
||||||
// socket.CanConnect = true;
|
|
||||||
// client.SubClient.ConnectSocketSub(new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, ""));
|
|
||||||
|
|
||||||
// // act
|
|
||||||
// var sub = client.SubClient.SubscribeToSomethingAsync(channel, onUpdate => {}, ct: default);
|
|
||||||
// socket.InvokeMessage(JsonSerializer.Serialize(new { channel, action = "subscribe", status = "confirmed" }));
|
|
||||||
// await sub;
|
|
||||||
|
|
||||||
// // assert
|
|
||||||
// Assert.That(client.SubClient.TestSubscription.Status == SubscriptionStatus.Subscribed);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
@@ -0,0 +1,235 @@
|
|||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.Sockets.Default;
|
||||||
|
using CryptoExchange.Net.Sockets.Default.Routing;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class QueryRouterTests
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void BuildFromRoutes_Should_GroupRoutesByTypeIdentifier_AndSetDeserializationType()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var routes = new MessageRoute[]
|
||||||
|
{
|
||||||
|
MessageRoute.CreateForEvent<string>("type1", (_, _, _, _) => null),
|
||||||
|
MessageRoute.CreateForEvent<string>("type1", "topic1", (_, _, _, _) => null),
|
||||||
|
MessageRoute.CreateForEvent<int>("type2", "topic2", (_, _, _, _) => null)
|
||||||
|
};
|
||||||
|
|
||||||
|
var router = new QueryRouter(routes);
|
||||||
|
|
||||||
|
// act
|
||||||
|
var type1Routes = router.GetRoutes("type1");
|
||||||
|
var type2Routes = router.GetRoutes("type2");
|
||||||
|
var missingRoutes = router.GetRoutes("missing");
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(type1Routes, Is.Not.Null);
|
||||||
|
Assert.That(type2Routes, Is.Not.Null);
|
||||||
|
Assert.That(missingRoutes, Is.Null);
|
||||||
|
|
||||||
|
Assert.That(type1Routes, Is.TypeOf<QueryRouteCollection>());
|
||||||
|
Assert.That(type2Routes, Is.TypeOf<QueryRouteCollection>());
|
||||||
|
Assert.That(type1Routes!.DeserializationType, Is.EqualTo(typeof(string)));
|
||||||
|
Assert.That(type2Routes!.DeserializationType, Is.EqualTo(typeof(int)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void AddRoute_Should_SetMultipleReaders_WhenAnyRouteAllowsMultipleReaders()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var collection = new QueryRouteCollection(typeof(string));
|
||||||
|
|
||||||
|
// act
|
||||||
|
collection.AddRoute(null, MessageRoute.CreateForEvent<string>("type", (_, _, _, _) => null));
|
||||||
|
var beforeMultipleReaders = collection.MultipleReaders;
|
||||||
|
|
||||||
|
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) => null, true));
|
||||||
|
var afterMultipleReaders = collection.MultipleReaders;
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(beforeMultipleReaders, Is.False);
|
||||||
|
Assert.That(afterMultipleReaders, Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Handle_Should_InvokeRoutesWithoutTopicFilter_WhenTopicFilterIsNull()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var calls = new List<string>();
|
||||||
|
var collection = new QueryRouteCollection(typeof(string));
|
||||||
|
collection.AddRoute(null, MessageRoute.CreateForEvent<string>("type", (_, _, _, _) =>
|
||||||
|
{
|
||||||
|
calls.Add("no-topic");
|
||||||
|
return null;
|
||||||
|
}));
|
||||||
|
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
|
||||||
|
{
|
||||||
|
calls.Add("topic");
|
||||||
|
return null;
|
||||||
|
}));
|
||||||
|
collection.Build();
|
||||||
|
|
||||||
|
// act
|
||||||
|
var handled = collection.Handle(null, null!, DateTime.UtcNow, "original", "data", out var result);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(handled, Is.True);
|
||||||
|
Assert.That(result, Is.Null);
|
||||||
|
Assert.That(calls, Is.EqualTo(new[] { "no-topic" }));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Handle_Should_ReturnFalse_WhenNoRoutesMatch()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var collection = new QueryRouteCollection(typeof(string));
|
||||||
|
collection.AddRoute("other-topic", MessageRoute.CreateForEvent<string>("type", "other-topic", (_, _, _, _) => null));
|
||||||
|
collection.Build();
|
||||||
|
|
||||||
|
// act
|
||||||
|
var handled = collection.Handle("topic", null!, DateTime.UtcNow, "original", "data", out var result);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(handled, Is.False);
|
||||||
|
Assert.That(result, Is.Null);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Handle_Should_InvokeRoutesWithoutTopicFilter_AndMatchingTopicRoutes()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var calls = new List<string>();
|
||||||
|
var collection = new QueryRouteCollection(typeof(string));
|
||||||
|
collection.AddRoute(null, MessageRoute.CreateForEvent<string>("type", (_, _, _, _) =>
|
||||||
|
{
|
||||||
|
calls.Add("no-topic");
|
||||||
|
return null;
|
||||||
|
}));
|
||||||
|
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
|
||||||
|
{
|
||||||
|
calls.Add("topic");
|
||||||
|
return null;
|
||||||
|
}));
|
||||||
|
collection.Build();
|
||||||
|
|
||||||
|
// act
|
||||||
|
var handled = collection.Handle("topic", null!, DateTime.UtcNow, "original", "data", out var result);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(handled, Is.True);
|
||||||
|
Assert.That(result, Is.Null);
|
||||||
|
Assert.That(calls, Is.EqualTo(new[] { "no-topic", "topic" }));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Handle_Should_StopAfterFirstNonNullMatchingResult_WhenMultipleReadersIsFalse()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var calls = new List<string>();
|
||||||
|
var expectedResult = CallResult.Ok();
|
||||||
|
var collection = new QueryRouteCollection(typeof(string));
|
||||||
|
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
|
||||||
|
{
|
||||||
|
calls.Add("first");
|
||||||
|
return expectedResult;
|
||||||
|
}));
|
||||||
|
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
|
||||||
|
{
|
||||||
|
calls.Add("second");
|
||||||
|
return CallResult.Ok();
|
||||||
|
}));
|
||||||
|
collection.Build();
|
||||||
|
|
||||||
|
// act
|
||||||
|
var handled = collection.Handle("topic", null!, DateTime.UtcNow, "original", "data", out var result);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(handled, Is.True);
|
||||||
|
Assert.That(result, Is.SameAs(expectedResult));
|
||||||
|
Assert.That(calls, Is.EqualTo(new[] { "first" }));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Handle_Should_ContinueAfterNonNullMatchingResult_WhenMultipleReadersIsTrue()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var calls = new List<string>();
|
||||||
|
var expectedResult = CallResult.Ok();
|
||||||
|
var collection = new QueryRouteCollection(typeof(string));
|
||||||
|
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
|
||||||
|
{
|
||||||
|
calls.Add("first");
|
||||||
|
return expectedResult;
|
||||||
|
}, true));
|
||||||
|
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
|
||||||
|
{
|
||||||
|
calls.Add("second");
|
||||||
|
return CallResult.Ok();
|
||||||
|
}));
|
||||||
|
collection.Build();
|
||||||
|
|
||||||
|
// act
|
||||||
|
var handled = collection.Handle("topic", null!, DateTime.UtcNow, "original", "data", out var result);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(handled, Is.True);
|
||||||
|
Assert.That(result, Is.SameAs(expectedResult));
|
||||||
|
Assert.That(calls, Is.EqualTo(new[] { "first", "second" }));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Handle_Should_ContinueUntilNonNullResult_WhenEarlierMatchingRoutesReturnNull()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var calls = new List<string>();
|
||||||
|
var expectedResult = CallResult.Ok();
|
||||||
|
var collection = new QueryRouteCollection(typeof(string));
|
||||||
|
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
|
||||||
|
{
|
||||||
|
calls.Add("first");
|
||||||
|
return null;
|
||||||
|
}));
|
||||||
|
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
|
||||||
|
{
|
||||||
|
calls.Add("second");
|
||||||
|
return expectedResult;
|
||||||
|
}));
|
||||||
|
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
|
||||||
|
{
|
||||||
|
calls.Add("third");
|
||||||
|
return CallResult.Ok();
|
||||||
|
}));
|
||||||
|
collection.Build();
|
||||||
|
|
||||||
|
// act
|
||||||
|
var handled = collection.Handle("topic", null!, DateTime.UtcNow, "original", "data", out var result);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(handled, Is.True);
|
||||||
|
Assert.That(result, Is.SameAs(expectedResult));
|
||||||
|
Assert.That(calls, Is.EqualTo(new[] { "first", "second" }));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Handle_Should_ReturnHandledTrue_WhenMatchingRoutesReturnNull()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var collection = new QueryRouteCollection(typeof(string));
|
||||||
|
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) => null));
|
||||||
|
collection.Build();
|
||||||
|
|
||||||
|
// act
|
||||||
|
var handled = collection.Handle("topic", null!, DateTime.UtcNow, "original", "data", out var result);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(handled, Is.True);
|
||||||
|
Assert.That(result, Is.Null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
using CryptoExchange.Net.Sockets.Default;
|
||||||
|
using CryptoExchange.Net.Sockets.Default.Routing;
|
||||||
|
using CryptoExchange.Net.Sockets.Interfaces;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class RoutingTableTests
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void Update_Should_CreateEntriesPerTypeIdentifier_WithCorrectDeserializationTypeAndHandlers()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var processor1 = new TestMessageProcessor(
|
||||||
|
1,
|
||||||
|
MessageRouter.Create(
|
||||||
|
MessageRoute.CreateForEvent<string>("type1", (_, _, _, _) => null),
|
||||||
|
MessageRoute.CreateForEvent<string>("type1", "topic1", (_, _, _, _) => null)));
|
||||||
|
|
||||||
|
var processor2 = new TestMessageProcessor(
|
||||||
|
2,
|
||||||
|
MessageRouter.Create(
|
||||||
|
MessageRoute.CreateForEvent<int>("type2", "topic2", (_, _, _, _) => null)));
|
||||||
|
|
||||||
|
var table = new RoutingTable();
|
||||||
|
|
||||||
|
// act
|
||||||
|
table.Update(new IMessageProcessor[] { processor1, processor2 });
|
||||||
|
|
||||||
|
var type1Entry = table.GetRouteTableEntry("type1");
|
||||||
|
var type2Entry = table.GetRouteTableEntry("type2");
|
||||||
|
var missingEntry = table.GetRouteTableEntry("missing");
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(type1Entry, Is.Not.Null);
|
||||||
|
Assert.That(type2Entry, Is.Not.Null);
|
||||||
|
Assert.That(missingEntry, Is.Null);
|
||||||
|
|
||||||
|
Assert.That(type1Entry!.DeserializationType, Is.EqualTo(typeof(string)));
|
||||||
|
Assert.That(type1Entry.IsStringOutput, Is.True);
|
||||||
|
Assert.That(type1Entry.Handlers, Has.Count.EqualTo(1));
|
||||||
|
Assert.That(type1Entry.Handlers.Single(), Is.SameAs(processor1));
|
||||||
|
|
||||||
|
Assert.That(type2Entry!.DeserializationType, Is.EqualTo(typeof(int)));
|
||||||
|
Assert.That(type2Entry.IsStringOutput, Is.False);
|
||||||
|
Assert.That(type2Entry.Handlers, Has.Count.EqualTo(1));
|
||||||
|
Assert.That(type2Entry.Handlers.Single(), Is.SameAs(processor2));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Update_Should_AddMultipleProcessors_ForSameTypeIdentifier()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var processor1 = new TestMessageProcessor(
|
||||||
|
1,
|
||||||
|
MessageRouter.Create(
|
||||||
|
MessageRoute.CreateForEvent<string>("type1", (_, _, _, _) => null)));
|
||||||
|
|
||||||
|
var processor2 = new TestMessageProcessor(
|
||||||
|
2,
|
||||||
|
MessageRouter.Create(
|
||||||
|
MessageRoute.CreateForEvent<string>("type1", "topic1", (_, _, _, _) => null)));
|
||||||
|
|
||||||
|
var table = new RoutingTable();
|
||||||
|
|
||||||
|
// act
|
||||||
|
table.Update(new IMessageProcessor[] { processor1, processor2 });
|
||||||
|
var entry = table.GetRouteTableEntry("type1");
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(entry, Is.Not.Null);
|
||||||
|
Assert.That(entry!.DeserializationType, Is.EqualTo(typeof(string)));
|
||||||
|
Assert.That(entry.Handlers, Has.Count.EqualTo(2));
|
||||||
|
Assert.That(entry.Handlers, Does.Contain(processor1));
|
||||||
|
Assert.That(entry.Handlers, Does.Contain(processor2));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Update_Should_ReplacePreviousEntries()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var initialProcessor = new TestMessageProcessor(
|
||||||
|
1,
|
||||||
|
MessageRouter.Create(
|
||||||
|
MessageRoute.CreateForEvent<string>("type1", (_, _, _, _) => null)));
|
||||||
|
|
||||||
|
var replacementProcessor = new TestMessageProcessor(
|
||||||
|
2,
|
||||||
|
MessageRouter.Create(
|
||||||
|
MessageRoute.CreateForEvent<int>("type2", (_, _, _, _) => null)));
|
||||||
|
|
||||||
|
var table = new RoutingTable();
|
||||||
|
table.Update(new IMessageProcessor[] { initialProcessor });
|
||||||
|
|
||||||
|
// act
|
||||||
|
table.Update(new IMessageProcessor[] { replacementProcessor });
|
||||||
|
|
||||||
|
var oldEntry = table.GetRouteTableEntry("type1");
|
||||||
|
var newEntry = table.GetRouteTableEntry("type2");
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(oldEntry, Is.Null);
|
||||||
|
Assert.That(newEntry, Is.Not.Null);
|
||||||
|
Assert.That(newEntry!.DeserializationType, Is.EqualTo(typeof(int)));
|
||||||
|
Assert.That(newEntry.Handlers, Has.Count.EqualTo(1));
|
||||||
|
Assert.That(newEntry.Handlers.Single(), Is.SameAs(replacementProcessor));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Update_WithEmptyProcessors_Should_ClearEntries()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var processor = new TestMessageProcessor(
|
||||||
|
1,
|
||||||
|
MessageRouter.Create(
|
||||||
|
MessageRoute.CreateForEvent<string>("type1", (_, _, _, _) => null)));
|
||||||
|
|
||||||
|
var table = new RoutingTable();
|
||||||
|
table.Update(new IMessageProcessor[] { processor });
|
||||||
|
|
||||||
|
// act
|
||||||
|
table.Update(Array.Empty<IMessageProcessor>());
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(table.GetRouteTableEntry("type1"), Is.Null);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TypeRoutingCollection_Should_SetIsStringOutput_BasedOnDeserializationType()
|
||||||
|
{
|
||||||
|
// arrange & act
|
||||||
|
var stringCollection = new TypeRoutingCollection(typeof(string));
|
||||||
|
var intCollection = new TypeRoutingCollection(typeof(int));
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(stringCollection.IsStringOutput, Is.True);
|
||||||
|
Assert.That(stringCollection.DeserializationType, Is.EqualTo(typeof(string)));
|
||||||
|
Assert.That(stringCollection.Handlers, Is.Empty);
|
||||||
|
|
||||||
|
Assert.That(intCollection.IsStringOutput, Is.False);
|
||||||
|
Assert.That(intCollection.DeserializationType, Is.EqualTo(typeof(int)));
|
||||||
|
Assert.That(intCollection.Handlers, Is.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TestMessageProcessor : IMessageProcessor
|
||||||
|
{
|
||||||
|
public int Id { get; }
|
||||||
|
public MessageRouter MessageRouter { get; }
|
||||||
|
|
||||||
|
public TestMessageProcessor(int id, MessageRouter messageRouter)
|
||||||
|
{
|
||||||
|
Id = id;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.Sockets.Default.Routing;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class SubscriptionRouterTests
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void BuildFromRoutes_Should_GroupRoutesByTypeIdentifier_AndSetDeserializationType()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var routes = new MessageRoute[]
|
||||||
|
{
|
||||||
|
MessageRoute.CreateForEvent<string>("type1", (_, _, _, _) => null),
|
||||||
|
MessageRoute.CreateForEvent<string>("type1", "topic1", (_, _, _, _) => null),
|
||||||
|
MessageRoute.CreateForEvent<int>("type2", "topic2", (_, _, _, _) => null)
|
||||||
|
};
|
||||||
|
|
||||||
|
var router = new SubscriptionRouter(routes);
|
||||||
|
|
||||||
|
// act
|
||||||
|
var type1Routes = router.GetRoutes("type1");
|
||||||
|
var type2Routes = router.GetRoutes("type2");
|
||||||
|
var missingRoutes = router.GetRoutes("missing");
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(type1Routes, Is.Not.Null);
|
||||||
|
Assert.That(type2Routes, Is.Not.Null);
|
||||||
|
Assert.That(missingRoutes, Is.Null);
|
||||||
|
|
||||||
|
Assert.That(type1Routes, Is.TypeOf<SubscriptionRouteCollection>());
|
||||||
|
Assert.That(type2Routes, Is.TypeOf<SubscriptionRouteCollection>());
|
||||||
|
Assert.That(type1Routes!.DeserializationType, Is.EqualTo(typeof(string)));
|
||||||
|
Assert.That(type2Routes!.DeserializationType, Is.EqualTo(typeof(int)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Handle_Should_InvokeRoutesWithoutTopicFilter_WhenTopicFilterIsNull()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var calls = new List<string>();
|
||||||
|
var collection = new SubscriptionRouteCollection(typeof(string));
|
||||||
|
collection.AddRoute(null, MessageRoute.CreateForEvent<string>("type", (_, _, _, _) =>
|
||||||
|
{
|
||||||
|
calls.Add("no-topic");
|
||||||
|
return null;
|
||||||
|
}));
|
||||||
|
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
|
||||||
|
{
|
||||||
|
calls.Add("topic");
|
||||||
|
return null;
|
||||||
|
}));
|
||||||
|
collection.Build();
|
||||||
|
|
||||||
|
// act
|
||||||
|
var handled = collection.Handle(null, null!, DateTime.UtcNow, "original", "data", out var result);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(handled, Is.True);
|
||||||
|
Assert.That(result, Is.SameAs(CallResult.Ok()));
|
||||||
|
Assert.That(calls, Is.EqualTo(new[] { "no-topic" }));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Handle_Should_ReturnFalse_WhenNoRoutesMatch()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var collection = new SubscriptionRouteCollection(typeof(string));
|
||||||
|
collection.AddRoute("other-topic", MessageRoute.CreateForEvent<string>("type", "other-topic", (_, _, _, _) => null));
|
||||||
|
collection.Build();
|
||||||
|
|
||||||
|
// act
|
||||||
|
var handled = collection.Handle("topic", null!, DateTime.UtcNow, "original", "data", out var result);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(handled, Is.False);
|
||||||
|
Assert.That(result, Is.SameAs(CallResult.Ok()));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Handle_Should_InvokeRoutesWithoutTopicFilter_AndMatchingTopicRoutes()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var calls = new List<string>();
|
||||||
|
var collection = new SubscriptionRouteCollection(typeof(string));
|
||||||
|
collection.AddRoute(null, MessageRoute.CreateForEvent<string>("type", (_, _, _, _) =>
|
||||||
|
{
|
||||||
|
calls.Add("no-topic");
|
||||||
|
return null;
|
||||||
|
}));
|
||||||
|
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
|
||||||
|
{
|
||||||
|
calls.Add("topic");
|
||||||
|
return null;
|
||||||
|
}));
|
||||||
|
collection.Build();
|
||||||
|
|
||||||
|
// act
|
||||||
|
var handled = collection.Handle("topic", null!, DateTime.UtcNow, "original", "data", out var result);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(handled, Is.True);
|
||||||
|
Assert.That(result, Is.SameAs(CallResult.Ok()));
|
||||||
|
Assert.That(calls, Is.EqualTo(new[] { "no-topic", "topic" }));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Handle_Should_InvokeAllMatchingTopicRoutes()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var calls = new List<string>();
|
||||||
|
var collection = new SubscriptionRouteCollection(typeof(string));
|
||||||
|
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
|
||||||
|
{
|
||||||
|
calls.Add("first");
|
||||||
|
return CallResult.Ok();
|
||||||
|
}));
|
||||||
|
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
|
||||||
|
{
|
||||||
|
calls.Add("second");
|
||||||
|
return null;
|
||||||
|
}));
|
||||||
|
collection.Build();
|
||||||
|
|
||||||
|
// act
|
||||||
|
var handled = collection.Handle("topic", null!, DateTime.UtcNow, "original", "data", out var result);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(handled, Is.True);
|
||||||
|
Assert.That(result, Is.SameAs(CallResult.Ok()));
|
||||||
|
Assert.That(calls, Is.EqualTo(new[] { "first", "second" }));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Handle_Should_NotInvokeTopicRoutes_WhenTopicFilterIsNull()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var calls = new List<string>();
|
||||||
|
var collection = new SubscriptionRouteCollection(typeof(string));
|
||||||
|
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
|
||||||
|
{
|
||||||
|
calls.Add("topic");
|
||||||
|
return null;
|
||||||
|
}));
|
||||||
|
collection.Build();
|
||||||
|
|
||||||
|
// act
|
||||||
|
var handled = collection.Handle(null, null!, DateTime.UtcNow, "original", "data", out var result);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(handled, Is.False);
|
||||||
|
Assert.That(result, Is.SameAs(CallResult.Ok()));
|
||||||
|
Assert.That(calls, Is.Empty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.Sockets;
|
||||||
|
using CryptoExchange.Net.Sockets.Default;
|
||||||
|
using CryptoExchange.Net.Sockets.Default.Routing;
|
||||||
|
using CryptoExchange.Net.UnitTests.Implementations;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class SubscriptionTests
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void Handle_Should_OnlyCompleteSubscriptionQuery_ForMatchingTopic()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var topicASubscription = new TopicSubscription("topic-a");
|
||||||
|
var topicBSubscription = new TopicSubscription("topic-b");
|
||||||
|
var topicAQuery = topicASubscription.CreateSubscriptionQuery(null!)!;
|
||||||
|
var topicBQuery = topicBSubscription.CreateSubscriptionQuery(null!)!;
|
||||||
|
|
||||||
|
// act
|
||||||
|
var topicAHandled = topicASubscription.Handle("type", "topic-a", null!, DateTime.UtcNow, "original", "data");
|
||||||
|
var topicBHandled = topicBSubscription.Handle("type", "topic-a", null!, DateTime.UtcNow, "original", "data");
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(topicAHandled, Is.True);
|
||||||
|
Assert.That(topicBHandled, Is.False);
|
||||||
|
Assert.That(topicAQuery.Completed, Is.True);
|
||||||
|
Assert.That(topicAQuery.Success, Is.True);
|
||||||
|
Assert.That(topicBQuery.Completed, Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Handle_Should_CompleteSubscriptionQuery_BeforeInvokingMatchingHandler()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
Query? query = null;
|
||||||
|
var queryCompletedWhenHandlerInvoked = false;
|
||||||
|
var subscription = new TopicSubscription("topic", () => queryCompletedWhenHandlerInvoked = query!.Completed);
|
||||||
|
query = subscription.CreateSubscriptionQuery(null!)!;
|
||||||
|
|
||||||
|
// act
|
||||||
|
subscription.Handle("type", "topic", null!, DateTime.UtcNow, "original", "data");
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(queryCompletedWhenHandlerInvoked, Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(null)]
|
||||||
|
[TestCase("topic")]
|
||||||
|
public void Handle_Should_CompleteSubscriptionQuery_ForUnfilteredRoute(string? topicFilter)
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var subscription = new TopicSubscription(topic: null);
|
||||||
|
var query = subscription.CreateSubscriptionQuery(null!)!;
|
||||||
|
|
||||||
|
// act
|
||||||
|
var handled = subscription.Handle("type", topicFilter, null!, DateTime.UtcNow, "original", "data");
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(handled, Is.True);
|
||||||
|
Assert.That(query.Completed, Is.True);
|
||||||
|
Assert.That(query.Success, Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Handle_Should_TreatEmptyTopicFilterAsUnfilteredRoute()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var subscription = new TopicSubscription(string.Empty);
|
||||||
|
var query = subscription.CreateSubscriptionQuery(null!)!;
|
||||||
|
|
||||||
|
// act
|
||||||
|
var handled = subscription.Handle("type", "topic", null!, DateTime.UtcNow, "original", "data");
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(handled, Is.True);
|
||||||
|
Assert.That(query.Completed, Is.True);
|
||||||
|
Assert.That(query.Success, Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Handle_Should_CompleteSubscriptionQuery_ForAnyMatchingTopic()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var subscription = new TopicSubscription(["topic-a", "topic-b"]);
|
||||||
|
var query = subscription.CreateSubscriptionQuery(null!)!;
|
||||||
|
|
||||||
|
// act
|
||||||
|
var handled = subscription.Handle("type", "topic-b", null!, DateTime.UtcNow, "original", "data");
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(handled, Is.True);
|
||||||
|
Assert.That(query.Completed, Is.True);
|
||||||
|
Assert.That(query.Success, Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TopicSubscription : Subscription
|
||||||
|
{
|
||||||
|
public TopicSubscription(string? topic, Action? handler = null)
|
||||||
|
: base(NullLogger.Instance, false)
|
||||||
|
{
|
||||||
|
MessageRouter = MessageRouter.CreateForEvent<string>("type", topic, (_, _, _, _) =>
|
||||||
|
{
|
||||||
|
handler?.Invoke();
|
||||||
|
return CallResult.Ok();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public TopicSubscription(string[] topics)
|
||||||
|
: base(NullLogger.Instance, false)
|
||||||
|
{
|
||||||
|
MessageRouter = MessageRouter.CreateForEvent<string>("type", topics, (_, _, _, _) => CallResult.Ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Query? GetSubQuery(SocketConnection connection)
|
||||||
|
{
|
||||||
|
return new TestQuery(new TestSocketMessage { Id = 1, Data = "Sub" }, false)
|
||||||
|
{
|
||||||
|
TimeoutBehavior = TimeoutBehavior.Succeed
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Query? GetUnsubQuery(SocketConnection connection) => null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,461 +0,0 @@
|
|||||||
using CryptoExchange.Net.Attributes;
|
|
||||||
using CryptoExchange.Net.Converters.SystemTextJson;
|
|
||||||
using System.Text.Json;
|
|
||||||
using NUnit.Framework;
|
|
||||||
using System;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
using CryptoExchange.Net.Converters;
|
|
||||||
using CryptoExchange.Net.SharedApis;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests
|
|
||||||
{
|
|
||||||
[TestFixture()]
|
|
||||||
public class SystemTextJsonConverterTests
|
|
||||||
{
|
|
||||||
[TestCase("2021-05-12")]
|
|
||||||
[TestCase("20210512")]
|
|
||||||
[TestCase("210512")]
|
|
||||||
[TestCase("1620777600.000")]
|
|
||||||
[TestCase("1620777600000")]
|
|
||||||
[TestCase("2021-05-12T00:00:00.000Z")]
|
|
||||||
[TestCase("2021-05-12T00:00:00.000000000Z")]
|
|
||||||
[TestCase("0.000000", true)]
|
|
||||||
[TestCase("0", true)]
|
|
||||||
[TestCase("", true)]
|
|
||||||
[TestCase(" ", true)]
|
|
||||||
public void TestDateTimeConverterString(string input, bool expectNull = false)
|
|
||||||
{
|
|
||||||
var output = JsonSerializer.Deserialize<STJTimeObject>($"{{ \"time\": \"{input}\" }}");
|
|
||||||
Assert.That(output.Time == (expectNull ? null: new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc)));
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase(1620777600.000)]
|
|
||||||
[TestCase(1620777600000d)]
|
|
||||||
public void TestDateTimeConverterDouble(double input)
|
|
||||||
{
|
|
||||||
var output = JsonSerializer.Deserialize<STJTimeObject>($"{{ \"time\": {input} }}");
|
|
||||||
Assert.That(output.Time == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase(1620777600)]
|
|
||||||
[TestCase(1620777600000)]
|
|
||||||
[TestCase(1620777600000000)]
|
|
||||||
[TestCase(1620777600000000000)]
|
|
||||||
[TestCase(0, true)]
|
|
||||||
public void TestDateTimeConverterLong(long input, bool expectNull = false)
|
|
||||||
{
|
|
||||||
var output = JsonSerializer.Deserialize<STJTimeObject>($"{{ \"time\": {input} }}");
|
|
||||||
Assert.That(output.Time == (expectNull ? null : new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc)));
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase(1620777600)]
|
|
||||||
[TestCase(1620777600.000)]
|
|
||||||
public void TestDateTimeConverterFromSeconds(double input)
|
|
||||||
{
|
|
||||||
var output = DateTimeConverter.ConvertFromSeconds(input);
|
|
||||||
Assert.That(output == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void TestDateTimeConverterToSeconds()
|
|
||||||
{
|
|
||||||
var output = DateTimeConverter.ConvertToSeconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
|
||||||
Assert.That(output == 1620777600);
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase(1620777600000)]
|
|
||||||
[TestCase(1620777600000.000)]
|
|
||||||
public void TestDateTimeConverterFromMilliseconds(double input)
|
|
||||||
{
|
|
||||||
var output = DateTimeConverter.ConvertFromMilliseconds(input);
|
|
||||||
Assert.That(output == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void TestDateTimeConverterToMilliseconds()
|
|
||||||
{
|
|
||||||
var output = DateTimeConverter.ConvertToMilliseconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
|
||||||
Assert.That(output == 1620777600000);
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase(1620777600000000)]
|
|
||||||
public void TestDateTimeConverterFromMicroseconds(long input)
|
|
||||||
{
|
|
||||||
var output = DateTimeConverter.ConvertFromMicroseconds(input);
|
|
||||||
Assert.That(output == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void TestDateTimeConverterToMicroseconds()
|
|
||||||
{
|
|
||||||
var output = DateTimeConverter.ConvertToMicroseconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
|
||||||
Assert.That(output == 1620777600000000);
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase(1620777600000000000)]
|
|
||||||
public void TestDateTimeConverterFromNanoseconds(long input)
|
|
||||||
{
|
|
||||||
var output = DateTimeConverter.ConvertFromNanoseconds(input);
|
|
||||||
Assert.That(output == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void TestDateTimeConverterToNanoseconds()
|
|
||||||
{
|
|
||||||
var output = DateTimeConverter.ConvertToNanoseconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
|
||||||
Assert.That(output == 1620777600000000000);
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase()]
|
|
||||||
public void TestDateTimeConverterNull()
|
|
||||||
{
|
|
||||||
var output = JsonSerializer.Deserialize<STJTimeObject>($"{{ \"time\": null }}");
|
|
||||||
Assert.That(output.Time == null);
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase(TestEnum.One, "1")]
|
|
||||||
[TestCase(TestEnum.Two, "2")]
|
|
||||||
[TestCase(TestEnum.Three, "three")]
|
|
||||||
[TestCase(TestEnum.Four, "Four")]
|
|
||||||
[TestCase(null, null)]
|
|
||||||
public void TestEnumConverterNullableGetStringTests(TestEnum? value, string expected)
|
|
||||||
{
|
|
||||||
var output = EnumConverter.GetString(value);
|
|
||||||
Assert.That(output == expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase(TestEnum.One, "1")]
|
|
||||||
[TestCase(TestEnum.Two, "2")]
|
|
||||||
[TestCase(TestEnum.Three, "three")]
|
|
||||||
[TestCase(TestEnum.Four, "Four")]
|
|
||||||
public void TestEnumConverterGetStringTests(TestEnum value, string expected)
|
|
||||||
{
|
|
||||||
var output = EnumConverter.GetString(value);
|
|
||||||
Assert.That(output == expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase("1", TestEnum.One)]
|
|
||||||
[TestCase("2", TestEnum.Two)]
|
|
||||||
[TestCase("3", TestEnum.Three)]
|
|
||||||
[TestCase("three", TestEnum.Three)]
|
|
||||||
[TestCase("Four", TestEnum.Four)]
|
|
||||||
[TestCase("four", TestEnum.Four)]
|
|
||||||
[TestCase("Four1", null)]
|
|
||||||
[TestCase(null, null)]
|
|
||||||
public void TestEnumConverterNullableDeserializeTests(string value, TestEnum? expected)
|
|
||||||
{
|
|
||||||
var val = value == null ? "null" : $"\"{value}\"";
|
|
||||||
var output = JsonSerializer.Deserialize<STJEnumObject>($"{{ \"Value\": {val} }}", SerializerOptions.WithConverters(new SerializationContext()));
|
|
||||||
Assert.That(output.Value == expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase("1", TestEnum.One)]
|
|
||||||
[TestCase("2", TestEnum.Two)]
|
|
||||||
[TestCase("3", TestEnum.Three)]
|
|
||||||
[TestCase("three", TestEnum.Three)]
|
|
||||||
[TestCase("Four", TestEnum.Four)]
|
|
||||||
[TestCase("four", TestEnum.Four)]
|
|
||||||
[TestCase("Four1", (TestEnum)(-9))]
|
|
||||||
[TestCase(null, (TestEnum)(-9))]
|
|
||||||
public void TestEnumConverterNotNullableDeserializeTests(string value, TestEnum expected)
|
|
||||||
{
|
|
||||||
var val = value == null ? "null" : $"\"{value}\"";
|
|
||||||
var output = JsonSerializer.Deserialize<NotNullableSTJEnumObject>($"{{ \"Value\": {val} }}");
|
|
||||||
Assert.That(output.Value == expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void TestEnumConverterMapsUndefinedValueCorrectlyIfDefaultIsDefined()
|
|
||||||
{
|
|
||||||
var output = JsonSerializer.Deserialize<TestEnum2>($"\"TestUndefined\"");
|
|
||||||
Assert.That((int)output == -99);
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase("1", TestEnum.One)]
|
|
||||||
[TestCase("2", TestEnum.Two)]
|
|
||||||
[TestCase("3", TestEnum.Three)]
|
|
||||||
[TestCase("three", TestEnum.Three)]
|
|
||||||
[TestCase("Four", TestEnum.Four)]
|
|
||||||
[TestCase("four", TestEnum.Four)]
|
|
||||||
[TestCase("Four1", null)]
|
|
||||||
[TestCase(null, null)]
|
|
||||||
public void TestEnumConverterParseStringTests(string value, TestEnum? expected)
|
|
||||||
{
|
|
||||||
var result = EnumConverter.ParseString<TestEnum>(value);
|
|
||||||
Assert.That(result == expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase("1", true)]
|
|
||||||
[TestCase("true", true)]
|
|
||||||
[TestCase("yes", true)]
|
|
||||||
[TestCase("y", true)]
|
|
||||||
[TestCase("on", true)]
|
|
||||||
[TestCase("-1", false)]
|
|
||||||
[TestCase("0", false)]
|
|
||||||
[TestCase("n", false)]
|
|
||||||
[TestCase("no", false)]
|
|
||||||
[TestCase("false", false)]
|
|
||||||
[TestCase("off", false)]
|
|
||||||
[TestCase("", null)]
|
|
||||||
public void TestBoolConverter(string value, bool? expected)
|
|
||||||
{
|
|
||||||
var val = value == null ? "null" : $"\"{value}\"";
|
|
||||||
var output = JsonSerializer.Deserialize<STJBoolObject>($"{{ \"Value\": {val} }}", SerializerOptions.WithConverters(new SerializationContext()));
|
|
||||||
Assert.That(output.Value == expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase("1", true)]
|
|
||||||
[TestCase("true", true)]
|
|
||||||
[TestCase("yes", true)]
|
|
||||||
[TestCase("y", true)]
|
|
||||||
[TestCase("on", true)]
|
|
||||||
[TestCase("-1", false)]
|
|
||||||
[TestCase("0", false)]
|
|
||||||
[TestCase("n", false)]
|
|
||||||
[TestCase("no", false)]
|
|
||||||
[TestCase("false", false)]
|
|
||||||
[TestCase("off", false)]
|
|
||||||
[TestCase("", false)]
|
|
||||||
public void TestBoolConverterNotNullable(string value, bool expected)
|
|
||||||
{
|
|
||||||
var val = value == null ? "null" : $"\"{value}\"";
|
|
||||||
var output = JsonSerializer.Deserialize<NotNullableSTJBoolObject>($"{{ \"Value\": {val} }}", SerializerOptions.WithConverters(new SerializationContext()));
|
|
||||||
Assert.That(output.Value == expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase("1", 1)]
|
|
||||||
[TestCase("1.1", 1.1)]
|
|
||||||
[TestCase("-1.1", -1.1)]
|
|
||||||
[TestCase(null, null)]
|
|
||||||
[TestCase("", null)]
|
|
||||||
[TestCase("null", null)]
|
|
||||||
[TestCase("nan", null)]
|
|
||||||
[TestCase("1E+2", 100)]
|
|
||||||
[TestCase("1E-2", 0.01)]
|
|
||||||
[TestCase("Infinity", 999)] // 999 is workaround for not being able to specify decimal.MinValue
|
|
||||||
[TestCase("-Infinity", -999)] // -999 is workaround for not being able to specify decimal.MaxValue
|
|
||||||
[TestCase("80228162514264337593543950335", 999)] // 999 is workaround for not being able to specify decimal.MaxValue
|
|
||||||
[TestCase("-80228162514264337593543950335", -999)] // -999 is workaround for not being able to specify decimal.MaxValue
|
|
||||||
public void TestDecimalConverterString(string value, decimal? expected)
|
|
||||||
{
|
|
||||||
var result = JsonSerializer.Deserialize<STJDecimalObject>("{ \"test\": \""+ value + "\"}");
|
|
||||||
Assert.That(result.Test, Is.EqualTo(expected == -999 ? decimal.MinValue : expected == 999 ? decimal.MaxValue: expected));
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase("1", 1)]
|
|
||||||
[TestCase("1.1", 1.1)]
|
|
||||||
[TestCase("-1.1", -1.1)]
|
|
||||||
[TestCase("null", null)]
|
|
||||||
[TestCase("1E+2", 100)]
|
|
||||||
[TestCase("1E-2", 0.01)]
|
|
||||||
[TestCase("80228162514264337593543950335", -999)] // -999 is workaround for not being able to specify decimal.MaxValue
|
|
||||||
public void TestDecimalConverterNumber(string value, decimal? expected)
|
|
||||||
{
|
|
||||||
var result = JsonSerializer.Deserialize<STJDecimalObject>("{ \"test\": " + value + "}");
|
|
||||||
Assert.That(result.Test, Is.EqualTo(expected == -999 ? decimal.MaxValue : expected));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test()]
|
|
||||||
public void TestArrayConverter()
|
|
||||||
{
|
|
||||||
var data = new Test()
|
|
||||||
{
|
|
||||||
Prop1 = 2,
|
|
||||||
Prop2 = null,
|
|
||||||
Prop3 = "123",
|
|
||||||
Prop3Again = "123",
|
|
||||||
Prop4 = null,
|
|
||||||
Prop5 = new Test2
|
|
||||||
{
|
|
||||||
Prop21 = 3,
|
|
||||||
Prop22 = "456"
|
|
||||||
},
|
|
||||||
Prop6 = new Test3
|
|
||||||
{
|
|
||||||
Prop31 = 4,
|
|
||||||
Prop32 = "789"
|
|
||||||
},
|
|
||||||
Prop7 = TestEnum.Two,
|
|
||||||
TestInternal = new Test
|
|
||||||
{
|
|
||||||
Prop1 = 10
|
|
||||||
},
|
|
||||||
Prop8 = new Test3
|
|
||||||
{
|
|
||||||
Prop31 = 5,
|
|
||||||
Prop32 = "101"
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
var options = new JsonSerializerOptions()
|
|
||||||
{
|
|
||||||
TypeInfoResolver = new SerializationContext()
|
|
||||||
};
|
|
||||||
var serialized = JsonSerializer.Serialize(data);
|
|
||||||
var deserialized = JsonSerializer.Deserialize<Test>(serialized);
|
|
||||||
|
|
||||||
Assert.That(deserialized.Prop1, Is.EqualTo(2));
|
|
||||||
Assert.That(deserialized.Prop2, Is.Null);
|
|
||||||
Assert.That(deserialized.Prop3, Is.EqualTo("123"));
|
|
||||||
Assert.That(deserialized.Prop3Again, Is.EqualTo("123"));
|
|
||||||
Assert.That(deserialized.Prop4, Is.Null);
|
|
||||||
Assert.That(deserialized.Prop5.Prop21, Is.EqualTo(3));
|
|
||||||
Assert.That(deserialized.Prop5.Prop22, Is.EqualTo("456"));
|
|
||||||
Assert.That(deserialized.Prop6.Prop31, Is.EqualTo(4));
|
|
||||||
Assert.That(deserialized.Prop6.Prop32, Is.EqualTo("789"));
|
|
||||||
Assert.That(deserialized.Prop7, Is.EqualTo(TestEnum.Two));
|
|
||||||
Assert.That(deserialized.TestInternal.Prop1, Is.EqualTo(10));
|
|
||||||
Assert.That(deserialized.Prop8.Prop31, Is.EqualTo(5));
|
|
||||||
Assert.That(deserialized.Prop8.Prop32, Is.EqualTo("101"));
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase(TradingMode.Spot, "ETH", "USDT", null)]
|
|
||||||
[TestCase(TradingMode.PerpetualLinear, "ETH", "USDT", null)]
|
|
||||||
[TestCase(TradingMode.DeliveryLinear, "ETH", "USDT", 1748432430)]
|
|
||||||
public void TestSharedSymbolConversion(TradingMode tradingMode, string baseAsset, string quoteAsset, int? deliverTime)
|
|
||||||
{
|
|
||||||
DateTime? time = deliverTime == null ? null : DateTimeConverter.ParseFromDouble(deliverTime.Value);
|
|
||||||
var symbol = new SharedSymbol(tradingMode, baseAsset, quoteAsset, time);
|
|
||||||
|
|
||||||
var serialized = JsonSerializer.Serialize(symbol);
|
|
||||||
var restored = JsonSerializer.Deserialize<SharedSymbol>(serialized);
|
|
||||||
|
|
||||||
Assert.That(restored.TradingMode, Is.EqualTo(symbol.TradingMode));
|
|
||||||
Assert.That(restored.BaseAsset, Is.EqualTo(symbol.BaseAsset));
|
|
||||||
Assert.That(restored.QuoteAsset, Is.EqualTo(symbol.QuoteAsset));
|
|
||||||
Assert.That(restored.DeliverTime, Is.EqualTo(symbol.DeliverTime));
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase(0.1, null, null)]
|
|
||||||
[TestCase(0.1, 0.1, null)]
|
|
||||||
[TestCase(0.1, 0.1, 0.1)]
|
|
||||||
[TestCase(null, 0.1, null)]
|
|
||||||
[TestCase(null, 0.1, 0.1)]
|
|
||||||
public void TestSharedQuantityConversion(double? baseQuantity, double? quoteQuantity, double? contractQuantity)
|
|
||||||
{
|
|
||||||
var symbol = new SharedOrderQuantity((decimal?)baseQuantity, (decimal?)quoteQuantity, (decimal?)contractQuantity);
|
|
||||||
|
|
||||||
var serialized = JsonSerializer.Serialize(symbol);
|
|
||||||
var restored = JsonSerializer.Deserialize<SharedOrderQuantity>(serialized);
|
|
||||||
|
|
||||||
Assert.That(restored.QuantityInBaseAsset, Is.EqualTo(symbol.QuantityInBaseAsset));
|
|
||||||
Assert.That(restored.QuantityInQuoteAsset, Is.EqualTo(symbol.QuantityInQuoteAsset));
|
|
||||||
Assert.That(restored.QuantityInContracts, Is.EqualTo(symbol.QuantityInContracts));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class STJDecimalObject
|
|
||||||
{
|
|
||||||
[JsonConverter(typeof(DecimalConverter))]
|
|
||||||
[JsonPropertyName("test")]
|
|
||||||
public decimal? Test { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class STJTimeObject
|
|
||||||
{
|
|
||||||
[JsonConverter(typeof(DateTimeConverter))]
|
|
||||||
[JsonPropertyName("time")]
|
|
||||||
public DateTime? Time { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class STJEnumObject
|
|
||||||
{
|
|
||||||
public TestEnum? Value { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class NotNullableSTJEnumObject
|
|
||||||
{
|
|
||||||
public TestEnum Value { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class STJBoolObject
|
|
||||||
{
|
|
||||||
public bool? Value { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class NotNullableSTJBoolObject
|
|
||||||
{
|
|
||||||
public bool Value { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
[JsonConverter(typeof(ArrayConverter<Test>))]
|
|
||||||
record Test
|
|
||||||
{
|
|
||||||
[ArrayProperty(0)]
|
|
||||||
public int Prop1 { get; set; }
|
|
||||||
[ArrayProperty(1)]
|
|
||||||
public int? Prop2 { get; set; }
|
|
||||||
[ArrayProperty(2)]
|
|
||||||
public string Prop3 { get; set; }
|
|
||||||
[ArrayProperty(2)]
|
|
||||||
public string Prop3Again { get; set; }
|
|
||||||
[ArrayProperty(3)]
|
|
||||||
public string Prop4 { get; set; }
|
|
||||||
[ArrayProperty(4)]
|
|
||||||
public Test2 Prop5 { get; set; }
|
|
||||||
[ArrayProperty(5)]
|
|
||||||
public Test3 Prop6 { get; set; }
|
|
||||||
[ArrayProperty(6), JsonConverter(typeof(EnumConverter<TestEnum>))]
|
|
||||||
public TestEnum? Prop7 { get; set; }
|
|
||||||
[ArrayProperty(7)]
|
|
||||||
public Test TestInternal { get; set; }
|
|
||||||
[ArrayProperty(8), JsonConversion]
|
|
||||||
public Test3 Prop8 { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
[JsonConverter(typeof(ArrayConverter<Test2>))]
|
|
||||||
record Test2
|
|
||||||
{
|
|
||||||
[ArrayProperty(0)]
|
|
||||||
public int Prop21 { get; set; }
|
|
||||||
[ArrayProperty(1)]
|
|
||||||
public string Prop22 { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
record Test3
|
|
||||||
{
|
|
||||||
[JsonPropertyName("prop31")]
|
|
||||||
public int Prop31 { get; set; }
|
|
||||||
[JsonPropertyName("prop32")]
|
|
||||||
public string Prop32 { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
[JsonConverter(typeof(EnumConverter<TestEnum>))]
|
|
||||||
public enum TestEnum
|
|
||||||
{
|
|
||||||
[Map("1")]
|
|
||||||
One,
|
|
||||||
[Map("2")]
|
|
||||||
Two,
|
|
||||||
[Map("three", "3")]
|
|
||||||
Three,
|
|
||||||
Four
|
|
||||||
}
|
|
||||||
|
|
||||||
[JsonConverter(typeof(EnumConverter<TestEnum2>))]
|
|
||||||
public enum TestEnum2
|
|
||||||
{
|
|
||||||
[Map("-9")]
|
|
||||||
Minus9 = -9,
|
|
||||||
[Map("1")]
|
|
||||||
One,
|
|
||||||
[Map("2")]
|
|
||||||
Two,
|
|
||||||
[Map("three", "3")]
|
|
||||||
Three,
|
|
||||||
Four
|
|
||||||
}
|
|
||||||
|
|
||||||
[JsonSerializable(typeof(Test))]
|
|
||||||
[JsonSerializable(typeof(Test2))]
|
|
||||||
[JsonSerializable(typeof(Test3))]
|
|
||||||
[JsonSerializable(typeof(NotNullableSTJBoolObject))]
|
|
||||||
[JsonSerializable(typeof(STJBoolObject))]
|
|
||||||
[JsonSerializable(typeof(NotNullableSTJEnumObject))]
|
|
||||||
[JsonSerializable(typeof(STJEnumObject))]
|
|
||||||
[JsonSerializable(typeof(STJDecimalObject))]
|
|
||||||
[JsonSerializable(typeof(STJTimeObject))]
|
|
||||||
internal partial class SerializationContext : JsonSerializerContext
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using CryptoExchange.Net.Authentication;
|
|
||||||
using CryptoExchange.Net.Clients;
|
|
||||||
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
|
|
||||||
using CryptoExchange.Net.Converters.SystemTextJson;
|
|
||||||
using CryptoExchange.Net.Interfaces;
|
|
||||||
using CryptoExchange.Net.Objects;
|
|
||||||
using CryptoExchange.Net.Objects.Errors;
|
|
||||||
using CryptoExchange.Net.Objects.Options;
|
|
||||||
using CryptoExchange.Net.SharedApis;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests
|
|
||||||
{
|
|
||||||
public class TestBaseClient: BaseClient
|
|
||||||
{
|
|
||||||
public TestSubClient SubClient { get; }
|
|
||||||
|
|
||||||
public TestBaseClient(): base(null, "Test")
|
|
||||||
{
|
|
||||||
var options = new TestClientOptions();
|
|
||||||
_logger = NullLogger.Instance;
|
|
||||||
Initialize(options);
|
|
||||||
SubClient = AddApiClient(new TestSubClient(options, new RestApiOptions()));
|
|
||||||
}
|
|
||||||
|
|
||||||
public TestBaseClient(TestClientOptions exchangeOptions) : base(null, "Test")
|
|
||||||
{
|
|
||||||
_logger = NullLogger.Instance;
|
|
||||||
Initialize(exchangeOptions);
|
|
||||||
SubClient = AddApiClient(new TestSubClient(exchangeOptions, new RestApiOptions()));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Log(LogLevel verbosity, string data)
|
|
||||||
{
|
|
||||||
_logger.Log(verbosity, data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class TestSubClient : RestApiClient<TestEnvironment, TestAuthProvider, HMACCredential>
|
|
||||||
{
|
|
||||||
protected override IRestMessageHandler MessageHandler => throw new NotImplementedException();
|
|
||||||
|
|
||||||
public TestSubClient(RestExchangeOptions<TestEnvironment, HMACCredential> options, RestApiOptions apiOptions) : base(new TraceLogger(), null, "https://localhost:123", options, apiOptions)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public CallResult<T> Deserialize<T>(string data)
|
|
||||||
{
|
|
||||||
return new CallResult<T>(JsonSerializer.Deserialize<T>(data));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
|
|
||||||
protected override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(new System.Text.Json.JsonSerializerOptions());
|
|
||||||
protected override TestAuthProvider CreateAuthenticationProvider(HMACCredential credentials) => throw new NotImplementedException();
|
|
||||||
protected override Task<WebCallResult<DateTime>> GetServerTimestampAsync() => throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public class TestAuthProvider : AuthenticationProvider<HMACCredential, HMACCredential>
|
|
||||||
{
|
|
||||||
public TestAuthProvider(HMACCredential credentials) : base(credentials, credentials)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void ProcessRequest(RestApiClient apiClient, RestRequestConfiguration requestConfig)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public string GetKey() => Credential.Key;
|
|
||||||
public string GetSecret() => Credential.Secret;
|
|
||||||
}
|
|
||||||
|
|
||||||
public class TestEnvironment : TradeEnvironment
|
|
||||||
{
|
|
||||||
public string TestAddress { get; }
|
|
||||||
|
|
||||||
public TestEnvironment(string name, string url) : base(name)
|
|
||||||
{
|
|
||||||
TestAddress = url;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using System.Reflection;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|
||||||
{
|
|
||||||
public class TestHelpers
|
|
||||||
{
|
|
||||||
[ExcludeFromCodeCoverage]
|
|
||||||
public static bool AreEqual<T>(T self, T to, params string[] ignore) where T : class
|
|
||||||
{
|
|
||||||
if (self != null && to != null)
|
|
||||||
{
|
|
||||||
var type = self.GetType();
|
|
||||||
var ignoreList = new List<string>(ignore);
|
|
||||||
foreach (var pi in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
|
|
||||||
{
|
|
||||||
if (ignoreList.Contains(pi.Name))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var selfValue = type.GetProperty(pi.Name).GetValue(self, null);
|
|
||||||
var toValue = type.GetProperty(pi.Name).GetValue(to, null);
|
|
||||||
|
|
||||||
if (pi.PropertyType.IsClass && !pi.PropertyType.Module.ScopeName.Equals("System.Private.CoreLib.dll"))
|
|
||||||
{
|
|
||||||
// Check of "CommonLanguageRuntimeLibrary" is needed because string is also a class
|
|
||||||
if (AreEqual(selfValue, toValue, ignore))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selfValue != toValue && (selfValue == null || !selfValue.Equals(toValue)))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return self == to;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,213 +0,0 @@
|
|||||||
using CryptoExchange.Net.Interfaces;
|
|
||||||
using CryptoExchange.Net.Objects;
|
|
||||||
using Moq;
|
|
||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using System.Net;
|
|
||||||
using System.Net.Http;
|
|
||||||
using System.Reflection;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using CryptoExchange.Net.Authentication;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using CryptoExchange.Net.Clients;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
using System.Linq;
|
|
||||||
using CryptoExchange.Net.Converters.SystemTextJson;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
using System.Net.Http.Headers;
|
|
||||||
using CryptoExchange.Net.SharedApis;
|
|
||||||
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|
||||||
{
|
|
||||||
public class TestRestClient: BaseRestClient
|
|
||||||
{
|
|
||||||
public TestRestApi1Client Api1 { get; }
|
|
||||||
public TestRestApi2Client Api2 { get; }
|
|
||||||
|
|
||||||
public TestRestClient(Action<TestClientOptions> optionsDelegate = null)
|
|
||||||
: this(null, null, Options.Create(ApplyOptionsDelegate(optionsDelegate)))
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public TestRestClient(HttpClient httpClient, ILoggerFactory loggerFactory, IOptions<TestClientOptions> options) : base(loggerFactory, "Test")
|
|
||||||
{
|
|
||||||
Initialize(options.Value);
|
|
||||||
|
|
||||||
Api1 = new TestRestApi1Client(options.Value);
|
|
||||||
Api2 = new TestRestApi2Client(options.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetResponse(string responseData, out IRequest requestObj)
|
|
||||||
{
|
|
||||||
var expectedBytes = Encoding.UTF8.GetBytes(responseData);
|
|
||||||
var responseStream = new MemoryStream();
|
|
||||||
responseStream.Write(expectedBytes, 0, expectedBytes.Length);
|
|
||||||
responseStream.Seek(0, SeekOrigin.Begin);
|
|
||||||
|
|
||||||
var response = new Mock<IResponse>();
|
|
||||||
response.Setup(c => c.IsSuccessStatusCode).Returns(true);
|
|
||||||
response.Setup(c => c.GetResponseStreamAsync(It.IsAny<CancellationToken>())).Returns(Task.FromResult((Stream)responseStream));
|
|
||||||
|
|
||||||
var headers = new HttpRequestMessage().Headers;
|
|
||||||
var request = new Mock<IRequest>();
|
|
||||||
request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com"));
|
|
||||||
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Returns(Task.FromResult(response.Object));
|
|
||||||
request.Setup(c => c.SetContent(It.IsAny<string>(), It.IsAny<Encoding>(), It.IsAny<string>())).Callback(new Action<string, Encoding, string>((content, encoding, type) => { request.Setup(r => r.Content).Returns(content); }));
|
|
||||||
request.Setup(c => c.AddHeader(It.IsAny<string>(), It.IsAny<string>())).Callback<string, string>((key, val) => headers.Add(key, new string[] { val }));
|
|
||||||
request.Setup(c => c.GetHeaders()).Returns(() => headers);
|
|
||||||
|
|
||||||
var factory = Mock.Get(Api1.RequestFactory);
|
|
||||||
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
|
||||||
.Callback<Version, HttpMethod, Uri, int>((version, method, uri, id) =>
|
|
||||||
{
|
|
||||||
request.Setup(a => a.Uri).Returns(uri);
|
|
||||||
request.Setup(a => a.Method).Returns(method);
|
|
||||||
})
|
|
||||||
.Returns(request.Object);
|
|
||||||
|
|
||||||
factory = Mock.Get(Api2.RequestFactory);
|
|
||||||
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
|
||||||
.Callback<Version, HttpMethod, Uri, int>((version, method, uri, id) =>
|
|
||||||
{
|
|
||||||
request.Setup(a => a.Uri).Returns(uri);
|
|
||||||
request.Setup(a => a.Method).Returns(method);
|
|
||||||
})
|
|
||||||
.Returns(request.Object);
|
|
||||||
requestObj = request.Object;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetErrorWithoutResponse(HttpStatusCode code, string message)
|
|
||||||
{
|
|
||||||
var we = new HttpRequestException();
|
|
||||||
typeof(HttpRequestException).GetField("_message", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).SetValue(we, message);
|
|
||||||
|
|
||||||
var request = new Mock<IRequest>();
|
|
||||||
request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com"));
|
|
||||||
request.Setup(c => c.GetHeaders()).Returns(new HttpRequestMessage().Headers);
|
|
||||||
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Throws(we);
|
|
||||||
|
|
||||||
var factory = Mock.Get(Api1.RequestFactory);
|
|
||||||
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
|
||||||
.Returns(request.Object);
|
|
||||||
|
|
||||||
|
|
||||||
factory = Mock.Get(Api2.RequestFactory);
|
|
||||||
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
|
||||||
.Returns(request.Object);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetErrorWithResponse(string responseData, HttpStatusCode code)
|
|
||||||
{
|
|
||||||
var expectedBytes = Encoding.UTF8.GetBytes(responseData);
|
|
||||||
var responseStream = new MemoryStream();
|
|
||||||
responseStream.Write(expectedBytes, 0, expectedBytes.Length);
|
|
||||||
responseStream.Seek(0, SeekOrigin.Begin);
|
|
||||||
|
|
||||||
var response = new Mock<IResponse>();
|
|
||||||
response.Setup(c => c.IsSuccessStatusCode).Returns(false);
|
|
||||||
response.Setup(c => c.GetResponseStreamAsync(It.IsAny<CancellationToken>())).Returns(Task.FromResult((Stream)responseStream));
|
|
||||||
|
|
||||||
var headers = new List<KeyValuePair<string, string[]>>();
|
|
||||||
var request = new Mock<IRequest>();
|
|
||||||
request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com"));
|
|
||||||
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Returns(Task.FromResult(response.Object));
|
|
||||||
request.Setup(c => c.AddHeader(It.IsAny<string>(), It.IsAny<string>())).Callback<string, string>((key, val) => headers.Add(new KeyValuePair<string, string[]>(key, new string[] { val })));
|
|
||||||
request.Setup(c => c.GetHeaders()).Returns(new HttpRequestMessage().Headers);
|
|
||||||
|
|
||||||
var factory = Mock.Get(Api1.RequestFactory);
|
|
||||||
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
|
||||||
.Callback<Version, HttpMethod, Uri, int>((version, method, uri, id) => request.Setup(a => a.Uri).Returns(uri))
|
|
||||||
.Returns(request.Object);
|
|
||||||
|
|
||||||
factory = Mock.Get(Api2.RequestFactory);
|
|
||||||
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
|
||||||
.Callback<Version, HttpMethod, Uri, int>((version, method, uri, id) => request.Setup(a => a.Uri).Returns(uri))
|
|
||||||
.Returns(request.Object);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class TestRestApi1Client : RestApiClient<TestEnvironment, TestAuthProvider, HMACCredential>
|
|
||||||
{
|
|
||||||
protected override IRestMessageHandler MessageHandler { get; } = new TestRestMessageHandler();
|
|
||||||
|
|
||||||
public TestRestApi1Client(TestClientOptions options) : base(new TraceLogger(), null, "https://localhost:123", options, options.Api1Options)
|
|
||||||
{
|
|
||||||
RequestFactory = new Mock<IRequestFactory>().Object;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
|
|
||||||
|
|
||||||
protected override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(new System.Text.Json.JsonSerializerOptions());
|
|
||||||
|
|
||||||
public async Task<CallResult<T>> Request<T>(CancellationToken ct = default) where T : class
|
|
||||||
{
|
|
||||||
return await SendAsync<T>("http://www.test.com", new RequestDefinition("/", HttpMethod.Get) { Weight = 0 }, null, ct);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<CallResult<T>> RequestWithParams<T>(HttpMethod method, ParameterCollection parameters, Dictionary<string, string> headers) where T : class
|
|
||||||
{
|
|
||||||
return await SendAsync<T>("http://www.test.com", new RequestDefinition("/", method) { Weight = 0 }, parameters, default, additionalHeaders: headers);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetParameterPosition(HttpMethod method, HttpMethodParameterPosition position)
|
|
||||||
{
|
|
||||||
ParameterPositions[method] = position;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override TestAuthProvider CreateAuthenticationProvider(HMACCredential credentials)
|
|
||||||
=> new TestAuthProvider(credentials);
|
|
||||||
|
|
||||||
protected override Task<WebCallResult<DateTime>> GetServerTimestampAsync()
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class TestRestApi2Client : RestApiClient<TestEnvironment, TestAuthProvider, HMACCredential>
|
|
||||||
{
|
|
||||||
protected override IRestMessageHandler MessageHandler { get; } = new TestRestMessageHandler();
|
|
||||||
|
|
||||||
public TestRestApi2Client(TestClientOptions options) : base(new TraceLogger(), null, "https://localhost:123", options, options.Api2Options)
|
|
||||||
{
|
|
||||||
RequestFactory = new Mock<IRequestFactory>().Object;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(new System.Text.Json.JsonSerializerOptions());
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
|
|
||||||
|
|
||||||
public async Task<CallResult<T>> Request<T>(CancellationToken ct = default) where T : class
|
|
||||||
{
|
|
||||||
return await SendAsync<T>("http://www.test.com", new RequestDefinition("/", HttpMethod.Get) { Weight = 0 }, null, ct);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override TestAuthProvider CreateAuthenticationProvider(HMACCredential credentials)
|
|
||||||
=> new TestAuthProvider(credentials);
|
|
||||||
|
|
||||||
protected override Task<WebCallResult<DateTime>> GetServerTimestampAsync()
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public class TestError
|
|
||||||
{
|
|
||||||
[JsonPropertyName("errorCode")]
|
|
||||||
public int ErrorCode { get; set; }
|
|
||||||
[JsonPropertyName("errorMessage")]
|
|
||||||
public string ErrorMessage { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ParseErrorTestRestClient: TestRestClient
|
|
||||||
{
|
|
||||||
public ParseErrorTestRestClient() { }
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
|
|
||||||
using CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers;
|
|
||||||
using CryptoExchange.Net.Objects;
|
|
||||||
using CryptoExchange.Net.Objects.Errors;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Net.Http.Headers;
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|
||||||
{
|
|
||||||
internal class TestRestMessageHandler : JsonRestMessageHandler
|
|
||||||
{
|
|
||||||
private ErrorMapping _errorMapping = new ErrorMapping([]);
|
|
||||||
public override JsonSerializerOptions Options => new JsonSerializerOptions();
|
|
||||||
|
|
||||||
public override async ValueTask<Error> ParseErrorResponse(int httpStatusCode, HttpResponseHeaders responseHeaders, Stream responseStream)
|
|
||||||
{
|
|
||||||
var result = await GetJsonDocument(responseStream).ConfigureAwait(false);
|
|
||||||
if (result.Item1 != null)
|
|
||||||
return result.Item1;
|
|
||||||
|
|
||||||
var errorData = result.Item2.Deserialize<TestError>();
|
|
||||||
return new ServerError(errorData.ErrorCode, _errorMapping.GetErrorInfo(errorData.ErrorCode.ToString(), errorData.ErrorMessage));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
using CryptoExchange.Net.UnitTests.TestImplementations;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests
|
|
||||||
{
|
|
||||||
[JsonSerializable(typeof(string))]
|
|
||||||
[JsonSerializable(typeof(int))]
|
|
||||||
[JsonSerializable(typeof(Dictionary<string, string>))]
|
|
||||||
[JsonSerializable(typeof(IDictionary<string, string>))]
|
|
||||||
[JsonSerializable(typeof(Dictionary<string, object>))]
|
|
||||||
[JsonSerializable(typeof(IDictionary<string, object>))]
|
|
||||||
[JsonSerializable(typeof(TestObject))]
|
|
||||||
internal partial class TestSerializerContext : JsonSerializerContext
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
using NUnit.Framework;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
using CryptoExchange.Net;
|
||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.UnitTests
|
||||||
|
{
|
||||||
|
internal class UriSerializationTests
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void CreateParamString_SerializesBasicValuesCorrectly()
|
||||||
|
{
|
||||||
|
var parameters = new Dictionary<string, object>()
|
||||||
|
{
|
||||||
|
{ "a", "1" },
|
||||||
|
{ "b", 2 },
|
||||||
|
{ "c", true }
|
||||||
|
};
|
||||||
|
|
||||||
|
var parameterString = parameters.CreateParamString(false, ArrayParametersSerialization.Array);
|
||||||
|
|
||||||
|
Assert.That(parameterString, Is.EqualTo("a=1&b=2&c=True"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void CreateParamString_SerializesArrayValuesCorrectly()
|
||||||
|
{
|
||||||
|
var parameters = new Dictionary<string, object>()
|
||||||
|
{
|
||||||
|
{ "a", new [] { "1", "2" } },
|
||||||
|
};
|
||||||
|
|
||||||
|
var parameterString = parameters.CreateParamString(false, ArrayParametersSerialization.Array);
|
||||||
|
|
||||||
|
Assert.That(parameterString, Is.EqualTo("a[]=1&a[]=2"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void CreateParamStringEncoded_SerializesArrayValuesCorrectly()
|
||||||
|
{
|
||||||
|
var parameters = new Dictionary<string, object>()
|
||||||
|
{
|
||||||
|
{ "a", new [] { "1+2", "2+3" } },
|
||||||
|
};
|
||||||
|
|
||||||
|
var parameterString = parameters.CreateParamString(true, ArrayParametersSerialization.Array);
|
||||||
|
|
||||||
|
Assert.That(parameterString, Is.EqualTo("a[]=1%2B2&a[]=2%2B3"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void CreateParamString_SerializesJsonArrayValuesCorrectly()
|
||||||
|
{
|
||||||
|
var parameters = new Dictionary<string, object>()
|
||||||
|
{
|
||||||
|
{ "a", new [] { "1", "2" } },
|
||||||
|
};
|
||||||
|
|
||||||
|
var parameterString = parameters.CreateParamString(false, ArrayParametersSerialization.JsonArray);
|
||||||
|
|
||||||
|
Assert.That(parameterString, Is.EqualTo("a=[1,2]"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void CreateParamStringEncoded_SerializesJsonArrayValuesCorrectly()
|
||||||
|
{
|
||||||
|
var parameters = new Dictionary<string, object>()
|
||||||
|
{
|
||||||
|
{ "a", new [] { "1+2", "2+3" } },
|
||||||
|
};
|
||||||
|
|
||||||
|
var parameterString = parameters.CreateParamString(true, ArrayParametersSerialization.JsonArray);
|
||||||
|
|
||||||
|
Assert.That(parameterString, Is.EqualTo("a=[1%2B2,2%2B3]"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void CreateParamString_SerializesMultipleValuesArrayCorrectly()
|
||||||
|
{
|
||||||
|
var parameters = new Dictionary<string, object>()
|
||||||
|
{
|
||||||
|
{ "a", new [] { "1", "2" } },
|
||||||
|
};
|
||||||
|
|
||||||
|
var parameterString = parameters.CreateParamString(false, ArrayParametersSerialization.MultipleValues);
|
||||||
|
|
||||||
|
Assert.That(parameterString, Is.EqualTo("a=1&a=2"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void CreateParamStringEncoded_SerializesMultipleValuesArrayCorrectly()
|
||||||
|
{
|
||||||
|
var parameters = new Dictionary<string, object>()
|
||||||
|
{
|
||||||
|
{ "a", new [] { "1+2", "2+3" } },
|
||||||
|
};
|
||||||
|
|
||||||
|
var parameterString = parameters.CreateParamString(true, ArrayParametersSerialization.MultipleValues);
|
||||||
|
|
||||||
|
Assert.That(parameterString, Is.EqualTo("a=1%2B2&a=2%2B3"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,8 +15,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleClient", "Examples\C
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharedClients", "Examples\SharedClients\SharedClients.csproj", "{988A87EF-EAEA-4313-A6CF-FA869813D5AB}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharedClients", "Examples\SharedClients\SharedClients.csproj", "{988A87EF-EAEA-4313-A6CF-FA869813D5AB}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CryptoExchange.Net.Protobuf", "CryptoExchange.Net.Protobuf\CryptoExchange.Net.Protobuf.csproj", "{CC6A807A-9183-6F41-8EF1-8A70172B0E83}"
|
|
||||||
EndProject
|
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
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}.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.ActiveCfg = Release|Any CPU
|
||||||
{988A87EF-EAEA-4313-A6CF-FA869813D5AB}.Release|Any CPU.Build.0 = 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
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#if NETSTANDARD2_0
|
namespace System.Diagnostics.CodeAnalysis
|
||||||
namespace System.Diagnostics.CodeAnalysis
|
|
||||||
{
|
{
|
||||||
using System;
|
using System;
|
||||||
|
#if NETSTANDARD2_0
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Specifies that <see langword="null"/> is allowed as an input even if the
|
/// Specifies that <see langword="null"/> is allowed as an input even if the
|
||||||
@@ -206,5 +206,26 @@ namespace System.Diagnostics.CodeAnalysis
|
|||||||
ReturnValue = returnValue;
|
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="serializer"></param>
|
||||||
/// <param name="parameters"></param>
|
/// <param name="parameters"></param>
|
||||||
/// <returns></returns>
|
/// <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)
|
if (serializer is not IStringMessageSerializer stringSerializer)
|
||||||
throw new InvalidOperationException("Non-string message serializer can't get serialized request body");
|
throw new InvalidOperationException("Non-string message serializer can't get serialized request body");
|
||||||
|
|
||||||
if (parameters?.Count == 1 && parameters.TryGetValue(Constants.BodyPlaceHolderKey, out object? value))
|
if (parameters?.BodyValue != null)
|
||||||
return stringSerializer.Serialize(value);
|
return stringSerializer.Serialize(parameters.BodyValue);
|
||||||
else
|
else
|
||||||
return stringSerializer.Serialize(parameters);
|
return stringSerializer.Serialize(parameters);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
using System;
|
|
||||||
using CryptoExchange.Net.Interfaces.Clients;
|
using CryptoExchange.Net.Interfaces.Clients;
|
||||||
using CryptoExchange.Net.Objects.Errors;
|
using CryptoExchange.Net.Objects.Errors;
|
||||||
using CryptoExchange.Net.Objects.Options;
|
using CryptoExchange.Net.Objects.Options;
|
||||||
|
using CryptoExchange.Net.RateLimiting;
|
||||||
using CryptoExchange.Net.SharedApis;
|
using CryptoExchange.Net.SharedApis;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Clients
|
namespace CryptoExchange.Net.Clients
|
||||||
{
|
{
|
||||||
@@ -25,7 +29,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// If we are disposing
|
/// If we are disposing
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected bool _disposing;
|
protected bool _disposed;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether a proxy is configured
|
/// Whether a proxy is configured
|
||||||
@@ -47,6 +51,11 @@ namespace CryptoExchange.Net.Clients
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The name of the exchange this client is for
|
||||||
|
/// </summary>
|
||||||
|
public string Exchange { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The environment this client communicates to
|
/// The environment this client communicates to
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -72,23 +81,34 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
protected internal virtual ErrorMapping ErrorMapping { get; } = new ErrorMapping([]);
|
protected internal virtual ErrorMapping ErrorMapping { get; } = new ErrorMapping([]);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Current rate limit admission override
|
||||||
|
/// </summary>
|
||||||
|
protected readonly AsyncLocal<RateLimitAdmission?> AdmissionOverride = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </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="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="baseAddress">Base address for this API client</param>
|
||||||
/// <param name="clientOptions">Client options</param>
|
/// <param name="clientOptions">Client options</param>
|
||||||
/// <param name="apiOptions">Api options</param>
|
/// <param name="apiOptions">Api options</param>
|
||||||
protected BaseApiClient(
|
protected BaseApiClient(
|
||||||
ILogger logger,
|
ILoggerFactory? loggerFactory,
|
||||||
|
string exchange,
|
||||||
bool outputOriginalData,
|
bool outputOriginalData,
|
||||||
string baseAddress,
|
string baseAddress,
|
||||||
ExchangeOptions clientOptions,
|
ExchangeOptions clientOptions,
|
||||||
ApiOptions apiOptions)
|
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;
|
ClientOptions = clientOptions;
|
||||||
ApiOptions = apiOptions;
|
ApiOptions = apiOptions;
|
||||||
OutputOriginalData = outputOriginalData;
|
OutputOriginalData = outputOriginalData;
|
||||||
@@ -110,12 +130,44 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public ErrorInfo GetErrorInfo(string code, string? message = null) => ErrorMapping.GetErrorInfo(code.ToString(), message);
|
public ErrorInfo GetErrorInfo(string code, string? message = null) => ErrorMapping.GetErrorInfo(code.ToString(), message);
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<TResult> WithRateLimitAdmissionAsync<TResult>(
|
||||||
|
RateLimitAdmission admission,
|
||||||
|
Func<Task<TResult>> operation)
|
||||||
|
{
|
||||||
|
if (admission == null)
|
||||||
|
throw new ArgumentNullException(nameof(admission));
|
||||||
|
if (operation == null)
|
||||||
|
throw new ArgumentNullException(nameof(operation));
|
||||||
|
|
||||||
|
var previous = AdmissionOverride.Value;
|
||||||
|
AdmissionOverride.Value = admission;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await operation().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
AdmissionOverride.Value = previous;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Dispose
|
/// Dispose
|
||||||
/// </summary>
|
/// </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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using CryptoExchange.Net.Authentication;
|
using CryptoExchange.Net.Authentication;
|
||||||
using CryptoExchange.Net.Objects.Options;
|
using CryptoExchange.Net.Objects.Options;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
@@ -89,7 +90,6 @@ namespace CryptoExchange.Net.Clients
|
|||||||
throw new ArgumentNullException(nameof(options));
|
throw new ArgumentNullException(nameof(options));
|
||||||
|
|
||||||
ClientOptions = options;
|
ClientOptions = options;
|
||||||
_logger.Log(LogLevel.Trace, $"Client configuration: {options}, CryptoExchange.Net: v{CryptoExchangeLibVersion}, {Exchange}.Net: v{ExchangeLibVersion}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -115,6 +115,12 @@ namespace CryptoExchange.Net.Clients
|
|||||||
return opts;
|
return opts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return $"{GetType().Name}, CryptoExchange.Net: v{CryptoExchangeLibVersion}, {Exchange}.Net: v{ExchangeLibVersion}, configuration: {ClientOptions}";
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Dispose
|
/// Dispose
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -14,6 +14,11 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class BaseRestClient : BaseClient, IRestClient
|
public abstract class BaseRestClient : BaseClient, IRestClient
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Api clients in this client
|
||||||
|
/// </summary>
|
||||||
|
internal new List<RestApiClient> ApiClients => base.ApiClients.OfType<RestApiClient>().ToList();
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public int TotalRequestsMade => ApiClients.OfType<RestApiClient>().Sum(s => s.TotalRequestsMade);
|
public int TotalRequestsMade => ApiClients.OfType<RestApiClient>().Sum(s => s.TotalRequestsMade);
|
||||||
|
|
||||||
@@ -29,6 +34,14 @@ namespace CryptoExchange.Net.Clients
|
|||||||
LibraryHelpers.StaticLogger = loggerFactory?.CreateLogger("CryptoExchange");
|
LibraryHelpers.StaticLogger = loggerFactory?.CreateLogger("CryptoExchange");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Update options
|
||||||
|
/// </summary>
|
||||||
|
public virtual void SetOptions(UpdateOptions options)
|
||||||
|
{
|
||||||
|
foreach (var apiClient in ApiClients)
|
||||||
|
apiClient.SetOptions(options);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -41,6 +54,11 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
internal new List<RestApiClient<TEnvironment, TApiCredentials>> ApiClients => base.ApiClients.OfType<RestApiClient<TEnvironment, TApiCredentials>>().ToList();
|
internal new List<RestApiClient<TEnvironment, TApiCredentials>> ApiClients => base.ApiClients.OfType<RestApiClient<TEnvironment, TApiCredentials>>().ToList();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provided client options
|
||||||
|
/// </summary>
|
||||||
|
public new RestExchangeOptions<TEnvironment, TApiCredentials> ClientOptions => (RestExchangeOptions<TEnvironment, TApiCredentials>)base.ClientOptions;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -54,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.
|
/// Set the API credentials for this client. All Api clients in this client will use the new credentials, regardless of earlier set options.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="credentials">The credentials to set</param>
|
/// <param name="credentials">The credentials to set</param>
|
||||||
public void SetApiCredentials(TApiCredentials credentials)
|
public virtual void SetApiCredentials(TApiCredentials credentials)
|
||||||
{
|
{
|
||||||
foreach (var apiClient in ApiClients)
|
foreach (var apiClient in ApiClients)
|
||||||
apiClient.SetApiCredentials(credentials);
|
apiClient.SetApiCredentials(credentials);
|
||||||
|
|||||||
@@ -21,6 +21,11 @@ namespace CryptoExchange.Net.Clients
|
|||||||
{
|
{
|
||||||
#region fields
|
#region fields
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Api clients in this client
|
||||||
|
/// </summary>
|
||||||
|
internal new List<SocketApiClient> ApiClients => base.ApiClients.OfType<SocketApiClient>().ToList();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// If client is disposing
|
/// If client is disposing
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -135,6 +140,15 @@ namespace CryptoExchange.Net.Clients
|
|||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Update options
|
||||||
|
/// </summary>
|
||||||
|
public virtual void SetOptions(UpdateOptions options)
|
||||||
|
{
|
||||||
|
foreach (var apiClient in ApiClients)
|
||||||
|
apiClient.SetOptions(options);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -147,6 +161,11 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
internal new List<SocketApiClient<TEnvironment, TApiCredentials>> ApiClients => base.ApiClients.OfType<SocketApiClient<TEnvironment, TApiCredentials>>().ToList();
|
internal new List<SocketApiClient<TEnvironment, TApiCredentials>> ApiClients => base.ApiClients.OfType<SocketApiClient<TEnvironment, TApiCredentials>>().ToList();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provided client options
|
||||||
|
/// </summary>
|
||||||
|
public new SocketExchangeOptions<TEnvironment, TApiCredentials> ClientOptions => (SocketExchangeOptions<TEnvironment, TApiCredentials>)base.ClientOptions;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -41,11 +41,6 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
protected internal RequestBodyFormat RequestBodyFormat = RequestBodyFormat.Json;
|
protected internal RequestBodyFormat RequestBodyFormat = RequestBodyFormat.Json;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// How to serialize array parameters when making requests
|
|
||||||
/// </summary>
|
|
||||||
protected internal ArrayParametersSerialization ArraySerialization = ArrayParametersSerialization.Array;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// What request body should be set when no data is send (only used in combination with postParametersPosition.InBody)
|
/// What request body should be set when no data is send (only used in combination with postParametersPosition.InBody)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -56,16 +51,6 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
protected Dictionary<string, string> StandardRequestHeaders { get; set; } = [];
|
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>
|
/// <summary>
|
||||||
/// Where to put the parameters for requests with different Http methods
|
/// Where to put the parameters for requests with different Http methods
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -108,26 +93,33 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// Get the AuthenticationProvider implementation, or null if no ApiCredentials are set
|
/// Get the AuthenticationProvider implementation, or null if no ApiCredentials are set
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual AuthenticationProvider? GetAuthenticationProvider() => null;
|
public virtual AuthenticationProvider? GetAuthenticationProvider() => null;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Configured environment name
|
/// Configured environment name
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract string EnvironmentName { get; }
|
public abstract string EnvironmentName { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Request coalescer
|
||||||
|
/// </summary>
|
||||||
|
protected RequestCoalescer RequestCoalescer { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </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="httpClient">HttpClient to use</param>
|
||||||
/// <param name="baseAddress">Base address for this API client</param>
|
/// <param name="baseAddress">Base address for this API client</param>
|
||||||
/// <param name="options">The base client options</param>
|
/// <param name="options">The base client options</param>
|
||||||
/// <param name="apiOptions">The Api client options</param>
|
/// <param name="apiOptions">The Api client options</param>
|
||||||
public RestApiClient(ILogger logger,
|
public RestApiClient(ILoggerFactory? loggerFactory,
|
||||||
|
string exchangeName,
|
||||||
HttpClient? httpClient,
|
HttpClient? httpClient,
|
||||||
string baseAddress,
|
string baseAddress,
|
||||||
RestExchangeOptions options,
|
RestExchangeOptions options,
|
||||||
RestApiOptions apiOptions)
|
RestApiOptions apiOptions)
|
||||||
: base(logger,
|
: base(loggerFactory,
|
||||||
|
exchangeName,
|
||||||
apiOptions.OutputOriginalData ?? options.OutputOriginalData,
|
apiOptions.OutputOriginalData ?? options.OutputOriginalData,
|
||||||
baseAddress,
|
baseAddress,
|
||||||
options,
|
options,
|
||||||
@@ -135,6 +127,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
{
|
{
|
||||||
TimeOffsetManager.RegisterRestApi(ClientName);
|
TimeOffsetManager.RegisterRestApi(ClientName);
|
||||||
|
|
||||||
|
RequestCoalescer = new RequestCoalescer(_logger);
|
||||||
RequestFactory.Configure(options, httpClient);
|
RequestFactory.Configure(options, httpClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,33 +137,10 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected abstract IMessageSerializer CreateSerializer();
|
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>
|
/// <summary>
|
||||||
/// Send a request to the base address based on the request definition
|
/// Send a request to the base address based on the request definition
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="T">Response type</typeparam>
|
/// <typeparam name="T">Response type</typeparam>
|
||||||
/// <param name="baseAddress">Host and schema</param>
|
|
||||||
/// <param name="definition">Request definition</param>
|
/// <param name="definition">Request definition</param>
|
||||||
/// <param name="parameters">Request parameters</param>
|
/// <param name="parameters">Request parameters</param>
|
||||||
/// <param name="cancellationToken">Cancellation token</param>
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
@@ -179,10 +149,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="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>
|
/// <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>
|
/// <returns></returns>
|
||||||
protected virtual Task<WebCallResult<T>> SendAsync<T>(
|
protected virtual Task<HttpResult<T>> SendAsync<T>(
|
||||||
string baseAddress,
|
|
||||||
RequestDefinition definition,
|
RequestDefinition definition,
|
||||||
ParameterCollection? parameters,
|
Parameters? parameters,
|
||||||
CancellationToken cancellationToken,
|
CancellationToken cancellationToken,
|
||||||
Dictionary<string, string>? additionalHeaders = null,
|
Dictionary<string, string>? additionalHeaders = null,
|
||||||
int? weight = null,
|
int? weight = null,
|
||||||
@@ -191,7 +160,6 @@ namespace CryptoExchange.Net.Clients
|
|||||||
{
|
{
|
||||||
var parameterPosition = definition.ParameterPosition ?? ParameterPositions[definition.Method];
|
var parameterPosition = definition.ParameterPosition ?? ParameterPositions[definition.Method];
|
||||||
return SendAsync<T>(
|
return SendAsync<T>(
|
||||||
baseAddress,
|
|
||||||
definition,
|
definition,
|
||||||
parameterPosition == HttpMethodParameterPosition.InUri ? parameters : null,
|
parameterPosition == HttpMethodParameterPosition.InUri ? parameters : null,
|
||||||
parameterPosition == HttpMethodParameterPosition.InBody ? parameters : null,
|
parameterPosition == HttpMethodParameterPosition.InBody ? parameters : null,
|
||||||
@@ -206,7 +174,6 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// Send a request to the base address based on the request definition
|
/// Send a request to the base address based on the request definition
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="T">Response type</typeparam>
|
/// <typeparam name="T">Response type</typeparam>
|
||||||
/// <param name="baseAddress">Host and schema</param>
|
|
||||||
/// <param name="definition">Request definition</param>
|
/// <param name="definition">Request definition</param>
|
||||||
/// <param name="uriParameters">Request query parameters</param>
|
/// <param name="uriParameters">Request query parameters</param>
|
||||||
/// <param name="bodyParameters">Request body parameters</param>
|
/// <param name="bodyParameters">Request body parameters</param>
|
||||||
@@ -215,12 +182,65 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <param name="weight">Override the request weight for this request definition, for example when the weight depends on the parameters</param>
|
/// <param name="weight">Override the request weight for this request definition, for example when the weight depends on the parameters</param>
|
||||||
/// <param name="weightSingleLimiter">Specify the weight to apply to the individual rate limit guard for this request</param>
|
/// <param name="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>
|
/// <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<HttpResult<T>> SendAsync<T>(
|
||||||
protected virtual async Task<WebCallResult<T>> SendAsync<T>(
|
|
||||||
string baseAddress,
|
|
||||||
RequestDefinition definition,
|
RequestDefinition definition,
|
||||||
ParameterCollection? uriParameters,
|
Parameters? uriParameters,
|
||||||
ParameterCollection? bodyParameters,
|
Parameters? bodyParameters,
|
||||||
|
CancellationToken cancellationToken,
|
||||||
|
Dictionary<string, string>? additionalHeaders = null,
|
||||||
|
int? weight = null,
|
||||||
|
int? weightSingleLimiter = null,
|
||||||
|
string? rateLimitKeySuffix = null)
|
||||||
|
{
|
||||||
|
if (!ShouldCoalesce(definition, additionalHeaders))
|
||||||
|
{
|
||||||
|
return SendCoreAsync<T>(
|
||||||
|
definition,
|
||||||
|
uriParameters,
|
||||||
|
bodyParameters,
|
||||||
|
cancellationToken,
|
||||||
|
additionalHeaders,
|
||||||
|
weight,
|
||||||
|
weightSingleLimiter,
|
||||||
|
rateLimitKeySuffix);
|
||||||
|
}
|
||||||
|
|
||||||
|
var key = GetRequestCoalescingKey<T>(
|
||||||
|
definition,
|
||||||
|
uriParameters);
|
||||||
|
|
||||||
|
return RequestCoalescer.ExecuteAsync(
|
||||||
|
key,
|
||||||
|
requestCancellationToken => SendCoreAsync<T>(
|
||||||
|
definition,
|
||||||
|
uriParameters,
|
||||||
|
bodyParameters,
|
||||||
|
requestCancellationToken,
|
||||||
|
additionalHeaders,
|
||||||
|
weight,
|
||||||
|
weightSingleLimiter,
|
||||||
|
rateLimitKeySuffix),
|
||||||
|
() => HttpResult.Fail<T>(
|
||||||
|
Exchange,
|
||||||
|
new CancellationRequestedError()),
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Send a request to the base address based on the request definition
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">Response type</typeparam>
|
||||||
|
/// <param name="definition">Request definition</param>
|
||||||
|
/// <param name="uriParameters">Request query parameters</param>
|
||||||
|
/// <param name="bodyParameters">Request body 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>
|
||||||
|
/// <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>
|
||||||
|
protected virtual async Task<HttpResult<T>> SendCoreAsync<T>(RequestDefinition definition,
|
||||||
|
Parameters? uriParameters,
|
||||||
|
Parameters? bodyParameters,
|
||||||
CancellationToken cancellationToken,
|
CancellationToken cancellationToken,
|
||||||
Dictionary<string, string>? additionalHeaders = null,
|
Dictionary<string, string>? additionalHeaders = null,
|
||||||
int? weight = null,
|
int? weight = null,
|
||||||
@@ -231,20 +251,20 @@ namespace CryptoExchange.Net.Clients
|
|||||||
if (definition.Authenticated && GetAuthenticationProvider() == null)
|
if (definition.Authenticated && GetAuthenticationProvider() == null)
|
||||||
{
|
{
|
||||||
_logger.RestApiNoApiCredentials(requestId, definition.Path);
|
_logger.RestApiNoApiCredentials(requestId, definition.Path);
|
||||||
return new WebCallResult<T>(new NoApiCredentialsError());
|
return HttpResult.Fail<T>(Exchange, new NoApiCredentialsError());
|
||||||
}
|
}
|
||||||
|
|
||||||
string? cacheKey = null;
|
string? cacheKey = null;
|
||||||
if (ShouldCache(definition))
|
if (ShouldCache(definition))
|
||||||
{
|
{
|
||||||
cacheKey = baseAddress + definition + uriParameters?.ToFormData();
|
cacheKey = GetCacheKey(definition, uriParameters);
|
||||||
_logger.CheckingCache(cacheKey);
|
_logger.CheckingCache(cacheKey);
|
||||||
var cachedValue = _cache.Get(cacheKey, ClientOptions.CachingMaxAge);
|
var cachedValue = _cache.Get(cacheKey, ClientOptions.CachingMaxAge);
|
||||||
if (cachedValue != null)
|
if (cachedValue != null)
|
||||||
{
|
{
|
||||||
_logger.CacheHit(cacheKey);
|
_logger.CacheHit(cacheKey);
|
||||||
var original = (WebCallResult<T>)cachedValue;
|
var original = (HttpResult<T>)cachedValue;
|
||||||
return original.Cached();
|
return original with { DataSource = ResultDataSource.Cache };
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.CacheNotHit(cacheKey);
|
_logger.CacheNotHit(cacheKey);
|
||||||
@@ -258,7 +278,6 @@ namespace CryptoExchange.Net.Clients
|
|||||||
await CheckTimeSync(requestId, definition).ConfigureAwait(false);
|
await CheckTimeSync(requestId, definition).ConfigureAwait(false);
|
||||||
|
|
||||||
var error = await RateLimitAsync(
|
var error = await RateLimitAsync(
|
||||||
baseAddress,
|
|
||||||
requestId,
|
requestId,
|
||||||
definition,
|
definition,
|
||||||
weight ?? definition.Weight,
|
weight ?? definition.Weight,
|
||||||
@@ -266,11 +285,10 @@ namespace CryptoExchange.Net.Clients
|
|||||||
weightSingleLimiter,
|
weightSingleLimiter,
|
||||||
rateLimitKeySuffix).ConfigureAwait(false);
|
rateLimitKeySuffix).ConfigureAwait(false);
|
||||||
if (error != null)
|
if (error != null)
|
||||||
return new WebCallResult<T>(error);
|
return HttpResult.Fail<T>(Exchange, error);
|
||||||
|
|
||||||
var request = CreateRequest(
|
var request = CreateRequest(
|
||||||
requestId,
|
requestId,
|
||||||
baseAddress,
|
|
||||||
definition,
|
definition,
|
||||||
uriParameters,
|
uriParameters,
|
||||||
bodyParameters,
|
bodyParameters,
|
||||||
@@ -284,7 +302,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
if (result.Error is not CancellationRequestedError)
|
if (result.Error is not CancellationRequestedError)
|
||||||
{
|
{
|
||||||
var originalData = OutputOriginalData ? result.OriginalData : "[Data only available when OutputOriginal = true]";
|
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);
|
_logger.RestApiErrorReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), result.Error?.ToString(), originalData, result.Error?.Exception);
|
||||||
}
|
}
|
||||||
@@ -316,7 +334,6 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// Check rate limits for the request
|
/// Check rate limits for the request
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected virtual async ValueTask<Error?> RateLimitAsync(
|
protected virtual async ValueTask<Error?> RateLimitAsync(
|
||||||
string host,
|
|
||||||
int requestId,
|
int requestId,
|
||||||
RequestDefinition definition,
|
RequestDefinition definition,
|
||||||
int weight,
|
int weight,
|
||||||
@@ -333,18 +350,21 @@ namespace CryptoExchange.Net.Clients
|
|||||||
|
|
||||||
if (ClientOptions.RateLimiterEnabled)
|
if (ClientOptions.RateLimiterEnabled)
|
||||||
{
|
{
|
||||||
|
var admissionOverride = AdmissionOverride.Value ?? ClientOptions.RateLimitAdmission?.Invoke(definition, requestWeight);
|
||||||
|
var rateRatio = admissionOverride?.MaxUtilizationRatio ?? 1.0;
|
||||||
|
|
||||||
var limitResult = await definition.RateLimitGate.ProcessAsync(
|
var limitResult = await definition.RateLimitGate.ProcessAsync(
|
||||||
_logger,
|
_logger,
|
||||||
requestId,
|
requestId,
|
||||||
RateLimitItemType.Request,
|
RateLimitItemType.Request,
|
||||||
definition,
|
definition,
|
||||||
host,
|
|
||||||
GetAuthenticationProvider()?.Key,
|
GetAuthenticationProvider()?.Key,
|
||||||
requestWeight,
|
requestWeight,
|
||||||
ClientOptions.RateLimitingBehaviour,
|
ClientOptions.RateLimitingBehaviour,
|
||||||
rateLimitKeySuffix,
|
rateLimitKeySuffix + ClientOptions.RateLimitGroup,
|
||||||
|
rateRatio,
|
||||||
cancellationToken).ConfigureAwait(false);
|
cancellationToken).ConfigureAwait(false);
|
||||||
if (!limitResult)
|
if (!limitResult.Success)
|
||||||
return limitResult.Error!;
|
return limitResult.Error!;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -358,19 +378,22 @@ namespace CryptoExchange.Net.Clients
|
|||||||
if (ClientOptions.RateLimiterEnabled)
|
if (ClientOptions.RateLimiterEnabled)
|
||||||
{
|
{
|
||||||
var singleRequestWeight = weightSingleLimiter ?? 1;
|
var singleRequestWeight = weightSingleLimiter ?? 1;
|
||||||
|
var admissionOverride = AdmissionOverride.Value ?? ClientOptions.RateLimitAdmission?.Invoke(definition, singleRequestWeight);
|
||||||
|
var rateRatio = admissionOverride?.MaxUtilizationRatio ?? 1.0;
|
||||||
|
|
||||||
var limitResult = await definition.RateLimitGate.ProcessSingleAsync(
|
var limitResult = await definition.RateLimitGate.ProcessSingleAsync(
|
||||||
_logger,
|
_logger,
|
||||||
requestId,
|
requestId,
|
||||||
definition.LimitGuard,
|
definition.LimitGuard,
|
||||||
RateLimitItemType.Request,
|
RateLimitItemType.Request,
|
||||||
definition,
|
definition,
|
||||||
host,
|
|
||||||
GetAuthenticationProvider()?.Key,
|
GetAuthenticationProvider()?.Key,
|
||||||
singleRequestWeight,
|
singleRequestWeight,
|
||||||
ClientOptions.RateLimitingBehaviour,
|
ClientOptions.RateLimitingBehaviour,
|
||||||
rateLimitKeySuffix,
|
rateLimitKeySuffix,
|
||||||
|
rateRatio,
|
||||||
cancellationToken).ConfigureAwait(false);
|
cancellationToken).ConfigureAwait(false);
|
||||||
if (!limitResult)
|
if (!limitResult.Success)
|
||||||
return limitResult.Error!;
|
return limitResult.Error!;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -382,7 +405,6 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// Creates a request object
|
/// Creates a request object
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="requestId">Id of the request</param>
|
/// <param name="requestId">Id of the request</param>
|
||||||
/// <param name="baseAddress">Host and schema</param>
|
|
||||||
/// <param name="definition">Request definition</param>
|
/// <param name="definition">Request definition</param>
|
||||||
/// <param name="uriParameters">The query parameters of the request</param>
|
/// <param name="uriParameters">The query parameters of the request</param>
|
||||||
/// <param name="bodyParameters">The body parameters of the request</param>
|
/// <param name="bodyParameters">The body parameters of the request</param>
|
||||||
@@ -390,19 +412,16 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected virtual IRequest CreateRequest(
|
protected virtual IRequest CreateRequest(
|
||||||
int requestId,
|
int requestId,
|
||||||
string baseAddress,
|
|
||||||
RequestDefinition definition,
|
RequestDefinition definition,
|
||||||
ParameterCollection? uriParameters,
|
Parameters? uriParameters,
|
||||||
ParameterCollection? bodyParameters,
|
Parameters? bodyParameters,
|
||||||
Dictionary<string, string>? additionalHeaders)
|
Dictionary<string, string>? additionalHeaders)
|
||||||
{
|
{
|
||||||
var requestConfiguration = new RestRequestConfiguration(
|
var requestConfiguration = new RestRequestConfiguration(
|
||||||
definition,
|
definition,
|
||||||
baseAddress,
|
uriParameters,
|
||||||
uriParameters == null ? null : CreateParameterDictionary(uriParameters),
|
bodyParameters,
|
||||||
bodyParameters == null ? null : CreateParameterDictionary(bodyParameters),
|
|
||||||
additionalHeaders,
|
additionalHeaders,
|
||||||
definition.ArraySerialization ?? ArraySerialization,
|
|
||||||
definition.ParameterPosition ?? ParameterPositions[definition.Method],
|
definition.ParameterPosition ?? ParameterPositions[definition.Method],
|
||||||
definition.RequestBodyFormat ?? RequestBodyFormat);
|
definition.RequestBodyFormat ?? RequestBodyFormat);
|
||||||
|
|
||||||
@@ -414,20 +433,16 @@ namespace CryptoExchange.Net.Clients
|
|||||||
{
|
{
|
||||||
throw new Exception("Failed to authenticate request, make sure your API credentials are correct", ex);
|
throw new Exception("Failed to authenticate request, make sure your API credentials are correct", ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
var queryString = requestConfiguration.GetQueryString(true);
|
var queryString = requestConfiguration.GetQueryString(true);
|
||||||
if (!string.IsNullOrEmpty(queryString) && !queryString.StartsWith("?"))
|
if (!string.IsNullOrEmpty(queryString) && !queryString.StartsWith("?"))
|
||||||
queryString = $"?{queryString}";
|
queryString = $"?{queryString}";
|
||||||
|
|
||||||
var path = baseAddress.AppendPath(definition.Path);
|
var uri = new Uri(definition.FullUrl + queryString);
|
||||||
if (definition.ForcePathEndWithSlash == true && !path.EndsWith("/"))
|
|
||||||
path += "/";
|
|
||||||
|
|
||||||
var uri = new Uri(path + queryString);
|
|
||||||
var request = RequestFactory.Create(ClientOptions.HttpVersion, definition.Method, uri, requestId);
|
var request = RequestFactory.Create(ClientOptions.HttpVersion, definition.Method, uri, requestId);
|
||||||
request.Accept = MessageHandler.AcceptHeader;
|
request.Accept = MessageHandler.AcceptHeader;
|
||||||
|
|
||||||
if (requestConfiguration.Headers != null)
|
if (requestConfiguration.Headers != null)
|
||||||
{
|
{
|
||||||
foreach (var header in requestConfiguration.Headers)
|
foreach (var header in requestConfiguration.Headers)
|
||||||
request.AddHeader(header.Key, header.Value);
|
request.AddHeader(header.Key, header.Value);
|
||||||
@@ -436,10 +451,12 @@ namespace CryptoExchange.Net.Clients
|
|||||||
foreach (var header in StandardRequestHeaders)
|
foreach (var header in StandardRequestHeaders)
|
||||||
{
|
{
|
||||||
// Only add it if it isn't overwritten
|
// Only add it if it isn't overwritten
|
||||||
requestConfiguration.Headers ??= new Dictionary<string, string>();
|
if (requestConfiguration.Headers == null
|
||||||
if (!requestConfiguration.Headers.ContainsKey(header.Key))
|
|| !requestConfiguration.Headers.ContainsKey(header.Key))
|
||||||
|
{
|
||||||
request.AddHeader(header.Key, header.Value);
|
request.AddHeader(header.Key, header.Value);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (requestConfiguration.ParameterPosition == HttpMethodParameterPosition.InBody)
|
if (requestConfiguration.ParameterPosition == HttpMethodParameterPosition.InBody)
|
||||||
{
|
{
|
||||||
@@ -451,10 +468,10 @@ namespace CryptoExchange.Net.Clients
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (requestConfiguration.BodyParameters != null && requestConfiguration.BodyParameters.Count != 0)
|
if (requestConfiguration.BodyParameters != null && !requestConfiguration.BodyParameters.Empty)
|
||||||
WriteParamBody(request, requestConfiguration.BodyParameters, contentType);
|
WriteParamBody(request, requestConfiguration.BodyParameters, contentType);
|
||||||
else if (OmitContentTypeHeaderWithoutContent != true)
|
else if (OmitContentTypeHeaderWithoutContent != true)
|
||||||
request.SetContent(RequestBodyEmptyContent, RequestBodyContentEncoding, contentType);
|
request.SetContent(RequestBodyEmptyContent, RequestBodyContentEncoding, contentType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -469,7 +486,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <param name="gate">The ratelimit gate used</param>
|
/// <param name="gate">The ratelimit gate used</param>
|
||||||
/// <param name="cancellationToken">Cancellation token</param>
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected virtual async Task<WebCallResult<T>> GetResponseAsync2<T>(
|
protected virtual async Task<HttpResult<T>> GetResponseAsync2<T>(
|
||||||
RequestDefinition requestDefinition,
|
RequestDefinition requestDefinition,
|
||||||
IRequest request,
|
IRequest request,
|
||||||
IRateLimitGate? gate,
|
IRateLimitGate? gate,
|
||||||
@@ -535,16 +552,16 @@ namespace CryptoExchange.Net.Clients
|
|||||||
{
|
{
|
||||||
_logger.LogError(ex, "Unhandled exception when parsing error response: {Message}", ex.Message);
|
_logger.LogError(ex, "Unhandled exception when parsing error response: {Message}", ex.Message);
|
||||||
var errorResult = new ServerError(ErrorInfo.Unknown with { 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
|
// 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
|
// Data response received, inspect the message and check if it is an error or not
|
||||||
var parsedError = await MessageHandler.CheckForErrorResponse(
|
var parsedError = await MessageHandler.CheckForErrorResponse(
|
||||||
@@ -563,7 +580,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Success status code, but TryParseError determined it was an error response
|
// 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)
|
if (MessageHandler.RequiresSeekableStream)
|
||||||
@@ -571,45 +588,45 @@ namespace CryptoExchange.Net.Clients
|
|||||||
responseStream.Position = 0;
|
responseStream.Position = 0;
|
||||||
|
|
||||||
// Try deserialization into the expected type
|
// 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)
|
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
|
try
|
||||||
{
|
{
|
||||||
// Check the deserialized response to see if it's an error or not
|
// Check the deserialized response to see if it's an error or not
|
||||||
var responseError = MessageHandler.CheckDeserializedResponse(response.ResponseHeaders, deserializeResult);
|
var responseError = MessageHandler.CheckDeserializedResponse(response.ResponseHeaders, deserializeResult);
|
||||||
if (responseError != null)
|
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)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Unhandled exception when checking deserialized response: {Message}", ex.Message);
|
_logger.LogError(ex, "Unhandled exception when checking deserialized response: {Message}", ex.Message);
|
||||||
var error = new ServerError(ErrorInfo.Unknown with { 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)
|
catch (HttpRequestException requestException)
|
||||||
{
|
{
|
||||||
// Request exception, can't reach server for instance
|
// Request exception, can't reach server for instance
|
||||||
var error = new WebError(requestException.Message, requestException);
|
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)
|
catch (OperationCanceledException canceledException)
|
||||||
{
|
{
|
||||||
if (cancellationToken != default && canceledException.CancellationToken == cancellationToken)
|
if (cancellationToken != default && canceledException.CancellationToken == cancellationToken)
|
||||||
{
|
{
|
||||||
// Cancellation token canceled by caller
|
// 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
|
else
|
||||||
{
|
{
|
||||||
// Request timed out
|
// Request timed out
|
||||||
var error = new WebError($"Request timed out", exception: canceledException);
|
var error = new WebError($"Request timed out", exception: canceledException);
|
||||||
error.ErrorType = ErrorType.Timeout;
|
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)
|
catch (ArgumentException argumentException)
|
||||||
@@ -618,7 +635,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
{
|
{
|
||||||
// Unsupported HTTP version error .net framework
|
// Unsupported HTTP version error .net framework
|
||||||
var error = ArgumentError.Invalid(nameof(RestExchangeOptions.HttpVersion), $"Invalid HTTP version {request.HttpVersion}: " + argumentException.Message);
|
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;
|
throw;
|
||||||
@@ -629,7 +646,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
{
|
{
|
||||||
// Unsupported HTTP version error dotnet code
|
// Unsupported HTTP version error dotnet code
|
||||||
var error = ArgumentError.Invalid(nameof(RestExchangeOptions.HttpVersion), $"Invalid HTTP version {request.HttpVersion}: " + notSupportedException.Message);
|
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;
|
throw;
|
||||||
@@ -641,16 +658,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>
|
/// <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.
|
/// 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
|
/// Note that this is always called; even when the request might be successful
|
||||||
/// </summary>
|
/// </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="gate">The rate limit gate the call used</param>
|
||||||
/// <param name="callResult">The result of the call</param>
|
/// <param name="callResult">The result of the call</param>
|
||||||
/// <param name="tries">The current try number</param>
|
/// <param name="tries">The current try number</param>
|
||||||
/// <returns>True if call should retry, false if the call should return</returns>
|
/// <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)
|
if (tries >= 2)
|
||||||
// Only retry once
|
// Only retry once
|
||||||
@@ -681,7 +737,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <param name="request">The request to set the parameters on</param>
|
/// <param name="request">The request to set the parameters on</param>
|
||||||
/// <param name="parameters">The parameters to set</param>
|
/// <param name="parameters">The parameters to set</param>
|
||||||
/// <param name="contentType">The content type of the data</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)
|
if (contentType == Constants.JsonContentHeader)
|
||||||
{
|
{
|
||||||
@@ -691,10 +747,18 @@ namespace CryptoExchange.Net.Clients
|
|||||||
|
|
||||||
// Write the parameters as json in the body
|
// Write the parameters as json in the body
|
||||||
string stringData;
|
string stringData;
|
||||||
if (parameters.Count == 1 && parameters.TryGetValue(Constants.BodyPlaceHolderKey, out object? value))
|
if (parameters.BodyValue != null)
|
||||||
stringData = stringSerializer.Serialize(value);
|
{
|
||||||
|
if (parameters.BodyValue is string bodyString)
|
||||||
|
stringData = bodyString;
|
||||||
|
else
|
||||||
|
stringData = stringSerializer.Serialize(parameters.BodyValue);
|
||||||
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
stringData = stringSerializer.Serialize(parameters);
|
stringData = stringSerializer.Serialize(parameters);
|
||||||
|
}
|
||||||
|
|
||||||
request.SetContent(stringData, RequestBodyContentEncoding, contentType);
|
request.SetContent(stringData, RequestBodyContentEncoding, contentType);
|
||||||
}
|
}
|
||||||
else if (contentType == Constants.FormContentHeader)
|
else if (contentType == Constants.FormContentHeader)
|
||||||
@@ -705,24 +769,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>
|
/// <summary>
|
||||||
/// Retrieve the server time for the purpose of syncing time between client and server to prevent authentication issues
|
/// Retrieve the server time for the purpose of syncing time between client and server to prevent authentication issues
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>Server time</returns>
|
/// <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)
|
private async ValueTask CheckTimeSync(int requestId, RequestDefinition definition)
|
||||||
{
|
{
|
||||||
@@ -757,7 +808,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
var localTime = DateTime.UtcNow;
|
var localTime = DateTime.UtcNow;
|
||||||
WebCallResult<DateTime> result;
|
HttpResult<DateTime> result;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
result = await GetServerTimestampAsync().ConfigureAwait(false);
|
result = await GetServerTimestampAsync().ConfigureAwait(false);
|
||||||
@@ -767,7 +818,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
throw new ArgumentException("AutoTimestamp is not available for this API");
|
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");
|
_logger.LogWarning("Failed to determine time offset between client and server, timestamping might fail");
|
||||||
return;
|
return;
|
||||||
@@ -778,7 +829,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
|
// If this was the first request make another one to calculate the offset since the first one can be slower
|
||||||
localTime = DateTime.UtcNow;
|
localTime = DateTime.UtcNow;
|
||||||
result = await GetServerTimestampAsync().ConfigureAwait(false);
|
result = await GetServerTimestampAsync().ConfigureAwait(false);
|
||||||
if (!result)
|
if (!result.Success)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Failed to determine time offset between client and server, timestamping might fail");
|
_logger.LogWarning("Failed to determine time offset between client and server, timestamping might fail");
|
||||||
return;
|
return;
|
||||||
@@ -819,6 +870,51 @@ namespace CryptoExchange.Net.Clients
|
|||||||
&& definition.Method == HttpMethod.Get
|
&& definition.Method == HttpMethod.Get
|
||||||
&& !definition.PreventCaching;
|
&& !definition.PreventCaching;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Produce a unique cache key for the request based on the request definition and parameters.
|
||||||
|
/// </summary>
|
||||||
|
protected virtual string GetCacheKey(RequestDefinition definition, Parameters? parameters)
|
||||||
|
{
|
||||||
|
return definition.FullUrl + definition + parameters?.ToFormData();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether a request can be coalesced or not
|
||||||
|
/// </summary>
|
||||||
|
protected virtual bool ShouldCoalesce(
|
||||||
|
RequestDefinition definition,
|
||||||
|
Dictionary<string, string>? additionalHeaders)
|
||||||
|
{
|
||||||
|
return ClientOptions.RequestCoalescingEnabled // Enabled in client options
|
||||||
|
&& !definition.PreventRequestCoalescing // Enabled in request definition
|
||||||
|
&& definition.Method == HttpMethod.Get // Is a GET request
|
||||||
|
&& !definition.Authenticated // Not an authenticated request
|
||||||
|
&& (additionalHeaders == null || additionalHeaders.Count == 0); // No additional headers that might change the response
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get a unique key for coalescing requests based on the request definition, parameters, and the type of the expected response.
|
||||||
|
/// </summary>
|
||||||
|
protected virtual RequestCoalescingKey GetRequestCoalescingKey<T>(
|
||||||
|
RequestDefinition definition,
|
||||||
|
Parameters? uriParameters)
|
||||||
|
{
|
||||||
|
return new RequestCoalescingKey(
|
||||||
|
definition.Method.Method,
|
||||||
|
definition.FullUrl,
|
||||||
|
uriParameters?.ToFormData(),
|
||||||
|
typeof(T));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public virtual void SetOptions(UpdateOptions options)
|
||||||
|
{
|
||||||
|
_proxyConfigured = options.Proxy != null;
|
||||||
|
ClientOptions.Proxy = options.Proxy;
|
||||||
|
ClientOptions.RequestTimeout = options.RequestTimeout ?? ClientOptions.RequestTimeout;
|
||||||
|
|
||||||
|
RequestFactory.UpdateSettings(options.Proxy, options.RequestTimeout ?? ClientOptions.RequestTimeout, ClientOptions.HttpKeepAliveInterval);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -835,12 +931,14 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected RestApiClient(
|
protected RestApiClient(
|
||||||
ILogger logger,
|
ILoggerFactory? loggerFactory,
|
||||||
|
string exchangeName,
|
||||||
HttpClient? httpClient,
|
HttpClient? httpClient,
|
||||||
string baseAddress,
|
string baseAddress,
|
||||||
RestExchangeOptions options,
|
RestExchangeOptions options,
|
||||||
RestApiOptions apiOptions) : base(
|
RestApiOptions apiOptions) : base(
|
||||||
logger,
|
loggerFactory,
|
||||||
|
exchangeName,
|
||||||
httpClient,
|
httpClient,
|
||||||
baseAddress,
|
baseAddress,
|
||||||
options,
|
options,
|
||||||
@@ -867,12 +965,14 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected RestApiClient(
|
protected RestApiClient(
|
||||||
ILogger logger,
|
ILoggerFactory? loggerFactory,
|
||||||
|
string exchangeName,
|
||||||
HttpClient? httpClient,
|
HttpClient? httpClient,
|
||||||
string baseAddress,
|
string baseAddress,
|
||||||
RestExchangeOptions<TEnvironment, TApiCredentials> options,
|
RestExchangeOptions<TEnvironment, TApiCredentials> options,
|
||||||
RestApiOptions apiOptions) : base(
|
RestApiOptions apiOptions) : base(
|
||||||
logger,
|
loggerFactory,
|
||||||
|
exchangeName,
|
||||||
httpClient,
|
httpClient,
|
||||||
baseAddress,
|
baseAddress,
|
||||||
options,
|
options,
|
||||||
@@ -890,12 +990,15 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public virtual void SetOptions(UpdateOptions<TApiCredentials> options)
|
public virtual void SetOptions(UpdateOptions<TApiCredentials> options)
|
||||||
{
|
{
|
||||||
_proxyConfigured = options.Proxy != null;
|
base.SetOptions(options);
|
||||||
ClientOptions.Proxy = options.Proxy;
|
|
||||||
ClientOptions.RequestTimeout = options.RequestTimeout ?? ClientOptions.RequestTimeout;
|
|
||||||
|
|
||||||
ApiCredentials = (TApiCredentials?)options.ApiCredentials?.Copy() ?? ApiCredentials;
|
ApiCredentials = (TApiCredentials?)options.ApiCredentials?.Copy() ?? ApiCredentials;
|
||||||
RequestFactory.UpdateSettings(options.Proxy, options.RequestTimeout ?? ClientOptions.RequestTimeout, ClientOptions.HttpKeepAliveInterval);
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override string GetCacheKey(RequestDefinition definition, Parameters? parameters)
|
||||||
|
{
|
||||||
|
return definition.FullUrl + definition + parameters?.ToFormData() + GetAuthenticationProvider()?.Key;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -905,13 +1008,18 @@ namespace CryptoExchange.Net.Clients
|
|||||||
where TAuthenticationProvider : AuthenticationProvider<TApiCredentials>
|
where TAuthenticationProvider : AuthenticationProvider<TApiCredentials>
|
||||||
where TEnvironment : TradeEnvironment
|
where TEnvironment : TradeEnvironment
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
private bool _authProviderInitialized = false;
|
/// Auth provider initialized field
|
||||||
private TAuthenticationProvider? _authenticationProvider;
|
/// </summary>
|
||||||
|
protected bool _authProviderInitialized = false;
|
||||||
|
/// <summary>
|
||||||
|
/// Auth provider field
|
||||||
|
/// </summary>
|
||||||
|
protected TAuthenticationProvider? _authenticationProvider;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The authentication provider for this API client. (null if no credentials are set)
|
/// The authentication provider for this API client. (null if no credentials are set)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public TAuthenticationProvider? AuthenticationProvider
|
public virtual TAuthenticationProvider? AuthenticationProvider
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
@@ -925,7 +1033,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
|
|
||||||
return _authenticationProvider;
|
return _authenticationProvider;
|
||||||
}
|
}
|
||||||
internal set => _authenticationProvider = value;
|
protected internal set => _authenticationProvider = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -935,12 +1043,14 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected RestApiClient(
|
protected RestApiClient(
|
||||||
ILogger logger,
|
ILoggerFactory? loggerFactory,
|
||||||
|
string exchangeName,
|
||||||
HttpClient? httpClient,
|
HttpClient? httpClient,
|
||||||
string baseAddress,
|
string baseAddress,
|
||||||
RestExchangeOptions<TEnvironment, TApiCredentials> options,
|
RestExchangeOptions<TEnvironment, TApiCredentials> options,
|
||||||
RestApiOptions apiOptions) : base(
|
RestApiOptions apiOptions) : base(
|
||||||
logger,
|
loggerFactory,
|
||||||
|
exchangeName,
|
||||||
httpClient,
|
httpClient,
|
||||||
baseAddress,
|
baseAddress,
|
||||||
options,
|
options,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ using CryptoExchange.Net.Sockets.Default.Interfaces;
|
|||||||
using CryptoExchange.Net.Sockets.HighPerf;
|
using CryptoExchange.Net.Sockets.HighPerf;
|
||||||
using CryptoExchange.Net.Sockets.HighPerf.Interfaces;
|
using CryptoExchange.Net.Sockets.HighPerf.Interfaces;
|
||||||
using CryptoExchange.Net.Sockets.Interfaces;
|
using CryptoExchange.Net.Sockets.Interfaces;
|
||||||
|
using CryptoExchange.Net.TokenManagement;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
@@ -71,11 +72,6 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
protected List<SystemSubscription> systemSubscriptions = new();
|
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>
|
/// <summary>
|
||||||
/// The rate limiters
|
/// The rate limiters
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -101,6 +97,11 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
protected bool AllowTopicsOnTheSameConnection { get; set; } = true;
|
protected bool AllowTopicsOnTheSameConnection { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether to allow usage of a current public WebSocket connection for an authenticated request
|
||||||
|
/// </summary>
|
||||||
|
protected bool UsePublicConnectionForAuth { get; set; } = false;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public double IncomingKbps
|
public double IncomingKbps
|
||||||
{
|
{
|
||||||
@@ -153,21 +154,26 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// Configured environment name
|
/// Configured environment name
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract string EnvironmentName { get; }
|
public abstract string EnvironmentName { get; }
|
||||||
|
|
||||||
|
private int _isDisposed;
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </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="options">Client options</param>
|
||||||
/// <param name="baseAddress">Base address for this API client</param>
|
/// <param name="baseAddress">Base address for this API client</param>
|
||||||
/// <param name="apiOptions">The Api client options</param>
|
/// <param name="apiOptions">The Api client options</param>
|
||||||
public SocketApiClient(
|
public SocketApiClient(
|
||||||
ILogger logger,
|
ILoggerFactory? loggerFactory,
|
||||||
|
string exchangeName,
|
||||||
string baseAddress,
|
string baseAddress,
|
||||||
SocketExchangeOptions options,
|
SocketExchangeOptions options,
|
||||||
SocketApiOptions apiOptions)
|
SocketApiOptions apiOptions)
|
||||||
: base(logger,
|
: base(loggerFactory,
|
||||||
|
exchangeName,
|
||||||
apiOptions.OutputOriginalData ?? options.OutputOriginalData,
|
apiOptions.OutputOriginalData ?? options.OutputOriginalData,
|
||||||
baseAddress,
|
baseAddress,
|
||||||
options,
|
options,
|
||||||
@@ -216,7 +222,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <param name="interval"></param>
|
/// <param name="interval"></param>
|
||||||
/// <param name="queryDelegate"></param>
|
/// <param name="queryDelegate"></param>
|
||||||
/// <param name="callback"></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
|
PeriodicTaskRegistrations.Add(new PeriodicTaskRegistration
|
||||||
{
|
{
|
||||||
@@ -233,7 +239,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <param name="subscription">The subscription</param>
|
/// <param name="subscription">The subscription</param>
|
||||||
/// <param name="ct">Cancellation token for closing this subscription</param>
|
/// <param name="ct">Cancellation token for closing this subscription</param>
|
||||||
/// <returns></returns>
|
/// <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);
|
return SubscribeAsync(BaseAddress, subscription, ct);
|
||||||
}
|
}
|
||||||
@@ -245,86 +251,102 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <param name="subscription">The subscription</param>
|
/// <param name="subscription">The subscription</param>
|
||||||
/// <param name="ct">Cancellation token for closing this subscription</param>
|
/// <param name="ct">Cancellation token for closing this subscription</param>
|
||||||
/// <returns></returns>
|
/// <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)
|
bool successResult = false;
|
||||||
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
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await semaphoreSlim.WaitAsync(ct).ConfigureAwait(false);
|
if (_disposed)
|
||||||
}
|
return WebSocketResult.Fail<UpdateSubscription>(Exchange, new InvalidOperationError("Client disposed, can't subscribe"));
|
||||||
catch (OperationCanceledException tce)
|
|
||||||
{
|
|
||||||
return new CallResult<UpdateSubscription>(new CancellationRequestedError(tce));
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
if (subscription.Authenticated && GetAuthenticationProvider() == null)
|
||||||
{
|
|
||||||
while (true)
|
|
||||||
{
|
{
|
||||||
// Get a new or existing socket connection
|
_logger.LogWarning("Failed to subscribe, private subscription but no API credentials set");
|
||||||
var socketResult = await GetSocketConnection(url, subscription.Authenticated, false, ct, subscription.Topic, subscription.IndividualSubscriptionCount).ConfigureAwait(false);
|
return WebSocketResult.Fail<UpdateSubscription>(Exchange, new NoApiCredentialsError());
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
finally
|
||||||
{
|
{
|
||||||
if (!released)
|
if (!successResult && subscription.TokenLease != null)
|
||||||
semaphoreSlim.Release();
|
_ = 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>
|
/// <summary>
|
||||||
@@ -335,14 +357,14 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <param name="connectionFactory">The factory for creating a socket connection</param>
|
/// <param name="connectionFactory">The factory for creating a socket connection</param>
|
||||||
/// <param name="ct">Cancellation token for closing this subscription</param>
|
/// <param name="ct">Cancellation token for closing this subscription</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected virtual async Task<CallResult<HighPerfUpdateSubscription>> SubscribeHighPerfAsync<TUpdateType>(
|
protected virtual async Task<WebSocketResult<HighPerfUpdateSubscription>> SubscribeHighPerfAsync<TUpdateType>(
|
||||||
string url,
|
string url,
|
||||||
HighPerfSubscription<TUpdateType> subscription,
|
HighPerfSubscription<TUpdateType> subscription,
|
||||||
IHighPerfConnectionFactory connectionFactory,
|
IHighPerfConnectionFactory connectionFactory,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
{
|
{
|
||||||
if (_disposing)
|
if (_disposed)
|
||||||
return new CallResult<HighPerfUpdateSubscription>(new InvalidOperationError("Client disposed, can't subscribe"));
|
return WebSocketResult.Fail<HighPerfUpdateSubscription>(Exchange, new InvalidOperationError("Client disposed, can't subscribe"));
|
||||||
|
|
||||||
HighPerfSocketConnection<TUpdateType> socketConnection;
|
HighPerfSocketConnection<TUpdateType> socketConnection;
|
||||||
var released = false;
|
var released = false;
|
||||||
@@ -354,7 +376,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
}
|
}
|
||||||
catch (OperationCanceledException tce)
|
catch (OperationCanceledException tce)
|
||||||
{
|
{
|
||||||
return new CallResult<HighPerfUpdateSubscription>(new CancellationRequestedError(tce));
|
return WebSocketResult.Fail<HighPerfUpdateSubscription>(Exchange, new CancellationRequestedError(tce));
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -363,8 +385,8 @@ namespace CryptoExchange.Net.Clients
|
|||||||
{
|
{
|
||||||
// Get a new or existing socket connection
|
// Get a new or existing socket connection
|
||||||
var socketResult = await GetHighPerfSocketConnection<TUpdateType>(url, connectionFactory, ct).ConfigureAwait(false);
|
var socketResult = await GetHighPerfSocketConnection<TUpdateType>(url, connectionFactory, ct).ConfigureAwait(false);
|
||||||
if (!socketResult)
|
if (!socketResult.Success)
|
||||||
return socketResult.As<HighPerfUpdateSubscription>(null);
|
return WebSocketResult.Fail<HighPerfUpdateSubscription>(Exchange, socketResult.Error);
|
||||||
|
|
||||||
socketConnection = socketResult.Data;
|
socketConnection = socketResult.Data;
|
||||||
|
|
||||||
@@ -383,11 +405,9 @@ namespace CryptoExchange.Net.Clients
|
|||||||
released = true;
|
released = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
var needsConnecting = !socketConnection.Connected;
|
|
||||||
|
|
||||||
var connectResult = await ConnectIfNeededAsync(socketConnection, false, ct).ConfigureAwait(false);
|
var connectResult = await ConnectIfNeededAsync(socketConnection, false, ct).ConfigureAwait(false);
|
||||||
if (!connectResult)
|
if (!connectResult.Success)
|
||||||
return new CallResult<HighPerfUpdateSubscription>(connectResult.Error!);
|
return WebSocketResult.Fail<HighPerfUpdateSubscription>(Exchange, connectResult.Error!);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -403,10 +423,10 @@ namespace CryptoExchange.Net.Clients
|
|||||||
{
|
{
|
||||||
// Send the request and wait for answer
|
// Send the request and wait for answer
|
||||||
var sendResult = await socketConnection.SendAsync(subRequest).ConfigureAwait(false);
|
var sendResult = await socketConnection.SendAsync(subRequest).ConfigureAwait(false);
|
||||||
if (!sendResult)
|
if (!sendResult.Success)
|
||||||
{
|
{
|
||||||
await socketConnection.CloseAsync().ConfigureAwait(false);
|
await socketConnection.CloseAsync().ConfigureAwait(false);
|
||||||
return new CallResult<HighPerfUpdateSubscription>(sendResult.Error!);
|
return WebSocketResult.Fail<HighPerfUpdateSubscription>(Exchange, sendResult.Error!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -420,7 +440,13 @@ namespace CryptoExchange.Net.Clients
|
|||||||
}
|
}
|
||||||
|
|
||||||
_logger.SubscriptionCompletedSuccessfully(socketConnection.SocketId, subscription.Id);
|
_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>
|
/// <summary>
|
||||||
@@ -430,7 +456,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <param name="query">The query</param>
|
/// <param name="query">The query</param>
|
||||||
/// <param name="ct">Cancellation token</param>
|
/// <param name="ct">Cancellation token</param>
|
||||||
/// <returns></returns>
|
/// <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);
|
return QueryAsync(BaseAddress, query, ct);
|
||||||
}
|
}
|
||||||
@@ -443,13 +469,13 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <param name="query">The query</param>
|
/// <param name="query">The query</param>
|
||||||
/// <param name="ct">Cancellation token</param>
|
/// <param name="ct">Cancellation token</param>
|
||||||
/// <returns></returns>
|
/// <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)
|
if (_disposed)
|
||||||
return new CallResult<THandlerResponse>(new InvalidOperationError("Client disposed, can't query"));
|
return QueryResult.Fail<THandlerResponse>(Exchange, new InvalidOperationError("Client disposed, can't query"));
|
||||||
|
|
||||||
if (ct.IsCancellationRequested)
|
if (ct.IsCancellationRequested)
|
||||||
return new CallResult<THandlerResponse>(new CancellationRequestedError());
|
return QueryResult.Fail<THandlerResponse>(Exchange, new CancellationRequestedError());
|
||||||
|
|
||||||
SocketConnection socketConnection;
|
SocketConnection socketConnection;
|
||||||
var released = false;
|
var released = false;
|
||||||
@@ -457,8 +483,8 @@ namespace CryptoExchange.Net.Clients
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var socketResult = await GetSocketConnection(url, query.Authenticated, true, ct).ConfigureAwait(false);
|
var socketResult = await GetSocketConnection(url, query.Authenticated, true, ct).ConfigureAwait(false);
|
||||||
if (!socketResult)
|
if (!socketResult.Success)
|
||||||
return socketResult.As<THandlerResponse>(default);
|
return QueryResult.Fail<THandlerResponse>(Exchange, socketResult.Error);
|
||||||
|
|
||||||
socketConnection = socketResult.Data;
|
socketConnection = socketResult.Data;
|
||||||
|
|
||||||
@@ -470,8 +496,8 @@ namespace CryptoExchange.Net.Clients
|
|||||||
}
|
}
|
||||||
|
|
||||||
var connectResult = await ConnectIfNeededAsync(socketConnection, query.Authenticated, ct).ConfigureAwait(false);
|
var connectResult = await ConnectIfNeededAsync(socketConnection, query.Authenticated, ct).ConfigureAwait(false);
|
||||||
if (!connectResult)
|
if (!connectResult.Success)
|
||||||
return new CallResult<THandlerResponse>(connectResult.Error!);
|
return QueryResult.Fail<THandlerResponse>(Exchange, connectResult.Error!);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -482,11 +508,11 @@ namespace CryptoExchange.Net.Clients
|
|||||||
if (socketConnection.PausedActivity)
|
if (socketConnection.PausedActivity)
|
||||||
{
|
{
|
||||||
_logger.HasBeenPausedCantSendQueryAtThisMoment(socketConnection.SocketId);
|
_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)
|
if (ct.IsCancellationRequested)
|
||||||
return new CallResult<THandlerResponse>(new CancellationRequestedError());
|
return QueryResult.Fail<THandlerResponse>(Exchange, new CancellationRequestedError());
|
||||||
|
|
||||||
return await socketConnection.SendAndWaitQueryAsync(query, ct).ConfigureAwait(false);
|
return await socketConnection.SendAndWaitQueryAsync(query, ct).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
@@ -501,23 +527,23 @@ namespace CryptoExchange.Net.Clients
|
|||||||
protected virtual async Task<CallResult> ConnectIfNeededAsync(ISocketConnection socket, bool authenticated, CancellationToken ct)
|
protected virtual async Task<CallResult> ConnectIfNeededAsync(ISocketConnection socket, bool authenticated, CancellationToken ct)
|
||||||
{
|
{
|
||||||
if (socket.Connected)
|
if (socket.Connected)
|
||||||
return CallResult.SuccessResult;
|
return CallResult.Ok();
|
||||||
|
|
||||||
var connectResult = await ConnectSocketAsync(socket, ct).ConfigureAwait(false);
|
var connectResult = await ConnectSocketAsync(socket, ct).ConfigureAwait(false);
|
||||||
if (!connectResult)
|
if (!connectResult.Success)
|
||||||
return connectResult;
|
return connectResult;
|
||||||
|
|
||||||
if (ClientOptions.DelayAfterConnect != TimeSpan.Zero)
|
if (ClientOptions.DelayAfterConnect != TimeSpan.Zero)
|
||||||
await Task.Delay(ClientOptions.DelayAfterConnect).ConfigureAwait(false);
|
await Task.Delay(ClientOptions.DelayAfterConnect).ConfigureAwait(false);
|
||||||
|
|
||||||
if (!authenticated || socket.Authenticated)
|
if (!authenticated || socket.Authenticated)
|
||||||
return CallResult.SuccessResult;
|
return CallResult.Ok();
|
||||||
|
|
||||||
if (socket is not SocketConnection sc)
|
if (socket is not SocketConnection sc)
|
||||||
throw new InvalidOperationException("HighPerfSocketConnection not supported for authentication");
|
throw new InvalidOperationException("HighPerfSocketConnection not supported for authentication");
|
||||||
|
|
||||||
var result = await AuthenticateSocketAsync(sc).ConfigureAwait(false);
|
var result = await AuthenticateSocketAsync(sc).ConfigureAwait(false);
|
||||||
if (!result)
|
if (!result.Success)
|
||||||
await socket.CloseAsync().ConfigureAwait(false);
|
await socket.CloseAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@@ -531,29 +557,28 @@ namespace CryptoExchange.Net.Clients
|
|||||||
public virtual async Task<CallResult> AuthenticateSocketAsync(SocketConnection socket)
|
public virtual async Task<CallResult> AuthenticateSocketAsync(SocketConnection socket)
|
||||||
{
|
{
|
||||||
if (GetAuthenticationProvider() == null)
|
if (GetAuthenticationProvider() == null)
|
||||||
return new CallResult(new NoApiCredentialsError());
|
return CallResult.Fail(new NoApiCredentialsError());
|
||||||
|
|
||||||
_logger.AttemptingToAuthenticate(socket.SocketId);
|
_logger.AttemptingToAuthenticate(socket.SocketId);
|
||||||
var authRequest = await GetAuthenticationRequestAsync(socket).ConfigureAwait(false);
|
var authRequest = await GetAuthenticationRequestAsync(socket).ConfigureAwait(false);
|
||||||
if (authRequest != null)
|
if (authRequest != null)
|
||||||
{
|
{
|
||||||
var result = await socket.SendAndWaitQueryAsync(authRequest).ConfigureAwait(false);
|
var result = await socket.SendAndWaitQueryAsync(authRequest).ConfigureAwait(false);
|
||||||
|
if (!result.Success)
|
||||||
if (!result)
|
|
||||||
{
|
{
|
||||||
_logger.AuthenticationFailed(socket.SocketId);
|
_logger.AuthenticationFailed(socket.SocketId);
|
||||||
if (socket.Connected)
|
if (socket.Connected)
|
||||||
await socket.CloseAsync().ConfigureAwait(false);
|
await socket.CloseAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
result.Error!.Message = "Authentication failed: " + result.Error.Message;
|
result.Error!.Message = "Authentication failed: " + result.Error.Message;
|
||||||
return new CallResult(result.Error)!;
|
return CallResult.Fail(result.Error)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.Authenticated(socket.SocketId);
|
_logger.Authenticated(socket.SocketId);
|
||||||
}
|
}
|
||||||
|
|
||||||
socket.Authenticated = true;
|
socket.Authenticated = true;
|
||||||
return CallResult.SuccessResult;
|
return CallResult.Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -582,7 +607,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected virtual Task<CallResult<string?>> GetConnectionUrlAsync(string address, bool authentication)
|
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>
|
/// <summary>
|
||||||
@@ -602,7 +627,22 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected internal virtual Task<CallResult> RevitalizeRequestAsync(Subscription subscription)
|
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>
|
/// <summary>
|
||||||
@@ -623,24 +663,20 @@ namespace CryptoExchange.Net.Clients
|
|||||||
string? topic = null,
|
string? topic = null,
|
||||||
int individualSubscriptionCount = 1)
|
int individualSubscriptionCount = 1)
|
||||||
{
|
{
|
||||||
var socketQuery = _socketConnections.Where(s => s.Value.Tag.TrimEnd('/') == address.TrimEnd('/')
|
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
|
||||||
&& s.Value.ApiClient.GetType() == GetType()
|
|
||||||
&& (AllowTopicsOnTheSameConnection || !s.Value.Topics.Contains(topic)))
|
|
||||||
.Select(x => x.Value)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
// If all current socket connections are reconnecting or resubscribing wait for that to finish as we can probably use the existing connection
|
// 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 delayStart = DateTime.UtcNow;
|
||||||
var delayed = false;
|
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 (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
|
// If after this time we still trying to reconnect/reprocess there is some issue in the connection
|
||||||
_logger.TimeoutWaitingForReconnectingSocket();
|
_logger.TimeoutWaitingForReconnectingSocket();
|
||||||
return new CallResult<SocketConnection>(new CantConnectError());
|
return CallResult.Fail<SocketConnection>(new CantConnectError());
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@@ -650,68 +686,54 @@ namespace CryptoExchange.Net.Clients
|
|||||||
try { await Task.Delay(50, ct).ConfigureAwait(false); } catch (Exception) { }
|
try { await Task.Delay(50, ct).ConfigureAwait(false); } catch (Exception) { }
|
||||||
|
|
||||||
if (ct.IsCancellationRequested)
|
if (ct.IsCancellationRequested)
|
||||||
return new CallResult<SocketConnection>(new CancellationRequestedError());
|
return CallResult.Fail<SocketConnection>(new CancellationRequestedError());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (delayed)
|
if (delayed)
|
||||||
_logger.WaitedForReconnectingSocket((long)(DateTime.UtcNow - delayStart).TotalMilliseconds);
|
_logger.WaitedForReconnectingSocket((long)(DateTime.UtcNow - delayStart).TotalMilliseconds);
|
||||||
|
|
||||||
socketQuery = socketQuery.Where(s => (s.Status == SocketStatus.None || s.Status == SocketStatus.Connected)
|
socketQuery = socketQuery.Where(s => (s.Status == SocketStatus.None || s.Status == SocketStatus.Connected)
|
||||||
&& (s.Authenticated == authenticated || !authenticated)
|
&& (s.Authenticated == authenticated || !authenticated || UsePublicConnectionForAuth)
|
||||||
&& s.Connected).ToList();
|
&& s.Connected).ToList();
|
||||||
|
|
||||||
SocketConnection? connection;
|
bool maxConnectionsReached = _socketConnections.Count >= (ApiOptions.MaxSocketConnections ?? ClientOptions.MaxSocketConnections);
|
||||||
if (!dedicatedRequestConnection)
|
SocketConnection? connection = null;
|
||||||
{
|
if (dedicatedRequestConnection)
|
||||||
connection = socketQuery.Where(s => !s.DedicatedRequestConnection.IsDedicatedRequestConnection).OrderBy(s => s.UserSubscriptionCount).FirstOrDefault();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
connection = socketQuery.Where(s => s.DedicatedRequestConnection.IsDedicatedRequestConnection).FirstOrDefault();
|
connection = socketQuery.Where(s => s.DedicatedRequestConnection.IsDedicatedRequestConnection).FirstOrDefault();
|
||||||
if (connection != null && !connection.DedicatedRequestConnection.Authenticated)
|
if (connection != null && !connection.DedicatedRequestConnection.Authenticated)
|
||||||
// Mark dedicated request connection as authenticated if the request is authenticated
|
// Mark dedicated request connection as authenticated if the request is authenticated
|
||||||
connection.DedicatedRequestConnection.Authenticated = authenticated;
|
connection.DedicatedRequestConnection.Authenticated = authenticated;
|
||||||
|
|
||||||
if (connection == null)
|
|
||||||
// Fall back to an existing connection if there is no dedicated request connection available
|
|
||||||
connection = socketQuery.OrderBy(s => s.UserSubscriptionCount).FirstOrDefault();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool maxConnectionsReached = _socketConnections.Count >= (ApiOptions.MaxSocketConnections ?? ClientOptions.MaxSocketConnections);
|
if (connection == null)
|
||||||
if (connection != null)
|
|
||||||
{
|
{
|
||||||
bool lessThanBatchSubCombineTarget = connection.UserSubscriptionCount < ClientOptions.SocketSubscriptionsCombineTarget;
|
// Use an eligible non-dedicated connection for subscriptions, or as fallback when no dedicated request connection is available
|
||||||
bool lessThanIndividualSubCombineTarget = connection.Subscriptions.Sum(x => x.IndividualSubscriptionCount) < ClientOptions.SocketIndividualSubscriptionCombineTarget;
|
connection = socketQuery
|
||||||
|
.Where(s => !s.DedicatedRequestConnection.IsDedicatedRequestConnection)
|
||||||
if ((lessThanBatchSubCombineTarget && lessThanIndividualSubCombineTarget)
|
.Where(s => IsConnectionEligible(s, individualSubscriptionCount, maxConnectionsReached))
|
||||||
|| maxConnectionsReached)
|
.OrderBy(s => s.UserSubscriptionCount)
|
||||||
{
|
.FirstOrDefault();
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (connection != null)
|
||||||
|
return CallResult.Ok(connection);
|
||||||
|
|
||||||
if (maxConnectionsReached)
|
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);
|
var connectionAddress = await GetConnectionUrlAsync(address, authenticated).ConfigureAwait(false);
|
||||||
if (!connectionAddress)
|
if (!connectionAddress.Success)
|
||||||
{
|
{
|
||||||
_logger.FailedToDetermineConnectionUrl(connectionAddress.Error?.ToString());
|
_logger.FailedToDetermineConnectionUrl(connectionAddress.Error.ToString());
|
||||||
return connectionAddress.As<SocketConnection>(null);
|
return CallResult.Fail<SocketConnection>(connectionAddress.Error);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (connectionAddress.Data != address)
|
if (connectionAddress.Data != address)
|
||||||
_logger.ConnectionAddressSetTo(connectionAddress.Data!);
|
_logger.ConnectionAddressSetTo(connectionAddress.Data!);
|
||||||
|
|
||||||
// Create new socket connection
|
// 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;
|
socketConnection.ConnectRateLimitedAsync += HandleConnectRateLimitedAsync;
|
||||||
if (dedicatedRequestConnection)
|
if (dedicatedRequestConnection)
|
||||||
{
|
{
|
||||||
@@ -728,7 +750,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
foreach (var systemSubscription in systemSubscriptions)
|
foreach (var systemSubscription in systemSubscriptions)
|
||||||
socketConnection.AddSubscription(systemSubscription);
|
socketConnection.AddSubscription(systemSubscription);
|
||||||
|
|
||||||
return new CallResult<SocketConnection>(socketConnection);
|
return CallResult.Ok(socketConnection);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -745,21 +767,38 @@ namespace CryptoExchange.Net.Clients
|
|||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
{
|
{
|
||||||
var connectionAddress = await GetConnectionUrlAsync(address, false).ConfigureAwait(false);
|
var connectionAddress = await GetConnectionUrlAsync(address, false).ConfigureAwait(false);
|
||||||
if (!connectionAddress)
|
if (!connectionAddress.Success)
|
||||||
{
|
{
|
||||||
_logger.FailedToDetermineConnectionUrl(connectionAddress.Error?.ToString());
|
_logger.FailedToDetermineConnectionUrl(connectionAddress.Error.ToString());
|
||||||
return connectionAddress.As<HighPerfSocketConnection<TUpdateType>>(null);
|
return CallResult.Fail<HighPerfSocketConnection<TUpdateType>>(connectionAddress.Error);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (connectionAddress.Data != address)
|
if (connectionAddress.Data != address)
|
||||||
_logger.ConnectionAddressSetTo(connectionAddress.Data!);
|
_logger.ConnectionAddressSetTo(connectionAddress.Data!);
|
||||||
|
|
||||||
// Create new socket connection
|
// 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)
|
foreach (var ptg in PeriodicTaskRegistrations)
|
||||||
socketConnection.QueryPeriodic(ptg.Identifier, ptg.Interval, (con) => ptg.QueryDelegate(con).Request);
|
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>
|
/// <summary>
|
||||||
@@ -793,7 +832,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
protected virtual async Task<CallResult> ConnectSocketAsync(ISocketConnection socketConnection, CancellationToken ct)
|
protected virtual async Task<CallResult> ConnectSocketAsync(ISocketConnection socketConnection, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var connectResult = await socketConnection.ConnectAsync(ct).ConfigureAwait(false);
|
var connectResult = await socketConnection.ConnectAsync(ct).ConfigureAwait(false);
|
||||||
if (connectResult)
|
if (connectResult.Success)
|
||||||
{
|
{
|
||||||
if (socketConnection is SocketConnection sc)
|
if (socketConnection is SocketConnection sc)
|
||||||
_socketConnections.TryAdd(socketConnection.SocketId, sc);
|
_socketConnections.TryAdd(socketConnection.SocketId, sc);
|
||||||
@@ -819,6 +858,8 @@ namespace CryptoExchange.Net.Clients
|
|||||||
ReconnectInterval = ClientOptions.ReconnectInterval,
|
ReconnectInterval = ClientOptions.ReconnectInterval,
|
||||||
RateLimiter = ClientOptions.RateLimiterEnabled ? RateLimiter : null,
|
RateLimiter = ClientOptions.RateLimiterEnabled ? RateLimiter : null,
|
||||||
RateLimitingBehavior = ClientOptions.RateLimitingBehaviour,
|
RateLimitingBehavior = ClientOptions.RateLimitingBehaviour,
|
||||||
|
RateLimitAdmissionCallbackOptions = (def, weight) => ClientOptions.RateLimitAdmission?.Invoke(def, weight),
|
||||||
|
RateLimitAdmissionCallbackRequest = () => AdmissionOverride.Value,
|
||||||
Proxy = ClientOptions.Proxy,
|
Proxy = ClientOptions.Proxy,
|
||||||
Timeout = ApiOptions.SocketNoDataTimeout ?? ClientOptions.SocketNoDataTimeout,
|
Timeout = ApiOptions.SocketNoDataTimeout ?? ClientOptions.SocketNoDataTimeout,
|
||||||
ReceiveBufferSize = ClientOptions.ReceiveBufferSize,
|
ReceiveBufferSize = ClientOptions.ReceiveBufferSize,
|
||||||
@@ -877,7 +918,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
|
|
||||||
_logger.UnsubscribingAll(sum);
|
_logger.UnsubscribingAll(sum);
|
||||||
var tasks = new List<Task>();
|
var tasks = new List<Task>();
|
||||||
|
|
||||||
var socketList = _socketConnections.Values;
|
var socketList = _socketConnections.Values;
|
||||||
foreach (var connection in socketList)
|
foreach (var connection in socketList)
|
||||||
{
|
{
|
||||||
@@ -916,15 +957,15 @@ namespace CryptoExchange.Net.Clients
|
|||||||
foreach (var item in DedicatedConnectionConfigs)
|
foreach (var item in DedicatedConnectionConfigs)
|
||||||
{
|
{
|
||||||
var socketResult = await GetSocketConnection(item.SocketAddress, item.Authenticated, true, CancellationToken.None).ConfigureAwait(false);
|
var socketResult = await GetSocketConnection(item.SocketAddress, item.Authenticated, true, CancellationToken.None).ConfigureAwait(false);
|
||||||
if (!socketResult)
|
if (!socketResult.Success)
|
||||||
return socketResult.AsDataless();
|
return CallResult.Fail(socketResult.Error);
|
||||||
|
|
||||||
var connectResult = await ConnectIfNeededAsync(socketResult.Data, item.Authenticated, default).ConfigureAwait(false);
|
var connectResult = await ConnectIfNeededAsync(socketResult.Data, item.Authenticated, default).ConfigureAwait(false);
|
||||||
if (!connectResult)
|
if (!connectResult.Success)
|
||||||
return new CallResult(connectResult.Error!);
|
return CallResult.Fail(connectResult.Error!);
|
||||||
}
|
}
|
||||||
|
|
||||||
return CallResult.SuccessResult;
|
return CallResult.Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -1006,23 +1047,28 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Dispose the client
|
/// Dispose the client
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public override void Dispose()
|
protected override void Dispose(bool disposing)
|
||||||
{
|
{
|
||||||
_disposing = true;
|
if (Interlocked.CompareExchange(ref _isDisposed, 1, 0) == 0)
|
||||||
var tasks = new List<Task>();
|
|
||||||
{
|
{
|
||||||
var socketList = _socketConnections.Values.Where(x => x.UserSubscriptionCount > 0 || x.Connected);
|
if (!disposing)
|
||||||
if (socketList.Any())
|
return;
|
||||||
_logger.DisposingSocketClient();
|
|
||||||
|
|
||||||
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();
|
foreach (var connection in socketList)
|
||||||
base.Dispose();
|
{
|
||||||
|
tasks.Add(connection.CloseAsync());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
semaphoreSlim?.Dispose();
|
||||||
|
base.Dispose(disposing);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -1035,6 +1081,28 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public abstract ISocketMessageHandler CreateMessageConverter(WebSocketMessageType messageType);
|
public abstract ISocketMessageHandler CreateMessageConverter(WebSocketMessageType messageType);
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public virtual void SetOptions(UpdateOptions options)
|
||||||
|
{
|
||||||
|
var previousProxyIsSet = _proxyConfigured;
|
||||||
|
|
||||||
|
ClientOptions.Proxy = options.Proxy;
|
||||||
|
ClientOptions.RequestTimeout = options.RequestTimeout ?? ClientOptions.RequestTimeout;
|
||||||
|
|
||||||
|
_proxyConfigured = options.Proxy != null;
|
||||||
|
if ((!previousProxyIsSet && options.Proxy == null)
|
||||||
|
|| _socketConnections.IsEmpty)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Reconnecting websockets to apply proxy");
|
||||||
|
|
||||||
|
// Update proxy, also triggers reconnect
|
||||||
|
foreach (var connection in _socketConnections)
|
||||||
|
_ = connection.Value.UpdateProxy(options.Proxy);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -1051,11 +1119,13 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected SocketApiClient(
|
protected SocketApiClient(
|
||||||
ILogger logger,
|
ILoggerFactory? loggerFactory,
|
||||||
|
string exchangeName,
|
||||||
string baseAddress,
|
string baseAddress,
|
||||||
SocketExchangeOptions<TEnvironment> options,
|
SocketExchangeOptions<TEnvironment> options,
|
||||||
SocketApiOptions apiOptions) : base(
|
SocketApiOptions apiOptions) : base(
|
||||||
logger,
|
loggerFactory,
|
||||||
|
exchangeName,
|
||||||
baseAddress,
|
baseAddress,
|
||||||
options,
|
options,
|
||||||
apiOptions)
|
apiOptions)
|
||||||
@@ -1081,11 +1151,13 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected SocketApiClient(
|
protected SocketApiClient(
|
||||||
ILogger logger,
|
ILoggerFactory? loggerFactory,
|
||||||
|
string exchangeName,
|
||||||
string baseAddress,
|
string baseAddress,
|
||||||
SocketExchangeOptions<TEnvironment, TApiCredentials> options,
|
SocketExchangeOptions<TEnvironment, TApiCredentials> options,
|
||||||
SocketApiOptions apiOptions) : base(
|
SocketApiOptions apiOptions) : base(
|
||||||
logger,
|
loggerFactory,
|
||||||
|
exchangeName,
|
||||||
baseAddress,
|
baseAddress,
|
||||||
options,
|
options,
|
||||||
apiOptions)
|
apiOptions)
|
||||||
@@ -1102,25 +1174,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public virtual void SetOptions(UpdateOptions<TApiCredentials> options)
|
public virtual void SetOptions(UpdateOptions<TApiCredentials> options)
|
||||||
{
|
{
|
||||||
var previousProxyIsSet = _proxyConfigured;
|
base.SetOptions(options);
|
||||||
|
|
||||||
ClientOptions.Proxy = options.Proxy;
|
|
||||||
ClientOptions.RequestTimeout = options.RequestTimeout ?? ClientOptions.RequestTimeout;
|
|
||||||
|
|
||||||
ApiCredentials = (TApiCredentials?)options.ApiCredentials?.Copy() ?? ApiCredentials;
|
|
||||||
|
|
||||||
_proxyConfigured = options.Proxy != null;
|
|
||||||
if ((!previousProxyIsSet && options.Proxy == null)
|
|
||||||
|| _socketConnections.IsEmpty)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogInformation("Reconnecting websockets to apply proxy");
|
|
||||||
|
|
||||||
// Update proxy, also triggers reconnect
|
|
||||||
foreach (var connection in _socketConnections)
|
|
||||||
_ = connection.Value.UpdateProxy(options.Proxy);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1130,13 +1184,18 @@ namespace CryptoExchange.Net.Clients
|
|||||||
where TApiCredentials : ApiCredentials
|
where TApiCredentials : ApiCredentials
|
||||||
where TEnvironment : TradeEnvironment
|
where TEnvironment : TradeEnvironment
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
private bool _authProviderInitialized = false;
|
/// Auth provider initialized field
|
||||||
private TAuthenticationProvider? _authenticationProvider;
|
/// </summary>
|
||||||
|
protected bool _authProviderInitialized = false;
|
||||||
|
/// <summary>
|
||||||
|
/// Auth provider field
|
||||||
|
/// </summary>
|
||||||
|
protected TAuthenticationProvider? _authenticationProvider;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The authentication provider for this API client. (null if no credentials are set)
|
/// The authentication provider for this API client. (null if no credentials are set)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public TAuthenticationProvider? AuthenticationProvider
|
public virtual TAuthenticationProvider? AuthenticationProvider
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
@@ -1150,7 +1209,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
|
|
||||||
return _authenticationProvider;
|
return _authenticationProvider;
|
||||||
}
|
}
|
||||||
internal set => _authenticationProvider = value;
|
protected internal set => _authenticationProvider = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -1160,11 +1219,13 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected SocketApiClient(
|
protected SocketApiClient(
|
||||||
ILogger logger,
|
ILoggerFactory? loggerFactory,
|
||||||
|
string exchangeName,
|
||||||
string baseAddress,
|
string baseAddress,
|
||||||
SocketExchangeOptions<TEnvironment, TApiCredentials> options,
|
SocketExchangeOptions<TEnvironment, TApiCredentials> options,
|
||||||
SocketApiOptions apiOptions) : base(
|
SocketApiOptions apiOptions) : base(
|
||||||
logger,
|
loggerFactory,
|
||||||
|
exchangeName,
|
||||||
baseAddress,
|
baseAddress,
|
||||||
options,
|
options,
|
||||||
apiOptions)
|
apiOptions)
|
||||||
|
|||||||
@@ -0,0 +1,212 @@
|
|||||||
|
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, TRestOptions, TCredentials, TEnvironment>
|
||||||
|
where TRestClient : IRestClient<TCredentials>
|
||||||
|
where TRestOptions : RestExchangeOptions<TEnvironment, TCredentials>, new()
|
||||||
|
where TCredentials : ApiCredentials
|
||||||
|
where TEnvironment : TradeEnvironment
|
||||||
|
{
|
||||||
|
private ConcurrentDictionary<string, TRestClient> _restClients = new ConcurrentDictionary<string, TRestClient>();
|
||||||
|
private readonly IOptions<TRestOptions> _restOptions;
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
/// <summary>
|
||||||
|
/// Logger factory
|
||||||
|
/// </summary>
|
||||||
|
protected readonly ILoggerFactory? _loggerFactory;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public abstract string ExchangeName { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
public UserClientProvider(
|
||||||
|
HttpClient? httpClient,
|
||||||
|
ILoggerFactory? loggerFactory,
|
||||||
|
IOptions<TRestOptions> restOptions)
|
||||||
|
{
|
||||||
|
_httpClient = httpClient ?? new HttpClient();
|
||||||
|
_httpClient.Timeout = restOptions.Value.RequestTimeout;
|
||||||
|
_loggerFactory = loggerFactory;
|
||||||
|
_restOptions = restOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public virtual void InitializeUserClient(string userIdentifier, TCredentials credentials, TEnvironment? environment = null)
|
||||||
|
{
|
||||||
|
CreateRestClient(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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Constructs a new instance of the rest client
|
||||||
|
/// </summary>
|
||||||
|
protected abstract TRestClient ConstructRestClient(
|
||||||
|
HttpClient client,
|
||||||
|
ILoggerFactory? loggerFactory,
|
||||||
|
IOptions<TRestOptions> options);
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public virtual void ClearUserClients(string userIdentifier)
|
||||||
|
{
|
||||||
|
_restClients.TryRemove(userIdentifier, out var restClient);
|
||||||
|
restClient?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public virtual void Clear()
|
||||||
|
{
|
||||||
|
foreach (var client in _restClients.Values)
|
||||||
|
client.Dispose();
|
||||||
|
_restClients.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public abstract class UserClientProvider<TRestClient, TSocketClient, TRestOptions, TSocketOptions, TCredentials, TEnvironment>
|
||||||
|
: UserClientProvider<TRestClient, TRestOptions, 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, TSocketClient> _socketClients = new ConcurrentDictionary<string, TSocketClient>();
|
||||||
|
private readonly IOptions<TSocketOptions> _socketOptions;
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
public UserClientProvider(
|
||||||
|
HttpClient? httpClient,
|
||||||
|
ILoggerFactory? loggerFactory,
|
||||||
|
IOptions<TRestOptions> restOptions,
|
||||||
|
IOptions<TSocketOptions> socketOptions)
|
||||||
|
: base(httpClient, loggerFactory, restOptions)
|
||||||
|
{
|
||||||
|
_socketOptions = socketOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 override void InitializeUserClient(string userIdentifier, TCredentials credentials, TEnvironment? environment = null)
|
||||||
|
{
|
||||||
|
base.InitializeUserClient(userIdentifier, credentials, environment);
|
||||||
|
CreateSocketClient(userIdentifier, credentials, environment);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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 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 socket client
|
||||||
|
/// </summary>
|
||||||
|
protected abstract TSocketClient ConstructSocketClient(
|
||||||
|
ILoggerFactory? loggerFactory,
|
||||||
|
IOptions<TSocketOptions> options);
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override void ClearUserClients(string userIdentifier)
|
||||||
|
{
|
||||||
|
base.ClearUserClients(userIdentifier);
|
||||||
|
|
||||||
|
_socketClients.TryRemove(userIdentifier, out var socketClient);
|
||||||
|
socketClient?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override void Clear()
|
||||||
|
{
|
||||||
|
base.Clear();
|
||||||
|
|
||||||
|
foreach (var client in _socketClients.Values)
|
||||||
|
client.Dispose();
|
||||||
|
_socketClients.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -173,6 +173,9 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
|
|
||||||
if (targetType.IsAssignableFrom(value?.GetType()))
|
if (targetType.IsAssignableFrom(value?.GetType()))
|
||||||
attribute.PropertyInfo.SetValue(result, value);
|
attribute.PropertyInfo.SetValue(result, value);
|
||||||
|
else if (value is string stringValue && targetType == typeof(decimal))
|
||||||
|
// Convert.ChangeType doesn't accept scientific notation, which some exchanges use for small values ("4.803E-5")
|
||||||
|
attribute.PropertyInfo.SetValue(result, ExchangeHelpers.ParseDecimal(stringValue));
|
||||||
else
|
else
|
||||||
attribute.PropertyInfo.SetValue(result, value == null ? null : Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture));
|
attribute.PropertyInfo.SetValue(result, value == null ? null : Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,14 +56,26 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
if (reader.TokenType == JsonTokenType.False)
|
if (reader.TokenType == JsonTokenType.False)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
var value = reader.TokenType switch
|
if (reader.TokenType == JsonTokenType.Number)
|
||||||
{
|
{
|
||||||
JsonTokenType.String => reader.GetString(),
|
var number = reader.GetInt16();
|
||||||
JsonTokenType.Number => reader.GetInt16().ToString(),
|
if (number >= 1)
|
||||||
_ => null
|
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 (string.IsNullOrEmpty(value))
|
||||||
{
|
{
|
||||||
if (typeToConvert == typeof(bool))
|
if (typeToConvert == typeof(bool))
|
||||||
@@ -73,12 +85,14 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
|
|
||||||
switch (value)
|
switch (value)
|
||||||
{
|
{
|
||||||
|
case "enabled":
|
||||||
case "true":
|
case "true":
|
||||||
case "yes":
|
case "yes":
|
||||||
case "y":
|
case "y":
|
||||||
case "1":
|
case "1":
|
||||||
case "on":
|
case "on":
|
||||||
return true;
|
return true;
|
||||||
|
case "disabled":
|
||||||
case "false":
|
case "false":
|
||||||
case "no":
|
case "no":
|
||||||
case "n":
|
case "n":
|
||||||
@@ -88,7 +102,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
return false;
|
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 long _ticksPerSecond = TimeSpan.TicksPerMillisecond * 1000;
|
||||||
private const decimal _ticksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000m;
|
private const decimal _ticksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000m;
|
||||||
private const decimal _ticksPerNanosecond = TimeSpan.TicksPerMillisecond / 1000m / 1000;
|
private const decimal _ticksPerNanosecond = TimeSpan.TicksPerMillisecond / 1000m / 1000;
|
||||||
|
private static Type _dateTimeType = typeof(DateTime);
|
||||||
|
private static Type _nullableDateTimeType = typeof(DateTime?);
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override bool CanConvert(Type typeToConvert)
|
public override bool CanConvert(Type typeToConvert)
|
||||||
{
|
{
|
||||||
return typeToConvert == typeof(DateTime) || typeToConvert == typeof(DateTime?);
|
return typeToConvert == _dateTimeType || typeToConvert == _nullableDateTimeType;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
|
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?>
|
private class NullableDateTimeConverterInner : JsonConverter<DateTime?>
|
||||||
@@ -68,7 +70,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
{
|
{
|
||||||
if (reader.TokenType == JsonTokenType.Null)
|
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);
|
LibraryHelpers.StaticLogger?.LogWarning("DateTime value of null, but property is not nullable. Resolver: {Resolver}", options.TypeInfoResolver?.GetType()?.Name);
|
||||||
return default;
|
return default;
|
||||||
}
|
}
|
||||||
@@ -76,7 +78,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
if (reader.TokenType is JsonTokenType.Number)
|
if (reader.TokenType is JsonTokenType.Number)
|
||||||
{
|
{
|
||||||
var decValue = reader.GetDecimal();
|
var decValue = reader.GetDecimal();
|
||||||
if (decValue == 0 || decValue < 0)
|
if (decValue <= 0)
|
||||||
return default;
|
return default;
|
||||||
|
|
||||||
return ParseFromDecimal(decValue);
|
return ParseFromDecimal(decValue);
|
||||||
@@ -86,8 +88,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
var stringValue = reader.GetString();
|
var stringValue = reader.GetString();
|
||||||
if (string.IsNullOrWhiteSpace(stringValue)
|
if (string.IsNullOrWhiteSpace(stringValue)
|
||||||
|| stringValue!.Equals("-1", StringComparison.Ordinal)
|
|| stringValue!.Equals("-1", StringComparison.Ordinal)
|
||||||
|| stringValue!.Equals("0001-01-01T00:00:00Z", StringComparison.OrdinalIgnoreCase)
|
|| stringValue!.Equals("0001-01-01T00:00:00Z", StringComparison.OrdinalIgnoreCase))
|
||||||
|| decimal.TryParse(stringValue, out var decVal) && decVal == 0)
|
|
||||||
{
|
{
|
||||||
return default;
|
return default;
|
||||||
}
|
}
|
||||||
@@ -124,7 +125,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Parse a string value to datetime
|
/// Parse a string value to datetime
|
||||||
/// </summary>
|
/// </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))
|
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);
|
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);
|
return DateTime.Parse(stringValue, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -67,29 +67,29 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
#endif
|
#endif
|
||||||
: JsonConverter<T>, INullableConverterFactory where T : struct, Enum
|
: 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
|
#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 FrozenDictionary<T, string>? _mappingToString = null;
|
||||||
|
|
||||||
|
private static bool RunOptimistic => true;
|
||||||
#else
|
#else
|
||||||
private static List<EnumMapping>? _mappingToEnum = null;
|
private static Dictionary<string, T>? _mappingToEnum = null;
|
||||||
private static Dictionary<T, string>? _mappingToString = null;
|
private static Dictionary<T, string>? _mappingToString = null;
|
||||||
|
|
||||||
|
// In NetStandard the `ValueTextEquals` method used is slower than just string comparing
|
||||||
|
// so only bother in newer frameworks
|
||||||
|
private static bool RunOptimistic => false;
|
||||||
#endif
|
#endif
|
||||||
private NullableEnumConverter? _nullableEnumConverter = null;
|
private NullableEnumConverter? _nullableEnumConverter = null;
|
||||||
|
|
||||||
|
private static Type _enumType = typeof(T);
|
||||||
private static T? _undefinedEnumValue;
|
private static T? _undefinedEnumValue;
|
||||||
|
private static bool _hasFlagsAttribute = _enumType.IsDefined(typeof(FlagsAttribute));
|
||||||
private static ConcurrentBag<string> _unknownValuesWarned = new ConcurrentBag<string>();
|
private static ConcurrentBag<string> _unknownValuesWarned = new ConcurrentBag<string>();
|
||||||
|
private static ConcurrentBag<string> _notOptimalValuesWarned = new ConcurrentBag<string>();
|
||||||
|
|
||||||
|
private const int _optimisticValueCountThreshold = 6;
|
||||||
|
|
||||||
internal class NullableEnumConverter : JsonConverter<T?>
|
internal class NullableEnumConverter : JsonConverter<T?>
|
||||||
{
|
{
|
||||||
@@ -120,13 +120,14 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
{
|
{
|
||||||
var t = ReadNullable(ref reader, typeToConvert, options, out var isEmptyString);
|
var t = ReadNullable(ref reader, typeToConvert, options, out var isEmptyStringOrNull);
|
||||||
if (t != null)
|
if (t != null)
|
||||||
return t.Value;
|
return t.Value;
|
||||||
|
|
||||||
if (isEmptyString && !_unknownValuesWarned.Contains(null))
|
if (isEmptyStringOrNull && !_unknownValuesWarned.Contains(null))
|
||||||
{
|
{
|
||||||
// We received an empty string and have no mapping for it, and the property isn't nullable
|
// We received an empty string and have no mapping for it, and the property isn't nullable
|
||||||
|
_unknownValuesWarned.Add(null!);
|
||||||
LibraryHelpers.StaticLogger?.LogWarning($"Received null or empty enum value, but property type is not a nullable enum. EnumType: {typeof(T).FullName}. If you think {typeof(T).FullName} should be nullable please open an issue on the Github repo");
|
LibraryHelpers.StaticLogger?.LogWarning($"Received null or empty enum value, but property type is not a nullable enum. EnumType: {typeof(T).FullName}. If you think {typeof(T).FullName} should be nullable please open an issue on the Github repo");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,13 +150,21 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
return (T)_undefinedEnumValue;
|
return (T)_undefinedEnumValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
private T? ReadNullable(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, out bool isEmptyString)
|
private T? ReadNullable(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, out bool isEmptyStringOrNull)
|
||||||
{
|
{
|
||||||
isEmptyString = false;
|
isEmptyStringOrNull = false;
|
||||||
var enumType = typeof(T);
|
|
||||||
if (_mappingToEnum == null)
|
if (_mappingToEnum == null)
|
||||||
CreateMapping();
|
CreateMapping();
|
||||||
|
|
||||||
|
bool optimisticCheckDone = false;
|
||||||
|
if (RunOptimistic)
|
||||||
|
{
|
||||||
|
var resultOptimistic = GetValueOptimistic(ref reader, ref optimisticCheckDone);
|
||||||
|
if (resultOptimistic != null)
|
||||||
|
return resultOptimistic.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
var isNumber = reader.TokenType == JsonTokenType.Number;
|
||||||
var stringValue = reader.TokenType switch
|
var stringValue = reader.TokenType switch
|
||||||
{
|
{
|
||||||
JsonTokenType.String => reader.GetString(),
|
JsonTokenType.String => reader.GetString(),
|
||||||
@@ -167,13 +176,17 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (stringValue is null)
|
if (stringValue is null)
|
||||||
return null;
|
|
||||||
|
|
||||||
if (!GetValue(enumType, stringValue, out var result))
|
|
||||||
{
|
{
|
||||||
|
isEmptyStringOrNull = true;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!GetValue(stringValue, optimisticCheckDone, out var result))
|
||||||
|
{
|
||||||
|
// Note: checking this here and before the GetValue seems redundant but it allows enum mapping for empty strings
|
||||||
if (string.IsNullOrWhiteSpace(stringValue))
|
if (string.IsNullOrWhiteSpace(stringValue))
|
||||||
{
|
{
|
||||||
isEmptyString = true;
|
isEmptyStringOrNull = true;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -181,13 +194,22 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
if (!_unknownValuesWarned.Contains(stringValue))
|
if (!_unknownValuesWarned.Contains(stringValue))
|
||||||
{
|
{
|
||||||
_unknownValuesWarned.Add(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 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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (optimisticCheckDone)
|
||||||
|
{
|
||||||
|
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.Key}: {m.Value}"))}]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,45 +220,78 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
writer.WriteStringValue(stringValue);
|
writer.WriteStringValue(stringValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool GetValue(Type objectType, string value, out T? result)
|
/// <summary>
|
||||||
|
/// Try to get the enum value based on the string value using the Utf8JsonReader's ValueTextEquals method.
|
||||||
|
/// This is an optimization to avoid string allocations when possible, but can only match case sensitively
|
||||||
|
/// </summary>
|
||||||
|
private static T? GetValueOptimistic(ref Utf8JsonReader reader, ref bool optimisticCheckDone)
|
||||||
{
|
{
|
||||||
if (_mappingToEnum != null)
|
if (reader.TokenType != JsonTokenType.String)
|
||||||
|
{
|
||||||
|
optimisticCheckDone = false;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_mappingToEnum!.Count >= _optimisticValueCountThreshold)
|
||||||
|
{
|
||||||
|
optimisticCheckDone = false;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
optimisticCheckDone = true;
|
||||||
|
foreach (var item in _mappingToEnum!)
|
||||||
|
{
|
||||||
|
if (reader.ValueTextEquals(item.Key))
|
||||||
|
return item.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool GetValue(string value, bool optimisticCheckDone, out T? result)
|
||||||
|
{
|
||||||
|
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;
|
|
||||||
// Try match on full equals
|
// Try match on full equals
|
||||||
foreach (var item in _mappingToEnum)
|
foreach (var item in _mappingToEnum)
|
||||||
{
|
{
|
||||||
if (item.StringValue.Equals(value, StringComparison.Ordinal))
|
if (item.Key.Equals(value, StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
mapping = item;
|
mapping = item.Value;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If not found, try matching ignoring case
|
// If not found, try matching ignoring case
|
||||||
if (mapping == null)
|
if (mapping == null)
|
||||||
|
{
|
||||||
|
foreach (var item in _mappingToEnum)
|
||||||
{
|
{
|
||||||
foreach (var item in _mappingToEnum)
|
if (item.Key.Equals(value, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
if (item.StringValue.Equals(value, StringComparison.OrdinalIgnoreCase))
|
mapping = item.Value;
|
||||||
{
|
break;
|
||||||
mapping = item;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mapping != null)
|
|
||||||
{
|
|
||||||
result = mapping.Value;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (objectType.IsDefined(typeof(FlagsAttribute)))
|
if (mapping != null)
|
||||||
|
{
|
||||||
|
result = mapping;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (_hasFlagsAttribute)
|
||||||
{
|
{
|
||||||
var intValue = int.Parse(value);
|
var intValue = int.Parse(value);
|
||||||
result = (T)Enum.ToObject(objectType, intValue);
|
result = (T)Enum.ToObject(_enumType, intValue);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,8 +313,12 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
// If no explicit mapping is found try to parse string
|
// If no explicit mapping is found try to parse string
|
||||||
result = (T)Enum.Parse(objectType, value, true);
|
#if NET8_0_OR_GREATER
|
||||||
if (!Enum.IsDefined(objectType, result))
|
result = Enum.Parse<T>(value, true);
|
||||||
|
#else
|
||||||
|
result = (T)Enum.Parse(_enumType, value, true);
|
||||||
|
#endif
|
||||||
|
if (!Enum.IsDefined(_enumType, result))
|
||||||
{
|
{
|
||||||
result = default;
|
result = default;
|
||||||
return false;
|
return false;
|
||||||
@@ -276,35 +335,43 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
|
|
||||||
private static void CreateMapping()
|
private static void CreateMapping()
|
||||||
{
|
{
|
||||||
var mappingToEnum = new List<EnumMapping>();
|
var mappingStringToEnum = new Dictionary<string, T>();
|
||||||
var mappingToString = new Dictionary<T, string>();
|
var mappingEnumToString = new Dictionary<T, string>();
|
||||||
|
|
||||||
var enumType = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
|
#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)
|
foreach (var member in enumMembers)
|
||||||
{
|
{
|
||||||
|
var enumVal = (T)member.GetValue(null)!;
|
||||||
var maps = member.GetCustomAttributes(typeof(MapAttribute), false);
|
var maps = member.GetCustomAttributes(typeof(MapAttribute), false);
|
||||||
foreach (MapAttribute attribute in maps)
|
foreach (MapAttribute attribute in maps)
|
||||||
{
|
{
|
||||||
foreach (var value in attribute.Values)
|
foreach (var value in attribute.Values)
|
||||||
{
|
{
|
||||||
var enumVal = (T)Enum.Parse(enumType, member.Name);
|
mappingStringToEnum.Add(value, enumVal);
|
||||||
mappingToEnum.Add(new EnumMapping(enumVal, value));
|
if (!mappingEnumToString.ContainsKey(enumVal))
|
||||||
if (!mappingToString.ContainsKey(enumVal))
|
mappingEnumToString.Add(enumVal, value);
|
||||||
mappingToString.Add(enumVal, value);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#if NET8_0_OR_GREATER
|
#if NET8_0_OR_GREATER
|
||||||
_mappingToEnum = mappingToEnum.ToFrozenSet();
|
_mappingToEnum = mappingStringToEnum.ToFrozenDictionary();
|
||||||
_mappingToString = mappingToString.ToFrozenDictionary();
|
_mappingToString = mappingEnumToString.ToFrozenDictionary();
|
||||||
#else
|
#else
|
||||||
_mappingToEnum = mappingToEnum;
|
_mappingToEnum = mappingStringToEnum;
|
||||||
_mappingToString = mappingToString;
|
_mappingToString = mappingEnumToString;
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For testing purposes only, allows resetting the static mapping and warnings
|
||||||
|
internal static void Reset()
|
||||||
|
{
|
||||||
|
_undefinedEnumValue = null;
|
||||||
|
_unknownValuesWarned = new ConcurrentBag<string>();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get the string value for an enum value using the MapAttribute mapping. When multiple values are mapped for a enum entry the first value will be returned
|
/// Get the string value for an enum value using the MapAttribute mapping. When multiple values are mapped for a enum entry the first value will be returned
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -326,41 +393,30 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static T? ParseString(string value)
|
public static T? ParseString(string value)
|
||||||
{
|
{
|
||||||
var type = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
|
|
||||||
if (_mappingToEnum == null)
|
if (_mappingToEnum == null)
|
||||||
CreateMapping();
|
CreateMapping();
|
||||||
|
|
||||||
EnumMapping? mapping = null;
|
|
||||||
// Try match on full equals
|
// Try match on full equals
|
||||||
foreach(var item in _mappingToEnum!)
|
foreach(var item in _mappingToEnum!)
|
||||||
{
|
{
|
||||||
if (item.StringValue.Equals(value, StringComparison.Ordinal))
|
if (item.Key.Equals(value, StringComparison.Ordinal))
|
||||||
{
|
return item.Value;
|
||||||
mapping = item;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If not found, try matching ignoring case
|
// If not found, try matching ignoring case
|
||||||
if (mapping == null)
|
foreach (var item in _mappingToEnum)
|
||||||
{
|
{
|
||||||
foreach (var item in _mappingToEnum)
|
if (item.Key.Equals(value, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
return item.Value;
|
||||||
if (item.StringValue.Equals(value, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
mapping = item;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mapping != null)
|
|
||||||
return mapping.Value;
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// If no explicit mapping is found try to parse string
|
#if NET8_0_OR_GREATER
|
||||||
return (T)Enum.Parse(type, value, true);
|
return Enum.Parse<T>(value, true);
|
||||||
|
#else
|
||||||
|
return (T)Enum.Parse(_enumType, value, true);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
|
|||||||
+1
-1
@@ -88,7 +88,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers
|
|||||||
stream.Seek(0, SeekOrigin.Begin);
|
stream.Seek(0, SeekOrigin.Begin);
|
||||||
var written = new StreamReader(stream).ReadBlock(dataSnippet, 0, _errorResponseSnippetLimit);
|
var written = new StreamReader(stream).ReadBlock(dataSnippet, 0, _errorResponseSnippetLimit);
|
||||||
var data = new string(dataSnippet, 0, written);
|
var data = new string(dataSnippet, 0, written);
|
||||||
errorMsg += $": {data}";
|
errorMsg += $": {(string.IsNullOrEmpty(data) ? "(empty)" : data)}";
|
||||||
if (data.Length == _errorResponseSnippetLimit)
|
if (data.Length == _errorResponseSnippetLimit)
|
||||||
errorMsg += " (truncated)";
|
errorMsg += " (truncated)";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
if (string.IsNullOrEmpty(value))
|
if (string.IsNullOrEmpty(value))
|
||||||
return default;
|
return default;
|
||||||
|
|
||||||
return (T?)JsonDocument.Parse(value!).Deserialize(typeof(T), options);
|
return JsonDocument.Parse(value!).Deserialize<T>(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|||||||
@@ -6,9 +6,9 @@
|
|||||||
<PackageId>CryptoExchange.Net</PackageId>
|
<PackageId>CryptoExchange.Net</PackageId>
|
||||||
<Authors>JKorf</Authors>
|
<Authors>JKorf</Authors>
|
||||||
<Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description>
|
<Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description>
|
||||||
<PackageVersion>11.0.1</PackageVersion>
|
<PackageVersion>13.0.0</PackageVersion>
|
||||||
<AssemblyVersion>11.0.1</AssemblyVersion>
|
<AssemblyVersion>13.0.0</AssemblyVersion>
|
||||||
<FileVersion>11.0.1</FileVersion>
|
<FileVersion>13.0.0</FileVersion>
|
||||||
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
||||||
<PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange;CryptoExchange.Net</PackageTags>
|
<PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange;CryptoExchange.Net</PackageTags>
|
||||||
<RepositoryType>git</RepositoryType>
|
<RepositoryType>git</RepositoryType>
|
||||||
|
|||||||
@@ -311,16 +311,16 @@ namespace CryptoExchange.Net
|
|||||||
/// <param name="request">The request parameters</param>
|
/// <param name="request">The request parameters</param>
|
||||||
/// <param name="ct">Cancellation token</param>
|
/// <param name="ct">Cancellation token</param>
|
||||||
/// <returns></returns>
|
/// <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>();
|
var result = new List<T>();
|
||||||
ExchangeWebResult<T[]> batch;
|
HttpResult<T[]> batch;
|
||||||
PageRequest? nextPageToken = null;
|
PageRequest? nextPageToken = null;
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
batch = await paginatedFunc(request, nextPageToken, ct).ConfigureAwait(false);
|
batch = await paginatedFunc(request, nextPageToken, ct).ConfigureAwait(false);
|
||||||
yield return batch;
|
yield return batch;
|
||||||
if (!batch || ct.IsCancellationRequested)
|
if (!batch.Success || ct.IsCancellationRequested)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
result.AddRange(batch.Data);
|
result.AddRange(batch.Data);
|
||||||
@@ -399,8 +399,8 @@ namespace CryptoExchange.Net
|
|||||||
/// <param name="asyncHandler">The async update handler</param>
|
/// <param name="asyncHandler">The async update handler</param>
|
||||||
/// <param name="maxQueuedItems">The max number of updates to be queued up. When happens when the queue is full and a new write is attempted can be specified with <see>fullMode</see></param>
|
/// <param name="maxQueuedItems">The max number of updates to be queued up. When happens when the queue is full and a new write is attempted can be specified with <see>fullMode</see></param>
|
||||||
/// <param name="fullBehavior">What should happen if the queue contains <see>maxQueuedItems</see> pending updates. If no max is set this setting is ignored</param>
|
/// <param name="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>(
|
public static async Task<WebSocketResult<UpdateSubscription>> ProcessQueuedAsync<T>(
|
||||||
Func<Action<DataEvent<T>>, Task<CallResult<UpdateSubscription>>> subscribeCall,
|
Func<Action<DataEvent<T>>, Task<WebSocketResult<UpdateSubscription>>> subscribeCall,
|
||||||
Func<DataEvent<T>, Task> asyncHandler,
|
Func<DataEvent<T>, Task> asyncHandler,
|
||||||
int? maxQueuedItems = null,
|
int? maxQueuedItems = null,
|
||||||
QueueFullBehavior? fullBehavior = null)
|
QueueFullBehavior? fullBehavior = null)
|
||||||
@@ -408,7 +408,7 @@ namespace CryptoExchange.Net
|
|||||||
var processor = new ProcessQueue<DataEvent<T>>(asyncHandler, maxQueuedItems, fullBehavior);
|
var processor = new ProcessQueue<DataEvent<T>>(asyncHandler, maxQueuedItems, fullBehavior);
|
||||||
await processor.StartAsync().ConfigureAwait(false);
|
await processor.StartAsync().ConfigureAwait(false);
|
||||||
var result = await subscribeCall(upd => processor.Write(upd)).ConfigureAwait(false);
|
var result = await subscribeCall(upd => processor.Write(upd)).ConfigureAwait(false);
|
||||||
if (!result)
|
if (!result.Success)
|
||||||
{
|
{
|
||||||
await processor.StopAsync().ConfigureAwait(false);
|
await processor.StopAsync().ConfigureAwait(false);
|
||||||
return result;
|
return result;
|
||||||
@@ -473,7 +473,7 @@ namespace CryptoExchange.Net
|
|||||||
}, maxQueuedItems, fullBehavior);
|
}, maxQueuedItems, fullBehavior);
|
||||||
await processor.StartAsync().ConfigureAwait(false);
|
await processor.StartAsync().ConfigureAwait(false);
|
||||||
var result = await subscribeCall(processor).ConfigureAwait(false);
|
var result = await subscribeCall(processor).ConfigureAwait(false);
|
||||||
if (!result)
|
if (!result.Success)
|
||||||
{
|
{
|
||||||
await processor.StopAsync().ConfigureAwait(false);
|
await processor.StopAsync().ConfigureAwait(false);
|
||||||
return result;
|
return result;
|
||||||
@@ -499,7 +499,7 @@ namespace CryptoExchange.Net
|
|||||||
return null;
|
return null;
|
||||||
|
|
||||||
// Try parse, only fails for these reasons:
|
// 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
|
// 2. value is larger or smaller than decimal max/min
|
||||||
// 3. unparsable format
|
// 3. unparsable format
|
||||||
if (decimal.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var decValue))
|
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))
|
if (string.Equals("Infinity", value, StringComparison.OrdinalIgnoreCase))
|
||||||
return decimal.MaxValue;
|
return decimal.MaxValue;
|
||||||
else if(string.Equals("-Infinity", value, StringComparison.OrdinalIgnoreCase))
|
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))
|
if (value!.Length > 27 && decimal.TryParse(value.Substring(0, 27), out var overflowValue))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -11,85 +11,92 @@ namespace CryptoExchange.Net
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static class ExchangeSymbolCache
|
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>
|
/// <summary>
|
||||||
/// Update the cached symbol data for an exchange
|
/// Update the cached symbol data for an exchange
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="topicId">Id for the provided data</param>
|
/// <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>
|
/// <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));
|
exchangeInfo = new ExchangeKeyedCache();
|
||||||
_symbolInfos.TryAdd(topicId, exchangeInfo);
|
_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;
|
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>
|
/// <summary>
|
||||||
/// Whether the specific topic has been cached
|
/// Whether the specific topic has been cached
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="topicId">Id</param>
|
/// <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 false;
|
||||||
|
|
||||||
return exchangeInfo.Symbols.Count > 0;
|
return exchangeInfo.HasCached(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether a specific exchange(topic) support the provided symbol
|
/// Whether a specific exchange(topic) support the provided symbol
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="topicId">Id for the provided data</param>
|
/// <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>
|
/// <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;
|
return false;
|
||||||
|
|
||||||
if (!exchangeInfo.Symbols.TryGetValue(symbolName, out var symbolInfo))
|
return exchangeInfo.SupportsSymbol(key, symbolName);
|
||||||
return false;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether a specific exchange(topic) support the provided symbol
|
/// Whether a specific exchange(topic) support the provided symbol
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="topicId">Id for the provided data</param>
|
/// <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>
|
/// <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 false;
|
||||||
|
|
||||||
return exchangeInfo.Symbols.Any(x =>
|
return exchangeInfo.SupportsSymbol(key, symbol);
|
||||||
x.Value.TradingMode == symbol.TradingMode
|
|
||||||
&& x.Value.BaseAsset == symbol.BaseAsset
|
|
||||||
&& x.Value.QuoteAsset == symbol.QuoteAsset);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get all symbols for a specific base asset
|
/// Get all symbols for a specific base asset
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="topicId">Id for the provided data</param>
|
/// <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>
|
/// <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 [];
|
||||||
|
|
||||||
return exchangeInfo.Symbols
|
return exchangeInfo.GetSymbolsForBaseAsset(key, baseAsset);
|
||||||
.Where(x => x.Value.BaseAsset.Equals(baseAsset, StringComparison.InvariantCultureIgnoreCase))
|
|
||||||
.Select(x => x.Value)
|
|
||||||
.ToArray();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -97,29 +104,261 @@ namespace CryptoExchange.Net
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="topicId">Id for the provided data</param>
|
/// <param name="topicId">Id for the provided data</param>
|
||||||
/// <param name="symbolName">Symbol name</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)
|
if (symbolName == null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
if (!_symbolInfos.TryGetValue(topicId, out var exchangeInfo))
|
var id = topicId + environment;
|
||||||
|
if (!_symbolInfos.TryGetValue(id, out var exchangeInfo))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
if (!exchangeInfo.Symbols.TryGetValue(symbolName, out var symbolInfo))
|
return exchangeInfo.ParseSymbol(key, symbolName);
|
||||||
return null;
|
|
||||||
|
|
||||||
return new SharedSymbol(symbolInfo.TradingMode, symbolInfo.BaseAsset, symbolInfo.QuoteAsset, symbolName)
|
|
||||||
{
|
|
||||||
DeliverTime = symbolInfo.DeliverTime
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <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
|
class ExchangeInfo
|
||||||
{
|
{
|
||||||
public DateTime UpdateTime { get; set; }
|
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;
|
UpdateTime = updateTime;
|
||||||
Symbols = symbols;
|
Symbols = symbols;
|
||||||
|
|||||||
@@ -1,14 +1,20 @@
|
|||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Clients;
|
||||||
|
using CryptoExchange.Net.Interfaces;
|
||||||
|
using CryptoExchange.Net.Interfaces.Clients;
|
||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.RateLimiting;
|
||||||
using CryptoExchange.Net.SharedApis;
|
using CryptoExchange.Net.SharedApis;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.IO.Compression;
|
using System.IO.Compression;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using System.Web;
|
using System.Web;
|
||||||
|
|
||||||
namespace CryptoExchange.Net
|
namespace CryptoExchange.Net
|
||||||
@@ -24,7 +30,7 @@ namespace CryptoExchange.Net
|
|||||||
/// <param name="parameters"></param>
|
/// <param name="parameters"></param>
|
||||||
/// <param name="key"></param>
|
/// <param name="key"></param>
|
||||||
/// <param name="value"></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);
|
parameters.Add(key, value);
|
||||||
}
|
}
|
||||||
@@ -35,7 +41,7 @@ namespace CryptoExchange.Net
|
|||||||
/// <param name="parameters"></param>
|
/// <param name="parameters"></param>
|
||||||
/// <param name="key"></param>
|
/// <param name="key"></param>
|
||||||
/// <param name="value"></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);
|
parameters.Add(key, value);
|
||||||
}
|
}
|
||||||
@@ -46,7 +52,7 @@ namespace CryptoExchange.Net
|
|||||||
/// <param name="parameters"></param>
|
/// <param name="parameters"></param>
|
||||||
/// <param name="key"></param>
|
/// <param name="key"></param>
|
||||||
/// <param name="value"></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)
|
if (value != null)
|
||||||
parameters.Add(key, value);
|
parameters.Add(key, value);
|
||||||
@@ -107,7 +113,7 @@ namespace CryptoExchange.Net
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
uriString.Append('[');
|
uriString.Append($"{parameter.Key}=[");
|
||||||
var firstArrayEntry = true;
|
var firstArrayEntry = true;
|
||||||
foreach (var entry in (Array)parameter.Value)
|
foreach (var entry in (Array)parameter.Value)
|
||||||
{
|
{
|
||||||
@@ -144,25 +150,42 @@ namespace CryptoExchange.Net
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static string ToFormData(this IDictionary<string, object> parameters)
|
public static string ToFormData(this IDictionary<string, object> parameters)
|
||||||
{
|
{
|
||||||
var formData = HttpUtility.ParseQueryString(string.Empty);
|
var result = new StringBuilder();
|
||||||
|
var first = true;
|
||||||
|
|
||||||
|
void Append(string key, object value)
|
||||||
|
{
|
||||||
|
if (!first)
|
||||||
|
result.Append('&');
|
||||||
|
first = false;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(key))
|
||||||
|
{
|
||||||
|
result.Append(HttpUtility.UrlEncode(key, Encoding.UTF8));
|
||||||
|
result.Append('=');
|
||||||
|
}
|
||||||
|
|
||||||
|
var text = string.Format(CultureInfo.InvariantCulture, "{0}", value);
|
||||||
|
result.Append(HttpUtility.UrlEncode(text, Encoding.UTF8));
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var kvp in parameters)
|
foreach (var kvp in parameters)
|
||||||
{
|
{
|
||||||
if (kvp.Value is null)
|
if (kvp.Value is null)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (kvp.Value.GetType().IsArray)
|
if (kvp.Value is Array array)
|
||||||
{
|
{
|
||||||
var array = (Array)kvp.Value;
|
|
||||||
foreach (var value in array)
|
foreach (var value in array)
|
||||||
formData.Add(kvp.Key, string.Format(CultureInfo.InvariantCulture, "{0}", value));
|
Append(kvp.Key, value);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
formData.Add(kvp.Key, string.Format(CultureInfo.InvariantCulture, "{0}", kvp.Value));
|
Append(kvp.Key, kvp.Value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return formData.ToString()!;
|
return result.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -378,8 +401,6 @@ namespace CryptoExchange.Net
|
|||||||
services.AddTransient(x => (IDepositRestClient)client(x)!);
|
services.AddTransient(x => (IDepositRestClient)client(x)!);
|
||||||
if (typeof(IKlineRestClient).IsAssignableFrom(typeof(T)))
|
if (typeof(IKlineRestClient).IsAssignableFrom(typeof(T)))
|
||||||
services.AddTransient(x => (IKlineRestClient)client(x)!);
|
services.AddTransient(x => (IKlineRestClient)client(x)!);
|
||||||
if (typeof(IListenKeyRestClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (IListenKeyRestClient)client(x)!);
|
|
||||||
if (typeof(IOrderBookRestClient).IsAssignableFrom(typeof(T)))
|
if (typeof(IOrderBookRestClient).IsAssignableFrom(typeof(T)))
|
||||||
services.AddTransient(x => (IOrderBookRestClient)client(x)!);
|
services.AddTransient(x => (IOrderBookRestClient)client(x)!);
|
||||||
if (typeof(IRecentTradeRestClient).IsAssignableFrom(typeof(T)))
|
if (typeof(IRecentTradeRestClient).IsAssignableFrom(typeof(T)))
|
||||||
@@ -396,7 +417,6 @@ namespace CryptoExchange.Net
|
|||||||
services.AddTransient(x => (IBookTickerRestClient)client(x)!);
|
services.AddTransient(x => (IBookTickerRestClient)client(x)!);
|
||||||
if (typeof(ITransferRestClient).IsAssignableFrom(typeof(T)))
|
if (typeof(ITransferRestClient).IsAssignableFrom(typeof(T)))
|
||||||
services.AddTransient(x => (ITransferRestClient)client(x)!);
|
services.AddTransient(x => (ITransferRestClient)client(x)!);
|
||||||
|
|
||||||
if (typeof(ISpotOrderRestClient).IsAssignableFrom(typeof(T)))
|
if (typeof(ISpotOrderRestClient).IsAssignableFrom(typeof(T)))
|
||||||
services.AddTransient(x => (ISpotOrderRestClient)client(x)!);
|
services.AddTransient(x => (ISpotOrderRestClient)client(x)!);
|
||||||
if (typeof(ISpotSymbolRestClient).IsAssignableFrom(typeof(T)))
|
if (typeof(ISpotSymbolRestClient).IsAssignableFrom(typeof(T)))
|
||||||
@@ -462,11 +482,15 @@ namespace CryptoExchange.Net
|
|||||||
|
|
||||||
if (typeof(ISpotOrderSocketClient).IsAssignableFrom(typeof(T)))
|
if (typeof(ISpotOrderSocketClient).IsAssignableFrom(typeof(T)))
|
||||||
services.AddTransient(x => (ISpotOrderSocketClient)client(x)!);
|
services.AddTransient(x => (ISpotOrderSocketClient)client(x)!);
|
||||||
|
if (typeof(ISpotOrderManagementSocketClient).IsAssignableFrom(typeof(T)))
|
||||||
|
services.AddTransient(x => (ISpotOrderManagementSocketClient)client(x)!);
|
||||||
|
|
||||||
if (typeof(IFuturesOrderSocketClient).IsAssignableFrom(typeof(T)))
|
if (typeof(IFuturesOrderSocketClient).IsAssignableFrom(typeof(T)))
|
||||||
services.AddTransient(x => (IFuturesOrderSocketClient)client(x)!);
|
services.AddTransient(x => (IFuturesOrderSocketClient)client(x)!);
|
||||||
if (typeof(IPositionSocketClient).IsAssignableFrom(typeof(T)))
|
if (typeof(IPositionSocketClient).IsAssignableFrom(typeof(T)))
|
||||||
services.AddTransient(x => (IPositionSocketClient)client(x)!);
|
services.AddTransient(x => (IPositionSocketClient)client(x)!);
|
||||||
|
if (typeof(IFuturesOrderManagementSocketClient).IsAssignableFrom(typeof(T)))
|
||||||
|
services.AddTransient(x => (IFuturesOrderManagementSocketClient)client(x)!);
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
@@ -490,6 +514,41 @@ namespace CryptoExchange.Net
|
|||||||
|
|
||||||
return bytes;
|
return bytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Execute an operation with a specific rate limit admission rule
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="client">Client</param>
|
||||||
|
/// <param name="admission">Rate limit admission rule</param>
|
||||||
|
/// <param name="operation">Operation to execute</param>
|
||||||
|
public static Task<TResult> WithRateLimitAdmissionAsync<TClient, TResult>(
|
||||||
|
this TClient client,
|
||||||
|
RateLimitAdmission admission,
|
||||||
|
Func<TClient, Task<TResult>> operation)
|
||||||
|
where TClient : IRateLimitAdmissionClient
|
||||||
|
{
|
||||||
|
return client.WithRateLimitAdmissionAsync(
|
||||||
|
admission,
|
||||||
|
() => operation(client));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Return the task results in the form of an IAsyncEnumerable, returning the first completed task first
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">Type of task result</typeparam>
|
||||||
|
/// <param name="tasks">Task list</param>
|
||||||
|
public static async IAsyncEnumerable<T> ParallelEnumerateAsync<T>(this IEnumerable<Task<T>> tasks)
|
||||||
|
{
|
||||||
|
var remaining = new List<Task<T>>(tasks);
|
||||||
|
|
||||||
|
while (remaining.Count != 0)
|
||||||
|
{
|
||||||
|
var task = await Task.WhenAny(remaining).ConfigureAwait(false);
|
||||||
|
remaining.Remove(task);
|
||||||
|
yield return await task.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
using CryptoExchange.Net.Authentication;
|
using CryptoExchange.Net.SharedApis;
|
||||||
using CryptoExchange.Net.Objects.Options;
|
|
||||||
using CryptoExchange.Net.SharedApis;
|
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Interfaces.Clients
|
namespace CryptoExchange.Net.Interfaces.Clients
|
||||||
@@ -8,8 +6,12 @@ namespace CryptoExchange.Net.Interfaces.Clients
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Base api client
|
/// Base api client
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IBaseApiClient
|
public interface IBaseApiClient : IRateLimitAdmissionClient
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Exchange name
|
||||||
|
/// </summary>
|
||||||
|
string Exchange { get; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Base address
|
/// Base address
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -28,6 +28,11 @@ namespace CryptoExchange.Net.Interfaces.Clients
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
bool Authenticated { get; }
|
bool Authenticated { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configured credentials
|
||||||
|
/// </summary>
|
||||||
|
TApiCredentials? ApiCredentials { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Set the API credentials for this API client
|
/// Set the API credentials for this API client
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -28,6 +28,12 @@ namespace CryptoExchange.Net.Interfaces.Clients
|
|||||||
/// Whether client is disposed
|
/// Whether client is disposed
|
||||||
/// </summary>
|
/// </summary>
|
||||||
bool Disposed { get; }
|
bool Disposed { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Update specific options
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="options">Options to update. Only specific options are changeable after the client has been created</param>
|
||||||
|
void SetOptions(UpdateOptions options);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|||||||
@@ -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.
|
/// Whether or not API credentials have been configured for this client. Does not check the credentials are actually valid.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
bool Authenticated { get; }
|
bool Authenticated { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configured credentials
|
||||||
|
/// </summary>
|
||||||
|
TApiCredentials? ApiCredentials { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Set the API credentials for this API client
|
/// Set the API credentials for this API client
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -60,6 +60,12 @@ namespace CryptoExchange.Net.Interfaces.Clients
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Task UnsubscribeAllAsync();
|
Task UnsubscribeAllAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Update specific options
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="options">Options to update. Only specific options are changeable after the client has been created</param>
|
||||||
|
void SetOptions(UpdateOptions options);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using CryptoExchange.Net.RateLimiting;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Interfaces
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A client supporting rate limit admission rules
|
||||||
|
/// </summary>
|
||||||
|
public interface IRateLimitAdmissionClient
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Execute an operation with a specific rate limit admission rule
|
||||||
|
/// </summary>
|
||||||
|
Task<TResult> WithRateLimitAdmissionAsync<TResult>(
|
||||||
|
RateLimitAdmission admission,
|
||||||
|
Func<Task<TResult>> operation);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -107,7 +107,7 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="ct">A cancellation token to stop the order book when canceled</param>
|
/// <param name="ct">A cancellation token to stop the order book when canceled</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Task<CallResult<bool>> StartAsync(CancellationToken? ct = null);
|
Task<CallResult> StartAsync(CancellationToken? ct = null);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Stop syncing the order book
|
/// Stop syncing the order book
|
||||||
|
|||||||
@@ -24,8 +24,9 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// <param name="interval">Kline interval</param>
|
/// <param name="interval">Kline interval</param>
|
||||||
/// <param name="limit">The max amount of klines to retain</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="period">The max period the data should be retained</param>
|
||||||
|
/// <param name="exchangeParameters">Exchange parameters</param>
|
||||||
/// <returns></returns>
|
/// <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>
|
/// <summary>
|
||||||
/// Whether the factory supports creating a TradeTracker instance for this symbol
|
/// 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="symbol">The symbol</param>
|
||||||
/// <param name="limit">The max amount of trades to retain</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="period">The max period the data should be retained</param>
|
||||||
|
/// <param name="exchangeParameters">Exchange parameters</param>
|
||||||
/// <returns></returns>
|
/// <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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ using CryptoExchange.Net.Objects.Options;
|
|||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.IO.Pipelines;
|
||||||
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
|
|
||||||
@@ -14,6 +16,61 @@ namespace CryptoExchange.Net
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static class LibraryHelpers
|
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;
|
private static ILogger? _staticLogger;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Static logger
|
/// Static logger
|
||||||
@@ -55,6 +112,7 @@ namespace CryptoExchange.Net
|
|||||||
{ "Kucoin.SpotKey", "f8ae62cb-2b3d-420c-8c98-e1c17dd4e30a" },
|
{ "Kucoin.SpotKey", "f8ae62cb-2b3d-420c-8c98-e1c17dd4e30a" },
|
||||||
{ "Mexc", "EASYT" },
|
{ "Mexc", "EASYT" },
|
||||||
{ "OKX", "1425d83a94fbBCDE" },
|
{ "OKX", "1425d83a94fbBCDE" },
|
||||||
|
{ "Weex", "b-WEEX111124-" },
|
||||||
{ "XT", "4XWeqN10M1fcoI5L" },
|
{ "XT", "4XWeqN10M1fcoI5L" },
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -104,6 +162,67 @@ namespace CryptoExchange.Net
|
|||||||
return _defaultClientReferences.TryGetValue(key, out var id) ? id : throw new KeyNotFoundException($"{exchange} not found in configuration");
|
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>
|
/// <summary>
|
||||||
/// Create a new HttpMessageHandler instance
|
/// Create a new HttpMessageHandler instance
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -18,32 +18,32 @@ namespace CryptoExchange.Net.Logging.Extensions
|
|||||||
_rateLimitRequestFailed = LoggerMessage.Define<int, string, string, string>(
|
_rateLimitRequestFailed = LoggerMessage.Define<int, string, string, string>(
|
||||||
LogLevel.Warning,
|
LogLevel.Warning,
|
||||||
new EventId(6000, "RateLimitRequestFailed"),
|
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>(
|
_rateLimitConnectionFailed = LoggerMessage.Define<int, string, string>(
|
||||||
LogLevel.Warning,
|
LogLevel.Warning,
|
||||||
new EventId(6001, "RateLimitConnectionFailed"),
|
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>(
|
_rateLimitDelayingRequest = LoggerMessage.Define<int, string, TimeSpan, string, string>(
|
||||||
LogLevel.Warning,
|
LogLevel.Warning,
|
||||||
new EventId(6002, "RateLimitDelayingRequest"),
|
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>(
|
_rateLimitDelayingConnection = LoggerMessage.Define<int, TimeSpan, string, string>(
|
||||||
LogLevel.Warning,
|
LogLevel.Warning,
|
||||||
new EventId(6003, "RateLimitDelayingConnection"),
|
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>(
|
_rateLimitAppliedConnection = LoggerMessage.Define<int, string, string, int>(
|
||||||
LogLevel.Trace,
|
LogLevel.Trace,
|
||||||
new EventId(6004, "RateLimitDelayingConnection"),
|
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>(
|
_rateLimitAppliedRequest = LoggerMessage.Define<int, string, string, string, int>(
|
||||||
LogLevel.Trace,
|
LogLevel.Trace,
|
||||||
new EventId(6005, "RateLimitAppliedRequest"),
|
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)
|
public static void RateLimitRequestFailed(this ILogger logger, int requestId, string path, string guard, string limit)
|
||||||
|
|||||||
@@ -22,74 +22,79 @@ namespace CryptoExchange.Net.Logging.Extensions
|
|||||||
private static readonly Action<ILogger, string, Exception?> _restApiCacheHit;
|
private static readonly Action<ILogger, string, Exception?> _restApiCacheHit;
|
||||||
private static readonly Action<ILogger, string, Exception?> _restApiCacheNotHit;
|
private static readonly Action<ILogger, string, Exception?> _restApiCacheNotHit;
|
||||||
private static readonly Action<ILogger, int?, Exception?> _restApiCancellationRequested;
|
private static readonly Action<ILogger, int?, Exception?> _restApiCancellationRequested;
|
||||||
|
private static readonly Action<ILogger, string, string, Exception?> _restApiRequestsJoined;
|
||||||
|
|
||||||
static RestApiClientLoggingExtensions()
|
static RestApiClientLoggingExtensions()
|
||||||
{
|
{
|
||||||
_restApiErrorReceived = LoggerMessage.Define<int?, int?, long, string?, string?>(
|
_restApiErrorReceived = LoggerMessage.Define<int?, int?, long, string?, string?>(
|
||||||
LogLevel.Warning,
|
LogLevel.Warning,
|
||||||
new EventId(4000, "RestApiErrorReceived"),
|
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?>(
|
_restApiResponseReceived = LoggerMessage.Define<int?, int?, long, string?>(
|
||||||
LogLevel.Debug,
|
LogLevel.Debug,
|
||||||
new EventId(4001, "RestApiResponseReceived"),
|
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>(
|
_restApiFailedToSyncTime = LoggerMessage.Define<int, string>(
|
||||||
LogLevel.Debug,
|
LogLevel.Debug,
|
||||||
new EventId(4002, "RestApiFailedToSyncTime"),
|
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>(
|
_restApiNoApiCredentials = LoggerMessage.Define<int, string>(
|
||||||
LogLevel.Warning,
|
LogLevel.Warning,
|
||||||
new EventId(4003, "RestApiNoApiCredentials"),
|
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>(
|
_restApiCreatingRequest = LoggerMessage.Define<int, Uri>(
|
||||||
LogLevel.Information,
|
LogLevel.Information,
|
||||||
new EventId(4004, "RestApiCreatingRequest"),
|
new EventId(4004, "RestApiCreatingRequest"),
|
||||||
"[Req {RequestId}] Creating request for {RestApiUri}");
|
"[Req {RequestId}] creating request for {RestApiUri}");
|
||||||
|
|
||||||
_restApiSendingRequest = LoggerMessage.Define<int, HttpMethod, string, Uri, string>(
|
_restApiSendingRequest = LoggerMessage.Define<int, HttpMethod, string, Uri, string>(
|
||||||
LogLevel.Trace,
|
LogLevel.Trace,
|
||||||
new EventId(4005, "RestApiSendingRequest"),
|
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>(
|
_restApiRateLimitRetry = LoggerMessage.Define<int, DateTime>(
|
||||||
LogLevel.Warning,
|
LogLevel.Warning,
|
||||||
new EventId(4006, "RestApiRateLimitRetry"),
|
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>(
|
_restApiRateLimitPauseUntil = LoggerMessage.Define<int, DateTime>(
|
||||||
LogLevel.Warning,
|
LogLevel.Warning,
|
||||||
new EventId(4007, "RestApiRateLimitPauseUntil"),
|
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>(
|
_restApiSendRequest = LoggerMessage.Define<int, RequestDefinition, string?, string, string>(
|
||||||
LogLevel.Debug,
|
LogLevel.Debug,
|
||||||
new EventId(4008, "RestApiSendRequest"),
|
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>(
|
_restApiCheckingCache = LoggerMessage.Define<string>(
|
||||||
LogLevel.Trace,
|
LogLevel.Trace,
|
||||||
new EventId(4009, "RestApiCheckingCache"),
|
new EventId(4009, "RestApiCheckingCache"),
|
||||||
"Checking cache for key {Key}");
|
"checking cache for key {Key}");
|
||||||
|
|
||||||
_restApiCacheHit = LoggerMessage.Define<string>(
|
_restApiCacheHit = LoggerMessage.Define<string>(
|
||||||
LogLevel.Trace,
|
LogLevel.Trace,
|
||||||
new EventId(4010, "RestApiCacheHit"),
|
new EventId(4010, "RestApiCacheHit"),
|
||||||
"Cache hit for key {Key}");
|
"cache hit for key {Key}");
|
||||||
|
|
||||||
_restApiCacheNotHit = LoggerMessage.Define<string>(
|
_restApiCacheNotHit = LoggerMessage.Define<string>(
|
||||||
LogLevel.Trace,
|
LogLevel.Trace,
|
||||||
new EventId(4011, "RestApiCacheNotHit"),
|
new EventId(4011, "RestApiCacheNotHit"),
|
||||||
"Cache not hit for key {Key}");
|
"cache not hit for key {Key}");
|
||||||
|
|
||||||
_restApiCancellationRequested = LoggerMessage.Define<int?>(
|
_restApiCancellationRequested = LoggerMessage.Define<int?>(
|
||||||
LogLevel.Debug,
|
LogLevel.Debug,
|
||||||
new EventId(4012, "RestApiCancellationRequested"),
|
new EventId(4012, "RestApiCancellationRequested"),
|
||||||
"[Req {RequestId}] Request cancelled by user");
|
"[Req {RequestId}] request cancelled by user");
|
||||||
|
|
||||||
|
_restApiRequestsJoined = LoggerMessage.Define<string, string>(
|
||||||
|
LogLevel.Debug,
|
||||||
|
new EventId(4013, "RestApiRequestsJoined"),
|
||||||
|
"Request for {Method} {Path} joined with in flight request");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void RestApiErrorReceived(this ILogger logger, int? requestId, HttpStatusCode? responseStatusCode, long responseTime, string? error, string? originalData, Exception? exception)
|
public static void RestApiErrorReceived(this ILogger logger, int? requestId, HttpStatusCode? responseStatusCode, long responseTime, string? error, string? originalData, Exception? exception)
|
||||||
@@ -151,9 +156,15 @@ namespace CryptoExchange.Net.Logging.Extensions
|
|||||||
{
|
{
|
||||||
_restApiCacheNotHit(logger, key, null);
|
_restApiCacheNotHit(logger, key, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void RestApiCancellationRequested(this ILogger logger, int? requestId)
|
public static void RestApiCancellationRequested(this ILogger logger, int? requestId)
|
||||||
{
|
{
|
||||||
_restApiCancellationRequested(logger, requestId, null);
|
_restApiCancellationRequested(logger, requestId, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void RestApiRequestsJoined(this ILogger logger, string method, string path)
|
||||||
|
{
|
||||||
|
_restApiRequestsJoined(logger, method, path, null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ namespace CryptoExchange.Net.Logging.Extensions
|
|||||||
_attemptingToAuthenticate = LoggerMessage.Define<int>(
|
_attemptingToAuthenticate = LoggerMessage.Define<int>(
|
||||||
LogLevel.Debug,
|
LogLevel.Debug,
|
||||||
new EventId(3006, "AttemptingToAuthenticate"),
|
new EventId(3006, "AttemptingToAuthenticate"),
|
||||||
"[Sckt {SocketId}] Attempting to authenticate");
|
"[Sckt {SocketId}] attempting to authenticate");
|
||||||
|
|
||||||
_authenticationFailed = LoggerMessage.Define<int>(
|
_authenticationFailed = LoggerMessage.Define<int>(
|
||||||
LogLevel.Warning,
|
LogLevel.Warning,
|
||||||
@@ -76,12 +76,12 @@ namespace CryptoExchange.Net.Logging.Extensions
|
|||||||
_failedToDetermineConnectionUrl = LoggerMessage.Define<string?>(
|
_failedToDetermineConnectionUrl = LoggerMessage.Define<string?>(
|
||||||
LogLevel.Warning,
|
LogLevel.Warning,
|
||||||
new EventId(3009, "FailedToDetermineConnectionUrl"),
|
new EventId(3009, "FailedToDetermineConnectionUrl"),
|
||||||
"Failed to determine connection url: {ErrorMessage}");
|
"failed to determine connection url: {ErrorMessage}");
|
||||||
|
|
||||||
_connectionAddressSetTo = LoggerMessage.Define<string>(
|
_connectionAddressSetTo = LoggerMessage.Define<string>(
|
||||||
LogLevel.Debug,
|
LogLevel.Debug,
|
||||||
new EventId(3010, "ConnectionAddressSetTo"),
|
new EventId(3010, "ConnectionAddressSetTo"),
|
||||||
"Connection address set to {ConnectionAddress}");
|
"connection address set to {ConnectionAddress}");
|
||||||
|
|
||||||
_socketCreatedForAddress = LoggerMessage.Define<int, string>(
|
_socketCreatedForAddress = LoggerMessage.Define<int, string>(
|
||||||
LogLevel.Debug,
|
LogLevel.Debug,
|
||||||
@@ -91,37 +91,37 @@ namespace CryptoExchange.Net.Logging.Extensions
|
|||||||
_unsubscribingAll = LoggerMessage.Define<int>(
|
_unsubscribingAll = LoggerMessage.Define<int>(
|
||||||
LogLevel.Information,
|
LogLevel.Information,
|
||||||
new EventId(3013, "UnsubscribingAll"),
|
new EventId(3013, "UnsubscribingAll"),
|
||||||
"Unsubscribing all {SubscriptionCount} subscriptions");
|
"unsubscribing all {SubscriptionCount} subscriptions");
|
||||||
|
|
||||||
_disposingSocketClient = LoggerMessage.Define(
|
_disposingSocketClient = LoggerMessage.Define(
|
||||||
LogLevel.Debug,
|
LogLevel.Debug,
|
||||||
new EventId(3015, "DisposingSocketClient"),
|
new EventId(3015, "DisposingSocketClient"),
|
||||||
"Disposing socket client, closing all subscriptions");
|
"disposing socket client, closing all subscriptions");
|
||||||
|
|
||||||
_unsubscribingSubscription = LoggerMessage.Define<int, int>(
|
_unsubscribingSubscription = LoggerMessage.Define<int, int>(
|
||||||
LogLevel.Information,
|
LogLevel.Information,
|
||||||
new EventId(3016, "UnsubscribingSubscription"),
|
new EventId(3016, "UnsubscribingSubscription"),
|
||||||
"[Sckt {SocketId}] Unsubscribing subscription {SubscriptionId}");
|
"[Sckt {SocketId}] unsubscribing subscription {SubscriptionId}");
|
||||||
|
|
||||||
_reconnectingAllConnections = LoggerMessage.Define<int>(
|
_reconnectingAllConnections = LoggerMessage.Define<int>(
|
||||||
LogLevel.Information,
|
LogLevel.Information,
|
||||||
new EventId(3017, "ReconnectingAll"),
|
new EventId(3017, "ReconnectingAll"),
|
||||||
"Reconnecting all {ConnectionCount} connections");
|
"reconnecting all {ConnectionCount} connections");
|
||||||
|
|
||||||
_addingRetryAfterGuard = LoggerMessage.Define<DateTime>(
|
_addingRetryAfterGuard = LoggerMessage.Define<DateTime>(
|
||||||
LogLevel.Warning,
|
LogLevel.Warning,
|
||||||
new EventId(3018, "AddRetryAfterGuard"),
|
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(
|
_timeoutWaitingForReconnectingSocket = LoggerMessage.Define(
|
||||||
LogLevel.Debug,
|
LogLevel.Debug,
|
||||||
new EventId(3019, "TimeoutWaitingForReconnectingSocket"),
|
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>(
|
_waitedForReconnectingSocket = LoggerMessage.Define<long>(
|
||||||
LogLevel.Trace,
|
LogLevel.Trace,
|
||||||
new EventId(3020, "WaitedForReconnectingSocket"),
|
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)
|
public static void FailedToAddSubscriptionRetryOnDifferentConnection(this ILogger logger, int socketId)
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ namespace CryptoExchange.Net.Logging.Extensions
|
|||||||
_unknownExceptionWhileProcessingReconnection = LoggerMessage.Define<int>(
|
_unknownExceptionWhileProcessingReconnection = LoggerMessage.Define<int>(
|
||||||
LogLevel.Warning,
|
LogLevel.Warning,
|
||||||
new EventId(2003, "UnknownExceptionWhileProcessingReconnection"),
|
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?>(
|
_webSocketErrorCodeAndDetails = LoggerMessage.Define<int, WebSocketError, string?>(
|
||||||
LogLevel.Warning,
|
LogLevel.Warning,
|
||||||
@@ -180,7 +180,7 @@ namespace CryptoExchange.Net.Logging.Extensions
|
|||||||
_receivedMessageNotMatchedToAnyListener = LoggerMessage.Define<int, string, string, string>(
|
_receivedMessageNotMatchedToAnyListener = LoggerMessage.Define<int, string, string, string>(
|
||||||
LogLevel.Warning,
|
LogLevel.Warning,
|
||||||
new EventId(2029, "ReceivedMessageNotMatchedToAnyListener"),
|
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>(
|
_failedToParse = LoggerMessage.Define<int, string>(
|
||||||
LogLevel.Warning,
|
LogLevel.Warning,
|
||||||
@@ -326,9 +326,9 @@ namespace CryptoExchange.Net.Logging.Extensions
|
|||||||
_sendingData(logger, socketId, requestId, data, null);
|
_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)
|
public static void SendingByteData(this ILogger logger, int socketId, int requestId, int length)
|
||||||
|
|||||||
@@ -1,589 +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()
|
|
||||||
{
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -149,6 +149,14 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class ServerError : Error
|
public class ServerError : Error
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
public ServerError(ErrorType type, string message, Exception? exception = null)
|
||||||
|
: base(null, new ErrorInfo(type, message), exception)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using CryptoExchange.Net.Authentication;
|
using CryptoExchange.Net.Authentication;
|
||||||
|
using CryptoExchange.Net.RateLimiting;
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Objects.Options
|
namespace CryptoExchange.Net.Objects.Options
|
||||||
@@ -22,6 +23,10 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
/// Note that this comes at a performance cost
|
/// Note that this comes at a performance cost
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool OutputOriginalData { get; set; } = false;
|
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>
|
/// <summary>
|
||||||
/// The max time a request is allowed to take
|
/// The max time a request is allowed to take
|
||||||
@@ -37,10 +42,16 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public RateLimitingBehaviour RateLimitingBehaviour { get; set; } = RateLimitingBehaviour.Wait;
|
public RateLimitingBehaviour RateLimitingBehaviour { get; set; } = RateLimitingBehaviour.Wait;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A delegate receiving the request definition and the request weight that can be used to determine whether a request should be admitted or rejected based
|
||||||
|
/// on the request definition and the current rate limit usage ratio. This allows for custom rate limiting logic to be implemented.
|
||||||
|
/// </summary>
|
||||||
|
public Func<RequestDefinition, int, RateLimitAdmission>? RateLimitAdmission { get; set; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return $"RequestTimeout: {RequestTimeout}, Proxy: {(Proxy == null ? "-" : "set")}";
|
return $"Proxy: {(Proxy == null ? "-" : "set")}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,11 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
= new Version(1, 1);
|
= new Version(1, 1);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether concurrent identical REST requests may share the same in-flight request when allowed by the request definition.
|
||||||
|
/// </summary>
|
||||||
|
public bool RequestCoalescingEnabled { get; set; } = true;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Http client keep alive interval for keeping connections open. Only applied when using dotnet8.0 or higher and dependency injection
|
/// Http client keep alive interval for keeping connections open. Only applied when using dotnet8.0 or higher and dependency injection
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -69,9 +74,12 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
item.RequestTimeout = RequestTimeout;
|
item.RequestTimeout = RequestTimeout;
|
||||||
item.RateLimiterEnabled = RateLimiterEnabled;
|
item.RateLimiterEnabled = RateLimiterEnabled;
|
||||||
item.RateLimitingBehaviour = RateLimitingBehaviour;
|
item.RateLimitingBehaviour = RateLimitingBehaviour;
|
||||||
|
item.RateLimitGroup = RateLimitGroup;
|
||||||
|
item.RateLimitAdmission = RateLimitAdmission;
|
||||||
item.CachingEnabled = CachingEnabled;
|
item.CachingEnabled = CachingEnabled;
|
||||||
item.CachingMaxAge = CachingMaxAge;
|
item.CachingMaxAge = CachingMaxAge;
|
||||||
item.HttpVersion = HttpVersion;
|
item.HttpVersion = HttpVersion;
|
||||||
|
item.RequestCoalescingEnabled = RequestCoalescingEnabled;
|
||||||
item.HttpKeepAliveInterval = HttpKeepAliveInterval;
|
item.HttpKeepAliveInterval = HttpKeepAliveInterval;
|
||||||
#if NET5_0_OR_GREATER
|
#if NET5_0_OR_GREATER
|
||||||
item.HttpMaxConnectionsPerServer = HttpMaxConnectionsPerServer;
|
item.HttpMaxConnectionsPerServer = HttpMaxConnectionsPerServer;
|
||||||
@@ -104,6 +112,12 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
target.Environment = Environment;
|
target.Environment = Environment;
|
||||||
return target;
|
return target;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return $"{base.ToString()} | Environment: {Environment.Name}";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -130,7 +144,7 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return $"{base.ToString()}, ApiCredentials: {(ApiCredentials == null ? "-" : "set")}";
|
return $"{base.ToString()} | ApiCredentials: {(ApiCredentials == null ? "-" : "set")}";
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,6 +104,8 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
item.RequestTimeout = RequestTimeout;
|
item.RequestTimeout = RequestTimeout;
|
||||||
item.RateLimitingBehaviour = RateLimitingBehaviour;
|
item.RateLimitingBehaviour = RateLimitingBehaviour;
|
||||||
item.RateLimiterEnabled = RateLimiterEnabled;
|
item.RateLimiterEnabled = RateLimiterEnabled;
|
||||||
|
item.RateLimitGroup = RateLimitGroup;
|
||||||
|
item.RateLimitAdmission = RateLimitAdmission;
|
||||||
item.ReceiveBufferSize = ReceiveBufferSize;
|
item.ReceiveBufferSize = ReceiveBufferSize;
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
@@ -131,6 +133,12 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
target.Environment = Environment;
|
target.Environment = Environment;
|
||||||
return target;
|
return target;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return $"{base.ToString()} | Environment: {Environment.Name}";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -158,7 +166,7 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return $"{base.ToString()}, ApiCredentials: {(ApiCredentials == null ? "-" : "set")}";
|
return $"{base.ToString()} | ApiCredentials: {(ApiCredentials == null ? "-" : "set")}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,19 +6,27 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Options to update
|
/// Options to update
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class UpdateOptions<TApiCredentials> where TApiCredentials : ApiCredentials
|
public class UpdateOptions
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Proxy setting. Note that if this is not provided any previously set proxy will be reset
|
/// Proxy setting. Note that if this is not provided any previously set proxy will be reset
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ApiProxy? Proxy { get; set; }
|
public ApiProxy? Proxy { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Api credentials
|
|
||||||
/// </summary>
|
|
||||||
public TApiCredentials? ApiCredentials { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Request timeout
|
/// Request timeout
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public TimeSpan? RequestTimeout { get; set; }
|
public TimeSpan? RequestTimeout { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Options to update
|
||||||
|
/// </summary>
|
||||||
|
public class UpdateOptions<TApiCredentials>: UpdateOptions
|
||||||
|
where TApiCredentials : ApiCredentials
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Api credentials
|
||||||
|
/// </summary>
|
||||||
|
public TApiCredentials? ApiCredentials { get; set; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user