mirror of
https://github.com/JKorf/CryptoExchange.Net.git
synced 2026-08-12 17:03:10 +00:00
Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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,71 @@
|
||||
---
|
||||
description: Conventions for cross-exchange code using CryptoExchange.Net SharedApis abstractions. Apply when generating C# code that interacts with multiple cryptocurrency exchanges through a unified interface.
|
||||
globs:
|
||||
- "**/*.cs"
|
||||
- "**/*.csproj"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# CryptoExchange.Net Conventions
|
||||
|
||||
This codebase uses **CryptoExchange.Net** abstractions for multi-exchange access. Each exchange has its own library (Binance.Net, Bybit.Net, OKX.Net, ...). Use `CryptoExchange.Net.SharedApis` for code that should work across exchanges.
|
||||
|
||||
## Multi-exchange pattern
|
||||
|
||||
```csharp
|
||||
using Binance.Net.Clients;
|
||||
using OKX.Net.Clients;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
|
||||
ISpotTickerRestClient binance = new BinanceRestClient().SpotApi.SharedClient;
|
||||
ISpotTickerRestClient okx = new OKXRestClient().UnifiedApi.SharedClient;
|
||||
|
||||
var symbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
|
||||
|
||||
var ticker = await binance.GetSpotTickerAsync(new GetTickerRequest(symbol));
|
||||
// ticker.Data.LastPrice — same model regardless of exchange
|
||||
```
|
||||
|
||||
## Symbol normalization
|
||||
|
||||
`SharedSymbol(TradingMode.Spot, "BTC", "USDT")` is portable. Each library translates to its native format internally. Don't pass raw strings like `"BTCUSDT"` to shared methods.
|
||||
|
||||
## Result pattern
|
||||
|
||||
Same `WebCallResult<T>` / `CallResult<T>` everywhere. Always check `.Success`. `.Exchange` property identifies which exchange responded — useful for logging.
|
||||
|
||||
## Available shared interfaces
|
||||
|
||||
- REST tickers/symbols/orderbook/klines/trades, orders (spot/futures, regular/trigger/TP-SL), balances, positions, fees, deposits/withdrawals, transfers
|
||||
- WebSocket tickers, book tickers, order book, trades, klines, user data
|
||||
|
||||
Each exchange documents which it implements. Not every exchange supports every operation.
|
||||
|
||||
## Multi-exchange aggregation
|
||||
|
||||
Run requests across exchanges concurrently via `Task.WhenAll` — the library is async-safe and concurrent requests are the norm.
|
||||
|
||||
```csharp
|
||||
var clients = new ISpotTickerRestClient[] { binance, okx, bybit };
|
||||
var tasks = clients.Select(c => c.GetSpotTickerAsync(new GetTickerRequest(symbol)));
|
||||
var results = await Task.WhenAll(tasks);
|
||||
```
|
||||
|
||||
## Hard rules
|
||||
|
||||
- ❌ Never install `CryptoExchange.Net` alone and expect to call exchanges — it's a base library; you need exchange-specific packages
|
||||
- ❌ Never mix exchange-specific models in cross-exchange code (use `SharedSymbol`, `SharedSpotTicker`, etc.)
|
||||
- ❌ Never use `.Result` / `.Wait()` — async-only
|
||||
- ❌ Never iterate sequentially when concurrency is fine — `Task.WhenAll` is your friend
|
||||
- ❌ Never instantiate clients per-request — reuse via DI
|
||||
- ✅ Always use `.SharedClient` for cross-exchange code
|
||||
- ✅ Always check `.Success` before reading `.Data`
|
||||
- ✅ Always log with `.Exchange` so multi-exchange logs are decipherable
|
||||
- ✅ Always handle "not supported on this exchange" errors gracefully
|
||||
|
||||
## Reference
|
||||
|
||||
- `AGENTS.md` in repo root has fuller examples
|
||||
- `llms.txt` for AI context
|
||||
- `Examples/ai-friendly/` for compilable examples
|
||||
- For single-exchange code, see that exchange's library (e.g., Binance.Net `AGENTS.md`)
|
||||
@@ -0,0 +1,52 @@
|
||||
# Copilot Instructions for CryptoExchange.Net
|
||||
|
||||
This repository is **CryptoExchange.Net** — the base library powering 28+ cryptocurrency exchange wrappers in C#/.NET (Binance.Net, Bybit.Net, OKX.Net, Kraken.Net, Coinbase.Net, etc.).
|
||||
|
||||
When generating code in this ecosystem, follow these conventions:
|
||||
|
||||
## You don't install CryptoExchange.Net directly
|
||||
|
||||
Install the exchange-specific library you need (`Binance.Net`, `JK.OKX.Net`, `Bybit.Net`, ...) or `CryptoClients.Net` for the bundle. CryptoExchange.Net is pulled in as a dependency.
|
||||
|
||||
## Multi-exchange code uses SharedApis
|
||||
|
||||
For code that must work against multiple exchanges, use `CryptoExchange.Net.SharedApis` interfaces accessed via `.SharedClient` properties on each exchange's API surface:
|
||||
|
||||
```csharp
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
|
||||
ISpotTickerRestClient binance = new BinanceRestClient().SpotApi.SharedClient;
|
||||
ISpotTickerRestClient okx = new OKXRestClient().UnifiedApi.SharedClient;
|
||||
|
||||
var symbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
|
||||
var ticker = await binance.GetSpotTickerAsync(new GetTickerRequest(symbol));
|
||||
```
|
||||
|
||||
Same code works on every exchange that implements the interface. Use `Task.WhenAll` for concurrent multi-exchange calls.
|
||||
|
||||
## Single-exchange code uses the exchange's own client
|
||||
|
||||
For Binance-only code, use `BinanceRestClient` directly (see Binance.Net repo `AGENTS.md`). SharedApis is for portability — use it when you need that.
|
||||
|
||||
## Result pattern
|
||||
|
||||
Every method returns `WebCallResult<T>` or `CallResult<T>`. Check `.Success` before `.Data`. `.Error` has structured info. `.Exchange` on shared clients identifies which exchange responded.
|
||||
|
||||
## Available shared interfaces
|
||||
|
||||
REST: tickers, symbols, orderbook, klines, trades, orders (spot/futures, trigger, TP-SL), balances, positions, fees, deposits/withdrawals, transfers.
|
||||
WebSocket: tickers, book tickers, orderbook, trades, klines, user data.
|
||||
|
||||
Each exchange library implements a subset. Check exchange docs for support matrix.
|
||||
|
||||
## Avoid
|
||||
|
||||
- Installing `CryptoExchange.Net` alone and trying to call exchange APIs (need exchange-specific packages)
|
||||
- Mixing exchange-native models in cross-exchange code (use Shared* types)
|
||||
- Synchronous `.Result` / `.Wait()` (use `await`)
|
||||
- Instantiating clients per-request (use DI, reuse instances)
|
||||
- Sequential per-exchange calls when parallel is fine (`Task.WhenAll`)
|
||||
|
||||
## Reference
|
||||
|
||||
For detailed patterns see `AGENTS.md` and `llms.txt` in repo root, `examples/ai-friendly/` for compilable examples.
|
||||
@@ -0,0 +1,169 @@
|
||||
---
|
||||
name: cryptoexchange-net
|
||||
description: Use CryptoExchange.Net abstractions when generating C#/.NET code that needs to work across MULTIPLE cryptocurrency exchanges (Binance + Bybit + OKX + Kraken + Coinbase + ...) — for arbitrage, best-execution routing, multi-exchange portfolio dashboards, exchange-agnostic trading bots, or comparison tools. Triggers on requests mentioning multi-exchange, cross-exchange, arbitrage, exchange-agnostic, or unified crypto API in C#. Also triggers when the user is implementing a new exchange library following the CryptoExchange.Net pattern.
|
||||
---
|
||||
|
||||
# CryptoExchange.Net Skill
|
||||
|
||||
## When to use
|
||||
|
||||
CryptoExchange.Net is the **base library** powering 28+ exchange-specific libraries (Binance.Net, Bybit.Net, OKX.Net, Kraken.Net, Coinbase.Net, etc.). You don't install it directly — you install the exchange libraries, which depend on it.
|
||||
|
||||
**Three usage modes:**
|
||||
|
||||
1. **You target ONE exchange** → use that exchange's library directly (e.g., Binance.Net), see its CLAUDE.md.
|
||||
2. **You target MULTIPLE exchanges** → install each library you need + use `CryptoExchange.Net.SharedApis` interfaces — write code once, runs against any exchange. **This is the main use case for this skill.**
|
||||
3. **You want ALL exchanges in one package** → install `CryptoClients.Net`, get `ExchangeRestClient` and `ExchangeSocketClient` with everything bundled.
|
||||
|
||||
## Installation
|
||||
|
||||
For a multi-exchange project:
|
||||
|
||||
```bash
|
||||
dotnet add package Binance.Net
|
||||
dotnet add package JK.OKX.Net
|
||||
dotnet add package Bybit.Net
|
||||
# ... etc
|
||||
```
|
||||
|
||||
Or the bundle:
|
||||
|
||||
```bash
|
||||
dotnet add package CryptoClients.Net
|
||||
```
|
||||
|
||||
## Core Pattern: Shared Interfaces
|
||||
|
||||
Every exchange library exposes `.SharedClient` properties on its API surfaces. These implement the same interfaces from `CryptoExchange.Net.SharedApis`.
|
||||
|
||||
```csharp
|
||||
using Binance.Net.Clients;
|
||||
using OKX.Net.Clients;
|
||||
using Bybit.Net.Clients;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
|
||||
// All three implement ISpotTickerRestClient
|
||||
ISpotTickerRestClient binance = new BinanceRestClient().SpotApi.SharedClient;
|
||||
ISpotTickerRestClient okx = new OKXRestClient().UnifiedApi.SharedClient;
|
||||
ISpotTickerRestClient bybit = new BybitRestClient().V5Api.SharedClient;
|
||||
|
||||
// Single agnostic call — works against any of them
|
||||
var symbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
|
||||
var ticker = await binance.GetSpotTickerAsync(new GetTickerRequest(symbol));
|
||||
// ticker.Data.LastPrice, ticker.Data.HighPrice, etc. — same model regardless of exchange
|
||||
```
|
||||
|
||||
## Core Pattern: SharedSymbol
|
||||
|
||||
Different exchanges format symbols differently — Binance uses `BTCUSDT`, OKX uses `BTC-USDT`, others may have other formats. `SharedSymbol` normalizes this:
|
||||
|
||||
```csharp
|
||||
var btcusdt = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
|
||||
// Each exchange library translates SharedSymbol → its native format internally.
|
||||
|
||||
// For futures:
|
||||
var btcusdtPerp = new SharedSymbol(TradingMode.PerpetualLinear, "BTC", "USDT");
|
||||
```
|
||||
|
||||
For exchanges that use exotic asset names, see the AssetAliases configuration.
|
||||
|
||||
## Available Shared Interfaces
|
||||
|
||||
**REST:**
|
||||
|
||||
- Market data: `ISpotTickerRestClient`, `IBookTickerRestClient`, `ISpotSymbolRestClient`, `IFuturesSymbolRestClient`, `IOrderBookRestClient`, `IRecentTradeRestClient`, `IKlineRestClient`
|
||||
- Orders: `ISpotOrderRestClient`, `IFuturesOrderRestClient`, `ISpotOrderClientIdRestClient`, `IFuturesOrderClientIdRestClient`, `ISpotTriggerOrderRestClient`, `IFuturesTriggerOrderRestClient`, `IFuturesTpSlRestClient`
|
||||
- Account: `IBalanceRestClient`, `IPositionRestClient`, `IFeeRestClient`, `ITransferRestClient`, `IDepositRestClient`, `IWithdrawalRestClient`
|
||||
|
||||
**WebSocket:**
|
||||
|
||||
- `ITickerSocketClient`, `IBookTickerSocketClient`
|
||||
- `IOrderBookSocketClient`, `ITradeSocketClient`, `IKlineSocketClient`
|
||||
- `IUserTradeSocketClient`, `ISpotOrderSocketClient`, `IFuturesOrderSocketClient`, `IPositionSocketClient`, `IBalanceSocketClient`
|
||||
|
||||
Each exchange documents which interfaces it implements (some exchanges don't support every operation).
|
||||
|
||||
## Core Pattern: Result Handling
|
||||
|
||||
Same as exchange-specific libraries — `WebCallResult<T>` (REST) or `CallResult<T>` (WebSocket) with `.Success`, `.Data`, `.Error`. Always check `.Success` first.
|
||||
|
||||
```csharp
|
||||
var result = await sharedClient.GetSpotTickerAsync(new GetTickerRequest(symbol));
|
||||
if (!result.Success)
|
||||
{
|
||||
Console.WriteLine($"[{sharedClient.Exchange}] Error: {result.Error}");
|
||||
return;
|
||||
}
|
||||
Console.WriteLine($"[{sharedClient.Exchange}] {result.Data.LastPrice}");
|
||||
```
|
||||
|
||||
`.Exchange` property on every shared client tells you which exchange you're talking to — useful for logging.
|
||||
|
||||
## Core Pattern: Multi-Exchange Aggregation
|
||||
|
||||
```csharp
|
||||
var clients = new ISpotTickerRestClient[]
|
||||
{
|
||||
new BinanceRestClient().SpotApi.SharedClient,
|
||||
new OKXRestClient().UnifiedApi.SharedClient,
|
||||
new BybitRestClient().V5Api.SharedClient,
|
||||
};
|
||||
|
||||
var symbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
|
||||
|
||||
// Fetch concurrently from all exchanges
|
||||
var tasks = clients.Select(c => c.GetSpotTickerAsync(new GetTickerRequest(symbol))).ToArray();
|
||||
var results = await Task.WhenAll(tasks);
|
||||
|
||||
for (int i = 0; i < clients.Length; i++)
|
||||
{
|
||||
if (results[i].Success)
|
||||
Console.WriteLine($"{clients[i].Exchange}: {results[i].Data!.LastPrice}");
|
||||
}
|
||||
```
|
||||
|
||||
## Per-Exchange Setup
|
||||
|
||||
Each exchange library has its own credentials class and options. See each library's CLAUDE.md for specifics. The pattern is consistent: `XxxRestClient(options => { options.ApiCredentials = new XxxCredentials(...); })`.
|
||||
|
||||
## Dependency Injection
|
||||
|
||||
Each exchange library has its own `services.AddXxx(...)` extension. They all share the same option-builder pattern. Register only the ones you use:
|
||||
|
||||
```csharp
|
||||
services.AddBinance(restOpts => { /*...*/ }, socketOpts => { /*...*/ });
|
||||
services.AddOKX(restOpts => { /*...*/ }, socketOpts => { /*...*/ });
|
||||
// Inject IBinanceRestClient, IOKXRestClient, etc.
|
||||
```
|
||||
|
||||
For one-package access: `services.AddCryptoClients(...)` from `CryptoClients.Net`.
|
||||
|
||||
## Common Pitfalls — AVOID
|
||||
|
||||
- **Do NOT install `CryptoExchange.Net` and try to call exchange APIs directly** — it's a base abstraction; you need an exchange library.
|
||||
- **Do NOT try to use one exchange's models with another's client** — use the SharedApis types (`SharedSymbol`, `SharedSpotTicker`, `SharedSpotOrder`, etc.) for cross-exchange code.
|
||||
- **Do NOT block on async operations** — use `await` throughout. `Task.WhenAll` for parallelism across exchanges.
|
||||
- **Do NOT assume every exchange supports every operation** — check exchange docs or the library's implementation. Operations may return errors like "not supported on this exchange".
|
||||
- **Do NOT instantiate clients per-request** — reuse via DI.
|
||||
- **Do NOT iterate exchanges sequentially when concurrency is fine** — use `Task.WhenAll` for ~Nx speedup.
|
||||
|
||||
## Implementing a New Exchange Library
|
||||
|
||||
If you're building a NEW exchange wrapper following the CryptoExchange.Net pattern (rare but valuable):
|
||||
|
||||
- Inherit from `RestApiClient` and `SocketApiClient` base classes
|
||||
- Define your own `XxxCredentials` extending `ApiCredentials` (or use `ApiCredentials` directly)
|
||||
- Implement `AuthenticationProvider` for the exchange's signing scheme
|
||||
- Implement the relevant `Shared*` interfaces on your API client classes for cross-exchange support
|
||||
- Follow the same `XxxRestOptions` / `XxxSocketOptions` pattern
|
||||
|
||||
See existing libraries (Binance.Net, Bybit.Net) as reference implementations.
|
||||
|
||||
## Reference
|
||||
|
||||
- Source: https://github.com/JKorf/CryptoExchange.Net
|
||||
- Documentation: https://cryptoexchange.jkorf.dev/
|
||||
- SharedApis docs: https://cryptoexchange.jkorf.dev/CryptoExchange.Net/idocs_shared.html
|
||||
- Bundle (all exchanges): https://github.com/JKorf/CryptoClients.Net
|
||||
- Demo app: https://github.com/JKorf/CryptoManager.Net
|
||||
- Discord: https://discord.gg/MSpeEtSY8t
|
||||
@@ -96,7 +96,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
waiters.Add(evnt.WaitAsync());
|
||||
}
|
||||
|
||||
List<bool> results = null;
|
||||
List<bool>? results = null;
|
||||
var resultsWaiter = Task.Run(async () =>
|
||||
{
|
||||
await Task.WhenAll(waiters);
|
||||
@@ -112,7 +112,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
|
||||
await resultsWaiter;
|
||||
|
||||
Assert.That(10 == results.Count(r => r));
|
||||
Assert.That(10 == results?.Count(r => r));
|
||||
}
|
||||
|
||||
[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.Legacy;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
|
||||
@@ -17,7 +16,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
var result = new CallResult(new ServerError("TestError", ErrorInfo.Unknown));
|
||||
|
||||
ClassicAssert.AreSame(result.Error.ErrorCode, "TestError");
|
||||
ClassicAssert.AreSame(result.Error!.ErrorCode, "TestError");
|
||||
ClassicAssert.IsFalse(result);
|
||||
ClassicAssert.IsFalse(result.Success);
|
||||
}
|
||||
@@ -37,7 +36,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
var result = new CallResult<object>(new ServerError("TestError", ErrorInfo.Unknown));
|
||||
|
||||
ClassicAssert.AreSame(result.Error.ErrorCode, "TestError");
|
||||
ClassicAssert.AreSame(result.Error!.ErrorCode, "TestError");
|
||||
ClassicAssert.IsNull(result.Data);
|
||||
ClassicAssert.IsFalse(result);
|
||||
ClassicAssert.IsFalse(result.Success);
|
||||
@@ -74,7 +73,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var asResult = result.As<TestObject2>(default);
|
||||
|
||||
ClassicAssert.IsNotNull(asResult.Error);
|
||||
ClassicAssert.AreSame(asResult.Error.ErrorCode, "TestError");
|
||||
ClassicAssert.AreSame(asResult.Error!.ErrorCode, "TestError");
|
||||
ClassicAssert.IsNull(asResult.Data);
|
||||
ClassicAssert.IsFalse(asResult);
|
||||
ClassicAssert.IsFalse(asResult.Success);
|
||||
@@ -87,7 +86,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var asResult = result.AsError<TestObject2>(new ServerError("TestError2", ErrorInfo.Unknown));
|
||||
|
||||
ClassicAssert.IsNotNull(asResult.Error);
|
||||
ClassicAssert.AreSame(asResult.Error.ErrorCode, "TestError2");
|
||||
ClassicAssert.AreSame(asResult.Error!.ErrorCode, "TestError2");
|
||||
ClassicAssert.IsNull(asResult.Data);
|
||||
ClassicAssert.IsFalse(asResult);
|
||||
ClassicAssert.IsFalse(asResult.Success);
|
||||
@@ -100,7 +99,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var asResult = result.AsError<TestObject2>(new ServerError("TestError2", ErrorInfo.Unknown));
|
||||
|
||||
ClassicAssert.IsNotNull(asResult.Error);
|
||||
ClassicAssert.AreSame(asResult.Error.ErrorCode, "TestError2");
|
||||
ClassicAssert.AreSame(asResult.Error!.ErrorCode, "TestError2");
|
||||
ClassicAssert.IsNull(asResult.Data);
|
||||
ClassicAssert.IsFalse(asResult);
|
||||
ClassicAssert.IsFalse(asResult.Success);
|
||||
@@ -127,7 +126,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var asResult = result.AsError<TestObject2>(new ServerError("TestError2", ErrorInfo.Unknown));
|
||||
|
||||
ClassicAssert.IsNotNull(asResult.Error);
|
||||
Assert.That(asResult.Error.ErrorCode == "TestError2");
|
||||
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");
|
||||
|
||||
+11
-12
@@ -1,23 +1,22 @@
|
||||
using NUnit.Framework;
|
||||
using NUnit.Framework.Legacy;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests
|
||||
namespace CryptoExchange.Net.UnitTests.ClientTests
|
||||
{
|
||||
[TestFixture()]
|
||||
public class BaseClientTests
|
||||
{
|
||||
[TestCase]
|
||||
public void DeserializingValidJson_Should_GiveSuccessfulResult()
|
||||
{
|
||||
// arrange
|
||||
var client = new TestBaseClient();
|
||||
//[TestCase]
|
||||
//public void DeserializingValidJson_Should_GiveSuccessfulResult()
|
||||
//{
|
||||
// // arrange
|
||||
// var client = new TestBaseClient();
|
||||
|
||||
// act
|
||||
var result = client.SubClient.Deserialize<object>("{\"testProperty\": 123}");
|
||||
// // act
|
||||
// var result = client.SubClient.Deserialize<object>("{\"testProperty\": 123}");
|
||||
|
||||
// assert
|
||||
Assert.That(result.Success);
|
||||
}
|
||||
// // assert
|
||||
// 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")]
|
||||
@@ -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 ParameterCollection
|
||||
{
|
||||
{ "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,177 @@
|
||||
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 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,109 @@
|
||||
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"));
|
||||
}
|
||||
}
|
||||
|
||||
[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,57 @@
|
||||
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("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,119 @@
|
||||
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("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>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<IsPackable>false</IsPackable>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -326,7 +326,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
|
||||
// assert
|
||||
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.TradingMode, Is.EqualTo(TradingMode.Spot));
|
||||
Assert.That(result.SymbolName, Is.EqualTo("BTCUSDT"));
|
||||
|
||||
@@ -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;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
namespace CryptoExchange.Net.UnitTests.Implementations
|
||||
{
|
||||
public class TestObject
|
||||
{
|
||||
[JsonPropertyName("other")]
|
||||
public string StringData { get; set; }
|
||||
public string StringData { get; set; } = string.Empty;
|
||||
[JsonPropertyName("intData")]
|
||||
public int IntData { get; set; }
|
||||
[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.CreateWithoutTopicFilter<TestSocketMessage>(request.Id.ToString(), HandleMessage);
|
||||
}
|
||||
|
||||
private CallResult? HandleMessage(SocketConnection connection, DateTime time, string? arg3, TestSocketMessage message)
|
||||
{
|
||||
if (message.Data != "OK")
|
||||
return new CallResult(new ServerError(ErrorInfo.Unknown with { Message = message.Data }));
|
||||
|
||||
return CallResult.SuccessResult;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(ILogger logger, HttpClient? httpClient, TestRestOptions options)
|
||||
: base(logger, 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<WebCallResult<T>> GetResponseAsync<T>(HttpMethod? httpMethod = null, ParameterCollection? collection = null, RateLimitGate? rateLimitGate = null)
|
||||
{
|
||||
var definition = new RequestDefinition("/path", httpMethod ?? HttpMethod.Get)
|
||||
{
|
||||
Weight = rateLimitGate == null ? 0 : 1,
|
||||
RateLimitGate = rateLimitGate
|
||||
};
|
||||
return await SendAsync<T>(BaseAddress, definition, collection ?? new ParameterCollection(), 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(_logger, httpClient, options.Value));
|
||||
ApiClient2 = AddApiClient(new TestRestApiClient(_logger, 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,29 @@
|
||||
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(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,44 @@
|
||||
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(ILogger logger, TestSocketOptions options)
|
||||
: base(logger, options.Environment.SocketClientAddress, options, options.ExchangeOptions)
|
||||
{
|
||||
}
|
||||
|
||||
public TestSocketApiClient(ILogger logger, HttpClient httpClient, string baseAddress, TestSocketOptions options, SocketApiOptions apiOptions)
|
||||
: base(logger, 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<CallResult<UpdateSubscription>> SubscribeToUpdatesAsync<T>(Action<DataEvent<T>> handler, bool subQuery, CancellationToken ct)
|
||||
{
|
||||
return await base.SubscribeAsync(new TestSubscription<T>(_logger, handler, subQuery, false), 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(_logger, options.Value));
|
||||
ApiClient2 = AddApiClient(new TestSocketApiClient(_logger, 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.CreateWithoutTopicFilter<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.SuccessResult;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.UnitTests.TestImplementations;
|
||||
using CryptoExchange.Net.UnitTests.Implementations;
|
||||
using NUnit.Framework;
|
||||
using System;
|
||||
|
||||
@@ -10,9 +11,9 @@ namespace CryptoExchange.Net.UnitTests
|
||||
public class OptionsTests
|
||||
{
|
||||
[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.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();
|
||||
});
|
||||
@@ -42,53 +43,110 @@ namespace CryptoExchange.Net.UnitTests
|
||||
public void TestBasicOptionsAreSet()
|
||||
{
|
||||
// arrange, act
|
||||
var options = new TestClientOptions
|
||||
var options = new TestRestOptions
|
||||
{
|
||||
ApiCredentials = new HMACCredential("123", "456"),
|
||||
ReceiveWindow = TimeSpan.FromSeconds(10)
|
||||
ApiCredentials = new TestCredentials("123", "456"),
|
||||
RequestTimeout = TimeSpan.FromSeconds(10)
|
||||
};
|
||||
|
||||
// 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.Secret == "456");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class TestClientOptions: RestExchangeOptions<TestEnvironment, HMACCredential>
|
||||
{
|
||||
/// <summary>
|
||||
/// Default options for the futures client
|
||||
/// </summary>
|
||||
public static TestClientOptions Default { get; set; } = new TestClientOptions()
|
||||
[Test]
|
||||
public void TestSetOptionsRest()
|
||||
{
|
||||
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>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public TestClientOptions()
|
||||
{
|
||||
Default?.Set(this);
|
||||
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)));
|
||||
}
|
||||
|
||||
/// <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)
|
||||
[Test]
|
||||
public void TestSetOptionsRestWithCredentials()
|
||||
{
|
||||
targetOptions = base.Set<TestClientOptions>(targetOptions);
|
||||
targetOptions.Api1Options = Api1Options.Set(targetOptions.Api1Options);
|
||||
targetOptions.Api2Options = Api2Options.Set(targetOptions.Api2Options);
|
||||
return targetOptions;
|
||||
var client = new TestRestClient();
|
||||
client.SetOptions(new UpdateOptions<TestCredentials>
|
||||
{
|
||||
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,331 @@
|
||||
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 ParameterCollection();
|
||||
parameters.Add("test", "value");
|
||||
Assert.That(parameters["test"], Is.EqualTo("value"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingBasicNullValue_ThrowsException()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
Assert.Throws<ArgumentNullException>(() => parameters.Add("test", null!));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingOptionalBasicValue_SetValueCorrectly()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddOptional("test", "value");
|
||||
Assert.That(parameters["test"], Is.EqualTo("value"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingOptionalBasicNullValue_DoesntSetValue()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddOptional("test", null);
|
||||
Assert.That(parameters.ContainsKey("test"), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingDecimalValueAsString_SetValueCorrectly()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddString("test", 0.1m);
|
||||
Assert.That(parameters["test"], Is.EqualTo("0.1"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingOptionalDecimalValueAsString_SetValueCorrectly()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddOptionalString("test", 0.1m);
|
||||
Assert.That(parameters["test"], Is.EqualTo("0.1"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingOptionalDecimalNullValueAsString_DoesntSetValue()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddOptionalString("test", (decimal?)null);
|
||||
Assert.That(parameters.ContainsKey("test"), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingIntValueAsString_SetValueCorrectly()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddString("test", 1);
|
||||
Assert.That(parameters["test"], Is.EqualTo("1"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingOptionalIntValueAsString_SetValueCorrectly()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddOptionalString("test", 1);
|
||||
Assert.That(parameters["test"], Is.EqualTo("1"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingOptionalIntNullValueAsString_DoesntSetValue()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddOptionalString("test", (int?)null);
|
||||
Assert.That(parameters.ContainsKey("test"), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingLongValueAsString_SetValueCorrectly()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddString("test", 1L);
|
||||
Assert.That(parameters["test"], Is.EqualTo("1"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingOptionalLongValueAsString_SetValueCorrectly()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddOptionalString("test", 1L);
|
||||
Assert.That(parameters["test"], Is.EqualTo("1"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingOptionalLongNullValueAsString_DoesntSetValue()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddOptionalString("test", (long?)null);
|
||||
Assert.That(parameters.ContainsKey("test"), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingMillisecondTimestamp_SetValueCorrectly()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddMilliseconds("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc));
|
||||
Assert.That(parameters["test"], Is.EqualTo(1735689600000));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingOptionalMillisecondTimestamp_SetValueCorrectly()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddOptionalMilliseconds("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc));
|
||||
Assert.That(parameters["test"], Is.EqualTo(1735689600000));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingOptionalMillisecondNullValue_DoesntSetValue()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddOptionalMilliseconds("test", null);
|
||||
Assert.That(parameters.ContainsKey("test"), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingMillisecondTimestampString_SetValueCorrectly()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddMillisecondsString("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc));
|
||||
Assert.That(parameters["test"], Is.EqualTo("1735689600000"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingOptionalMillisecondTimestampString_SetValueCorrectly()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddOptionalMillisecondsString("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc));
|
||||
Assert.That(parameters["test"], Is.EqualTo("1735689600000"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingOptionalMillisecondStringNullValue_DoesntSetValue()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddOptionalMillisecondsString("test", null);
|
||||
Assert.That(parameters.ContainsKey("test"), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingSecondTimestamp_SetValueCorrectly()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddSeconds("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc));
|
||||
Assert.That(parameters["test"], Is.EqualTo(1735689600));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingOptionalSecondTimestamp_SetValueCorrectly()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddOptionalSeconds("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc));
|
||||
Assert.That(parameters["test"], Is.EqualTo(1735689600));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingSecondNullValue_DoesntSetValue()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddOptionalSeconds("test", null);
|
||||
Assert.That(parameters.ContainsKey("test"), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingSecondTimestampString_SetValueCorrectly()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddSecondsString("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc));
|
||||
Assert.That(parameters["test"], Is.EqualTo("1735689600"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingOptionalSecondTimestampString_SetValueCorrectly()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddOptionalSecondsString("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc));
|
||||
Assert.That(parameters["test"], Is.EqualTo("1735689600"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingSecondStringNullValue_DoesntSetValue()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddOptionalSecondsString("test", null);
|
||||
Assert.That(parameters.ContainsKey("test"), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingEnum_SetValueCorrectly()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddEnum("test", TestEnum.Two);
|
||||
Assert.That(parameters["test"], Is.EqualTo("2"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingOptionalEnum_SetValueCorrectly()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddOptionalEnum("test", (TestEnum?)TestEnum.Two);
|
||||
Assert.That(parameters["test"], Is.EqualTo("2"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingOptionalEnumNullValue_DoesntSetValue()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddOptionalEnum("test", (TestEnum?)null);
|
||||
Assert.That(parameters.ContainsKey("test"), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingEnumAsInt_SetValueCorrectly()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddEnumAsInt("test", TestEnum.Two);
|
||||
Assert.That(parameters["test"], Is.EqualTo(2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingOptionalEnumAsInt_SetValueCorrectly()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddOptionalEnumAsInt("test", (TestEnum?)TestEnum.Two);
|
||||
Assert.That(parameters["test"], Is.EqualTo(2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingOptionalEnumAsIntNullValue_DoesntSetValue()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddOptionalEnumAsInt("test", (TestEnum?)null);
|
||||
Assert.That(parameters.ContainsKey("test"), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingCommaSeparated_SetValueCorrectly()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddCommaSeparated("test", ["1", "2"]);
|
||||
Assert.That(parameters["test"], Is.EqualTo("1,2"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingOptionalCommaSeparated_SetValueCorrectly()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddOptionalCommaSeparated("test", ["1", "2"]);
|
||||
Assert.That(parameters["test"], Is.EqualTo("1,2"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingOptionalCommaSeparatedNullValue_DoesntSetValue()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddOptionalCommaSeparated("test", (string[]?)null);
|
||||
Assert.That(parameters.ContainsKey("test"), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingCommaSeparatedEnum_SetValueCorrectly()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddCommaSeparated("test", [TestEnum.Two, TestEnum.One]);
|
||||
Assert.That(parameters["test"], Is.EqualTo("2,1"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingOptionalCommaSeparatedEnum_SetValueCorrectly()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddOptionalCommaSeparated("test", [TestEnum.Two, TestEnum.One]);
|
||||
Assert.That(parameters["test"], Is.EqualTo("2,1"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingOptionalCommaSeparatedEnumNullValue_DoesntSetValue()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddOptionalCommaSeparated("test", (TestEnum[]?)null);
|
||||
Assert.That(parameters.ContainsKey("test"), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingBoolString_SetValueCorrectly()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddBoolString("test", true);
|
||||
Assert.That(parameters["test"], Is.EqualTo("true"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingOptionalBoolString_SetValueCorrectly()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddOptionalBoolString("test", true);
|
||||
Assert.That(parameters["test"], Is.EqualTo("true"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingOptionalBoolStringNullValue_DoesntSetValue()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddOptionalBoolString("test", null);
|
||||
Assert.That(parameters.ContainsKey("test"), Is.False);
|
||||
}
|
||||
}
|
||||
}
|
||||
+104
-180
@@ -1,178 +1,23 @@
|
||||
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.UnitTests.Implementations;
|
||||
using NUnit.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
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;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
[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(2, 0.1)]
|
||||
[TestCase(5, 1)]
|
||||
@@ -188,12 +33,12 @@ namespace CryptoExchange.Net.UnitTests
|
||||
|
||||
for (var i = 0; i < requests + 1; i++)
|
||||
{
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
Assert.That(i == requests? triggered : !triggered);
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
Assert.That(i == requests ? triggered : !triggered);
|
||||
}
|
||||
triggered = false;
|
||||
await Task.Delay((int)Math.Round(perSeconds * 1000) + 10);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
Assert.That(!triggered);
|
||||
}
|
||||
|
||||
@@ -209,12 +54,12 @@ namespace CryptoExchange.Net.UnitTests
|
||||
|
||||
var requestDefinition = new RequestDefinition(endpoint, HttpMethod.Get);
|
||||
|
||||
RateLimitEvent evnt = null;
|
||||
RateLimitEvent? evnt = null;
|
||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||
for (var i = 0; i < 2; i++)
|
||||
{
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -231,7 +76,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get);
|
||||
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get);
|
||||
|
||||
RateLimitEvent evnt = null;
|
||||
RateLimitEvent? evnt = null;
|
||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
@@ -271,15 +116,15 @@ namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
var rateLimiter = new RateLimitGate("Test");
|
||||
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerEndpoint, new ExactPathFilter("/sapi/test"), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
|
||||
|
||||
|
||||
var requestDefinition = new RequestDefinition(endpoint, HttpMethod.Get);
|
||||
|
||||
RateLimitEvent evnt = null;
|
||||
RateLimitEvent? evnt = null;
|
||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||
for (var i = 0; i < 2; i++)
|
||||
{
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -294,12 +139,12 @@ namespace CryptoExchange.Net.UnitTests
|
||||
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);
|
||||
|
||||
RateLimitEvent evnt = null;
|
||||
RateLimitEvent? evnt = null;
|
||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||
for (var i = 0; i < 2; i++)
|
||||
{
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -318,7 +163,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get) { Authenticated = key1 != null };
|
||||
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get) { Authenticated = key2 != null };
|
||||
|
||||
RateLimitEvent evnt = null;
|
||||
RateLimitEvent? evnt = null;
|
||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", key1, 1, RateLimitingBehaviour.Wait, null, default);
|
||||
@@ -337,7 +182,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get);
|
||||
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get) { Authenticated = true };
|
||||
|
||||
RateLimitEvent evnt = null;
|
||||
RateLimitEvent? evnt = null;
|
||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
@@ -357,7 +202,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get);
|
||||
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get) { Authenticated = true };
|
||||
|
||||
RateLimitEvent evnt = null;
|
||||
RateLimitEvent? evnt = null;
|
||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, host1, "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
@@ -374,7 +219,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var rateLimiter = new RateLimitGate("Test");
|
||||
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; };
|
||||
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), host1, "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
@@ -389,7 +234,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var rateLimiter = new RateLimitGate("Test");
|
||||
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; };
|
||||
var ct = new CancellationTokenSource(TimeSpan.FromSeconds(0.2));
|
||||
|
||||
@@ -397,5 +242,84 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, ct.Token);
|
||||
Assert.That(result2.Error, Is.TypeOf<CancellationRequestedError>());
|
||||
}
|
||||
|
||||
[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("1", HttpMethod.Get) { ConnectionId = 1 };
|
||||
|
||||
RateLimitEvent? evnt = null;
|
||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||
var ct = new CancellationTokenSource(TimeSpan.FromSeconds(0.2));
|
||||
|
||||
// act
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition, "https://test.com", null, 1, RateLimitingBehaviour.Fail, null, ct.Token);
|
||||
await rateLimiter.ResetAsync(RateLimitItemType.Request, definition, "https://test.com", null, null, default);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition, "https://test.com", null, 1, RateLimitingBehaviour.Fail, null, ct.Token);
|
||||
|
||||
// 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("1", HttpMethod.Get) { ConnectionId = 1 };
|
||||
var definition2 = new RequestDefinition("2", HttpMethod.Get) { ConnectionId = 2 };
|
||||
|
||||
RateLimitEvent? evnt = null;
|
||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||
|
||||
// act
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition1, "https://test.com", null, 1, RateLimitingBehaviour.Fail, null, default);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition2, "https://test.com", null, 1, RateLimitingBehaviour.Fail, null, default);
|
||||
await rateLimiter.ResetAsync(RateLimitItemType.Request, definition1, "https://test.com", null, null, default);
|
||||
var result3 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition2, "https://test.com", null, 1, RateLimitingBehaviour.Fail, null, default);
|
||||
|
||||
// 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using NUnit.Framework;
|
||||
using System;
|
||||
|
||||
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<string>.CreateWithoutTopicFilter("type1", (_, _, _, _) => null),
|
||||
MessageRoute<string>.CreateWithTopicFilter("type1", "topic1", (_, _, _, _) => null),
|
||||
MessageRoute<int>.CreateWithTopicFilter("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<string>.CreateWithoutTopicFilter("type", (_, _, _, _) => null));
|
||||
var beforeMultipleReaders = collection.MultipleReaders;
|
||||
|
||||
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("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<string>.CreateWithoutTopicFilter("type", (_, _, _, _) =>
|
||||
{
|
||||
calls.Add("no-topic");
|
||||
return null;
|
||||
}));
|
||||
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("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<string>.CreateWithTopicFilter("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<string>.CreateWithoutTopicFilter("type", (_, _, _, _) =>
|
||||
{
|
||||
calls.Add("no-topic");
|
||||
return null;
|
||||
}));
|
||||
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("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.SuccessResult;
|
||||
var collection = new QueryRouteCollection(typeof(string));
|
||||
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
|
||||
{
|
||||
calls.Add("first");
|
||||
return expectedResult;
|
||||
}));
|
||||
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
|
||||
{
|
||||
calls.Add("second");
|
||||
return new CallResult(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(expectedResult));
|
||||
Assert.That(calls, Is.EqualTo(new[] { "first" }));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Handle_Should_ContinueAfterNonNullMatchingResult_WhenMultipleReadersIsTrue()
|
||||
{
|
||||
// arrange
|
||||
var calls = new List<string>();
|
||||
var expectedResult = CallResult.SuccessResult;
|
||||
var collection = new QueryRouteCollection(typeof(string));
|
||||
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
|
||||
{
|
||||
calls.Add("first");
|
||||
return expectedResult;
|
||||
}, true));
|
||||
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
|
||||
{
|
||||
calls.Add("second");
|
||||
return new CallResult(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(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.SuccessResult;
|
||||
var collection = new QueryRouteCollection(typeof(string));
|
||||
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
|
||||
{
|
||||
calls.Add("first");
|
||||
return null;
|
||||
}));
|
||||
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
|
||||
{
|
||||
calls.Add("second");
|
||||
return expectedResult;
|
||||
}));
|
||||
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
|
||||
{
|
||||
calls.Add("third");
|
||||
return new CallResult(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(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<string>.CreateWithTopicFilter("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,167 @@
|
||||
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<string>.CreateWithoutTopicFilter("type1", (_, _, _, _) => null),
|
||||
MessageRoute<string>.CreateWithTopicFilter("type1", "topic1", (_, _, _, _) => null)));
|
||||
|
||||
var processor2 = new TestMessageProcessor(
|
||||
2,
|
||||
MessageRouter.Create(
|
||||
MessageRoute<int>.CreateWithTopicFilter("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<string>.CreateWithoutTopicFilter("type1", (_, _, _, _) => null)));
|
||||
|
||||
var processor2 = new TestMessageProcessor(
|
||||
2,
|
||||
MessageRouter.Create(
|
||||
MessageRoute<string>.CreateWithTopicFilter("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<string>.CreateWithoutTopicFilter("type1", (_, _, _, _) => null)));
|
||||
|
||||
var replacementProcessor = new TestMessageProcessor(
|
||||
2,
|
||||
MessageRouter.Create(
|
||||
MessageRoute<int>.CreateWithoutTopicFilter("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<string>.CreateWithoutTopicFilter("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;
|
||||
}
|
||||
|
||||
public event Action? OnMessageRouterUpdated;
|
||||
|
||||
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<string>.CreateWithoutTopicFilter("type1", (_, _, _, _) => null),
|
||||
MessageRoute<string>.CreateWithTopicFilter("type1", "topic1", (_, _, _, _) => null),
|
||||
MessageRoute<int>.CreateWithTopicFilter("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<string>.CreateWithoutTopicFilter("type", (_, _, _, _) =>
|
||||
{
|
||||
calls.Add("no-topic");
|
||||
return null;
|
||||
}));
|
||||
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("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.SuccessResult));
|
||||
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<string>.CreateWithTopicFilter("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.SuccessResult));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Handle_Should_InvokeRoutesWithoutTopicFilter_AndMatchingTopicRoutes()
|
||||
{
|
||||
// arrange
|
||||
var calls = new List<string>();
|
||||
var collection = new SubscriptionRouteCollection(typeof(string));
|
||||
collection.AddRoute(null, MessageRoute<string>.CreateWithoutTopicFilter("type", (_, _, _, _) =>
|
||||
{
|
||||
calls.Add("no-topic");
|
||||
return null;
|
||||
}));
|
||||
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("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.SuccessResult));
|
||||
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<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
|
||||
{
|
||||
calls.Add("first");
|
||||
return CallResult.SuccessResult;
|
||||
}));
|
||||
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("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.SuccessResult));
|
||||
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<string>.CreateWithTopicFilter("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.SuccessResult));
|
||||
Assert.That(calls, Is.Empty);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,11 @@ namespace CryptoExchange.Net.Clients
|
||||
/// </summary>
|
||||
public abstract class BaseRestClient : BaseClient, IRestClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Api clients in this client
|
||||
/// </summary>
|
||||
internal new List<RestApiClient> ApiClients => base.ApiClients.OfType<RestApiClient>().ToList();
|
||||
|
||||
/// <inheritdoc />
|
||||
public int TotalRequestsMade => ApiClients.OfType<RestApiClient>().Sum(s => s.TotalRequestsMade);
|
||||
|
||||
@@ -29,6 +34,14 @@ namespace CryptoExchange.Net.Clients
|
||||
LibraryHelpers.StaticLogger = loggerFactory?.CreateLogger("CryptoExchange");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update options
|
||||
/// </summary>
|
||||
public virtual void SetOptions(UpdateOptions options)
|
||||
{
|
||||
foreach (var apiClient in ApiClients)
|
||||
apiClient.SetOptions(options);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -41,6 +54,11 @@ namespace CryptoExchange.Net.Clients
|
||||
/// </summary>
|
||||
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>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
|
||||
@@ -21,6 +21,11 @@ namespace CryptoExchange.Net.Clients
|
||||
{
|
||||
#region fields
|
||||
|
||||
/// <summary>
|
||||
/// Api clients in this client
|
||||
/// </summary>
|
||||
internal new List<SocketApiClient> ApiClients => base.ApiClients.OfType<SocketApiClient>().ToList();
|
||||
|
||||
/// <summary>
|
||||
/// If client is disposing
|
||||
/// </summary>
|
||||
@@ -135,6 +140,15 @@ namespace CryptoExchange.Net.Clients
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update options
|
||||
/// </summary>
|
||||
public virtual void SetOptions(UpdateOptions options)
|
||||
{
|
||||
foreach (var apiClient in ApiClients)
|
||||
apiClient.SetOptions(options);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -147,6 +161,11 @@ namespace CryptoExchange.Net.Clients
|
||||
/// </summary>
|
||||
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>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
|
||||
@@ -342,7 +342,7 @@ namespace CryptoExchange.Net.Clients
|
||||
GetAuthenticationProvider()?.Key,
|
||||
requestWeight,
|
||||
ClientOptions.RateLimitingBehaviour,
|
||||
rateLimitKeySuffix,
|
||||
rateLimitKeySuffix + ClientOptions.RateLimitGroup,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
if (!limitResult)
|
||||
return limitResult.Error!;
|
||||
@@ -819,6 +819,16 @@ namespace CryptoExchange.Net.Clients
|
||||
&& definition.Method == HttpMethod.Get
|
||||
&& !definition.PreventCaching;
|
||||
|
||||
|
||||
/// <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 />
|
||||
@@ -890,12 +900,9 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <inheritdoc />
|
||||
public virtual void SetOptions(UpdateOptions<TApiCredentials> options)
|
||||
{
|
||||
_proxyConfigured = options.Proxy != null;
|
||||
ClientOptions.Proxy = options.Proxy;
|
||||
ClientOptions.RequestTimeout = options.RequestTimeout ?? ClientOptions.RequestTimeout;
|
||||
base.SetOptions(options);
|
||||
|
||||
ApiCredentials = (TApiCredentials?)options.ApiCredentials?.Copy() ?? ApiCredentials;
|
||||
RequestFactory.UpdateSettings(options.Proxy, options.RequestTimeout ?? ClientOptions.RequestTimeout, ClientOptions.HttpKeepAliveInterval);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -383,8 +383,6 @@ namespace CryptoExchange.Net.Clients
|
||||
released = true;
|
||||
}
|
||||
|
||||
var needsConnecting = !socketConnection.Connected;
|
||||
|
||||
var connectResult = await ConnectIfNeededAsync(socketConnection, false, ct).ConfigureAwait(false);
|
||||
if (!connectResult)
|
||||
return new CallResult<HighPerfUpdateSubscription>(connectResult.Error!);
|
||||
@@ -1035,6 +1033,28 @@ namespace CryptoExchange.Net.Clients
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
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 />
|
||||
@@ -1102,25 +1122,7 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <inheritdoc />
|
||||
public virtual void SetOptions(UpdateOptions<TApiCredentials> options)
|
||||
{
|
||||
var previousProxyIsSet = _proxyConfigured;
|
||||
|
||||
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);
|
||||
base.SetOptions(options);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -67,29 +67,29 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
#endif
|
||||
: JsonConverter<T>, INullableConverterFactory where T : struct, Enum
|
||||
{
|
||||
class EnumMapping
|
||||
{
|
||||
public T Value { get; set; }
|
||||
public string StringValue { get; set; }
|
||||
|
||||
public EnumMapping(T value, string stringValue)
|
||||
{
|
||||
Value = value;
|
||||
StringValue = stringValue;
|
||||
}
|
||||
}
|
||||
|
||||
#if NET8_0_OR_GREATER
|
||||
private static FrozenSet<EnumMapping>? _mappingToEnum = null;
|
||||
private static FrozenDictionary<string, T>? _mappingToEnum = null;
|
||||
private static FrozenDictionary<T, string>? _mappingToString = null;
|
||||
|
||||
private static bool RunOptimistic => true;
|
||||
#else
|
||||
private static List<EnumMapping>? _mappingToEnum = null;
|
||||
private static Dictionary<string, T>? _mappingToEnum = null;
|
||||
private static Dictionary<T, string>? _mappingToString = null;
|
||||
|
||||
// In NetStandard the `ValueTextEquals` method used is slower than just string comparing
|
||||
// so only bother in newer frameworks
|
||||
private static bool RunOptimistic => false;
|
||||
#endif
|
||||
private NullableEnumConverter? _nullableEnumConverter = null;
|
||||
|
||||
private static Type _enumType = typeof(T);
|
||||
private static T? _undefinedEnumValue;
|
||||
private static bool _hasFlagsAttribute = _enumType.IsDefined(typeof(FlagsAttribute));
|
||||
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?>
|
||||
{
|
||||
@@ -120,13 +120,14 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
/// <inheritdoc />
|
||||
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)
|
||||
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
|
||||
_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");
|
||||
}
|
||||
|
||||
@@ -149,13 +150,21 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
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;
|
||||
var enumType = typeof(T);
|
||||
isEmptyStringOrNull = false;
|
||||
if (_mappingToEnum == null)
|
||||
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
|
||||
{
|
||||
JsonTokenType.String => reader.GetString(),
|
||||
@@ -167,13 +176,17 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
};
|
||||
|
||||
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))
|
||||
{
|
||||
isEmptyString = true;
|
||||
isEmptyStringOrNull = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -181,13 +194,22 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
if (!_unknownValuesWarned.Contains(stringValue))
|
||||
{
|
||||
_unknownValuesWarned.Add(stringValue!);
|
||||
LibraryHelpers.StaticLogger?.LogWarning($"Cannot map enum value. EnumType: {enumType.FullName}, Value: {stringValue}, Known values: [{string.Join(", ", _mappingToEnum!.Select(m => $"{m.StringValue}: {m.Value}"))}]. If you think {stringValue} should 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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -198,45 +220,78 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
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
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If not found, try matching ignoring case
|
||||
if (mapping == null)
|
||||
// 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))
|
||||
{
|
||||
if (item.StringValue.Equals(value, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
mapping = item;
|
||||
break;
|
||||
}
|
||||
mapping = item.Value;
|
||||
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);
|
||||
result = (T)Enum.ToObject(objectType, intValue);
|
||||
result = (T)Enum.ToObject(_enumType, intValue);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -258,8 +313,12 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
try
|
||||
{
|
||||
// If no explicit mapping is found try to parse string
|
||||
result = (T)Enum.Parse(objectType, value, true);
|
||||
if (!Enum.IsDefined(objectType, result))
|
||||
#if NET8_0_OR_GREATER
|
||||
result = Enum.Parse<T>(value, true);
|
||||
#else
|
||||
result = (T)Enum.Parse(_enumType, value, true);
|
||||
#endif
|
||||
if (!Enum.IsDefined(_enumType, result))
|
||||
{
|
||||
result = default;
|
||||
return false;
|
||||
@@ -276,35 +335,43 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
|
||||
private static void CreateMapping()
|
||||
{
|
||||
var mappingToEnum = new List<EnumMapping>();
|
||||
var mappingToString = new Dictionary<T, string>();
|
||||
var mappingStringToEnum = new Dictionary<string, T>();
|
||||
var mappingEnumToString = new Dictionary<T, string>();
|
||||
|
||||
var enumType = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
|
||||
var enumMembers = enumType.GetFields();
|
||||
#pragma warning disable IL2080
|
||||
var enumMembers = _enumType.GetFields(BindingFlags.Public | BindingFlags.Static);
|
||||
#pragma warning restore IL2080
|
||||
foreach (var member in enumMembers)
|
||||
{
|
||||
var enumVal = (T)member.GetValue(null)!;
|
||||
var maps = member.GetCustomAttributes(typeof(MapAttribute), false);
|
||||
foreach (MapAttribute attribute in maps)
|
||||
{
|
||||
foreach (var value in attribute.Values)
|
||||
{
|
||||
var enumVal = (T)Enum.Parse(enumType, member.Name);
|
||||
mappingToEnum.Add(new EnumMapping(enumVal, value));
|
||||
if (!mappingToString.ContainsKey(enumVal))
|
||||
mappingToString.Add(enumVal, value);
|
||||
mappingStringToEnum.Add(value, enumVal);
|
||||
if (!mappingEnumToString.ContainsKey(enumVal))
|
||||
mappingEnumToString.Add(enumVal, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if NET8_0_OR_GREATER
|
||||
_mappingToEnum = mappingToEnum.ToFrozenSet();
|
||||
_mappingToString = mappingToString.ToFrozenDictionary();
|
||||
_mappingToEnum = mappingStringToEnum.ToFrozenDictionary();
|
||||
_mappingToString = mappingEnumToString.ToFrozenDictionary();
|
||||
#else
|
||||
_mappingToEnum = mappingToEnum;
|
||||
_mappingToString = mappingToString;
|
||||
_mappingToEnum = mappingStringToEnum;
|
||||
_mappingToString = mappingEnumToString;
|
||||
#endif
|
||||
}
|
||||
|
||||
// For testing purposes only, allows resetting the static mapping and warnings
|
||||
internal static void Reset()
|
||||
{
|
||||
_undefinedEnumValue = null;
|
||||
_unknownValuesWarned = new ConcurrentBag<string>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the string value for an enum value using the MapAttribute mapping. When multiple values are mapped for a enum entry the first value will be returned
|
||||
/// </summary>
|
||||
@@ -326,41 +393,30 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
/// <returns></returns>
|
||||
public static T? ParseString(string value)
|
||||
{
|
||||
var type = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
|
||||
if (_mappingToEnum == null)
|
||||
CreateMapping();
|
||||
|
||||
EnumMapping? mapping = null;
|
||||
// Try match on full equals
|
||||
foreach(var item in _mappingToEnum!)
|
||||
{
|
||||
if (item.StringValue.Equals(value, StringComparison.Ordinal))
|
||||
{
|
||||
mapping = item;
|
||||
break;
|
||||
}
|
||||
if (item.Key.Equals(value, StringComparison.Ordinal))
|
||||
return item.Value;
|
||||
}
|
||||
|
||||
// If not found, try matching ignoring case
|
||||
if (mapping == null)
|
||||
foreach (var item in _mappingToEnum)
|
||||
{
|
||||
foreach (var item in _mappingToEnum)
|
||||
{
|
||||
if (item.StringValue.Equals(value, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
mapping = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (item.Key.Equals(value, StringComparison.OrdinalIgnoreCase))
|
||||
return item.Value;
|
||||
}
|
||||
|
||||
if (mapping != null)
|
||||
return mapping.Value;
|
||||
|
||||
try
|
||||
{
|
||||
// If no explicit mapping is found try to parse string
|
||||
return (T)Enum.Parse(type, value, true);
|
||||
#if NET8_0_OR_GREATER
|
||||
return Enum.Parse<T>(value, true);
|
||||
#else
|
||||
return (T)Enum.Parse(_enumType, value, true);
|
||||
#endif
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
if (string.IsNullOrEmpty(value))
|
||||
return default;
|
||||
|
||||
return (T?)JsonDocument.Parse(value!).Deserialize(typeof(T), options);
|
||||
return JsonDocument.Parse(value!).Deserialize<T>(options);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
<PackageId>CryptoExchange.Net</PackageId>
|
||||
<Authors>JKorf</Authors>
|
||||
<Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description>
|
||||
<PackageVersion>11.0.1</PackageVersion>
|
||||
<AssemblyVersion>11.0.1</AssemblyVersion>
|
||||
<FileVersion>11.0.1</FileVersion>
|
||||
<PackageVersion>11.2.1</PackageVersion>
|
||||
<AssemblyVersion>11.2.1</AssemblyVersion>
|
||||
<FileVersion>11.2.1</FileVersion>
|
||||
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
||||
<PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange;CryptoExchange.Net</PackageTags>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
|
||||
@@ -107,7 +107,7 @@ namespace CryptoExchange.Net
|
||||
}
|
||||
else
|
||||
{
|
||||
uriString.Append('[');
|
||||
uriString.Append($"{parameter.Key}=[");
|
||||
var firstArrayEntry = true;
|
||||
foreach (var entry in (Array)parameter.Value)
|
||||
{
|
||||
|
||||
@@ -28,6 +28,12 @@ namespace CryptoExchange.Net.Interfaces.Clients
|
||||
/// Whether client is disposed
|
||||
/// </summary>
|
||||
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 />
|
||||
|
||||
@@ -60,6 +60,12 @@ namespace CryptoExchange.Net.Interfaces.Clients
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
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 />
|
||||
|
||||
@@ -55,6 +55,7 @@ namespace CryptoExchange.Net
|
||||
{ "Kucoin.SpotKey", "f8ae62cb-2b3d-420c-8c98-e1c17dd4e30a" },
|
||||
{ "Mexc", "EASYT" },
|
||||
{ "OKX", "1425d83a94fbBCDE" },
|
||||
{ "Weex", "b-WEEX111124-" },
|
||||
{ "XT", "4XWeqN10M1fcoI5L" },
|
||||
};
|
||||
|
||||
|
||||
@@ -154,6 +154,9 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <returns></returns>
|
||||
public CallResult AsDataless()
|
||||
{
|
||||
if (Error != null )
|
||||
return new CallResult(Error);
|
||||
|
||||
return SuccessResult;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,10 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
/// Note that this comes at a performance cost
|
||||
/// </summary>
|
||||
public bool OutputOriginalData { get; set; } = false;
|
||||
/// <summary>
|
||||
/// A group name to use for client side rate limiting. Requests with the same group name will be counted together for rate limiting purposes. If null all requests will be counted together.
|
||||
/// </summary>
|
||||
public string? RateLimitGroup { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The max time a request is allowed to take
|
||||
|
||||
@@ -69,6 +69,7 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
item.RequestTimeout = RequestTimeout;
|
||||
item.RateLimiterEnabled = RateLimiterEnabled;
|
||||
item.RateLimitingBehaviour = RateLimitingBehaviour;
|
||||
item.RateLimitGroup = RateLimitGroup;
|
||||
item.CachingEnabled = CachingEnabled;
|
||||
item.CachingMaxAge = CachingMaxAge;
|
||||
item.HttpVersion = HttpVersion;
|
||||
|
||||
@@ -104,6 +104,7 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
item.RequestTimeout = RequestTimeout;
|
||||
item.RateLimitingBehaviour = RateLimitingBehaviour;
|
||||
item.RateLimiterEnabled = RateLimiterEnabled;
|
||||
item.RateLimitGroup = RateLimitGroup;
|
||||
item.ReceiveBufferSize = ReceiveBufferSize;
|
||||
return item;
|
||||
}
|
||||
|
||||
@@ -6,19 +6,27 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
/// <summary>
|
||||
/// Options to update
|
||||
/// </summary>
|
||||
public class UpdateOptions<TApiCredentials> where TApiCredentials : ApiCredentials
|
||||
public class UpdateOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Proxy setting. Note that if this is not provided any previously set proxy will be reset
|
||||
/// </summary>
|
||||
public ApiProxy? Proxy { get; set; }
|
||||
/// <summary>
|
||||
/// Api credentials
|
||||
/// </summary>
|
||||
public TApiCredentials? ApiCredentials { get; set; }
|
||||
/// <summary>
|
||||
/// Request timeout
|
||||
/// </summary>
|
||||
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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,6 +278,35 @@ namespace CryptoExchange.Net.Objects
|
||||
base.Add(key, string.Join(",", values));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add key as comma separated values
|
||||
/// </summary>
|
||||
#if NET5_0_OR_GREATER
|
||||
public void AddCommaSeparated<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string key, IEnumerable<T> values)
|
||||
#else
|
||||
public void AddCommaSeparated<T>(string key, IEnumerable<T> values)
|
||||
#endif
|
||||
where T : struct, Enum
|
||||
{
|
||||
base.Add(key, string.Join(",", values.Select(x => EnumConverter.GetString(x))));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add key as comma separated values if there are values provided
|
||||
/// </summary>
|
||||
#if NET5_0_OR_GREATER
|
||||
public void AddOptionalCommaSeparated<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string key, IEnumerable<T>? values)
|
||||
#else
|
||||
public void AddOptionalCommaSeparated<T>(string key, IEnumerable<T>? values)
|
||||
#endif
|
||||
where T : struct, Enum
|
||||
{
|
||||
if (values == null || !values.Any())
|
||||
return;
|
||||
|
||||
base.Add(key, string.Join(",", values.Select(x => EnumConverter.GetString(x))));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add key as boolean lower case value
|
||||
/// </summary>
|
||||
|
||||
@@ -333,17 +333,19 @@ namespace CryptoExchange.Net.OrderBook
|
||||
{
|
||||
_logger.OrderBookStopping(Api, Symbol);
|
||||
_cts?.Cancel();
|
||||
_queueEvent.Set();
|
||||
if (_processTask != null)
|
||||
await _processTask.ConfigureAwait(false);
|
||||
|
||||
if (_subscription != null) {
|
||||
if (_subscription != null)
|
||||
{
|
||||
await _subscription.CloseAsync().ConfigureAwait(false);
|
||||
_subscription.ConnectionLost -= HandleConnectionLost;
|
||||
_subscription.ConnectionClosed -= HandleConnectionClosed;
|
||||
_subscription.ConnectionRestored -= HandleConnectionRestored;
|
||||
}
|
||||
|
||||
_queueEvent.Set();
|
||||
if (_processTask != null)
|
||||
await _processTask.ConfigureAwait(false);
|
||||
|
||||
Status = OrderBookStatus.Disconnected;
|
||||
_logger.OrderBookStopped(Api, Symbol);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ using CryptoExchange.Net.RateLimiting.Interfaces;
|
||||
using CryptoExchange.Net.RateLimiting.Trackers;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
|
||||
namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
{
|
||||
@@ -36,6 +38,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
private readonly double? _decayRate;
|
||||
private readonly int? _connectionWeight;
|
||||
private readonly Func<RequestDefinition, string, string?, string> _keySelector;
|
||||
private readonly SemaphoreSlim? _sharedGuardSemaphore;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => "RateLimitGuard";
|
||||
@@ -52,6 +55,11 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
/// </summary>
|
||||
public TimeSpan TimeSpan { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this guard is shared between multiple gates
|
||||
/// </summary>
|
||||
public bool SharedGuard { get; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
@@ -62,8 +70,9 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
/// <param name="windowType">Type of rate limit window</param>
|
||||
/// <param name="decayPerTimeSpan">The decay per timespan if windowType is DecayWindowTracker</param>
|
||||
/// <param name="connectionWeight">The weight of a new connection</param>
|
||||
public RateLimitGuard(Func<RequestDefinition, string, string?, string> keySelector, IGuardFilter filter, int limit, TimeSpan timeSpan, RateLimitWindowType windowType, double? decayPerTimeSpan = null, int? connectionWeight = null)
|
||||
: this(keySelector, new[] { filter }, limit, timeSpan, windowType, decayPerTimeSpan, connectionWeight)
|
||||
/// <param name="shared">Whether this guard is shared between multiple gates</param>
|
||||
public RateLimitGuard(Func<RequestDefinition, string, string?, string> keySelector, IGuardFilter filter, int limit, TimeSpan timeSpan, RateLimitWindowType windowType, double? decayPerTimeSpan = null, int? connectionWeight = null, bool shared = false)
|
||||
: this(keySelector, new[] { filter }, limit, timeSpan, windowType, decayPerTimeSpan, connectionWeight, shared)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -77,22 +86,27 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
/// <param name="windowType">Type of rate limit window</param>
|
||||
/// <param name="decayPerTimeSpan">The decay per timespan if windowType is DecayWindowTracker</param>
|
||||
/// <param name="connectionWeight">The weight of a new connection</param>
|
||||
public RateLimitGuard(Func<RequestDefinition, string, string?, string> keySelector, IEnumerable<IGuardFilter> filters, int limit, TimeSpan timeSpan, RateLimitWindowType windowType, double? decayPerTimeSpan = null, int? connectionWeight = null)
|
||||
/// <param name="shared">Whether this guard is shared between multiple gates</param>
|
||||
public RateLimitGuard(Func<RequestDefinition, string, string?, string> keySelector, IEnumerable<IGuardFilter> filters, int limit, TimeSpan timeSpan, RateLimitWindowType windowType, double? decayPerTimeSpan = null, int? connectionWeight = null, bool shared = false)
|
||||
{
|
||||
_filters = filters;
|
||||
_trackers = new Dictionary<string, IWindowTracker>();
|
||||
_windowType = windowType;
|
||||
Limit = limit;
|
||||
TimeSpan = timeSpan;
|
||||
SharedGuard = shared;
|
||||
_keySelector = keySelector;
|
||||
_decayRate = decayPerTimeSpan;
|
||||
_connectionWeight = connectionWeight;
|
||||
|
||||
if (SharedGuard)
|
||||
_sharedGuardSemaphore = new SemaphoreSlim(1, 1);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix)
|
||||
{
|
||||
foreach(var filter in _filters)
|
||||
foreach (var filter in _filters)
|
||||
{
|
||||
if (!filter.Passes(type, definition, host, apiKey))
|
||||
return LimitCheck.NotApplicable;
|
||||
@@ -101,18 +115,29 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
if (type == RateLimitItemType.Connection)
|
||||
requestWeight = _connectionWeight ?? requestWeight;
|
||||
|
||||
var key = _keySelector(definition, host, apiKey) + keySuffix;
|
||||
if (!_trackers.TryGetValue(key, out var tracker))
|
||||
if (SharedGuard)
|
||||
_sharedGuardSemaphore!.Wait();
|
||||
|
||||
try
|
||||
{
|
||||
tracker = CreateTracker();
|
||||
_trackers.Add(key, tracker);
|
||||
var key = _keySelector(definition, host, apiKey) + keySuffix;
|
||||
if (!_trackers.TryGetValue(key, out var tracker))
|
||||
{
|
||||
tracker = CreateTracker();
|
||||
_trackers.Add(key, tracker);
|
||||
}
|
||||
|
||||
var delay = tracker.GetWaitTime(requestWeight);
|
||||
if (delay == default)
|
||||
return LimitCheck.NotNeeded(Limit, TimeSpan, tracker.Current);
|
||||
|
||||
return LimitCheck.Needed(delay, Limit, TimeSpan, tracker.Current);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (SharedGuard)
|
||||
_sharedGuardSemaphore!.Release();
|
||||
}
|
||||
|
||||
var delay = tracker.GetWaitTime(requestWeight);
|
||||
if (delay == default)
|
||||
return LimitCheck.NotNeeded(Limit, TimeSpan, tracker.Current);
|
||||
|
||||
return LimitCheck.Needed(delay, Limit, TimeSpan, tracker.Current);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -127,12 +152,53 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
if (type == RateLimitItemType.Connection)
|
||||
requestWeight = _connectionWeight ?? requestWeight;
|
||||
|
||||
|
||||
var key = _keySelector(definition, host, apiKey) + keySuffix;
|
||||
var tracker = _trackers[key];
|
||||
tracker.ApplyWeight(requestWeight);
|
||||
|
||||
if (SharedGuard)
|
||||
_sharedGuardSemaphore!.Wait();
|
||||
|
||||
try
|
||||
{
|
||||
tracker.ApplyWeight(requestWeight);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (SharedGuard)
|
||||
_sharedGuardSemaphore!.Release();
|
||||
}
|
||||
|
||||
return RateLimitState.Applied(Limit, TimeSpan, tracker.Current);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Reset(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, string? keySuffix)
|
||||
{
|
||||
foreach (var filter in _filters)
|
||||
{
|
||||
if (!filter.Passes(type, definition, host, apiKey))
|
||||
return;
|
||||
}
|
||||
|
||||
if (SharedGuard)
|
||||
_sharedGuardSemaphore!.Wait();
|
||||
|
||||
try
|
||||
{
|
||||
var key = _keySelector(definition, host, apiKey) + keySuffix;
|
||||
if (!_trackers.TryGetValue(key, out var tracker))
|
||||
return;
|
||||
|
||||
tracker.Reset();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (SharedGuard)
|
||||
_sharedGuardSemaphore!.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new WindowTracker
|
||||
/// </summary>
|
||||
|
||||
@@ -65,5 +65,11 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
/// </summary>
|
||||
/// <param name="after"></param>
|
||||
public void UpdateAfter(DateTime after) => After = after;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Reset(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, string? keySuffix)
|
||||
{
|
||||
After = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,5 +88,15 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
: _windowType == RateLimitWindowType.Fixed ? new FixedWindowTracker(_limit, _period) :
|
||||
new DecayWindowTracker(_limit, _period, _decayRate ?? throw new InvalidOperationException("Decay rate not provided"));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Reset(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, string? keySuffix)
|
||||
{
|
||||
var key = _keySelector(definition, host, apiKey) + keySuffix;
|
||||
if (!_trackers.TryGetValue(key, out var tracker))
|
||||
return;
|
||||
|
||||
tracker.Reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,5 +74,22 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
|
||||
/// <param name="ct">Cancelation token</param>
|
||||
/// <returns>Error if RateLimitingBehaviour is Fail and rate limit is hit</returns>
|
||||
ValueTask<CallResult> ProcessSingleAsync(ILogger logger, int itemId, IRateLimitGuard guard, RateLimitItemType type, RequestDefinition definition, string baseAddress, string? apiKey, int requestWeight, RateLimitingBehaviour behaviour, string? keySuffix, CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Reset the limit for the specified parameters
|
||||
/// </summary>
|
||||
/// <param name="type">The rate limit item type</param>
|
||||
/// <param name="definition">The request definition</param>
|
||||
/// <param name="host">The host address</param>
|
||||
/// <param name="apiKey">The API key</param>
|
||||
/// <param name="keySuffix">An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters.</param>
|
||||
/// <param name="ct">Cancelation token</param>
|
||||
Task ResetAsync(
|
||||
RateLimitItemType type,
|
||||
RequestDefinition definition,
|
||||
string host,
|
||||
string? apiKey,
|
||||
string? keySuffix,
|
||||
CancellationToken ct);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,5 +40,15 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
|
||||
/// <param name="keySuffix">An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters.</param>
|
||||
/// <returns></returns>
|
||||
RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix);
|
||||
|
||||
/// <summary>
|
||||
/// Reset the limit for the specified parameters
|
||||
/// </summary>
|
||||
/// <param name="type">The rate limit item type</param>
|
||||
/// <param name="definition">The request definition</param>
|
||||
/// <param name="host">The host address</param>
|
||||
/// <param name="apiKey">The API key</param>
|
||||
/// <param name="keySuffix">An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters.</param>
|
||||
void Reset(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, string? keySuffix);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,5 +30,9 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
|
||||
/// </summary>
|
||||
/// <param name="weight">Request weight</param>
|
||||
void ApplyWeight(int weight);
|
||||
/// <summary>
|
||||
/// Reset the limit counter for this tracker
|
||||
/// </summary>
|
||||
void Reset();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,5 +192,27 @@ namespace CryptoExchange.Net.RateLimiting
|
||||
_semaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task ResetAsync(
|
||||
RateLimitItemType type,
|
||||
RequestDefinition definition,
|
||||
string host,
|
||||
string? apiKey,
|
||||
string? keySuffix,
|
||||
CancellationToken ct)
|
||||
{
|
||||
await _semaphore.WaitAsync(ct).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
foreach (var guard in _guards)
|
||||
guard.Reset(type, definition, host, apiKey, keySuffix);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_semaphore.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,13 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
|
||||
DecreaseRate = decayRate;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Reset()
|
||||
{
|
||||
_currentWeight = 0;
|
||||
_lastDecrease = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public TimeSpan GetWaitTime(int weight)
|
||||
{
|
||||
|
||||
@@ -29,6 +29,14 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
|
||||
_entries = new Queue<LimitEntry>();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Reset()
|
||||
{
|
||||
_entries.Clear();
|
||||
_currentWeight = 0;
|
||||
_nextReset = null;
|
||||
}
|
||||
|
||||
public TimeSpan GetWaitTime(int weight)
|
||||
{
|
||||
// Remove requests no longer in time period from the history
|
||||
|
||||
@@ -28,6 +28,13 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
|
||||
_entries = new Queue<LimitEntry>();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Reset()
|
||||
{
|
||||
_entries.Clear();
|
||||
_currentWeight = 0;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public TimeSpan GetWaitTime(int weight)
|
||||
{
|
||||
|
||||
@@ -28,6 +28,13 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
|
||||
_entries = new List<LimitEntry>();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Reset()
|
||||
{
|
||||
_entries.Clear();
|
||||
_currentWeight = 0;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public TimeSpan GetWaitTime(int weight)
|
||||
{
|
||||
|
||||
@@ -45,7 +45,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
public override Error? ValidateRequest(string exchange, GetOrderBookRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes)
|
||||
{
|
||||
if (request.Limit == null)
|
||||
return null;
|
||||
return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes);
|
||||
|
||||
if (MaxLimit.HasValue && request.Limit.Value > MaxLimit)
|
||||
return ArgumentError.Invalid(nameof(GetOrderBookRequest.Limit), $"Max limit is {MaxLimit}");
|
||||
|
||||
@@ -22,8 +22,12 @@ namespace CryptoExchange.Net.SharedApis
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Error? Validate(GetRecentTradesRequest request)
|
||||
public override Error? ValidateRequest(string exchange, GetRecentTradesRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes)
|
||||
{
|
||||
var baseError = base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes);
|
||||
if (baseError != null)
|
||||
return baseError;
|
||||
|
||||
if (request.Limit > MaxLimit)
|
||||
return ArgumentError.Invalid(nameof(GetRecentTradesRequest.Limit), $"Only the most recent {MaxLimit} trades are available");
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ namespace CryptoExchange.Net.Sockets.Default
|
||||
private readonly string _baseAddress;
|
||||
private int _reconnectAttempt;
|
||||
private readonly int _receiveBufferSize;
|
||||
private readonly RequestDefinition _requestDefinition;
|
||||
|
||||
private const int _sendBufferSize = 4096;
|
||||
|
||||
@@ -137,6 +138,7 @@ namespace CryptoExchange.Net.Sockets.Default
|
||||
_sendBuffer = new ConcurrentQueue<SendItem>();
|
||||
_ctsSource = new CancellationTokenSource();
|
||||
_receiveBufferSize = websocketParameters.ReceiveBufferSize ?? 65536;
|
||||
_requestDefinition = new RequestDefinition(Uri.AbsolutePath, HttpMethod.Get) { ConnectionId = Id };
|
||||
|
||||
_closeSem = new SemaphoreSlim(1, 1);
|
||||
_socket = CreateSocket();
|
||||
@@ -206,8 +208,7 @@ namespace CryptoExchange.Net.Sockets.Default
|
||||
{
|
||||
if (Parameters.RateLimiter != null)
|
||||
{
|
||||
var definition = new RequestDefinition(Uri.AbsolutePath, HttpMethod.Get) { ConnectionId = Id };
|
||||
var limitResult = await Parameters.RateLimiter.ProcessAsync(_logger, Id, RateLimitItemType.Connection, definition, _baseAddress, null, 1, Parameters.RateLimitingBehavior, null, _ctsSource.Token).ConfigureAwait(false);
|
||||
var limitResult = await Parameters.RateLimiter.ProcessAsync(_logger, Id, RateLimitItemType.Connection, _requestDefinition, _baseAddress, null, 1, Parameters.RateLimitingBehavior, null, _ctsSource.Token).ConfigureAwait(false);
|
||||
if (!limitResult)
|
||||
return new CallResult(new ClientRateLimitError("Connection limit reached"));
|
||||
}
|
||||
@@ -296,6 +297,9 @@ namespace CryptoExchange.Net.Sockets.Default
|
||||
await (OnReconnecting?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (Parameters.RateLimiter != null)
|
||||
await Parameters.RateLimiter.ResetAsync(RateLimitItemType.Request, _requestDefinition, _baseAddress, null, null, default).ConfigureAwait(false);
|
||||
|
||||
// Delay here to prevent very rapid looping when a connection to the server is accepted and immediately disconnected
|
||||
var initialDelay = GetReconnectDelay();
|
||||
await Task.Delay(initialDelay).ConfigureAwait(false);
|
||||
@@ -496,7 +500,6 @@ namespace CryptoExchange.Net.Sockets.Default
|
||||
/// <returns></returns>
|
||||
private async Task SendLoopAsync()
|
||||
{
|
||||
var requestDefinition = new RequestDefinition(Uri.AbsolutePath, HttpMethod.Get) { ConnectionId = Id };
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
@@ -520,7 +523,7 @@ namespace CryptoExchange.Net.Sockets.Default
|
||||
{
|
||||
try
|
||||
{
|
||||
var limitResult = await Parameters.RateLimiter.ProcessAsync(_logger, data.Id, RateLimitItemType.Request, requestDefinition, _baseAddress, null, data.Weight, Parameters.RateLimitingBehavior, null, _ctsSource.Token).ConfigureAwait(false);
|
||||
var limitResult = await Parameters.RateLimiter.ProcessAsync(_logger, data.Id, RateLimitItemType.Request, _requestDefinition, _baseAddress, null, data.Weight, Parameters.RateLimitingBehavior, null, _ctsSource.Token).ConfigureAwait(false);
|
||||
if (!limitResult)
|
||||
{
|
||||
await (OnRequestRateLimited?.Invoke(data.Id) ?? Task.CompletedTask).ConfigureAwait(false);
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.Sockets.Default.Routing
|
||||
{
|
||||
/// <summary>
|
||||
/// Message route
|
||||
/// </summary>
|
||||
public abstract class MessageRoute
|
||||
{
|
||||
/// <summary>
|
||||
/// Type identifier
|
||||
/// </summary>
|
||||
public string TypeIdentifier { get; set; }
|
||||
/// <summary>
|
||||
/// Optional topic filter
|
||||
/// </summary>
|
||||
public string? TopicFilter { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether responses to this route might be read by multiple listeners
|
||||
/// </summary>
|
||||
public bool MultipleReaders { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Deserialization type
|
||||
/// </summary>
|
||||
public abstract Type DeserializationType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public MessageRoute(string typeIdentifier, string? topicFilter)
|
||||
{
|
||||
TypeIdentifier = typeIdentifier;
|
||||
TopicFilter = topicFilter;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Message handler
|
||||
/// </summary>
|
||||
public abstract CallResult? Handle(SocketConnection connection, DateTime receiveTime, string? originalData, object data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Message route
|
||||
/// </summary>
|
||||
public class MessageRoute<TMessage> : MessageRoute
|
||||
{
|
||||
private Func<SocketConnection, DateTime, string?, TMessage, CallResult?> _handler;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Type DeserializationType { get; } = typeof(TMessage);
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
internal MessageRoute(string typeIdentifier, string? topicFilter, Func<SocketConnection, DateTime, string?, TMessage, CallResult?> handler, bool multipleReaders = false)
|
||||
: base(typeIdentifier, topicFilter)
|
||||
{
|
||||
_handler = handler;
|
||||
MultipleReaders = multipleReaders;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create route without topic filter
|
||||
/// </summary>
|
||||
public static MessageRoute<TMessage> CreateWithoutTopicFilter(string typeIdentifier, Func<SocketConnection, DateTime, string?, TMessage, CallResult?> handler, bool multipleReaders = false)
|
||||
{
|
||||
return new MessageRoute<TMessage>(typeIdentifier, null, handler)
|
||||
{
|
||||
MultipleReaders = multipleReaders
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create route with optional topic filter
|
||||
/// </summary>
|
||||
public static MessageRoute<TMessage> CreateWithOptionalTopicFilter(string typeIdentifier, string? topicFilter, Func<SocketConnection, DateTime, string?, TMessage, CallResult?> handler, bool multipleReaders = false)
|
||||
{
|
||||
return new MessageRoute<TMessage>(typeIdentifier, topicFilter, handler)
|
||||
{
|
||||
MultipleReaders = multipleReaders
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create route with topic filter
|
||||
/// </summary>
|
||||
public static MessageRoute<TMessage> CreateWithTopicFilter(string typeIdentifier, string topicFilter, Func<SocketConnection, DateTime, string?, TMessage, CallResult?> handler, bool multipleReaders = false)
|
||||
{
|
||||
return new MessageRoute<TMessage>(typeIdentifier, topicFilter, handler)
|
||||
{
|
||||
MultipleReaders = multipleReaders
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override CallResult? Handle(SocketConnection connection, DateTime receiveTime, string? originalData, object data)
|
||||
{
|
||||
return _handler(connection, receiveTime, originalData, (TMessage)data);
|
||||
}
|
||||
}
|
||||
}
|
||||
+32
-103
@@ -1,16 +1,17 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Sockets.Default;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace CryptoExchange.Net.Sockets
|
||||
namespace CryptoExchange.Net.Sockets.Default.Routing
|
||||
{
|
||||
/// <summary>
|
||||
/// Message router
|
||||
/// </summary>
|
||||
public class MessageRouter
|
||||
{
|
||||
private ProcessorRouter? _routingTable;
|
||||
|
||||
/// <summary>
|
||||
/// The routes registered for this router
|
||||
/// </summary>
|
||||
@@ -24,12 +25,40 @@ namespace CryptoExchange.Net.Sockets
|
||||
Routes = routes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Build the route mapping
|
||||
/// </summary>
|
||||
public void BuildQueryRouter()
|
||||
{
|
||||
_routingTable = new QueryRouter(Routes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Build the route mapping
|
||||
/// </summary>
|
||||
public void BuildSubscriptionRouter()
|
||||
{
|
||||
_routingTable = new SubscriptionRouter(Routes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle message
|
||||
/// </summary>
|
||||
public bool Handle(string typeIdentifier, string? topicFilter, SocketConnection connection, DateTime receiveTime, string? originalData, object data, out CallResult? result)
|
||||
{
|
||||
var routeCollection = (_routingTable ?? throw new NullReferenceException("Routing table not build before handling")).GetRoutes(typeIdentifier);
|
||||
if (routeCollection == null)
|
||||
throw new InvalidOperationException($"No routes for {typeIdentifier} message type");
|
||||
|
||||
return routeCollection.Handle(topicFilter, connection, receiveTime, originalData, data, out result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create message router without specific message handler
|
||||
/// </summary>
|
||||
public static MessageRouter CreateWithoutHandler<T>(string typeIdentifier, bool multipleReaders = false)
|
||||
{
|
||||
return new MessageRouter(new MessageRoute<T>(typeIdentifier, (string?)null, (con, receiveTime, originalData, msg) => new CallResult<T>(default, null, null), multipleReaders));
|
||||
return new MessageRouter(new MessageRoute<T>(typeIdentifier, null, (con, receiveTime, originalData, msg) => new CallResult<T>(default, null, null), multipleReaders));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -165,104 +194,4 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// </summary>
|
||||
public bool ContainsCheck(MessageRoute route) => Routes.Any(x => x.TypeIdentifier == route.TypeIdentifier && x.TopicFilter == route.TopicFilter);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Message route
|
||||
/// </summary>
|
||||
public abstract class MessageRoute
|
||||
{
|
||||
/// <summary>
|
||||
/// Type identifier
|
||||
/// </summary>
|
||||
public string TypeIdentifier { get; set; }
|
||||
/// <summary>
|
||||
/// Optional topic filter
|
||||
/// </summary>
|
||||
public string? TopicFilter { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether responses to this route might be read by multiple listeners
|
||||
/// </summary>
|
||||
public bool MultipleReaders { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Deserialization type
|
||||
/// </summary>
|
||||
public abstract Type DeserializationType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public MessageRoute(string typeIdentifier, string? topicFilter)
|
||||
{
|
||||
TypeIdentifier = typeIdentifier;
|
||||
TopicFilter = topicFilter;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Message handler
|
||||
/// </summary>
|
||||
public abstract CallResult? Handle(SocketConnection connection, DateTime receiveTime, string? originalData, object data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Message route
|
||||
/// </summary>
|
||||
public class MessageRoute<TMessage> : MessageRoute
|
||||
{
|
||||
private Func<SocketConnection, DateTime, string?, TMessage, CallResult?> _handler;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Type DeserializationType { get; } = typeof(TMessage);
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
internal MessageRoute(string typeIdentifier, string? topicFilter, Func<SocketConnection, DateTime, string?, TMessage, CallResult?> handler, bool multipleReaders = false)
|
||||
: base(typeIdentifier, topicFilter)
|
||||
{
|
||||
_handler = handler;
|
||||
MultipleReaders = multipleReaders;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create route without topic filter
|
||||
/// </summary>
|
||||
public static MessageRoute<TMessage> CreateWithoutTopicFilter(string typeIdentifier, Func<SocketConnection, DateTime, string?, TMessage, CallResult?> handler, bool multipleReaders = false)
|
||||
{
|
||||
return new MessageRoute<TMessage>(typeIdentifier, null, handler)
|
||||
{
|
||||
MultipleReaders = multipleReaders
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create route with optional topic filter
|
||||
/// </summary>
|
||||
public static MessageRoute<TMessage> CreateWithOptionalTopicFilter(string typeIdentifier, string? topicFilter, Func<SocketConnection, DateTime, string?, TMessage, CallResult?> handler, bool multipleReaders = false)
|
||||
{
|
||||
return new MessageRoute<TMessage>(typeIdentifier, topicFilter, handler)
|
||||
{
|
||||
MultipleReaders = multipleReaders
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create route with topic filter
|
||||
/// </summary>
|
||||
public static MessageRoute<TMessage> CreateWithTopicFilter(string typeIdentifier, string topicFilter, Func<SocketConnection, DateTime, string?, TMessage, CallResult?> handler, bool multipleReaders = false)
|
||||
{
|
||||
return new MessageRoute<TMessage>(typeIdentifier, topicFilter, handler)
|
||||
{
|
||||
MultipleReaders = multipleReaders
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override CallResult? Handle(SocketConnection connection, DateTime receiveTime, string? originalData, object data)
|
||||
{
|
||||
return _handler(connection, receiveTime, originalData, (TMessage)data);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using System.Collections.Generic;
|
||||
#if NET8_0_OR_GREATER
|
||||
using System.Collections.Frozen;
|
||||
#endif
|
||||
|
||||
namespace CryptoExchange.Net.Sockets.Default.Routing
|
||||
{
|
||||
internal abstract class ProcessorRouter
|
||||
{
|
||||
public abstract RouteCollection? GetRoutes(string identifier);
|
||||
}
|
||||
|
||||
internal abstract class ProcessorRouter<T> : ProcessorRouter
|
||||
where T : RouteCollection
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
private FrozenDictionary<string, T> _routeMap;
|
||||
#else
|
||||
private Dictionary<string, T> _routeMap;
|
||||
#endif
|
||||
|
||||
public ProcessorRouter(IEnumerable<MessageRoute> routes)
|
||||
{
|
||||
var map = BuildFromRoutes(routes);
|
||||
#if NET8_0_OR_GREATER
|
||||
_routeMap = map.ToFrozenDictionary();
|
||||
#else
|
||||
_routeMap = map;
|
||||
#endif
|
||||
}
|
||||
|
||||
public abstract Dictionary<string, T> BuildFromRoutes(IEnumerable<MessageRoute> routes);
|
||||
|
||||
public override RouteCollection? GetRoutes(string identifier) => _routeMap.TryGetValue(identifier, out var routes) ? routes : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace CryptoExchange.Net.Sockets.Default.Routing
|
||||
{
|
||||
internal class QueryRouter : ProcessorRouter<QueryRouteCollection>
|
||||
{
|
||||
public QueryRouter(IEnumerable<MessageRoute> routes) : base(routes)
|
||||
{
|
||||
}
|
||||
|
||||
public override Dictionary<string, QueryRouteCollection> BuildFromRoutes(IEnumerable<MessageRoute> routes)
|
||||
{
|
||||
var newMap = new Dictionary<string, QueryRouteCollection>();
|
||||
foreach (var route in routes)
|
||||
{
|
||||
if (!newMap.TryGetValue(route.TypeIdentifier, out var typeMap))
|
||||
{
|
||||
typeMap = new QueryRouteCollection(route.DeserializationType);
|
||||
newMap.Add(route.TypeIdentifier, typeMap);
|
||||
}
|
||||
|
||||
typeMap.AddRoute(route.TopicFilter, route);
|
||||
}
|
||||
|
||||
foreach (var subEntry in newMap.Values)
|
||||
subEntry.Build();
|
||||
|
||||
return newMap;
|
||||
}
|
||||
}
|
||||
|
||||
internal class QueryRouteCollection : RouteCollection
|
||||
{
|
||||
public bool MultipleReaders { get; private set; }
|
||||
|
||||
public QueryRouteCollection(Type routeType) : base(routeType)
|
||||
{
|
||||
}
|
||||
|
||||
public override void AddRoute(string? topicFilter, MessageRoute route)
|
||||
{
|
||||
base.AddRoute(topicFilter, route);
|
||||
|
||||
if (route.MultipleReaders)
|
||||
MultipleReaders = true;
|
||||
}
|
||||
|
||||
public override bool Handle(string? topicFilter, SocketConnection connection, DateTime receiveTime, string? originalData, object data, out CallResult? result)
|
||||
{
|
||||
result = null;
|
||||
|
||||
// Routes without topic filter handle both when the message topic is empty and when it is not, so we always call them
|
||||
var handled = false;
|
||||
foreach (var route in _routesWithoutTopicFilter)
|
||||
{
|
||||
var thisResult = route.Handle(connection, receiveTime, originalData, data);
|
||||
if (thisResult != null)
|
||||
result ??= thisResult;
|
||||
|
||||
handled = true;
|
||||
}
|
||||
|
||||
// Forward to routes with matching topic filter, if any
|
||||
if (topicFilter == null)
|
||||
return handled;
|
||||
|
||||
var matchingTopicRoutes = GetRoutesWithMatchingTopicFilter(topicFilter);
|
||||
if (matchingTopicRoutes == null)
|
||||
return handled;
|
||||
|
||||
foreach (var route in matchingTopicRoutes)
|
||||
{
|
||||
var thisResult = route.Handle(connection, receiveTime, originalData, data);
|
||||
handled = true;
|
||||
|
||||
if (thisResult != null)
|
||||
{
|
||||
result ??= thisResult;
|
||||
|
||||
if (!MultipleReaders)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return handled;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using System;
|
||||
#if NET8_0_OR_GREATER
|
||||
using System.Collections.Frozen;
|
||||
#endif
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace CryptoExchange.Net.Sockets.Default.Routing
|
||||
{
|
||||
internal abstract class RouteCollection
|
||||
{
|
||||
protected List<MessageRoute> _routesWithoutTopicFilter;
|
||||
protected Dictionary<string, List<MessageRoute>> _routesWithTopicFilter;
|
||||
#if NET8_0_OR_GREATER
|
||||
protected FrozenDictionary<string, List<MessageRoute>>? _routesWithTopicFilterFrozen;
|
||||
#endif
|
||||
|
||||
public Type DeserializationType { get; }
|
||||
|
||||
public RouteCollection(Type routeType)
|
||||
{
|
||||
_routesWithoutTopicFilter = new List<MessageRoute>();
|
||||
_routesWithTopicFilter = new Dictionary<string, List<MessageRoute>>();
|
||||
|
||||
DeserializationType = routeType;
|
||||
}
|
||||
|
||||
public virtual void AddRoute(string? topicFilter, MessageRoute route)
|
||||
{
|
||||
if (string.IsNullOrEmpty(topicFilter))
|
||||
{
|
||||
_routesWithoutTopicFilter.Add(route);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!_routesWithTopicFilter.TryGetValue(topicFilter!, out var list))
|
||||
{
|
||||
list = new List<MessageRoute>();
|
||||
_routesWithTopicFilter.Add(topicFilter!, list);
|
||||
}
|
||||
|
||||
list.Add(route);
|
||||
}
|
||||
}
|
||||
|
||||
public void Build()
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
_routesWithTopicFilterFrozen = _routesWithTopicFilter.ToFrozenDictionary();
|
||||
#endif
|
||||
}
|
||||
|
||||
protected List<MessageRoute>? GetRoutesWithMatchingTopicFilter(string topicFilter)
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
_routesWithTopicFilterFrozen!.TryGetValue(topicFilter, out var matchingTopicRoutes);
|
||||
#else
|
||||
_routesWithTopicFilter.TryGetValue(topicFilter, out var matchingTopicRoutes);
|
||||
#endif
|
||||
return matchingTopicRoutes;
|
||||
}
|
||||
|
||||
public abstract bool Handle(string? topicFilter, SocketConnection connection, DateTime receiveTime, string? originalData, object data, out CallResult? result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
using CryptoExchange.Net.Sockets.Interfaces;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
#if NET8_0_OR_GREATER
|
||||
using System.Collections.Frozen;
|
||||
#endif
|
||||
|
||||
namespace CryptoExchange.Net.Sockets.Default.Routing
|
||||
{
|
||||
/// <summary>
|
||||
/// Routing table
|
||||
/// </summary>
|
||||
public class RoutingTable
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
private FrozenDictionary<string, TypeRoutingCollection> _typeRoutingCollections = new Dictionary<string, TypeRoutingCollection>().ToFrozenDictionary();
|
||||
#else
|
||||
private Dictionary<string, TypeRoutingCollection> _typeRoutingCollections = new();
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Update the routing table
|
||||
/// </summary>
|
||||
/// <param name="processors"></param>
|
||||
public void Update(IEnumerable<IMessageProcessor> processors)
|
||||
{
|
||||
var newTypeMap = new Dictionary<string, TypeRoutingCollection>();
|
||||
foreach (var entry in processors)
|
||||
{
|
||||
foreach (var route in entry.MessageRouter.Routes)
|
||||
{
|
||||
if (!newTypeMap.ContainsKey(route.TypeIdentifier))
|
||||
newTypeMap.Add(route.TypeIdentifier, new TypeRoutingCollection(route.DeserializationType));
|
||||
|
||||
if (!newTypeMap[route.TypeIdentifier].Handlers.Contains(entry))
|
||||
newTypeMap[route.TypeIdentifier].Handlers.Add(entry);
|
||||
}
|
||||
}
|
||||
|
||||
#if NET8_0_OR_GREATER
|
||||
_typeRoutingCollections = newTypeMap.ToFrozenDictionary();
|
||||
#else
|
||||
_typeRoutingCollections = newTypeMap;
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get route table entry for a type identifier
|
||||
/// </summary>
|
||||
public TypeRoutingCollection? GetRouteTableEntry(string typeIdentifier)
|
||||
{
|
||||
return _typeRoutingCollections.TryGetValue(typeIdentifier, out var entry) ? entry : null;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
foreach (var entry in _typeRoutingCollections)
|
||||
{
|
||||
sb.AppendLine($"{entry.Key}, {entry.Value.DeserializationType.Name}");
|
||||
foreach(var item in entry.Value.Handlers)
|
||||
{
|
||||
sb.AppendLine($" - Processor {item.GetType().Name}");
|
||||
foreach(var route in item.MessageRouter.Routes)
|
||||
{
|
||||
if (route.TypeIdentifier == entry.Key)
|
||||
{
|
||||
if (route.TopicFilter == null)
|
||||
sb.AppendLine($" - Route without topic filter");
|
||||
else
|
||||
sb.AppendLine($" - Route with topic filter {route.TopicFilter}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Routing table entry
|
||||
/// </summary>
|
||||
public record TypeRoutingCollection
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the deserialization type is string
|
||||
/// </summary>
|
||||
public bool IsStringOutput { get; set; }
|
||||
/// <summary>
|
||||
/// The deserialization type
|
||||
/// </summary>
|
||||
public Type DeserializationType { get; set; }
|
||||
/// <summary>
|
||||
/// Message processors
|
||||
/// </summary>
|
||||
public List<IMessageProcessor> Handlers { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public TypeRoutingCollection(Type deserializationType)
|
||||
{
|
||||
IsStringOutput = deserializationType == typeof(string);
|
||||
DeserializationType = deserializationType;
|
||||
Handlers = new List<IMessageProcessor>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace CryptoExchange.Net.Sockets.Default.Routing
|
||||
{
|
||||
internal class SubscriptionRouter : ProcessorRouter<SubscriptionRouteCollection>
|
||||
{
|
||||
public SubscriptionRouter(IEnumerable<MessageRoute> routes) : base(routes)
|
||||
{
|
||||
}
|
||||
|
||||
public override Dictionary<string, SubscriptionRouteCollection> BuildFromRoutes(IEnumerable<MessageRoute> routes)
|
||||
{
|
||||
var newMap = new Dictionary<string, SubscriptionRouteCollection>();
|
||||
foreach (var route in routes)
|
||||
{
|
||||
if (!newMap.TryGetValue(route.TypeIdentifier, out var typeMap))
|
||||
{
|
||||
typeMap = new SubscriptionRouteCollection(route.DeserializationType);
|
||||
newMap.Add(route.TypeIdentifier, typeMap);
|
||||
}
|
||||
|
||||
typeMap.AddRoute(route.TopicFilter, route);
|
||||
}
|
||||
|
||||
foreach (var subEntry in newMap.Values)
|
||||
subEntry.Build();
|
||||
|
||||
return newMap;
|
||||
}
|
||||
}
|
||||
|
||||
internal class SubscriptionRouteCollection : RouteCollection
|
||||
{
|
||||
public SubscriptionRouteCollection(Type routeType) : base(routeType)
|
||||
{
|
||||
}
|
||||
|
||||
public override bool Handle(string? topicFilter, SocketConnection connection, DateTime receiveTime, string? originalData, object data, out CallResult? result)
|
||||
{
|
||||
result = CallResult.SuccessResult;
|
||||
|
||||
// Routes without topic filter handle both when the message topic is empty and when it is not, so we always call them
|
||||
var handled = false;
|
||||
foreach (var route in _routesWithoutTopicFilter)
|
||||
{
|
||||
route.Handle(connection, receiveTime, originalData, data);
|
||||
handled = true;
|
||||
}
|
||||
|
||||
// Forward to routes with matching topic filter, if any
|
||||
if (topicFilter == null)
|
||||
return handled;
|
||||
|
||||
var matchingTopicRoutes = GetRoutesWithMatchingTopicFilter(topicFilter);
|
||||
if (matchingTopicRoutes == null)
|
||||
return handled;
|
||||
|
||||
foreach (var route in matchingTopicRoutes)
|
||||
{
|
||||
route.Handle(connection, receiveTime, originalData, data);
|
||||
handled = true;
|
||||
}
|
||||
|
||||
return handled;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,13 +5,12 @@ using CryptoExchange.Net.Logging.Extensions;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Sockets;
|
||||
using CryptoExchange.Net.Sockets.Default.Interfaces;
|
||||
using CryptoExchange.Net.Sockets.Default.Routing;
|
||||
using CryptoExchange.Net.Sockets.Interfaces;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Net.WebSockets;
|
||||
using System.Text;
|
||||
@@ -262,6 +261,9 @@ namespace CryptoExchange.Net.Sockets.Default
|
||||
#else
|
||||
private readonly object _listenersLock = new object();
|
||||
#endif
|
||||
|
||||
private RoutingTable _routingTable = new RoutingTable();
|
||||
|
||||
private ReadOnlyCollection<IMessageProcessor> _listeners;
|
||||
private readonly ILogger _logger;
|
||||
private SocketStatus _status;
|
||||
@@ -489,6 +491,14 @@ namespace CryptoExchange.Net.Sockets.Default
|
||||
/// </summary>
|
||||
protected internal virtual void HandleStreamMessage2(WebSocketMessageType type, ReadOnlySpan<byte> data)
|
||||
{
|
||||
// Forward message rules:
|
||||
// | Message Topic | Route Topic Filter | Topics Match | Forward | Description
|
||||
// | N | N | - | Y | No topic filter applied
|
||||
// | N | Y | - | N | Route only listens to specific topic
|
||||
// | Y | N | - | Y | Route listens to all message regardless of topic
|
||||
// | Y | Y | Y | Y | Route listens to specific message topic
|
||||
// | Y | Y | N | N | Route listens to different topic
|
||||
|
||||
var receiveTime = DateTime.UtcNow;
|
||||
|
||||
// 1. Decrypt/Preprocess if necessary
|
||||
@@ -521,38 +531,22 @@ namespace CryptoExchange.Net.Sockets.Default
|
||||
return;
|
||||
}
|
||||
|
||||
Type? deserializationType = null;
|
||||
foreach (var subscription in _listeners)
|
||||
{
|
||||
foreach (var route in subscription.MessageRouter.Routes)
|
||||
{
|
||||
if (!route.TypeIdentifier.Equals(typeIdentifier, StringComparison.Ordinal))
|
||||
continue;
|
||||
|
||||
deserializationType = route.DeserializationType;
|
||||
break;
|
||||
}
|
||||
|
||||
if (deserializationType != null)
|
||||
break;
|
||||
}
|
||||
|
||||
if (deserializationType == null)
|
||||
var routingEntry = _routingTable.GetRouteTableEntry(typeIdentifier);
|
||||
if (routingEntry == null)
|
||||
{
|
||||
if (!ApiClient.HandleUnhandledMessage(this, typeIdentifier, data))
|
||||
{
|
||||
// No handler found for identifier either, can't process
|
||||
_logger.LogWarning("Failed to determine message type for identifier {Identifier}. Data: {Message}", typeIdentifier, Encoding.UTF8.GetString(data.ToArray()));
|
||||
}
|
||||
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
object result;
|
||||
try
|
||||
{
|
||||
if (deserializationType == typeof(string))
|
||||
if (routingEntry.IsStringOutput)
|
||||
{
|
||||
#if NETSTANDARD2_0
|
||||
result = Encoding.UTF8.GetString(data.ToArray());
|
||||
@@ -562,7 +556,7 @@ namespace CryptoExchange.Net.Sockets.Default
|
||||
}
|
||||
else
|
||||
{
|
||||
result = messageConverter.Deserialize(data, deserializationType);
|
||||
result = messageConverter.Deserialize(data, routingEntry.DeserializationType);
|
||||
}
|
||||
}
|
||||
catch(Exception ex)
|
||||
@@ -579,60 +573,12 @@ namespace CryptoExchange.Net.Sockets.Default
|
||||
}
|
||||
|
||||
var topicFilter = messageConverter.GetTopicFilter(result);
|
||||
|
||||
bool processed = false;
|
||||
foreach (var processor in _listeners)
|
||||
var processed = false;
|
||||
foreach (var handler in routingEntry.Handlers)
|
||||
{
|
||||
bool isQuery = false;
|
||||
Query? query = null;
|
||||
if (processor is Query cquery)
|
||||
{
|
||||
isQuery = true;
|
||||
query = cquery;
|
||||
}
|
||||
|
||||
var complete = false;
|
||||
|
||||
foreach (var route in processor.MessageRouter.Routes)
|
||||
{
|
||||
if (route.TypeIdentifier != typeIdentifier)
|
||||
continue;
|
||||
|
||||
// Forward message rules:
|
||||
// | Message Topic | Route Topic Filter | Topics Match | Forward | Description
|
||||
// | N | N | - | Y | No topic filter applied
|
||||
// | N | Y | - | N | Route only listens to specific topic
|
||||
// | Y | N | - | Y | Route listens to all message regardless of topic
|
||||
// | Y | Y | Y | Y | Route listens to specific message topic
|
||||
// | Y | Y | N | N | Route listens to different topic
|
||||
if (topicFilter == null)
|
||||
{
|
||||
if (route.TopicFilter != null)
|
||||
// No topic on message, but route is filtering on topic
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (route.TopicFilter != null && !route.TopicFilter.Equals(topicFilter, StringComparison.Ordinal))
|
||||
// Message has a topic, and the route has a filter for another topic
|
||||
continue;
|
||||
}
|
||||
|
||||
var thisHandled = handler.Handle(typeIdentifier, topicFilter, this, receiveTime, originalData, result);
|
||||
if (thisHandled)
|
||||
processed = true;
|
||||
|
||||
if (isQuery && query!.Completed)
|
||||
continue;
|
||||
|
||||
processor.Handle(this, receiveTime, originalData, result, route);
|
||||
if (isQuery && !route.MultipleReaders)
|
||||
{
|
||||
complete = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (complete)
|
||||
break;
|
||||
}
|
||||
|
||||
if (!processed)
|
||||
@@ -1193,13 +1139,26 @@ namespace CryptoExchange.Net.Sockets.Default
|
||||
});
|
||||
}
|
||||
|
||||
private void UpdateRoutingTable()
|
||||
{
|
||||
_routingTable.Update(_listeners);
|
||||
}
|
||||
|
||||
private void AddMessageProcessor(IMessageProcessor processor)
|
||||
{
|
||||
lock (_listenersLock)
|
||||
{
|
||||
var updatedList = new List<IMessageProcessor>(_listeners);
|
||||
updatedList.Add(processor);
|
||||
processor.OnMessageRouterUpdated += UpdateRoutingTable;
|
||||
_listeners = updatedList.AsReadOnly();
|
||||
if (processor.MessageRouter.Routes.Length > 0)
|
||||
{
|
||||
UpdateRoutingTable();
|
||||
#if DEBUG
|
||||
_logger.LogTrace("Processor added, new routing table:\r\n" + _routingTable.ToString());
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1208,8 +1167,15 @@ namespace CryptoExchange.Net.Sockets.Default
|
||||
lock (_listenersLock)
|
||||
{
|
||||
var updatedList = new List<IMessageProcessor>(_listeners);
|
||||
updatedList.Remove(processor);
|
||||
processor.OnMessageRouterUpdated -= UpdateRoutingTable;
|
||||
if (!updatedList.Remove(processor))
|
||||
return; // If nothing removed nothing has changed
|
||||
|
||||
_listeners = updatedList.AsReadOnly();
|
||||
UpdateRoutingTable();
|
||||
#if DEBUG
|
||||
_logger.LogTrace("Processor removed, new routing table:\r\n" + _routingTable.ToString());
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1218,12 +1184,24 @@ namespace CryptoExchange.Net.Sockets.Default
|
||||
lock (_listenersLock)
|
||||
{
|
||||
var updatedList = new List<IMessageProcessor>(_listeners);
|
||||
var anyRemoved = false;
|
||||
foreach (var processor in processors)
|
||||
updatedList.Remove(processor);
|
||||
{
|
||||
processor.OnMessageRouterUpdated -= UpdateRoutingTable;
|
||||
if (updatedList.Remove(processor))
|
||||
anyRemoved = true;
|
||||
}
|
||||
|
||||
if (!anyRemoved)
|
||||
return; // If nothing removed nothing has changed
|
||||
|
||||
_listeners = updatedList.AsReadOnly();
|
||||
UpdateRoutingTable();
|
||||
#if DEBUG
|
||||
_logger.LogTrace("Processors removed, new routing table:\r\n" + _routingTable.ToString());
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Sockets.Default.Routing;
|
||||
using CryptoExchange.Net.Sockets.Interfaces;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
@@ -70,10 +71,21 @@ namespace CryptoExchange.Net.Sockets.Default
|
||||
/// </summary>
|
||||
public bool Authenticated { get; }
|
||||
|
||||
|
||||
private MessageRouter _router;
|
||||
/// <summary>
|
||||
/// Router for this subscription
|
||||
/// </summary>
|
||||
public MessageRouter MessageRouter { get; set; }
|
||||
public MessageRouter MessageRouter
|
||||
{
|
||||
get => _router;
|
||||
set
|
||||
{
|
||||
_router = value;
|
||||
_router.BuildSubscriptionRouter();
|
||||
OnMessageRouterUpdated?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancellation token registration
|
||||
@@ -109,6 +121,9 @@ namespace CryptoExchange.Net.Sockets.Default
|
||||
/// </summary>
|
||||
public int IndividualSubscriptionCount { get; set; } = 1;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event Action? OnMessageRouterUpdated;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
@@ -170,10 +185,11 @@ namespace CryptoExchange.Net.Sockets.Default
|
||||
/// <summary>
|
||||
/// Handle an update message
|
||||
/// </summary>
|
||||
public CallResult? Handle(SocketConnection connection, DateTime receiveTime, string? originalData, object data, MessageRoute route)
|
||||
public bool Handle(string typeIdentifier, string? topicFilter, SocketConnection connection, DateTime receiveTime, string? originalData, object data)
|
||||
{
|
||||
ConnectionInvocations++;
|
||||
TotalInvocations++;
|
||||
|
||||
if (SubscriptionQuery != null && !SubscriptionQuery.Completed && SubscriptionQuery.TimeoutBehavior == TimeoutBehavior.Succeed)
|
||||
{
|
||||
// The subscription query is one where it is successful if there is no error returned
|
||||
@@ -182,7 +198,7 @@ namespace CryptoExchange.Net.Sockets.Default
|
||||
SubscriptionQuery.Timeout();
|
||||
}
|
||||
|
||||
return route.Handle(connection, receiveTime, originalData, data);
|
||||
return MessageRouter.Handle(typeIdentifier, topicFilter, connection, receiveTime, originalData, data, out _);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Sockets.Default;
|
||||
using CryptoExchange.Net.Sockets.Default.Routing;
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.Sockets.Interfaces
|
||||
@@ -19,8 +20,12 @@ namespace CryptoExchange.Net.Sockets.Interfaces
|
||||
/// </summary>
|
||||
public MessageRouter MessageRouter { get; }
|
||||
/// <summary>
|
||||
/// Event when the message router for this processor has been changed
|
||||
/// </summary>
|
||||
public event Action? OnMessageRouterUpdated;
|
||||
/// <summary>
|
||||
/// Handle a message
|
||||
/// </summary>
|
||||
CallResult? Handle(SocketConnection connection, DateTime receiveTime, string? originalData, object result, MessageRoute route);
|
||||
bool Handle(string typeIdentifier, string? topicFilter, SocketConnection socketConnection, DateTime receiveTime, string? originalData, object result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Sockets.Default;
|
||||
using CryptoExchange.Net.Sockets.Default.Routing;
|
||||
using CryptoExchange.Net.Sockets.Interfaces;
|
||||
using System;
|
||||
using System.Threading;
|
||||
@@ -59,10 +60,20 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// </summary>
|
||||
public object? Response { get; set; }
|
||||
|
||||
private MessageRouter _router;
|
||||
/// <summary>
|
||||
/// Router for this query
|
||||
/// </summary>
|
||||
public MessageRouter MessageRouter { get; set; }
|
||||
public MessageRouter MessageRouter
|
||||
{
|
||||
get => _router;
|
||||
set
|
||||
{
|
||||
_router = value;
|
||||
_router.BuildQueryRouter();
|
||||
OnMessageRouterUpdated?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The query request object
|
||||
@@ -99,6 +110,9 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// </summary>
|
||||
public Action? OnComplete { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public event Action? OnMessageRouterUpdated;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
@@ -155,7 +169,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <summary>
|
||||
/// Handle a response message
|
||||
/// </summary>
|
||||
public abstract CallResult Handle(SocketConnection connection, DateTime receiveTime, string? originalData, object message, MessageRoute route);
|
||||
public abstract bool Handle(string typeIdentifier, string? topicFilter, SocketConnection connection, DateTime receiveTime, string? originalData, object message);
|
||||
|
||||
}
|
||||
|
||||
@@ -185,17 +199,23 @@ namespace CryptoExchange.Net.Sockets
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override CallResult Handle(SocketConnection connection, DateTime receiveTime, string? originalData, object message, MessageRoute route)
|
||||
public override bool Handle(string typeIdentifier, string? topicFilter, SocketConnection connection, DateTime receiveTime, string? originalData, object message)
|
||||
{
|
||||
if (Completed)
|
||||
return false;
|
||||
|
||||
CurrentResponses++;
|
||||
if (CurrentResponses == RequiredResponses)
|
||||
Response = message;
|
||||
|
||||
var handled = false;
|
||||
if (Result?.Success != false)
|
||||
{
|
||||
// If an error result is already set don't override that
|
||||
Result = route.Handle(connection, receiveTime, originalData, message);
|
||||
if (Result == null)
|
||||
MessageRouter.Handle(typeIdentifier, topicFilter, connection, receiveTime, originalData, message, out var result);
|
||||
Result = result;
|
||||
handled = Result != null;
|
||||
if (!handled)
|
||||
// Null from Handle means it wasn't actually for this query
|
||||
CurrentResponses -= 1;
|
||||
}
|
||||
@@ -207,7 +227,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
OnComplete?.Invoke();
|
||||
}
|
||||
|
||||
return Result ?? CallResult.SuccessResult;
|
||||
return handled;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -395,9 +395,9 @@ namespace CryptoExchange.Net.Testing.Comparers
|
||||
}
|
||||
else if (objectValue is bool bl)
|
||||
{
|
||||
if (bl && (stringValue != "1" && stringValue != "true" && stringValue != "True"))
|
||||
if (bl && (stringValue != "1" && stringValue != "true" && stringValue != "True" && stringValue != "yes" && stringValue != "YES"))
|
||||
throw new Exception($"{method}: {property} not equal: {stringValue} vs {bl}");
|
||||
if (!bl && (stringValue != "0" && stringValue != "-1" && stringValue != "false" && stringValue != "False"))
|
||||
if (!bl && (stringValue != "0" && stringValue != "-1" && stringValue != "false" && stringValue != "False" && stringValue != "no" && stringValue != "NO"))
|
||||
throw new Exception($"{method}: {property} not equal: {stringValue} vs {bl}");
|
||||
}
|
||||
else if (propertyType.IsEnum || Nullable.GetUnderlyingType(propertyType)?.IsEnum == true)
|
||||
|
||||
@@ -13,8 +13,11 @@ namespace CryptoExchange.Net.Testing
|
||||
if (message.Contains("Cannot map"))
|
||||
throw new Exception("Enum value error: " + message);
|
||||
|
||||
if (message.Contains("Received null enum value"))
|
||||
if (message.Contains("Received null or empty enum value"))
|
||||
throw new Exception("Enum null error: " + message);
|
||||
|
||||
if (message.Contains("Enum mapping sub-optimal."))
|
||||
throw new Exception("Enum mapping error: " + message);
|
||||
}
|
||||
|
||||
public override void WriteLine(string? message)
|
||||
@@ -25,8 +28,11 @@ namespace CryptoExchange.Net.Testing
|
||||
if (message.Contains("Cannot map"))
|
||||
throw new Exception("Enum value error: " + message);
|
||||
|
||||
if (message.Contains("Received null enum value"))
|
||||
if (message.Contains("Received null or empty enum value"))
|
||||
throw new Exception("Enum null error: " + message);
|
||||
|
||||
if (message.Contains("Enum mapping sub-optimal."))
|
||||
throw new Exception("Enum mapping error: " + message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,20 @@ using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Testing.Comparers;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
|
||||
#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
|
||||
#pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
|
||||
|
||||
namespace CryptoExchange.Net.Testing
|
||||
{
|
||||
@@ -48,6 +57,7 @@ namespace CryptoExchange.Net.Testing
|
||||
/// <param name="name">Method name for looking up json test values</param>
|
||||
/// <param name="nestedJsonProperty">Use nested json property for compare</param>
|
||||
/// <param name="ignoreProperties">Ignore certain properties</param>
|
||||
/// <param name="ignoreParamValidation">Ignore certain parameter validation</param>
|
||||
/// <param name="useSingleArrayItem">Use the first item of an json array response</param>
|
||||
/// <param name="skipResponseValidation">Whether to skip the response model validation</param>
|
||||
/// <returns></returns>
|
||||
@@ -57,9 +67,10 @@ namespace CryptoExchange.Net.Testing
|
||||
string name,
|
||||
string? nestedJsonProperty = null,
|
||||
List<string>? ignoreProperties = null,
|
||||
List<string>? ignoreParamValidation = null,
|
||||
bool useSingleArrayItem = false,
|
||||
bool skipResponseValidation = false)
|
||||
=> ValidateAsync<TResponse, TResponse>(methodInvoke, name, nestedJsonProperty, ignoreProperties, useSingleArrayItem, skipResponseValidation);
|
||||
=> ValidateAsync<TResponse, TResponse>(methodInvoke, name, nestedJsonProperty, ignoreProperties, ignoreParamValidation, useSingleArrayItem, skipResponseValidation);
|
||||
|
||||
/// <summary>
|
||||
/// Validate a request
|
||||
@@ -70,6 +81,7 @@ namespace CryptoExchange.Net.Testing
|
||||
/// <param name="name">Method name for looking up json test values</param>
|
||||
/// <param name="nestedJsonProperty">Use nested json property for compare</param>
|
||||
/// <param name="ignoreProperties">Ignore certain properties</param>
|
||||
/// <param name="ignoreParamValidation">Ignore certain parameter validation</param>
|
||||
/// <param name="useSingleArrayItem">Use the first item of an json array response</param>
|
||||
/// <param name="skipResponseValidation">Whether to skip the response model validation</param>
|
||||
/// <returns></returns>
|
||||
@@ -79,6 +91,7 @@ namespace CryptoExchange.Net.Testing
|
||||
string name,
|
||||
string? nestedJsonProperty = null,
|
||||
List<string>? ignoreProperties = null,
|
||||
List<string>? ignoreParamValidation = null,
|
||||
bool useSingleArrayItem = false,
|
||||
bool skipResponseValidation = false) where TActualResponse : TResponse
|
||||
{
|
||||
@@ -105,8 +118,26 @@ namespace CryptoExchange.Net.Testing
|
||||
var expectedMethod = reader.ReadLine();
|
||||
var expectedPath = reader.ReadLine();
|
||||
var expectedAuth = bool.Parse(reader.ReadLine()!);
|
||||
var response = reader.ReadToEnd();
|
||||
var paramsAndResponseBody = reader.ReadToEnd();
|
||||
var paramsAndResponseBodySplit = paramsAndResponseBody.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
var uriParamsLine = paramsAndResponseBodySplit.FirstOrDefault(x => x.StartsWith("UriParams: "));
|
||||
Dictionary<string, object>? expectedUriParams = null;
|
||||
var bodyParamsLine = paramsAndResponseBodySplit.FirstOrDefault(x => x.StartsWith("BodyParams: "));
|
||||
Dictionary<string, object>? expectedBodyParams = null;
|
||||
var response = string.Join("\r\n", paramsAndResponseBodySplit.Where(x => !x.StartsWith("UriParams: ") && !x.StartsWith("BodyParams: ")));
|
||||
|
||||
if (uriParamsLine != null)
|
||||
{
|
||||
var expectedUriParamsJson = uriParamsLine.Substring(11);
|
||||
expectedUriParams = JsonSerializer.Deserialize<Dictionary<string, object>>(expectedUriParamsJson)!;
|
||||
}
|
||||
|
||||
if (bodyParamsLine != null)
|
||||
{
|
||||
var expectedBodyParamsJson = bodyParamsLine.Substring(12);
|
||||
expectedBodyParams = JsonSerializer.Deserialize<Dictionary<string, object>>(expectedBodyParamsJson)!;
|
||||
}
|
||||
TestHelpers.ConfigureRestClient(_client, response, System.Net.HttpStatusCode.OK);
|
||||
var result = await methodInvoke(_client).ConfigureAwait(false);
|
||||
|
||||
@@ -120,6 +151,35 @@ namespace CryptoExchange.Net.Testing
|
||||
if (expectedPath != result.RequestUrl!.Replace(_baseAddress, "").Split(new char[] { '?' })[0])
|
||||
throw new Exception(name + $" path not matched. Expected: {expectedPath}, Actual: {result.RequestUrl!.Replace(_baseAddress, "").Split(new char[] { '?' })[0]}");
|
||||
|
||||
if (expectedUriParams != null)
|
||||
{
|
||||
// Validate request parameters
|
||||
var urlParamsSplit = result.RequestUrl!.Split(new char[] { '?' });
|
||||
var urlParametersString = urlParamsSplit.Length > 1 ? urlParamsSplit[1] : null;
|
||||
var urlParameters = (urlParametersString != null
|
||||
? urlParametersString.Split('&').ToDictionary(x => x.Split('=')[0], x => (object)x.Split('=')[1])
|
||||
: new());
|
||||
|
||||
CompareParameters(expectedUriParams, urlParameters, ignoreParamValidation);
|
||||
}
|
||||
|
||||
if (expectedBodyParams != null)
|
||||
{
|
||||
// Validate request body
|
||||
Dictionary<string, object> bodyParameters;
|
||||
if (result.RequestBody!.StartsWith("{") || result.RequestBody.StartsWith("["))
|
||||
{
|
||||
bodyParameters = JsonSerializer.Deserialize<Dictionary<string, object>>(result.RequestBody!)!;
|
||||
}
|
||||
else
|
||||
{
|
||||
var splitKvp = result.RequestBody.Split('&');
|
||||
bodyParameters = splitKvp.Select(x => x.Split('=')).ToDictionary(x => x[0], x => (object)x[1]);
|
||||
}
|
||||
|
||||
CompareParameters(expectedBodyParams, bodyParameters, ignoreParamValidation);
|
||||
}
|
||||
|
||||
if (!skipResponseValidation)
|
||||
{
|
||||
// Check response data
|
||||
@@ -130,16 +190,37 @@ namespace CryptoExchange.Net.Testing
|
||||
Trace.Listeners.Remove(listener);
|
||||
}
|
||||
|
||||
private void CompareParameters(Dictionary<string, object> expectedUrlParameters, Dictionary<string, object> parameters, List<string>? ignoreParamValidation)
|
||||
{
|
||||
if (expectedUrlParameters.Count > parameters.Count)
|
||||
throw new Exception($"Url parameters count not matched. Expected: {expectedUrlParameters.Count}, Actual: {parameters.Count}");
|
||||
|
||||
foreach (var kvp in expectedUrlParameters)
|
||||
{
|
||||
if (ignoreParamValidation != null && ignoreParamValidation.Contains(kvp.Key))
|
||||
continue;
|
||||
|
||||
if (!parameters.TryGetValue(kvp.Key, out var value))
|
||||
throw new Exception($"Url parameter {kvp.Key} not found in actual parameters");
|
||||
|
||||
if (Convert.ToString(kvp.Value, CultureInfo.InvariantCulture) != Convert.ToString(value, CultureInfo.InvariantCulture))
|
||||
throw new Exception($"Url parameter {kvp.Key} value not matched. Expected: {kvp.Value}, Actual: {value}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validate a request
|
||||
/// </summary>
|
||||
/// <param name="methodInvoke">Method invocation</param>
|
||||
/// <param name="name">Method name for looking up json test values</param>
|
||||
/// <param name="ignoreParamValidation">Ignore certain parameter validation</param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="Exception"></exception>
|
||||
public async Task ValidateAsync(
|
||||
Func<TClient, Task<WebCallResult>> methodInvoke,
|
||||
string name)
|
||||
string name,
|
||||
List<string>? ignoreParamValidation = null
|
||||
)
|
||||
{
|
||||
var listener = new EnumValueTraceListener();
|
||||
Trace.Listeners.Add(listener);
|
||||
@@ -164,7 +245,26 @@ namespace CryptoExchange.Net.Testing
|
||||
var expectedMethod = reader.ReadLine();
|
||||
var expectedPath = reader.ReadLine();
|
||||
var expectedAuth = bool.Parse(reader.ReadLine()!);
|
||||
var response = reader.ReadToEnd();
|
||||
var paramsAndResponseBody = reader.ReadToEnd();
|
||||
var paramsAndResponseBodySplit = paramsAndResponseBody.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
var uriParamsLine = paramsAndResponseBodySplit.FirstOrDefault(x => x.StartsWith("UriParams: "));
|
||||
Dictionary<string, object>? expectedUriParams = null;
|
||||
var bodyParamsLine = paramsAndResponseBodySplit.FirstOrDefault(x => x.StartsWith("BodyParams: "));
|
||||
Dictionary<string, object>? expectedBodyParams = null;
|
||||
var response = string.Join("\r\n", paramsAndResponseBodySplit.Where(x => !x.StartsWith("UriParams: ") && !x.StartsWith("BodyParams: ")));
|
||||
|
||||
if (uriParamsLine != null)
|
||||
{
|
||||
var expectedUriParamsJson = uriParamsLine.Substring(11);
|
||||
expectedUriParams = JsonSerializer.Deserialize<Dictionary<string, object>>(expectedUriParamsJson)!;
|
||||
}
|
||||
|
||||
if (bodyParamsLine != null)
|
||||
{
|
||||
var expectedBodyParamsJson = bodyParamsLine.Substring(12);
|
||||
expectedBodyParams = JsonSerializer.Deserialize<Dictionary<string, object>>(expectedBodyParamsJson)!;
|
||||
}
|
||||
|
||||
TestHelpers.ConfigureRestClient(_client, response, System.Net.HttpStatusCode.OK);
|
||||
var result = await methodInvoke(_client).ConfigureAwait(false);
|
||||
@@ -178,6 +278,35 @@ namespace CryptoExchange.Net.Testing
|
||||
throw new Exception(name + $" http method not matched. Expected {expectedMethod}, Actual: {result.RequestMethod}");
|
||||
if (expectedPath != result.RequestUrl!.Replace(_baseAddress, "").Split(new char[] { '?' })[0])
|
||||
throw new Exception(name + $" path not matched. Expected: {expectedPath}, Actual: {result.RequestUrl!.Replace(_baseAddress, "").Split(new char[] { '?' })[0]}");
|
||||
|
||||
if (expectedUriParams != null)
|
||||
{
|
||||
// Validate request parameters
|
||||
var urlParamsSplit = result.RequestUrl!.Split(new char[] { '?' });
|
||||
var urlParametersString = urlParamsSplit.Length > 1 ? urlParamsSplit[1] : null;
|
||||
var urlParameters = (urlParametersString != null
|
||||
? urlParametersString.Split('&').ToDictionary(x => x.Split('=')[0], x => (object)x.Split('=')[1])
|
||||
: new());
|
||||
|
||||
CompareParameters(expectedUriParams, urlParameters, ignoreParamValidation);
|
||||
}
|
||||
|
||||
if (expectedBodyParams != null)
|
||||
{
|
||||
// Validate request body
|
||||
Dictionary<string, object> bodyParameters;
|
||||
if (result.RequestBody!.StartsWith("{") || result.RequestBody.StartsWith("["))
|
||||
{
|
||||
bodyParameters = JsonSerializer.Deserialize<Dictionary<string, object>>(result.RequestBody!)!;
|
||||
}
|
||||
else
|
||||
{
|
||||
var splitKvp = result.RequestBody.Split('&');
|
||||
bodyParameters = splitKvp.Select(x => x.Split('=')).ToDictionary(x => x[0], x => (object)x[1]);
|
||||
}
|
||||
|
||||
CompareParameters(expectedBodyParams, bodyParameters, ignoreParamValidation);
|
||||
}
|
||||
|
||||
Trace.Listeners.Remove(listener);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
using CryptoExchange.Net.Clients;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using CryptoExchange.Net.Testing.Comparers;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.Testing
|
||||
{
|
||||
/// <summary>
|
||||
/// Validator for REST requests, comparing path, http method, authentication and response parsing
|
||||
/// </summary>
|
||||
/// <typeparam name="TClient">The Rest client</typeparam>
|
||||
public class SharedRestRequestValidator<TClient> where TClient : BaseRestClient
|
||||
{
|
||||
private readonly TClient _client;
|
||||
private readonly Func<WebCallResult, bool> _isAuthenticated;
|
||||
private readonly string _folder;
|
||||
private readonly string _baseAddress;
|
||||
private readonly string? _nestedPropertyForCompare;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="client">Client to test</param>
|
||||
/// <param name="folder">Folder for json test values</param>
|
||||
/// <param name="baseAddress">The base address that is expected</param>
|
||||
/// <param name="isAuthenticated">Func for checking if the request is authenticated</param>
|
||||
/// <param name="nestedPropertyForCompare">Property to use for compare</param>
|
||||
public SharedRestRequestValidator(TClient client, string folder, string baseAddress, Func<WebCallResult, bool> isAuthenticated, string? nestedPropertyForCompare = null)
|
||||
{
|
||||
_client = client;
|
||||
_folder = folder;
|
||||
_baseAddress = baseAddress;
|
||||
_nestedPropertyForCompare = nestedPropertyForCompare;
|
||||
_isAuthenticated = isAuthenticated;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validate a request
|
||||
/// </summary>
|
||||
/// <typeparam name="TResponse">Expected response type</typeparam>
|
||||
/// <param name="methodInvoke">Method invocation</param>
|
||||
/// <param name="name">Method name for looking up json test values</param>
|
||||
/// <param name="endpointOptions">Request options</param>
|
||||
/// <param name="validation">Callback to validate the response model</param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="Exception"></exception>
|
||||
public Task ValidateAsync<TResponse>(
|
||||
Func<TClient, Task<ExchangeWebResult<TResponse>>> methodInvoke,
|
||||
string name,
|
||||
EndpointOptions endpointOptions,
|
||||
params Func<TResponse, bool>[] validation)
|
||||
=> ValidateAsync<TResponse, TResponse>(methodInvoke, name, endpointOptions, validation);
|
||||
|
||||
/// <summary>
|
||||
/// Validate a request
|
||||
/// </summary>
|
||||
/// <typeparam name="TResponse">Expected response type</typeparam>
|
||||
/// <typeparam name="TActualResponse">The concrete response type</typeparam>
|
||||
/// <param name="methodInvoke">Method invocation</param>
|
||||
/// <param name="name">Method name for looking up json test values</param>
|
||||
/// <param name="endpointOptions">Request options</param>
|
||||
/// <param name="validation">Callback to validate the response model</param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="Exception"></exception>
|
||||
public async Task ValidateAsync<TResponse, TActualResponse>(
|
||||
Func<TClient, Task<ExchangeWebResult<TResponse>>> methodInvoke,
|
||||
string name,
|
||||
EndpointOptions endpointOptions,
|
||||
params Func<TResponse, bool>[] validation) where TActualResponse : TResponse
|
||||
{
|
||||
var listener = new EnumValueTraceListener();
|
||||
Trace.Listeners.Add(listener);
|
||||
|
||||
var path = Directory.GetParent(Environment.CurrentDirectory)!.Parent!.Parent!.FullName;
|
||||
FileStream file;
|
||||
try
|
||||
{
|
||||
file = File.OpenRead(Path.Combine(path, _folder, $"{name}.txt"));
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
throw new Exception($"Response file not found for {name}: {path}");
|
||||
}
|
||||
|
||||
var buffer = new byte[file.Length];
|
||||
await file.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
|
||||
file.Close();
|
||||
|
||||
var data = Encoding.UTF8.GetString(buffer);
|
||||
using var reader = new StringReader(data);
|
||||
var expectedMethod = reader.ReadLine();
|
||||
var expectedPath = reader.ReadLine();
|
||||
var expectedAuth = bool.Parse(reader.ReadLine()!);
|
||||
var response = reader.ReadToEnd();
|
||||
|
||||
TestHelpers.ConfigureRestClient(_client, response, System.Net.HttpStatusCode.OK);
|
||||
var result = await methodInvoke(_client).ConfigureAwait(false);
|
||||
|
||||
// Check request/response properties
|
||||
if (result.Error != null)
|
||||
throw new Exception(name + " returned error " + result.Error);
|
||||
if (endpointOptions.NeedsAuthentication != expectedAuth)
|
||||
throw new Exception(name + $" authentication not matched. Expected: {expectedAuth}, Actual: {_isAuthenticated(result.AsDataless())}");
|
||||
if (result.RequestMethod != new HttpMethod(expectedMethod!))
|
||||
throw new Exception(name + $" http method not matched. Expected {expectedMethod}, Actual: {result.RequestMethod}");
|
||||
if (expectedPath != result.RequestUrl!.Replace(_baseAddress, "").Split(new char[] { '?' })[0])
|
||||
throw new Exception(name + $" path not matched. Expected: {expectedPath}, Actual: {result.RequestUrl!.Replace(_baseAddress, "").Split(new char[] { '?' })[0]}");
|
||||
|
||||
var index = 0;
|
||||
foreach(var validate in validation)
|
||||
{
|
||||
if (!validate(result.Data!))
|
||||
throw new Exception(name + $" response validation #{index} failed");
|
||||
|
||||
index++;
|
||||
}
|
||||
|
||||
Trace.Listeners.Remove(listener);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,32 +5,33 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Binance.Net" Version="12.5.0" />
|
||||
<PackageReference Include="Bitfinex.Net" Version="10.6.0" />
|
||||
<PackageReference Include="BitMart.Net" Version="3.5.0" />
|
||||
<PackageReference Include="BloFin.Net" Version="2.5.0" />
|
||||
<PackageReference Include="Bybit.Net" Version="6.5.0" />
|
||||
<PackageReference Include="CoinEx.Net" Version="10.5.0" />
|
||||
<PackageReference Include="CoinW.Net" Version="2.5.0" />
|
||||
<PackageReference Include="CryptoCom.Net" Version="3.5.0" />
|
||||
<PackageReference Include="DeepCoin.Net" Version="3.5.0" />
|
||||
<PackageReference Include="GateIo.Net" Version="3.5.0" />
|
||||
<PackageReference Include="HyperLiquid.Net" Version="3.7.0" />
|
||||
<PackageReference Include="JK.BingX.Net" Version="3.5.0" />
|
||||
<PackageReference Include="JK.Bitget.Net" Version="3.5.0" />
|
||||
<PackageReference Include="JK.Mexc.Net" Version="4.5.0" />
|
||||
<PackageReference Include="JK.OKX.Net" Version="4.5.0" />
|
||||
<PackageReference Include="Jkorf.Aster.Net" Version="2.5.0" />
|
||||
<PackageReference Include="JKorf.BitMEX.Net" Version="3.5.0" />
|
||||
<PackageReference Include="JKorf.Coinbase.Net" Version="3.5.1" />
|
||||
<PackageReference Include="JKorf.HTX.Net" Version="8.5.0" />
|
||||
<PackageReference Include="JKorf.Upbit.Net" Version="2.5.0" />
|
||||
<PackageReference Include="KrakenExchange.Net" Version="7.5.0" />
|
||||
<PackageReference Include="Kucoin.Net" Version="8.5.0" />
|
||||
<PackageReference Include="Binance.Net" Version="12.11.3" />
|
||||
<PackageReference Include="Bitfinex.Net" Version="10.10.2" />
|
||||
<PackageReference Include="BitMart.Net" Version="3.10.0" />
|
||||
<PackageReference Include="BloFin.Net" Version="2.10.2" />
|
||||
<PackageReference Include="Bybit.Net" Version="6.11.0" />
|
||||
<PackageReference Include="CoinEx.Net" Version="10.9.2" />
|
||||
<PackageReference Include="CoinW.Net" Version="2.9.2" />
|
||||
<PackageReference Include="CryptoCom.Net" Version="3.10.0" />
|
||||
<PackageReference Include="DeepCoin.Net" Version="3.9.2" />
|
||||
<PackageReference Include="GateIo.Net" Version="3.10.2" />
|
||||
<PackageReference Include="HyperLiquid.Net" Version="4.3.0" />
|
||||
<PackageReference Include="JK.BingX.Net" Version="3.10.0" />
|
||||
<PackageReference Include="JK.Bitget.Net" Version="3.10.0" />
|
||||
<PackageReference Include="JK.Mexc.Net" Version="5.0.1" />
|
||||
<PackageReference Include="JK.OKX.Net" Version="4.12.0" />
|
||||
<PackageReference Include="Jkorf.Aster.Net" Version="3.1.0" />
|
||||
<PackageReference Include="JKorf.BitMEX.Net" Version="3.9.2" />
|
||||
<PackageReference Include="JKorf.Coinbase.Net" Version="3.9.2" />
|
||||
<PackageReference Include="JKorf.HTX.Net" Version="8.9.1" />
|
||||
<PackageReference Include="JKorf.Upbit.Net" Version="2.9.2" />
|
||||
<PackageReference Include="KrakenExchange.Net" Version="7.9.1" />
|
||||
<PackageReference Include="Kucoin.Net" Version="8.11.0" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
|
||||
<PackageReference Include="Toobit.Net" Version="3.5.0" />
|
||||
<PackageReference Include="WhiteBit.Net" Version="3.5.0" />
|
||||
<PackageReference Include="XT.Net" Version="3.5.0" />
|
||||
<PackageReference Include="Toobit.Net" Version="3.9.2" />
|
||||
<PackageReference Include="Weex.Net" Version="1.0.0" />
|
||||
<PackageReference Include="WhiteBit.Net" Version="3.9.2" />
|
||||
<PackageReference Include="XT.Net" Version="3.9.2" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
@inject IOKXRestClient okxClient
|
||||
@inject IToobitRestClient toobitClient
|
||||
@inject IUpbitRestClient upbitClient
|
||||
@inject IWeexRestClient weexClient
|
||||
@inject IWhiteBitRestClient whitebitClient
|
||||
@inject IXTRestClient xtClient
|
||||
|
||||
@@ -59,6 +60,7 @@
|
||||
var okxTask = okxClient.UnifiedApi.ExchangeData.GetTickerAsync("BTC-USDT");
|
||||
var toobitTask = toobitClient.SpotApi.ExchangeData.GetTickersAsync("BTCUSDT");
|
||||
var upbitTask = upbitClient.SpotApi.ExchangeData.GetTickerAsync("USDT-BTC");
|
||||
var weexTask = weexClient.SpotApi.ExchangeData.GetTickersAsync(["BTCUSDT"]);
|
||||
var whitebitTask = whitebitClient.V4Api.ExchangeData.GetTickersAsync();
|
||||
var xtTask = xtClient.SpotApi.ExchangeData.GetTickersAsync("btc_usdt");
|
||||
|
||||
@@ -141,6 +143,9 @@
|
||||
if (upbitTask.Result.Success)
|
||||
_prices.Add("Upbit", upbitTask.Result.Data.LastPrice ?? 0);
|
||||
|
||||
if (weexTask.Result.Success)
|
||||
_prices.Add("Weex", weexTask.Result.Data.Single().LastPrice);
|
||||
|
||||
if (whitebitTask.Result.Success){
|
||||
// WhiteBit API doesn't offer an endpoint to filter for a specific ticker, so we have to filter client side
|
||||
var tickers = whitebitTask.Result.Data;
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
@inject IOKXSocketClient okxSocketClient
|
||||
@inject IToobitSocketClient toobitSocketClient
|
||||
@inject IUpbitSocketClient upbitSocketClient
|
||||
@inject IWeexSocketClient weexSocketClient
|
||||
@inject IWhiteBitSocketClient whitebitSocketClient
|
||||
@inject IXTSocketClient xtSocketClient
|
||||
@using System.Collections.Concurrent
|
||||
|
||||
@@ -21,12 +21,14 @@
|
||||
@using HTX.Net.Interfaces
|
||||
@using HyperLiquid.Net.Interfaces
|
||||
@using Kraken.Net.Interfaces
|
||||
@using Kucoin.Net
|
||||
@using Kucoin.Net.Clients
|
||||
@using Kucoin.Net.Interfaces
|
||||
@using Mexc.Net.Interfaces
|
||||
@using OKX.Net.Interfaces;
|
||||
@using Upbit.Net.Interfaces;
|
||||
@using Toobit.Net.Interfaces;
|
||||
@using Weex.Net.Interfaces
|
||||
@using WhiteBit.Net.Interfaces
|
||||
@using XT.Net.Interfaces
|
||||
@inject IAsterOrderBookFactory asterFactory
|
||||
@@ -52,6 +54,7 @@
|
||||
@inject IOKXOrderBookFactory okxFactory
|
||||
@inject IToobitOrderBookFactory toobitFactory
|
||||
@inject IUpbitOrderBookFactory upbitFactory
|
||||
@inject IWeexOrderBookFactory weexFactory
|
||||
@inject IWhiteBitOrderBookFactory whitebitFactory
|
||||
@inject IXTOrderBookFactory xtFactory
|
||||
@implements IDisposable
|
||||
@@ -83,7 +86,7 @@
|
||||
// Since the Kucoin order book stream needs authentication we will need to provide API credentials beforehand
|
||||
KucoinRestClient.SetDefaultOptions(options =>
|
||||
{
|
||||
options.ApiCredentials = new ApiCredentials("KEY", "SECRET", "PASSPHRASE");
|
||||
options.ApiCredentials = new KucoinCredentials("KEY", "SECRET", "PASSPHRASE");
|
||||
});
|
||||
|
||||
_books = new Dictionary<string, ISymbolOrderBook>
|
||||
@@ -111,6 +114,7 @@
|
||||
{ "OKX", okxFactory.Create("ETH-BTC") },
|
||||
{ "Toobit", toobitFactory.CreateSpot("ETHUSDT") },
|
||||
{ "Upbit", upbitFactory.CreateSpot("BTC-ETH") },
|
||||
{ "Weex", weexFactory.CreateSpot("ETHUSDT") },
|
||||
{ "WhiteBit", whitebitFactory.CreateV4("ETH_BTC") },
|
||||
{ "XT", xtFactory.CreateSpot("eth_btc") },
|
||||
};
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
@using OKX.Net.Interfaces;
|
||||
@using Upbit.Net.Interfaces;
|
||||
@using Toobit.Net.Interfaces;
|
||||
@using Weex.Net.Interfaces
|
||||
@using WhiteBit.Net.Interfaces
|
||||
@using XT.Net.Interfaces
|
||||
@inject IAsterTrackerFactory asterFactory
|
||||
@@ -53,6 +54,7 @@
|
||||
@inject IOKXTrackerFactory okxFactory
|
||||
@inject IToobitTrackerFactory toobitFactory
|
||||
@inject IUpbitTrackerFactory upbitFactory
|
||||
@inject IWeexTrackerFactory weexFactory
|
||||
@inject IWhiteBitTrackerFactory whitebitFactory
|
||||
@inject IXTTrackerFactory xtFactory
|
||||
@implements IDisposable
|
||||
@@ -105,6 +107,7 @@
|
||||
{ okxFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ toobitFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ upbitFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ weexFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ whitebitFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ xtFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user