mirror of
https://github.com/JKorf/CryptoExchange.Net.git
synced 2026-08-11 16:32:57 +00:00
Compare commits
51 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 860d753ad6 | |||
| e16c91792c | |||
| a3d95da9fa | |||
| 907399b878 | |||
| 9eab0d967e | |||
| 0dee68e8ae | |||
| d838d3377f | |||
| 2141ad9061 | |||
| 9be8798ccf | |||
| 4803ed91cd | |||
| 20bddd5c37 | |||
| 0e75ddb3d0 | |||
| 0e5b46002c | |||
| 8f7c71f9ce | |||
| 73377fbb87 | |||
| 3a00d6371a | |||
| caf6d36bcd | |||
| e078a373da | |||
| 007743f5a1 | |||
| d06f891cee | |||
| 8dcbb687f5 | |||
| fcb36f7ee0 | |||
| 3cffd67518 | |||
| ecd00ea707 | |||
| 38a7b981ce | |||
| c41cc3c4c7 | |||
| 4accc8039b | |||
| 87c86ec0c0 | |||
| d9850da282 | |||
| c4a8b02054 | |||
| 34b7258496 | |||
| 69099922c9 | |||
| 64c1cd5fa8 | |||
| 936ac6640b | |||
| 68ad9ae114 | |||
| 6238c17471 | |||
| 52e6fbfe47 | |||
| 4129622d71 | |||
| 95e0aefb9f | |||
| dd1cefdc90 | |||
| f4736bcf49 | |||
| e823114623 | |||
| afb84a1bf0 | |||
| 89cbd85875 | |||
| cc25a405c6 | |||
| d64c1171a3 | |||
| d4aded1ee2 | |||
| 271503c426 | |||
| f0ece589f7 | |||
| 3a9382ca0f | |||
| 1e1a02324a |
@@ -30,9 +30,21 @@ var ticker = await binance.GetSpotTickerAsync(new GetTickerRequest(symbol));
|
||||
|
||||
`SharedSymbol(TradingMode.Spot, "BTC", "USDT")` is portable. Each library translates to its native format internally. Don't pass raw strings like `"BTCUSDT"` to shared methods.
|
||||
|
||||
## Symbol metadata and catalogs
|
||||
|
||||
In 12.2.0, `SharedSpotSymbol` and `SharedFuturesSymbol` include `DisplayName` and base/quote asset classification through `SharedAssetType` (`Crypto`, `Fiat`, `TradFi`) and `SharedAssetSubType` (`StableCoin`, `Equity`, `Commodity`). Pass the matching base/quote filters to `GetSymbolsRequest` when discovery should return only a class of markets.
|
||||
|
||||
After calling `GetSpotSymbolsAsync`, `ISpotSymbolRestClient.SpotSymbolCatalog` maps asset and symbol names to shared metadata. `IFuturesSymbolRestClient.FuturesSymbolCatalog` works the same way after `GetFuturesSymbolsAsync`. Treat either property as unavailable before its corresponding request has populated the cache.
|
||||
|
||||
When implementing an exchange library, use `LibraryHelpers.IsStableCoin`, `IsCommodity`, and `IsEquity` only as best-effort classifiers and supply exchange-specific additions where needed.
|
||||
|
||||
## Shared market-data quantities
|
||||
|
||||
In 12.4.0, use `SharedOrderQuantity`-valued `Volumes` on shared spot/futures tickers and klines, and `Quantities` on shared trades. The scalar `Volume`, `QuoteVolume`, and `Quantity` members are obsolete.
|
||||
|
||||
## Result pattern
|
||||
|
||||
Same `WebCallResult<T>` / `CallResult<T>` everywhere. Always check `.Success`. `.Exchange` property identifies which exchange responded — useful for logging.
|
||||
REST methods return `HttpResult<T>` and websocket subscription methods return `WebSocketResult<UpdateSubscription>`. Always check `.Success`. `.Exchange` property identifies which exchange responded — useful for logging.
|
||||
|
||||
## Available shared interfaces
|
||||
|
||||
|
||||
@@ -24,13 +24,23 @@ var ticker = await binance.GetSpotTickerAsync(new GetTickerRequest(symbol));
|
||||
|
||||
Same code works on every exchange that implements the interface. Use `Task.WhenAll` for concurrent multi-exchange calls.
|
||||
|
||||
## Shared symbol metadata
|
||||
|
||||
CryptoExchange.Net 12.2.0 classifies the base and quote sides of `SharedSpotSymbol` and `SharedFuturesSymbol` with `SharedAssetType` (`Crypto`, `Fiat`, `TradFi`) and optional `SharedAssetSubType` (`StableCoin`, `Equity`, `Commodity`). The models also expose `DisplayName`. Use the corresponding base/quote fields on `GetSymbolsRequest` to filter symbol discovery.
|
||||
|
||||
`ISpotSymbolRestClient.SpotSymbolCatalog` is populated by `GetSpotSymbolsAsync`; `IFuturesSymbolRestClient.FuturesSymbolCatalog` is populated by `GetFuturesSymbolsAsync`. Do not assume a catalog is available before that request. For exchange-library implementations, `LibraryHelpers.IsStableCoin`, `IsCommodity`, and `IsEquity` offer best-effort classification and can be extended with exchange-specific values.
|
||||
|
||||
## Shared market-data quantities
|
||||
|
||||
CryptoExchange.Net 12.4.0 uses `SharedOrderQuantity` for market-data quantities. Prefer `Volumes` on `SharedSpotTicker`, `SharedFuturesTicker`, and `SharedKline`, and `Quantities` on `SharedTrade`; the scalar `Volume`, `QuoteVolume`, and `Quantity` members are obsolete.
|
||||
|
||||
## Single-exchange code uses the exchange's own client
|
||||
|
||||
For Binance-only code, use `BinanceRestClient` directly (see Binance.Net repo `AGENTS.md`). SharedApis is for portability — use it when you need that.
|
||||
|
||||
## Result pattern
|
||||
|
||||
Every method returns `WebCallResult<T>` or `CallResult<T>`. Check `.Success` before `.Data`. `.Error` has structured info. `.Exchange` on shared clients identifies which exchange responded.
|
||||
REST methods return `HttpResult<T>` and websocket subscription methods return `WebSocketResult<UpdateSubscription>`. Check `.Success` before `.Data`. `.Error` has structured info. `.Exchange` on shared clients identifies which exchange responded.
|
||||
|
||||
## Available shared interfaces
|
||||
|
||||
|
||||
@@ -67,6 +67,28 @@ var btcusdtPerp = new SharedSymbol(TradingMode.PerpetualLinear, "BTC", "USDT");
|
||||
|
||||
For exchanges that use exotic asset names, see the AssetAliases configuration.
|
||||
|
||||
## Symbol Metadata and Asset Classification
|
||||
|
||||
Since CryptoExchange.Net 12.2.0, shared symbol responses describe both sides of a market with `BaseAssetType`, `BaseAssetSubType`, `QuoteAssetType`, and `QuoteAssetSubType`. `SharedAssetType` distinguishes `Crypto`, `Fiat`, and `TradFi`; `SharedAssetSubType` distinguishes `StableCoin`, `Equity`, and `Commodity`. `SharedSpotSymbol` and `SharedFuturesSymbol` also expose `DisplayName`.
|
||||
|
||||
The same fields on `GetSymbolsRequest` filter spot or futures symbol discovery:
|
||||
|
||||
```csharp
|
||||
var request = new GetSymbolsRequest(
|
||||
baseAssetType: SharedAssetType.Crypto,
|
||||
quoteAssetSubType: SharedAssetSubType.StableCoin);
|
||||
|
||||
var result = await symbolClient.GetSpotSymbolsAsync(request);
|
||||
```
|
||||
|
||||
After calling `GetSpotSymbolsAsync` or `GetFuturesSymbolsAsync`, use the client's `SpotSymbolCatalog` or `FuturesSymbolCatalog` to look up normalized asset and symbol metadata by name. The catalog is unavailable until the corresponding symbol request has populated the cache.
|
||||
|
||||
For exchange-library implementations, `LibraryHelpers.IsStableCoin`, `IsCommodity`, and `IsEquity` provide best-effort classification of known assets and accept exchange-specific additions. These helpers are heuristics, not an exhaustive source of truth.
|
||||
|
||||
## Shared Market-Data Quantities
|
||||
|
||||
Since CryptoExchange.Net 12.4.0, shared market-data models use `SharedOrderQuantity` so base-asset, quote-asset, and contract quantities remain explicit. Read `SharedSpotTicker.Volumes`, `SharedFuturesTicker.Volumes`, and `SharedKline.Volumes`; read `SharedTrade.Quantities`. The former scalar `Volume`, `QuoteVolume`, and `Quantity` members are obsolete.
|
||||
|
||||
## Available Shared Interfaces
|
||||
|
||||
**REST:**
|
||||
@@ -85,7 +107,7 @@ Each exchange documents which interfaces it implements (some exchanges don't sup
|
||||
|
||||
## Core Pattern: Result Handling
|
||||
|
||||
Same as exchange-specific libraries — `WebCallResult<T>` (REST) or `CallResult<T>` (WebSocket) with `.Success`, `.Data`, `.Error`. Always check `.Success` first.
|
||||
Same as exchange-specific libraries: REST calls return `HttpResult<T>` and websocket subscription calls return `WebSocketResult<UpdateSubscription>`, both with `.Success`, `.Data`, and `.Error`. Always check `.Success` first.
|
||||
|
||||
```csharp
|
||||
var result = await sharedClient.GetSpotTickerAsync(new GetTickerRequest(symbol));
|
||||
|
||||
@@ -1,530 +0,0 @@
|
||||
using CryptoExchange.Net.Converters.MessageParsing;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using ProtoBuf;
|
||||
using ProtoBuf.Meta;
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices.ComTypes;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.Protobuf
|
||||
{
|
||||
/// <summary>
|
||||
/// System.Text.Json message accessor
|
||||
/// </summary>
|
||||
#if NET5_0_OR_GREATER
|
||||
public abstract class ProtobufMessageAccessor<
|
||||
[DynamicallyAccessedMembers(
|
||||
#if NET8_0_OR_GREATER
|
||||
DynamicallyAccessedMemberTypes.NonPublicConstructors |
|
||||
DynamicallyAccessedMemberTypes.PublicFields |
|
||||
DynamicallyAccessedMemberTypes.NonPublicFields |
|
||||
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
|
||||
DynamicallyAccessedMemberTypes.PublicProperties |
|
||||
DynamicallyAccessedMemberTypes.NonPublicProperties |
|
||||
#endif
|
||||
DynamicallyAccessedMemberTypes.PublicNestedTypes |
|
||||
DynamicallyAccessedMemberTypes.NonPublicMethods |
|
||||
DynamicallyAccessedMemberTypes.PublicMethods
|
||||
)]
|
||||
TIntermediateType> : IMessageAccessor
|
||||
#else
|
||||
public abstract class ProtobufMessageAccessor<TIntermediateType> : IMessageAccessor
|
||||
#endif
|
||||
{
|
||||
/// <summary>
|
||||
/// The intermediate deserialization object
|
||||
/// </summary>
|
||||
protected TIntermediateType? _intermediateType;
|
||||
/// <summary>
|
||||
/// Runtime type model
|
||||
/// </summary>
|
||||
protected RuntimeTypeModel _model;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsValid { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract bool OriginalDataAvailable { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public object? Underlying => _intermediateType;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public ProtobufMessageAccessor(RuntimeTypeModel model)
|
||||
{
|
||||
_model = model;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public NodeType? GetNodeType()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public NodeType? GetNodeType(MessagePath path)
|
||||
{
|
||||
if (_intermediateType == null)
|
||||
throw new InvalidOperationException("Data not read");
|
||||
|
||||
object? value = _intermediateType;
|
||||
foreach (var step in path)
|
||||
{
|
||||
if (value == null)
|
||||
break;
|
||||
|
||||
if (step.Type == 0)
|
||||
{
|
||||
// array index
|
||||
}
|
||||
else if (step.Type == 1)
|
||||
{
|
||||
// property value
|
||||
#pragma warning disable IL2075 // Type is already annotated
|
||||
value = value.GetType().GetProperty(step.Property!)?.GetValue(value);
|
||||
#pragma warning restore
|
||||
}
|
||||
else
|
||||
{
|
||||
// property name
|
||||
}
|
||||
}
|
||||
|
||||
if (value == null)
|
||||
return null;
|
||||
|
||||
var valueType = value.GetType();
|
||||
if (valueType.IsArray)
|
||||
return NodeType.Array;
|
||||
|
||||
if (IsSimple(valueType))
|
||||
return NodeType.Value;
|
||||
|
||||
return NodeType.Object;
|
||||
}
|
||||
|
||||
private static bool IsSimple(Type type)
|
||||
{
|
||||
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
|
||||
{
|
||||
// nullable type, check if the nested type is simple.
|
||||
return IsSimple(type.GetGenericArguments()[0]);
|
||||
}
|
||||
return type.IsPrimitive
|
||||
|| type.IsEnum
|
||||
|| type == typeof(string)
|
||||
|| type == typeof(decimal);
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
#if NET5_0_OR_GREATER
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2075:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
#endif
|
||||
public T? GetValue<T>(MessagePath path)
|
||||
{
|
||||
if (_intermediateType == null)
|
||||
throw new InvalidOperationException("Data not read");
|
||||
|
||||
object? value = _intermediateType;
|
||||
foreach(var step in path)
|
||||
{
|
||||
if (value == null)
|
||||
break;
|
||||
|
||||
if (step.Type == 0)
|
||||
{
|
||||
// array index
|
||||
}
|
||||
else if (step.Type == 1)
|
||||
{
|
||||
// property value
|
||||
#pragma warning disable IL2075 // Type is already annotated
|
||||
value = value.GetType().GetProperty(step.Property!)?.GetValue(value);
|
||||
#pragma warning restore
|
||||
}
|
||||
else
|
||||
{
|
||||
// property name
|
||||
}
|
||||
}
|
||||
|
||||
return (T?)value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public T?[]? GetValues<T>(MessagePath path)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract string GetOriginalString();
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract void Clear();
|
||||
|
||||
/// <inheritdoc />
|
||||
#if NET5_0_OR_GREATER
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
#endif
|
||||
public abstract CallResult<object> Deserialize(
|
||||
#if NET5_0_OR_GREATER
|
||||
[DynamicallyAccessedMembers(
|
||||
#if NET8_0_OR_GREATER
|
||||
DynamicallyAccessedMemberTypes.NonPublicConstructors |
|
||||
DynamicallyAccessedMemberTypes.PublicFields |
|
||||
DynamicallyAccessedMemberTypes.NonPublicFields |
|
||||
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
|
||||
DynamicallyAccessedMemberTypes.PublicProperties |
|
||||
DynamicallyAccessedMemberTypes.NonPublicProperties |
|
||||
DynamicallyAccessedMemberTypes.PublicConstructors |
|
||||
#endif
|
||||
DynamicallyAccessedMemberTypes.PublicNestedTypes |
|
||||
DynamicallyAccessedMemberTypes.NonPublicMethods |
|
||||
DynamicallyAccessedMemberTypes.PublicMethods
|
||||
)]
|
||||
#endif
|
||||
Type type, MessagePath? path = null);
|
||||
|
||||
/// <inheritdoc />
|
||||
#if NET5_0_OR_GREATER
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
#endif
|
||||
public abstract CallResult<T> Deserialize<
|
||||
#if NET5_0_OR_GREATER
|
||||
[DynamicallyAccessedMembers(
|
||||
#if NET8_0_OR_GREATER
|
||||
DynamicallyAccessedMemberTypes.NonPublicConstructors |
|
||||
DynamicallyAccessedMemberTypes.PublicFields |
|
||||
DynamicallyAccessedMemberTypes.NonPublicFields |
|
||||
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
|
||||
DynamicallyAccessedMemberTypes.PublicProperties |
|
||||
DynamicallyAccessedMemberTypes.NonPublicProperties |
|
||||
DynamicallyAccessedMemberTypes.PublicConstructors |
|
||||
#endif
|
||||
DynamicallyAccessedMemberTypes.PublicNestedTypes |
|
||||
DynamicallyAccessedMemberTypes.NonPublicMethods |
|
||||
DynamicallyAccessedMemberTypes.PublicMethods
|
||||
)]
|
||||
#endif
|
||||
T>(MessagePath? path = null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// System.Text.Json stream message accessor
|
||||
/// </summary>
|
||||
public class ProtobufStreamMessageAccessor<
|
||||
#if NET5_0_OR_GREATER
|
||||
[DynamicallyAccessedMembers(
|
||||
#if NET8_0_OR_GREATER
|
||||
DynamicallyAccessedMemberTypes.NonPublicConstructors |
|
||||
DynamicallyAccessedMemberTypes.PublicFields |
|
||||
DynamicallyAccessedMemberTypes.NonPublicFields |
|
||||
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
|
||||
DynamicallyAccessedMemberTypes.PublicProperties |
|
||||
DynamicallyAccessedMemberTypes.NonPublicProperties |
|
||||
DynamicallyAccessedMemberTypes.PublicConstructors |
|
||||
#endif
|
||||
DynamicallyAccessedMemberTypes.PublicNestedTypes |
|
||||
DynamicallyAccessedMemberTypes.NonPublicMethods |
|
||||
DynamicallyAccessedMemberTypes.PublicMethods
|
||||
)]
|
||||
#endif
|
||||
TIntermediate> : ProtobufMessageAccessor<TIntermediate>, IStreamMessageAccessor
|
||||
{
|
||||
private Stream? _stream;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool OriginalDataAvailable => _stream?.CanSeek == true;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public ProtobufStreamMessageAccessor(RuntimeTypeModel model) : base(model)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
#if NET5_0_OR_GREATER
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
#endif
|
||||
public override CallResult<object> Deserialize(
|
||||
#if NET5_0_OR_GREATER
|
||||
[DynamicallyAccessedMembers(
|
||||
#if NET8_0_OR_GREATER
|
||||
DynamicallyAccessedMemberTypes.NonPublicConstructors |
|
||||
DynamicallyAccessedMemberTypes.PublicFields |
|
||||
DynamicallyAccessedMemberTypes.NonPublicFields |
|
||||
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
|
||||
DynamicallyAccessedMemberTypes.PublicProperties |
|
||||
DynamicallyAccessedMemberTypes.NonPublicProperties |
|
||||
DynamicallyAccessedMemberTypes.PublicConstructors |
|
||||
#endif
|
||||
DynamicallyAccessedMemberTypes.PublicNestedTypes |
|
||||
DynamicallyAccessedMemberTypes.NonPublicMethods |
|
||||
DynamicallyAccessedMemberTypes.PublicMethods
|
||||
)]
|
||||
#endif
|
||||
Type type, MessagePath? path = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = _model.Deserialize(type, _stream);
|
||||
return new CallResult<object>(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new CallResult<object>(new DeserializeError("Protobuf deserialization failed: " + ex.Message, ex));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
#if NET5_0_OR_GREATER
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
#endif
|
||||
public override CallResult<T> Deserialize<
|
||||
#if NET5_0_OR_GREATER
|
||||
[DynamicallyAccessedMembers(
|
||||
#if NET8_0_OR_GREATER
|
||||
DynamicallyAccessedMemberTypes.NonPublicConstructors |
|
||||
DynamicallyAccessedMemberTypes.PublicFields |
|
||||
DynamicallyAccessedMemberTypes.NonPublicFields |
|
||||
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
|
||||
DynamicallyAccessedMemberTypes.PublicProperties |
|
||||
DynamicallyAccessedMemberTypes.NonPublicProperties |
|
||||
DynamicallyAccessedMemberTypes.PublicConstructors |
|
||||
#endif
|
||||
DynamicallyAccessedMemberTypes.PublicNestedTypes |
|
||||
DynamicallyAccessedMemberTypes.NonPublicMethods |
|
||||
DynamicallyAccessedMemberTypes.PublicMethods
|
||||
)]
|
||||
#endif
|
||||
T>(MessagePath? path = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = _model.Deserialize<T>(_stream);
|
||||
return new CallResult<T>(result);
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
return new CallResult<T>(new DeserializeError("Protobuf deserialization failed: " + ex.Message, ex));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<CallResult> Read(Stream stream, bool bufferStream)
|
||||
{
|
||||
if (bufferStream && stream is not MemoryStream)
|
||||
{
|
||||
// We need to be buffer the stream, and it's not currently a seekable stream, so copy it to a new memory stream
|
||||
_stream = new MemoryStream();
|
||||
stream.CopyTo(_stream);
|
||||
_stream.Position = 0;
|
||||
}
|
||||
else if (bufferStream)
|
||||
{
|
||||
// We need to buffer the stream, and the current stream is seekable, store as is
|
||||
_stream = stream;
|
||||
}
|
||||
else
|
||||
{
|
||||
// We don't need to buffer the stream, so don't bother keeping the reference
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_intermediateType = _model.Deserialize<TIntermediate>(_stream);
|
||||
IsValid = true;
|
||||
return Task.FromResult(CallResult.SuccessResult);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Not a json message
|
||||
IsValid = false;
|
||||
return Task.FromResult(new CallResult(new DeserializeError("Protobuf deserialization failed: " + ex.Message, ex)));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string GetOriginalString()
|
||||
{
|
||||
if (_stream is null)
|
||||
throw new NullReferenceException("Stream not initialized");
|
||||
|
||||
_stream.Position = 0;
|
||||
using var textReader = new StreamReader(_stream, Encoding.UTF8, false, 1024, true);
|
||||
return textReader.ReadToEnd();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Clear()
|
||||
{
|
||||
_stream?.Dispose();
|
||||
_stream = null;
|
||||
_intermediateType = default;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Protobuf byte message accessor
|
||||
/// </summary>
|
||||
public class ProtobufByteMessageAccessor<
|
||||
#if NET5_0_OR_GREATER
|
||||
[DynamicallyAccessedMembers(
|
||||
#if NET8_0_OR_GREATER
|
||||
DynamicallyAccessedMemberTypes.NonPublicConstructors |
|
||||
DynamicallyAccessedMemberTypes.PublicFields |
|
||||
DynamicallyAccessedMemberTypes.NonPublicFields |
|
||||
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
|
||||
DynamicallyAccessedMemberTypes.PublicProperties |
|
||||
DynamicallyAccessedMemberTypes.NonPublicProperties |
|
||||
DynamicallyAccessedMemberTypes.PublicConstructors |
|
||||
#endif
|
||||
DynamicallyAccessedMemberTypes.PublicNestedTypes |
|
||||
DynamicallyAccessedMemberTypes.NonPublicMethods |
|
||||
DynamicallyAccessedMemberTypes.PublicMethods
|
||||
)]
|
||||
#endif
|
||||
TIntermediate> : ProtobufMessageAccessor<TIntermediate>, IByteMessageAccessor
|
||||
{
|
||||
private ReadOnlyMemory<byte> _bytes;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public ProtobufByteMessageAccessor(RuntimeTypeModel model) : base(model)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
#if NET5_0_OR_GREATER
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
#endif
|
||||
public override CallResult<object> Deserialize(
|
||||
|
||||
#if NET5_0_OR_GREATER
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
[DynamicallyAccessedMembers(
|
||||
#if NET8_0_OR_GREATER
|
||||
DynamicallyAccessedMemberTypes.NonPublicConstructors |
|
||||
DynamicallyAccessedMemberTypes.PublicFields |
|
||||
DynamicallyAccessedMemberTypes.NonPublicFields |
|
||||
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
|
||||
DynamicallyAccessedMemberTypes.PublicProperties |
|
||||
DynamicallyAccessedMemberTypes.NonPublicProperties |
|
||||
DynamicallyAccessedMemberTypes.PublicConstructors |
|
||||
#endif
|
||||
DynamicallyAccessedMemberTypes.PublicNestedTypes |
|
||||
DynamicallyAccessedMemberTypes.NonPublicMethods |
|
||||
DynamicallyAccessedMemberTypes.PublicMethods
|
||||
)]
|
||||
#endif
|
||||
Type type, MessagePath? path = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var stream = new MemoryStream(_bytes.ToArray());
|
||||
stream.Position = 0;
|
||||
var result = _model.Deserialize(type, stream);
|
||||
return new CallResult<object>(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new CallResult<object>(new DeserializeError("Protobuf deserialization failed: " + ex.Message, ex));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
#if NET5_0_OR_GREATER
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
#endif
|
||||
#if NET5_0_OR_GREATER
|
||||
public override CallResult<T> Deserialize<
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
[DynamicallyAccessedMembers(
|
||||
#if NET8_0_OR_GREATER
|
||||
DynamicallyAccessedMemberTypes.NonPublicConstructors |
|
||||
DynamicallyAccessedMemberTypes.PublicFields |
|
||||
DynamicallyAccessedMemberTypes.NonPublicFields |
|
||||
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
|
||||
DynamicallyAccessedMemberTypes.PublicProperties |
|
||||
DynamicallyAccessedMemberTypes.NonPublicProperties |
|
||||
DynamicallyAccessedMemberTypes.PublicConstructors |
|
||||
#endif
|
||||
DynamicallyAccessedMemberTypes.PublicNestedTypes |
|
||||
DynamicallyAccessedMemberTypes.NonPublicMethods |
|
||||
DynamicallyAccessedMemberTypes.PublicMethods
|
||||
)]
|
||||
T>(MessagePath? path = null)
|
||||
#else
|
||||
public override CallResult<T> Deserialize<T>(MessagePath? path = null)
|
||||
#endif
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = _model.Deserialize<T>(_bytes);
|
||||
return new CallResult<T>(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new CallResult<T>(new DeserializeError("Protobuf deserialization failed: " + ex.Message, ex));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public CallResult Read(ReadOnlyMemory<byte> data)
|
||||
{
|
||||
_bytes = data;
|
||||
|
||||
try
|
||||
{
|
||||
_intermediateType = _model.Deserialize<TIntermediate>(data);
|
||||
IsValid = true;
|
||||
return CallResult.SuccessResult;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Not a json message
|
||||
IsValid = false;
|
||||
return new CallResult(new DeserializeError("Protobuf deserialization failed: " + ex.Message, ex));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string GetOriginalString() =>
|
||||
// NetStandard 2.0 doesn't support GetString from a ReadonlySpan<byte>, so use ToArray there instead
|
||||
#if NETSTANDARD2_0
|
||||
Encoding.UTF8.GetString(_bytes.ToArray());
|
||||
#else
|
||||
Encoding.UTF8.GetString(_bytes.Span);
|
||||
#endif
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool OriginalDataAvailable => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Clear()
|
||||
{
|
||||
_bytes = null;
|
||||
_intermediateType = default;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using ProtoBuf.Meta;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.Protobuf
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public class ProtobufMessageSerializer : IByteMessageSerializer
|
||||
{
|
||||
private RuntimeTypeModel _model;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public ProtobufMessageSerializer(RuntimeTypeModel model)
|
||||
{
|
||||
_model = model;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
#if NET5_0_OR_GREATER
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
#endif
|
||||
#if NET5_0_OR_GREATER
|
||||
public byte[] Serialize<
|
||||
[DynamicallyAccessedMembers(
|
||||
#if NET8_0_OR_GREATER
|
||||
DynamicallyAccessedMemberTypes.NonPublicConstructors |
|
||||
DynamicallyAccessedMemberTypes.PublicFields |
|
||||
DynamicallyAccessedMemberTypes.NonPublicFields |
|
||||
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
|
||||
DynamicallyAccessedMemberTypes.PublicProperties |
|
||||
DynamicallyAccessedMemberTypes.NonPublicProperties |
|
||||
DynamicallyAccessedMemberTypes.PublicConstructors |
|
||||
#endif
|
||||
DynamicallyAccessedMemberTypes.PublicNestedTypes |
|
||||
DynamicallyAccessedMemberTypes.NonPublicMethods |
|
||||
DynamicallyAccessedMemberTypes.PublicMethods
|
||||
)]
|
||||
T>(T message)
|
||||
#else
|
||||
public byte[] Serialize<T>(T message)
|
||||
#endif
|
||||
{
|
||||
using var memoryStream = new MemoryStream();
|
||||
_model.Serialize(memoryStream, message);
|
||||
return memoryStream.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;netstandard2.1;net8.0;net9.0;net10.0</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<PackageId>CryptoExchange.Net.Protobuf</PackageId>
|
||||
<Authors>JKorf</Authors>
|
||||
<Description>Protobuf support for CryptoExchange.Net</Description>
|
||||
<PackageVersion>10.0.1</PackageVersion>
|
||||
<AssemblyVersion>10.0.1</AssemblyVersion>
|
||||
<FileVersion>10.0.1</FileVersion>
|
||||
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
||||
<PackageTags>CryptoExchange;CryptoExchange.Net</PackageTags>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<RepositoryUrl>https://github.com/JKorf/CryptoExchange.Net.git</RepositoryUrl>
|
||||
<PackageProjectUrl>https://github.com/JKorf/CryptoExchange.Net/tree/master/CryptoExchange.Net.Protobuf</PackageProjectUrl>
|
||||
<NeutralLanguage>en</NeutralLanguage>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<PackageIcon>icon.png</PackageIcon>
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
<PackageReleaseNotes>https://github.com/JKorf/CryptoExchange.Net?tab=readme-ov-file#release-notes</PackageReleaseNotes>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>12.0</LangVersion>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<None Include="..\CryptoExchange.Net\Icon\icon.png" Pack="true" PackagePath="\" />
|
||||
<None Include="README.md" Pack="true" PackagePath="\" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="AOT" Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net7.0'))">
|
||||
<IsAotCompatible>true</IsAotCompatible>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Label="Deterministic Build" Condition="'$(Configuration)' == 'Release'">
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||
<EmbedUntrackedSources>true</EmbedUntrackedSources>
|
||||
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<DocumentationFile>CryptoExchange.Net.Protobuf.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CryptoExchange.Net" Version="10.0.2" />
|
||||
<PackageReference Include="protobuf-net" Version="3.2.56" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,128 +0,0 @@
|
||||
<?xml version="1.0"?>
|
||||
<doc>
|
||||
<assembly>
|
||||
<name>CryptoExchange.Net.Protobuf</name>
|
||||
</assembly>
|
||||
<members>
|
||||
<member name="T:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1">
|
||||
<summary>
|
||||
System.Text.Json message accessor
|
||||
</summary>
|
||||
</member>
|
||||
<member name="F:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1._intermediateType">
|
||||
<summary>
|
||||
The intermediate deserialization object
|
||||
</summary>
|
||||
</member>
|
||||
<member name="F:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1._model">
|
||||
<summary>
|
||||
Runtime type model
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.IsValid">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="P:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.OriginalDataAvailable">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="P:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.Underlying">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.#ctor(ProtoBuf.Meta.RuntimeTypeModel)">
|
||||
<summary>
|
||||
ctor
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.GetNodeType">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.GetNodeType(CryptoExchange.Net.Converters.MessageParsing.MessagePath)">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.GetValue``1(CryptoExchange.Net.Converters.MessageParsing.MessagePath)">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.GetValues``1(CryptoExchange.Net.Converters.MessageParsing.MessagePath)">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.GetOriginalString">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.Clear">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.Deserialize(System.Type,System.Nullable{CryptoExchange.Net.Converters.MessageParsing.MessagePath})">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.Deserialize``1(System.Nullable{CryptoExchange.Net.Converters.MessageParsing.MessagePath})">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="T:CryptoExchange.Net.Converters.Protobuf.ProtobufStreamMessageAccessor`1">
|
||||
<summary>
|
||||
System.Text.Json stream message accessor
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:CryptoExchange.Net.Converters.Protobuf.ProtobufStreamMessageAccessor`1.OriginalDataAvailable">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufStreamMessageAccessor`1.#ctor(ProtoBuf.Meta.RuntimeTypeModel)">
|
||||
<summary>
|
||||
ctor
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufStreamMessageAccessor`1.Deserialize(System.Type,System.Nullable{CryptoExchange.Net.Converters.MessageParsing.MessagePath})">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufStreamMessageAccessor`1.Deserialize``1(System.Nullable{CryptoExchange.Net.Converters.MessageParsing.MessagePath})">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufStreamMessageAccessor`1.Read(System.IO.Stream,System.Boolean)">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufStreamMessageAccessor`1.GetOriginalString">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufStreamMessageAccessor`1.Clear">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="T:CryptoExchange.Net.Converters.Protobuf.ProtobufByteMessageAccessor`1">
|
||||
<summary>
|
||||
Protobuf byte message accessor
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufByteMessageAccessor`1.#ctor(ProtoBuf.Meta.RuntimeTypeModel)">
|
||||
<summary>
|
||||
ctor
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufByteMessageAccessor`1.Deserialize(System.Type,System.Nullable{CryptoExchange.Net.Converters.MessageParsing.MessagePath})">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufByteMessageAccessor`1.Deserialize``1(System.Nullable{CryptoExchange.Net.Converters.MessageParsing.MessagePath})">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufByteMessageAccessor`1.Read(System.ReadOnlyMemory{System.Byte})">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufByteMessageAccessor`1.GetOriginalString">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="P:CryptoExchange.Net.Converters.Protobuf.ProtobufByteMessageAccessor`1.OriginalDataAvailable">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufByteMessageAccessor`1.Clear">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="T:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageSerializer">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageSerializer.#ctor(ProtoBuf.Meta.RuntimeTypeModel)">
|
||||
<summary>
|
||||
ctor
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageSerializer.Serialize``1(``0)">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
</members>
|
||||
</doc>
|
||||
@@ -1,52 +0,0 @@
|
||||
#  CryptoExchange.Net.Proto
|
||||
|
||||
[](https://github.com/JKorf/CryptoExchange.Net/actions/workflows/dotnet.yml) [](https://www.nuget.org/packages/CryptoExchange.Net.Protobuf) 
|
||||
|
||||
Protobuf support for CryptoExchange.Net.
|
||||
|
||||
## Release notes
|
||||
* Version 10.0.1 - 16 Dec 2025
|
||||
* Updated CryptoExchange.Net version to 10.0.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||
|
||||
* Version 10.0.0 - 16 Dec 2025
|
||||
* Updated CryptoExchange.Net version to 10.0.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||
|
||||
* Version 9.13.0 - 10 Nov 2025
|
||||
* Updated CryptoExchange.Net version to 9.13.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||
|
||||
* Version 9.12.0 - 03 Nov 2025
|
||||
* Updated CryptoExchange.Net version to 9.12.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||
|
||||
* Version 9.11.1 - 30 Oct 2025
|
||||
* Updated CryptoExchange.Net version to 9.11.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||
|
||||
* Version 9.11.0 - 30 Oct 2025
|
||||
* Updated CryptoExchange.Net version to 9.11.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||
|
||||
* Version 9.10.0 - 15 Oct 2025
|
||||
* Updated CryptoExchange.Net version to 9.10.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||
|
||||
* Version 9.9.0 - 06 Oct 2025
|
||||
* Updated CryptoExchange.Net version to 9.9.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||
|
||||
* Version 9.8.0 - 30 Sep 2025
|
||||
* Updated CryptoExchange.Net version to 9.8.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||
|
||||
* Version 9.7.0 - 01 Sep 2025
|
||||
* Updated CryptoExchange.Net version to 9.7.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||
|
||||
* Version 9.6.0 - 25 Aug 2025
|
||||
* Updated CryptoExchange.Net version to 9.6.0
|
||||
|
||||
* Version 9.5.0 - 19 Aug 2025
|
||||
* Updated CryptoExchange.Net version to 9.5.0
|
||||
|
||||
* Version 9.4.0 - 04 Aug 2025
|
||||
* Updated CryptoExchange.Net to version 9.4.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||
* Updated protobuf-net package version to 3.2.56
|
||||
|
||||
* Version 9.3.0 - 23 Jul 2025
|
||||
* Updated CryptoExchange.Net to version 9.3.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||
|
||||
* Version 9.2.0 - 14 Jul 2025
|
||||
* Initial release
|
||||
@@ -91,28 +91,25 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var evnt = new AsyncResetEvent(false, true);
|
||||
|
||||
var waiters = new List<Task<bool>>();
|
||||
for(var i = 0; i < 10; i++)
|
||||
for (var i = 0; i < 10; i++)
|
||||
{
|
||||
waiters.Add(evnt.WaitAsync());
|
||||
}
|
||||
|
||||
List<bool>? results = null;
|
||||
var resultsWaiter = Task.Run(async () =>
|
||||
{
|
||||
await Task.WhenAll(waiters);
|
||||
results = waiters.Select(w => w.Result).ToList();
|
||||
});
|
||||
var remaining = waiters.ToList();
|
||||
|
||||
for(var i = 1; i <= 10; i++)
|
||||
for (var i = 0; i < 10; i++)
|
||||
{
|
||||
evnt.Set();
|
||||
await Task.Delay(1); // Wait for the continuation.
|
||||
Assert.That(10 - i == waiters.Count(w => w.Status != TaskStatus.RanToCompletion));
|
||||
|
||||
var completed = await Task.WhenAny(remaining);
|
||||
Assert.That(await completed, Is.True);
|
||||
|
||||
remaining.Remove(completed);
|
||||
Assert.That(remaining.Count(w => w.IsCompleted), Is.Zero);
|
||||
}
|
||||
|
||||
await resultsWaiter;
|
||||
|
||||
Assert.That(10 == results?.Count(r => r));
|
||||
Assert.That(remaining, Is.Empty);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -14,157 +14,41 @@ namespace CryptoExchange.Net.UnitTests
|
||||
[Test]
|
||||
public void TestBasicErrorCallResult()
|
||||
{
|
||||
var result = new CallResult(new ServerError("TestError", ErrorInfo.Unknown));
|
||||
var result = CallResult.Fail(new ServerError("TestError", ErrorInfo.Unknown));
|
||||
|
||||
ClassicAssert.AreSame(result.Error!.ErrorCode, "TestError");
|
||||
ClassicAssert.IsFalse(result);
|
||||
ClassicAssert.IsFalse(result.Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBasicSuccessCallResult()
|
||||
{
|
||||
var result = new CallResult(null);
|
||||
var result = CallResult.Ok();
|
||||
|
||||
ClassicAssert.IsNull(result.Error);
|
||||
Assert.That(result);
|
||||
Assert.That(result.Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCallResultError()
|
||||
{
|
||||
var result = new CallResult<object>(new ServerError("TestError", ErrorInfo.Unknown));
|
||||
var result = CallResult.Fail<object>(new ServerError("TestError", ErrorInfo.Unknown));
|
||||
|
||||
ClassicAssert.AreSame(result.Error!.ErrorCode, "TestError");
|
||||
ClassicAssert.IsNull(result.Data);
|
||||
ClassicAssert.IsFalse(result);
|
||||
ClassicAssert.IsFalse(result.Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCallResultSuccess()
|
||||
{
|
||||
var result = new CallResult<object>(new object());
|
||||
var result = CallResult.Ok<object>(new object());
|
||||
|
||||
ClassicAssert.IsNull(result.Error);
|
||||
ClassicAssert.IsNotNull(result.Data);
|
||||
Assert.That(result);
|
||||
Assert.That(result.Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCallResultSuccessAs()
|
||||
{
|
||||
var result = new CallResult<TestObjectResult>(new TestObjectResult());
|
||||
var asResult = result.As<TestObject2>(result.Data.InnerData);
|
||||
|
||||
ClassicAssert.IsNull(asResult.Error);
|
||||
ClassicAssert.IsNotNull(asResult.Data);
|
||||
Assert.That(asResult.Data is not null);
|
||||
Assert.That(asResult);
|
||||
Assert.That(asResult.Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCallResultErrorAs()
|
||||
{
|
||||
var result = new CallResult<TestObjectResult>(new ServerError("TestError", ErrorInfo.Unknown));
|
||||
var asResult = result.As<TestObject2>(default);
|
||||
|
||||
ClassicAssert.IsNotNull(asResult.Error);
|
||||
ClassicAssert.AreSame(asResult.Error!.ErrorCode, "TestError");
|
||||
ClassicAssert.IsNull(asResult.Data);
|
||||
ClassicAssert.IsFalse(asResult);
|
||||
ClassicAssert.IsFalse(asResult.Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCallResultErrorAsError()
|
||||
{
|
||||
var result = new CallResult<TestObjectResult>(new ServerError("TestError", ErrorInfo.Unknown));
|
||||
var asResult = result.AsError<TestObject2>(new ServerError("TestError2", ErrorInfo.Unknown));
|
||||
|
||||
ClassicAssert.IsNotNull(asResult.Error);
|
||||
ClassicAssert.AreSame(asResult.Error!.ErrorCode, "TestError2");
|
||||
ClassicAssert.IsNull(asResult.Data);
|
||||
ClassicAssert.IsFalse(asResult);
|
||||
ClassicAssert.IsFalse(asResult.Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestWebCallResultErrorAsError()
|
||||
{
|
||||
var result = new WebCallResult<TestObjectResult>(new ServerError("TestError", ErrorInfo.Unknown));
|
||||
var asResult = result.AsError<TestObject2>(new ServerError("TestError2", ErrorInfo.Unknown));
|
||||
|
||||
ClassicAssert.IsNotNull(asResult.Error);
|
||||
ClassicAssert.AreSame(asResult.Error!.ErrorCode, "TestError2");
|
||||
ClassicAssert.IsNull(asResult.Data);
|
||||
ClassicAssert.IsFalse(asResult);
|
||||
ClassicAssert.IsFalse(asResult.Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestWebCallResultSuccessAsError()
|
||||
{
|
||||
var result = new WebCallResult<TestObjectResult>(
|
||||
System.Net.HttpStatusCode.OK,
|
||||
HttpVersion.Version11,
|
||||
new HttpResponseMessage().Headers,
|
||||
TimeSpan.FromSeconds(1),
|
||||
null,
|
||||
"{}",
|
||||
1,
|
||||
"https://test.com/api",
|
||||
null,
|
||||
HttpMethod.Get,
|
||||
new HttpRequestMessage().Headers,
|
||||
ResultDataSource.Server,
|
||||
new TestObjectResult(),
|
||||
null);
|
||||
var asResult = result.AsError<TestObject2>(new ServerError("TestError2", ErrorInfo.Unknown));
|
||||
|
||||
ClassicAssert.IsNotNull(asResult.Error);
|
||||
Assert.That(asResult.Error!.ErrorCode == "TestError2");
|
||||
Assert.That(asResult.ResponseStatusCode == System.Net.HttpStatusCode.OK);
|
||||
Assert.That(asResult.ResponseTime == TimeSpan.FromSeconds(1));
|
||||
Assert.That(asResult.RequestUrl == "https://test.com/api");
|
||||
Assert.That(asResult.RequestMethod == HttpMethod.Get);
|
||||
ClassicAssert.IsNull(asResult.Data);
|
||||
ClassicAssert.IsFalse(asResult);
|
||||
ClassicAssert.IsFalse(asResult.Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestWebCallResultSuccessAsSuccess()
|
||||
{
|
||||
var result = new WebCallResult<TestObjectResult>(
|
||||
System.Net.HttpStatusCode.OK,
|
||||
HttpVersion.Version11,
|
||||
new HttpResponseMessage().Headers,
|
||||
TimeSpan.FromSeconds(1),
|
||||
null,
|
||||
"{}",
|
||||
1,
|
||||
"https://test.com/api",
|
||||
null,
|
||||
HttpMethod.Get,
|
||||
new HttpRequestMessage().Headers,
|
||||
ResultDataSource.Server,
|
||||
new TestObjectResult(),
|
||||
null);
|
||||
var asResult = result.As<TestObject2>(result.Data.InnerData);
|
||||
|
||||
ClassicAssert.IsNull(asResult.Error);
|
||||
Assert.That(asResult.ResponseStatusCode == System.Net.HttpStatusCode.OK);
|
||||
Assert.That(asResult.ResponseTime == TimeSpan.FromSeconds(1));
|
||||
Assert.That(asResult.RequestUrl == "https://test.com/api");
|
||||
Assert.That(asResult.RequestMethod == HttpMethod.Get);
|
||||
ClassicAssert.IsNotNull(asResult.Data);
|
||||
Assert.That(asResult);
|
||||
Assert.That(asResult.Success);
|
||||
}
|
||||
}
|
||||
|
||||
public class TestObjectResult
|
||||
|
||||
@@ -134,7 +134,7 @@ namespace CryptoExchange.Net.UnitTests.ClientTests
|
||||
client.ApiClient1.SetParameterPosition(httpMethod, pos);
|
||||
client.ApiClient1.SetNextResponse("{}", System.Net.HttpStatusCode.OK);
|
||||
|
||||
var result = await client.ApiClient1.GetResponseAsync<TestObject>(httpMethod, new ParameterCollection
|
||||
var result = await client.ApiClient1.GetResponseAsync<TestObject>(httpMethod, new Parameters(new ParameterSerializationSettings())
|
||||
{
|
||||
{ "TestParam1", "Value1" },
|
||||
{ "TestParam2", 2 },
|
||||
|
||||
@@ -110,7 +110,7 @@ namespace CryptoExchange.Net.UnitTests.ClientTests
|
||||
var result = await client.ApiClient1.SubscribeToUpdatesAsync<TestObject>(x => {}, false, default);
|
||||
|
||||
// act
|
||||
await client.UnsubscribeAsync(result.Data);
|
||||
await client.UnsubscribeAsync(result.Data!);
|
||||
|
||||
// assert
|
||||
Assert.That(socket.Connected == false);
|
||||
@@ -138,6 +138,68 @@ namespace CryptoExchange.Net.UnitTests.ClientTests
|
||||
Assert.That(socket2.Connected == false);
|
||||
}
|
||||
|
||||
[TestCase()]
|
||||
public async Task BatchedSubscription_Should_NotExceedIndividualCombineTarget()
|
||||
{
|
||||
// arrange
|
||||
var client = new TestSocketClient(options =>
|
||||
{
|
||||
options.SocketSubscriptionsCombineTarget = 10;
|
||||
options.SocketIndividualSubscriptionCombineTarget = 10;
|
||||
});
|
||||
TestHelpers.ConfigureSocketClient(client, "wss://localhost");
|
||||
|
||||
// act
|
||||
await client.ApiClient1.SubscribeToUpdatesAsync<TestObject>(x => { }, false, default, individualSubscriptionCount: 6);
|
||||
TestHelpers.ConfigureSocketClient(client, "wss://localhost");
|
||||
await client.ApiClient1.SubscribeToUpdatesAsync<TestObject>(x => { }, false, default, individualSubscriptionCount: 6);
|
||||
|
||||
// assert
|
||||
Assert.That(client.ApiClient1._socketConnections.Count == 2);
|
||||
Assert.That(client.ApiClient1._socketConnections.Values.All(connection => connection.Subscriptions.Sum(subscription => subscription.IndividualSubscriptionCount) <= 10));
|
||||
}
|
||||
|
||||
[TestCase()]
|
||||
public async Task BatchedSubscription_FullIndividualConnection_Should_NotPreventEligibleConnectionReuse()
|
||||
{
|
||||
// arrange
|
||||
var client = new TestSocketClient(options =>
|
||||
{
|
||||
options.SocketSubscriptionsCombineTarget = 5;
|
||||
options.SocketIndividualSubscriptionCombineTarget = 10;
|
||||
});
|
||||
TestHelpers.ConfigureSocketClient(client, "wss://localhost");
|
||||
await client.ApiClient1.SubscribeToUpdatesAsync<TestObject>(x => { }, false, default);
|
||||
await client.ApiClient1.SubscribeToUpdatesAsync<TestObject>(x => { }, false, default);
|
||||
|
||||
TestHelpers.ConfigureSocketClient(client, "wss://localhost");
|
||||
await client.ApiClient1.SubscribeToUpdatesAsync<TestObject>(x => { }, false, default, individualSubscriptionCount: 10);
|
||||
|
||||
TestHelpers.ConfigureSocketClient(client, "wss://localhost");
|
||||
|
||||
// act
|
||||
await client.ApiClient1.SubscribeToUpdatesAsync<TestObject>(x => { }, false, default);
|
||||
|
||||
// assert
|
||||
Assert.That(
|
||||
client.ApiClient1._socketConnections.Count,
|
||||
Is.EqualTo(2),
|
||||
"The eligible connection should be reused instead of opening a new connection after selecting a full individual-subscription connection");
|
||||
|
||||
var fullConnection = client.ApiClient1._socketConnections.Values
|
||||
.Single(connection => connection.Subscriptions.Sum(subscription => subscription.IndividualSubscriptionCount) == 10);
|
||||
Assert.That(
|
||||
fullConnection.UserSubscriptionCount,
|
||||
Is.EqualTo(1),
|
||||
"The full connection should not receive the normal subscription");
|
||||
|
||||
var eligibleConnection = client.ApiClient1._socketConnections.Values.Single(connection => connection != fullConnection);
|
||||
Assert.That(
|
||||
eligibleConnection.UserSubscriptionCount,
|
||||
Is.EqualTo(3),
|
||||
"The existing eligible connection should receive the normal subscription");
|
||||
}
|
||||
|
||||
[TestCase()]
|
||||
public async Task ErrorResponse_ShouldNot_ConfirmSubscription()
|
||||
{
|
||||
|
||||
@@ -25,6 +25,17 @@ namespace CryptoExchange.Net.UnitTests.ConverterTests
|
||||
Assert.That(output!.Value == expected);
|
||||
}
|
||||
|
||||
[TestCase(1, true)]
|
||||
[TestCase(2, true)]
|
||||
[TestCase(0, false)]
|
||||
[TestCase(-1, false)]
|
||||
public void TestBoolConverterInts(int value, bool? expected)
|
||||
{
|
||||
var val = $"{value}";
|
||||
var output = JsonSerializer.Deserialize<STJBoolObject>($"{{ \"Value\": {val} }}", SerializerOptions.WithConverters(new TestSerializerContext()));
|
||||
Assert.That(output!.Value == expected);
|
||||
}
|
||||
|
||||
[TestCase("1", true)]
|
||||
[TestCase("true", true)]
|
||||
[TestCase("yes", true)]
|
||||
|
||||
@@ -15,6 +15,7 @@ namespace CryptoExchange.Net.UnitTests.ConverterTests
|
||||
[TestCase("1620777600000")]
|
||||
[TestCase("2021-05-12T00:00:00.000Z")]
|
||||
[TestCase("2021-05-12T00:00:00.000000000Z")]
|
||||
[TestCase("2021-05-12 00:00:00.000000+00:00:00")]
|
||||
[TestCase("0.000000", true)]
|
||||
[TestCase("0", true)]
|
||||
[TestCase("", true)]
|
||||
|
||||
@@ -37,8 +37,8 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var symbols = CreateTestSymbols();
|
||||
|
||||
// act
|
||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
|
||||
var hasCached = ExchangeSymbolCache.HasCached(topicId);
|
||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
|
||||
var hasCached = ExchangeSymbolCache.HasCached(topicId, "Env", null);
|
||||
|
||||
// assert
|
||||
Assert.That(hasCached, Is.True);
|
||||
@@ -52,14 +52,14 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var symbols = CreateTestSymbols();
|
||||
|
||||
// act
|
||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
|
||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
|
||||
|
||||
// assert
|
||||
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "BTCUSDT"), Is.True);
|
||||
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "ETHUSDT"), Is.True);
|
||||
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "BTCEUR"), Is.True);
|
||||
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "ETHBTC"), Is.True);
|
||||
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "XRPUSDT"), Is.True);
|
||||
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, "BTCUSDT"), Is.True);
|
||||
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, "ETHUSDT"), Is.True);
|
||||
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, "BTCEUR"), Is.True);
|
||||
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, "ETHBTC"), Is.True);
|
||||
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, "XRPUSDT"), Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -78,13 +78,13 @@ namespace CryptoExchange.Net.UnitTests
|
||||
};
|
||||
|
||||
// act
|
||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, initialSymbols);
|
||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, updatedSymbols);
|
||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, initialSymbols);
|
||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, updatedSymbols);
|
||||
|
||||
// assert - should still have only the initial symbol since less than 60 minutes passed
|
||||
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "BTCUSDT"), Is.True);
|
||||
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, "BTCUSDT"), Is.True);
|
||||
// The second update should not have been applied
|
||||
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "ETHUSDT"), Is.False);
|
||||
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, "ETHUSDT"), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -95,8 +95,8 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var symbols = Array.Empty<SharedSpotSymbol>();
|
||||
|
||||
// act
|
||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
|
||||
var hasCached = ExchangeSymbolCache.HasCached(topicId);
|
||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
|
||||
var hasCached = ExchangeSymbolCache.HasCached(topicId, "Env", null);
|
||||
|
||||
// assert
|
||||
Assert.That(hasCached, Is.False);
|
||||
@@ -109,7 +109,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var nonExistentTopic = "NonExistent_" + Guid.NewGuid();
|
||||
|
||||
// act
|
||||
var result = ExchangeSymbolCache.HasCached(nonExistentTopic);
|
||||
var result = ExchangeSymbolCache.HasCached(nonExistentTopic, "Env", null);
|
||||
|
||||
// assert
|
||||
Assert.That(result, Is.False);
|
||||
@@ -121,10 +121,10 @@ namespace CryptoExchange.Net.UnitTests
|
||||
// arrange
|
||||
var topicId = "ExchangeWithData";
|
||||
var symbols = CreateTestSymbols();
|
||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
|
||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
|
||||
|
||||
// act
|
||||
var result = ExchangeSymbolCache.HasCached(topicId);
|
||||
var result = ExchangeSymbolCache.HasCached(topicId, "Env", null);
|
||||
|
||||
// assert
|
||||
Assert.That(result, Is.True);
|
||||
@@ -135,10 +135,10 @@ namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
// arrange
|
||||
var topicId = "ExchangeNoData";
|
||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, Array.Empty<SharedSpotSymbol>());
|
||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, Array.Empty<SharedSpotSymbol>());
|
||||
|
||||
// act
|
||||
var result = ExchangeSymbolCache.HasCached(topicId);
|
||||
var result = ExchangeSymbolCache.HasCached(topicId, "Env", null);
|
||||
|
||||
// assert
|
||||
Assert.That(result, Is.False);
|
||||
@@ -150,10 +150,10 @@ namespace CryptoExchange.Net.UnitTests
|
||||
// arrange
|
||||
var topicId = "ExchangeSupports";
|
||||
var symbols = CreateTestSymbols();
|
||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
|
||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
|
||||
|
||||
// act
|
||||
var result = ExchangeSymbolCache.SupportsSymbol(topicId, "BTCUSDT");
|
||||
var result = ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, "BTCUSDT");
|
||||
|
||||
// assert
|
||||
Assert.That(result, Is.True);
|
||||
@@ -165,10 +165,10 @@ namespace CryptoExchange.Net.UnitTests
|
||||
// arrange
|
||||
var topicId = "ExchangeNoSupport";
|
||||
var symbols = CreateTestSymbols();
|
||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
|
||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
|
||||
|
||||
// act
|
||||
var result = ExchangeSymbolCache.SupportsSymbol(topicId, "LINKUSDT");
|
||||
var result = ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, "LINKUSDT");
|
||||
|
||||
// assert
|
||||
Assert.That(result, Is.False);
|
||||
@@ -181,7 +181,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var nonExistentTopic = "NonExistent_" + Guid.NewGuid();
|
||||
|
||||
// act
|
||||
var result = ExchangeSymbolCache.SupportsSymbol(nonExistentTopic, "BTCUSDT");
|
||||
var result = ExchangeSymbolCache.SupportsSymbol(nonExistentTopic, "Env", null, "BTCUSDT");
|
||||
|
||||
// assert
|
||||
Assert.That(result, Is.False);
|
||||
@@ -193,11 +193,11 @@ namespace CryptoExchange.Net.UnitTests
|
||||
// arrange
|
||||
var topicId = "ExchangeSharedSymbol";
|
||||
var symbols = CreateTestSymbols();
|
||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
|
||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
|
||||
var sharedSymbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
|
||||
|
||||
// act
|
||||
var result = ExchangeSymbolCache.SupportsSymbol(topicId, sharedSymbol);
|
||||
var result = ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, sharedSymbol);
|
||||
|
||||
// assert
|
||||
Assert.That(result, Is.True);
|
||||
@@ -209,11 +209,11 @@ namespace CryptoExchange.Net.UnitTests
|
||||
// arrange
|
||||
var topicId = "ExchangeNoSharedSymbol";
|
||||
var symbols = CreateTestSymbols();
|
||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
|
||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
|
||||
var sharedSymbol = new SharedSymbol(TradingMode.Spot, "LINK", "USDT");
|
||||
|
||||
// act
|
||||
var result = ExchangeSymbolCache.SupportsSymbol(topicId, sharedSymbol);
|
||||
var result = ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, sharedSymbol);
|
||||
|
||||
// assert
|
||||
Assert.That(result, Is.False);
|
||||
@@ -225,11 +225,11 @@ namespace CryptoExchange.Net.UnitTests
|
||||
// arrange
|
||||
var topicId = "ExchangeDifferentMode";
|
||||
var symbols = CreateTestSymbols();
|
||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
|
||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
|
||||
var sharedSymbol = new SharedSymbol(TradingMode.PerpetualLinear, "BTC", "USDT");
|
||||
|
||||
// act
|
||||
var result = ExchangeSymbolCache.SupportsSymbol(topicId, sharedSymbol);
|
||||
var result = ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, sharedSymbol);
|
||||
|
||||
// assert
|
||||
Assert.That(result, Is.False);
|
||||
@@ -243,7 +243,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var sharedSymbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
|
||||
|
||||
// act
|
||||
var result = ExchangeSymbolCache.SupportsSymbol(nonExistentTopic, sharedSymbol);
|
||||
var result = ExchangeSymbolCache.SupportsSymbol(nonExistentTopic, "Env", null, sharedSymbol);
|
||||
|
||||
// assert
|
||||
Assert.That(result, Is.False);
|
||||
@@ -255,10 +255,10 @@ namespace CryptoExchange.Net.UnitTests
|
||||
// arrange
|
||||
var topicId = "ExchangeBaseAsset";
|
||||
var symbols = CreateTestSymbols();
|
||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
|
||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
|
||||
|
||||
// act
|
||||
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "BTC");
|
||||
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "Env", null, "BTC");
|
||||
|
||||
// assert
|
||||
Assert.That(result, Is.Not.Null);
|
||||
@@ -273,10 +273,10 @@ namespace CryptoExchange.Net.UnitTests
|
||||
// arrange
|
||||
var topicId = "ExchangeCaseInsensitive";
|
||||
var symbols = CreateTestSymbols();
|
||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
|
||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
|
||||
|
||||
// act
|
||||
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "btc");
|
||||
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "Env", null, "btc");
|
||||
|
||||
// assert
|
||||
Assert.That(result, Is.Not.Null);
|
||||
@@ -289,10 +289,10 @@ namespace CryptoExchange.Net.UnitTests
|
||||
// arrange
|
||||
var topicId = "ExchangeNoBaseAsset";
|
||||
var symbols = CreateTestSymbols();
|
||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
|
||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
|
||||
|
||||
// act
|
||||
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "LINK");
|
||||
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "Env", null, "LINK");
|
||||
|
||||
// assert
|
||||
Assert.That(result, Is.Not.Null);
|
||||
@@ -306,7 +306,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var nonExistentTopic = "NonExistent_" + Guid.NewGuid();
|
||||
|
||||
// act
|
||||
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(nonExistentTopic, "BTC");
|
||||
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(nonExistentTopic, "Env", null, "BTC");
|
||||
|
||||
// assert
|
||||
Assert.That(result, Is.Not.Null);
|
||||
@@ -319,10 +319,10 @@ namespace CryptoExchange.Net.UnitTests
|
||||
// arrange
|
||||
var topicId = "ExchangeParse";
|
||||
var symbols = CreateTestSymbols();
|
||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
|
||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
|
||||
|
||||
// act
|
||||
var result = ExchangeSymbolCache.ParseSymbol(topicId, "BTCUSDT");
|
||||
var result = ExchangeSymbolCache.ParseSymbol(topicId, "Env", null, "BTCUSDT");
|
||||
|
||||
// assert
|
||||
Assert.That(result, Is.Not.Null);
|
||||
@@ -338,10 +338,10 @@ namespace CryptoExchange.Net.UnitTests
|
||||
// arrange
|
||||
var topicId = "ExchangeNoParse";
|
||||
var symbols = CreateTestSymbols();
|
||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
|
||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
|
||||
|
||||
// act
|
||||
var result = ExchangeSymbolCache.ParseSymbol(topicId, "LINKUSDT");
|
||||
var result = ExchangeSymbolCache.ParseSymbol(topicId, "Env", null, "LINKUSDT");
|
||||
|
||||
// assert
|
||||
Assert.That(result, Is.Null);
|
||||
@@ -353,10 +353,10 @@ namespace CryptoExchange.Net.UnitTests
|
||||
// arrange
|
||||
var topicId = "ExchangeNullSymbol";
|
||||
var symbols = CreateTestSymbols();
|
||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
|
||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
|
||||
|
||||
// act
|
||||
var result = ExchangeSymbolCache.ParseSymbol(topicId, null);
|
||||
var result = ExchangeSymbolCache.ParseSymbol(topicId, "Env", null, null);
|
||||
|
||||
// assert
|
||||
Assert.That(result, Is.Null);
|
||||
@@ -369,7 +369,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var nonExistentTopic = "NonExistent_" + Guid.NewGuid();
|
||||
|
||||
// act
|
||||
var result = ExchangeSymbolCache.ParseSymbol(nonExistentTopic, "BTCUSDT");
|
||||
var result = ExchangeSymbolCache.ParseSymbol(nonExistentTopic, "Env", null, "BTCUSDT");
|
||||
|
||||
// assert
|
||||
Assert.That(result, Is.Null);
|
||||
@@ -391,14 +391,14 @@ namespace CryptoExchange.Net.UnitTests
|
||||
};
|
||||
|
||||
// act
|
||||
ExchangeSymbolCache.UpdateSymbolInfo(topic1, symbols1);
|
||||
ExchangeSymbolCache.UpdateSymbolInfo(topic2, symbols2);
|
||||
ExchangeSymbolCache.UpdateSymbolInfo(topic1, "Env", null, symbols1);
|
||||
ExchangeSymbolCache.UpdateSymbolInfo(topic2, "Env", null, symbols2);
|
||||
|
||||
// assert
|
||||
Assert.That(ExchangeSymbolCache.SupportsSymbol(topic1, "BTCUSDT"), Is.True);
|
||||
Assert.That(ExchangeSymbolCache.SupportsSymbol(topic1, "ETHUSDT"), Is.False);
|
||||
Assert.That(ExchangeSymbolCache.SupportsSymbol(topic2, "ETHUSDT"), Is.True);
|
||||
Assert.That(ExchangeSymbolCache.SupportsSymbol(topic2, "BTCUSDT"), Is.False);
|
||||
Assert.That(ExchangeSymbolCache.SupportsSymbol(topic1, "Env", null, "BTCUSDT"), Is.True);
|
||||
Assert.That(ExchangeSymbolCache.SupportsSymbol(topic1, "Env", null, "ETHUSDT"), Is.False);
|
||||
Assert.That(ExchangeSymbolCache.SupportsSymbol(topic2, "Env", null, "ETHUSDT"), Is.True);
|
||||
Assert.That(ExchangeSymbolCache.SupportsSymbol(topic2, "Env", null, "BTCUSDT"), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -411,14 +411,14 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var allSymbols = spotSymbols.Concat(futuresSymbols).ToArray();
|
||||
|
||||
// act
|
||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, allSymbols);
|
||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, allSymbols);
|
||||
|
||||
// assert
|
||||
var spotSymbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
|
||||
var futuresSymbol = new SharedSymbol(TradingMode.PerpetualLinear, "BTC", "USDT");
|
||||
|
||||
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, spotSymbol), Is.True);
|
||||
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, futuresSymbol), Is.True);
|
||||
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, spotSymbol), Is.True);
|
||||
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, futuresSymbol), Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -429,10 +429,10 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var spotSymbols = CreateTestSymbols();
|
||||
var futuresSymbols = CreateFuturesSymbols();
|
||||
var allSymbols = spotSymbols.Concat(futuresSymbols).ToArray();
|
||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, allSymbols);
|
||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, allSymbols);
|
||||
|
||||
// act
|
||||
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "BTC");
|
||||
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "Env", null, "BTC");
|
||||
|
||||
// assert
|
||||
Assert.That(result.Length, Is.GreaterThanOrEqualTo(2));
|
||||
@@ -451,15 +451,119 @@ namespace CryptoExchange.Net.UnitTests
|
||||
new SharedSpotSymbol("ETH", "BTC", "ETHBTC", true, TradingMode.Spot),
|
||||
new SharedSpotSymbol("ETH", "EUR", "ETHEUR", true, TradingMode.Spot)
|
||||
};
|
||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
|
||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
|
||||
|
||||
// act
|
||||
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "ETH");
|
||||
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "Env", null, "ETH");
|
||||
|
||||
// assert
|
||||
Assert.That(result.Length, Is.EqualTo(3));
|
||||
Assert.That(result.All(x => x.BaseAsset == "ETH"), Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetSymbolsForBaseAsset_WithDifferentEnvironments_Should_ReturnNone()
|
||||
{
|
||||
// arrange
|
||||
var topicId = "Topic1";
|
||||
var spotSymbols = CreateTestSymbols();
|
||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Live", null, spotSymbols);
|
||||
|
||||
// act
|
||||
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "Test", null, "BTC");
|
||||
|
||||
// assert
|
||||
Assert.That(result.Length, Is.EqualTo(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetSymbolsForBaseAsset_WithDifferentKey_Should_ReturnNone()
|
||||
{
|
||||
// arrange
|
||||
var topicId = "Topic2";
|
||||
var spotSymbols = CreateTestSymbols();
|
||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Live", "1", spotSymbols);
|
||||
|
||||
// act
|
||||
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "Live", "2", "BTC");
|
||||
|
||||
// assert
|
||||
Assert.That(result.Length, Is.EqualTo(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetSymbolsForBaseAsset_WithSetKey_Should_ReturnNone()
|
||||
{
|
||||
// arrange
|
||||
var topicId = "Topic3";
|
||||
var spotSymbols = CreateTestSymbols();
|
||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Live", null, spotSymbols);
|
||||
|
||||
// act
|
||||
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "Live", "2", "BTC");
|
||||
|
||||
// assert
|
||||
Assert.That(result.Length, Is.EqualTo(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetSymbolsForBaseAsset_WithNotSetKey_Should_ReturnNone()
|
||||
{
|
||||
// arrange
|
||||
var topicId = "Topic4";
|
||||
var spotSymbols = CreateTestSymbols();
|
||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Live", "2", spotSymbols);
|
||||
|
||||
// act
|
||||
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "Live", null, "BTC");
|
||||
|
||||
// assert
|
||||
Assert.That(result.Length, Is.EqualTo(2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ParseSymbol_WithDifferentKey_Should_ReturnNull()
|
||||
{
|
||||
// arrange
|
||||
var topicId = "Topic5";
|
||||
var spotSymbols = CreateTestSymbols();
|
||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Live", "1", spotSymbols);
|
||||
|
||||
// act
|
||||
var result = ExchangeSymbolCache.ParseSymbol(topicId, "Live", "2", "BTCUSDT");
|
||||
|
||||
// assert
|
||||
Assert.That(result, Is.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ParseSymbol_WithSetKey_Should_ReturnNull()
|
||||
{
|
||||
// arrange
|
||||
var topicId = "Topic6";
|
||||
var spotSymbols = CreateTestSymbols();
|
||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Live", null, spotSymbols);
|
||||
|
||||
// act
|
||||
var result = ExchangeSymbolCache.ParseSymbol(topicId, "Live", "2", "BTCUSDT");
|
||||
|
||||
// assert
|
||||
Assert.That(result, Is.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ParseSymbol_WithNotSetKey_Should_ReturnNull()
|
||||
{
|
||||
// arrange
|
||||
var topicId = "Topic7";
|
||||
var spotSymbols = CreateTestSymbols();
|
||||
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Live", "1", spotSymbols);
|
||||
|
||||
// act
|
||||
var result = ExchangeSymbolCache.ParseSymbol(topicId, "Live", null, "BTCUSDT");
|
||||
|
||||
// assert
|
||||
Assert.That(result, Is.Not.Null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,15 +11,15 @@ namespace CryptoExchange.Net.UnitTests.Implementations
|
||||
{
|
||||
public TestQuery(TestSocketMessage request, bool authenticated) : base(request, authenticated, 1)
|
||||
{
|
||||
MessageRouter = MessageRouter.CreateWithoutTopicFilter<TestSocketMessage>(request.Id.ToString(), HandleMessage);
|
||||
MessageRouter = MessageRouter.CreateForQuery<TestSocketMessage>(request.Id.ToString(), HandleMessage);
|
||||
}
|
||||
|
||||
private CallResult? HandleMessage(SocketConnection connection, DateTime time, string? arg3, TestSocketMessage message)
|
||||
private CallResult<TestSocketMessage>? HandleMessage(SocketConnection connection, DateTime time, string? arg3, TestSocketMessage message)
|
||||
{
|
||||
if (message.Data != "OK")
|
||||
return new CallResult(new ServerError(ErrorInfo.Unknown with { Message = message.Data }));
|
||||
return CallResult.Fail<TestSocketMessage>(new ServerError(ErrorInfo.Unknown with { Message = message.Data }));
|
||||
|
||||
return CallResult.SuccessResult;
|
||||
return CallResult.Ok(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,8 +20,8 @@ namespace CryptoExchange.Net.UnitTests.Implementations
|
||||
{
|
||||
protected override IRestMessageHandler MessageHandler { get; } = new TestRestMessageHandler();
|
||||
|
||||
public TestRestApiClient(ILogger logger, HttpClient? httpClient, TestRestOptions options)
|
||||
: base(logger, httpClient, options.Environment.RestClientAddress, options, options.ExchangeOptions)
|
||||
public TestRestApiClient(ILoggerFactory? loggerFactory, HttpClient? httpClient, TestRestOptions options)
|
||||
: base(loggerFactory, "Test", httpClient, options.Environment.RestClientAddress, options, options.ExchangeOptions)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -48,14 +48,14 @@ namespace CryptoExchange.Net.UnitTests.Implementations
|
||||
RequestFactory = factory;
|
||||
}
|
||||
|
||||
internal async Task<WebCallResult<T>> GetResponseAsync<T>(HttpMethod? httpMethod = null, ParameterCollection? collection = null, RateLimitGate? rateLimitGate = null)
|
||||
internal async Task<HttpResult<T>> GetResponseAsync<T>(HttpMethod? httpMethod = null, Parameters? collection = null, RateLimitGate? rateLimitGate = null)
|
||||
{
|
||||
var definition = new RequestDefinition("/path", httpMethod ?? HttpMethod.Get)
|
||||
var definition = new RequestDefinition(BaseAddress, "/path", httpMethod ?? HttpMethod.Get)
|
||||
{
|
||||
Weight = rateLimitGate == null ? 0 : 1,
|
||||
RateLimitGate = rateLimitGate
|
||||
};
|
||||
return await SendAsync<T>(BaseAddress, definition, collection ?? new ParameterCollection(), default);
|
||||
return await SendAsync<T>(definition, collection ?? new Parameters(new ParameterSerializationSettings()), default);
|
||||
}
|
||||
|
||||
internal void SetParameterPosition(HttpMethod httpMethod, HttpMethodParameterPosition pos)
|
||||
|
||||
@@ -20,8 +20,8 @@ namespace CryptoExchange.Net.UnitTests.Implementations
|
||||
{
|
||||
Initialize(options.Value);
|
||||
|
||||
ApiClient1 = AddApiClient(new TestRestApiClient(_logger, httpClient, options.Value));
|
||||
ApiClient2 = AddApiClient(new TestRestApiClient(_logger, httpClient, options.Value));
|
||||
ApiClient1 = AddApiClient(new TestRestApiClient(loggerFactory, httpClient, options.Value));
|
||||
ApiClient2 = AddApiClient(new TestRestApiClient(loggerFactory, httpClient, options.Value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using CryptoExchange.Net.UnitTests.ConverterTests;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.UnitTests.ConverterTests;
|
||||
using CryptoExchange.Net.UnitTests.Implementations;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
@@ -11,6 +12,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
[JsonSerializable(typeof(IDictionary<string, string>))]
|
||||
[JsonSerializable(typeof(Dictionary<string, object>))]
|
||||
[JsonSerializable(typeof(IDictionary<string, object>))]
|
||||
[JsonSerializable(typeof(Parameters))]
|
||||
[JsonSerializable(typeof(TestObject))]
|
||||
|
||||
[JsonSerializable(typeof(TestSocketMessage))]
|
||||
|
||||
@@ -17,13 +17,13 @@ namespace CryptoExchange.Net.UnitTests.Implementations
|
||||
{
|
||||
internal class TestSocketApiClient : SocketApiClient<TestEnvironment, TestAuthenticationProvider, TestCredentials>
|
||||
{
|
||||
public TestSocketApiClient(ILogger logger, TestSocketOptions options)
|
||||
: base(logger, options.Environment.SocketClientAddress, options, options.ExchangeOptions)
|
||||
public TestSocketApiClient(ILoggerFactory? loggerFactory, TestSocketOptions options)
|
||||
: base(loggerFactory, "Test", options.Environment.SocketClientAddress, options, options.ExchangeOptions)
|
||||
{
|
||||
}
|
||||
|
||||
public TestSocketApiClient(ILogger logger, HttpClient httpClient, string baseAddress, TestSocketOptions options, SocketApiOptions apiOptions)
|
||||
: base(logger, baseAddress, options, apiOptions)
|
||||
public TestSocketApiClient(ILoggerFactory? loggerFactory, HttpClient httpClient, string baseAddress, TestSocketOptions options, SocketApiOptions apiOptions)
|
||||
: base(loggerFactory, "Test", baseAddress, options, apiOptions)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -36,9 +36,13 @@ namespace CryptoExchange.Net.UnitTests.Implementations
|
||||
protected override TestAuthenticationProvider CreateAuthenticationProvider(TestCredentials credentials) =>
|
||||
new TestAuthenticationProvider(credentials);
|
||||
|
||||
public async Task<CallResult<UpdateSubscription>> SubscribeToUpdatesAsync<T>(Action<DataEvent<T>> handler, bool subQuery, CancellationToken ct)
|
||||
public async Task<WebSocketResult<UpdateSubscription>> SubscribeToUpdatesAsync<T>(Action<DataEvent<T>> handler, bool subQuery, CancellationToken ct, int individualSubscriptionCount = 1)
|
||||
{
|
||||
return await base.SubscribeAsync(new TestSubscription<T>(_logger, handler, subQuery, false), ct);
|
||||
var subscription = new TestSubscription<T>(_logger, handler, subQuery, false)
|
||||
{
|
||||
IndividualSubscriptionCount = individualSubscriptionCount
|
||||
};
|
||||
return await base.SubscribeAsync(subscription, ct);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,8 +19,8 @@ namespace CryptoExchange.Net.UnitTests.Implementations
|
||||
{
|
||||
Initialize(options.Value);
|
||||
|
||||
ApiClient1 = AddApiClient(new TestSocketApiClient(_logger, options.Value));
|
||||
ApiClient2 = AddApiClient(new TestSocketApiClient(_logger, options.Value));
|
||||
ApiClient1 = AddApiClient(new TestSocketApiClient(loggerFactory, options.Value));
|
||||
ApiClient2 = AddApiClient(new TestSocketApiClient(loggerFactory, options.Value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace CryptoExchange.Net.UnitTests.Implementations
|
||||
_handler = handler;
|
||||
_subQuery = subQuery;
|
||||
|
||||
MessageRouter = MessageRouter.CreateWithoutTopicFilter<T>("test", HandleUpdate);
|
||||
MessageRouter = MessageRouter.CreateForEvent<T>("test", HandleUpdate);
|
||||
}
|
||||
|
||||
protected override Query? GetSubQuery(SocketConnection connection)
|
||||
@@ -44,7 +44,7 @@ namespace CryptoExchange.Net.UnitTests.Implementations
|
||||
private CallResult? HandleUpdate(SocketConnection connection, DateTime time, string? originalData, T data)
|
||||
{
|
||||
_handler(new DataEvent<T>("Test", data, time, originalData));
|
||||
return CallResult.SuccessResult;
|
||||
return CallResult.Ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Errors;
|
||||
using CryptoExchange.Net.Objects.Sockets;
|
||||
using CryptoExchange.Net.Sockets.Default;
|
||||
using NUnit.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
[TestFixture]
|
||||
public class ManualUpdateSubscriptionTests
|
||||
{
|
||||
[Test]
|
||||
public void Constructor_Should_CreateSubscribedVirtualSubscription()
|
||||
{
|
||||
var controller = new ManualUpdateSubscription(socketId: 12);
|
||||
|
||||
Assert.That(controller.Subscription.SocketId, Is.EqualTo(12));
|
||||
Assert.That(controller.Subscription.Id, Is.GreaterThan(0));
|
||||
Assert.That(controller.Subscription.SocketStatus, Is.EqualTo(SocketStatus.Connected));
|
||||
Assert.That(controller.Subscription.SubscriptionStatus, Is.EqualTo(SubscriptionStatus.Subscribed));
|
||||
Assert.That(controller.Subscription.LastReceiveTime, Is.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void StateChanges_Should_BeVisibleOnSubscription()
|
||||
{
|
||||
var controller = new ManualUpdateSubscription();
|
||||
var timestamp = new DateTime(2026, 8, 5, 12, 0, 0, DateTimeKind.Utc);
|
||||
var statuses = new List<SubscriptionStatus>();
|
||||
controller.Subscription.SubscriptionStatusChanged += statuses.Add;
|
||||
|
||||
controller.SetLastReceiveTime(timestamp);
|
||||
controller.SetSocketStatus(SocketStatus.Reconnecting);
|
||||
controller.SetSubscriptionStatus(SubscriptionStatus.Subscribing);
|
||||
controller.SetSubscriptionStatus(SubscriptionStatus.Subscribed);
|
||||
|
||||
Assert.That(controller.Subscription.LastReceiveTime, Is.EqualTo(timestamp));
|
||||
Assert.That(controller.Subscription.SocketStatus, Is.EqualTo(SocketStatus.Reconnecting));
|
||||
Assert.That(controller.Subscription.SubscriptionStatus, Is.EqualTo(SubscriptionStatus.Subscribed));
|
||||
Assert.That(statuses, Is.EqualTo(new[]
|
||||
{
|
||||
SubscriptionStatus.Subscribing,
|
||||
SubscriptionStatus.Subscribed
|
||||
}));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void LifecycleMethods_Should_InvokeSubscriptionEvents()
|
||||
{
|
||||
var controller = new ManualUpdateSubscription();
|
||||
var error = new ServerError("Test error", ErrorInfo.Unknown);
|
||||
var exception = new InvalidOperationException("Test exception");
|
||||
var disconnectedPeriod = TimeSpan.FromMinutes(2);
|
||||
var lost = 0;
|
||||
var restored = TimeSpan.Zero;
|
||||
Error? resubscribeError = null;
|
||||
var paused = 0;
|
||||
var unpaused = 0;
|
||||
Exception? receivedException = null;
|
||||
|
||||
controller.Subscription.ConnectionLost += () => lost++;
|
||||
controller.Subscription.ConnectionRestored += x => restored = x;
|
||||
controller.Subscription.ResubscribingFailed += x => resubscribeError = x;
|
||||
controller.Subscription.ActivityPaused += () => paused++;
|
||||
controller.Subscription.ActivityUnpaused += () => unpaused++;
|
||||
controller.Subscription.Exception += x => receivedException = x;
|
||||
|
||||
controller.InvokeConnectionLost();
|
||||
controller.InvokeConnectionRestored(disconnectedPeriod);
|
||||
controller.InvokeResubscribingFailed(error);
|
||||
controller.InvokeActivityPaused();
|
||||
controller.InvokeActivityUnpaused();
|
||||
controller.InvokeException(exception);
|
||||
|
||||
Assert.That(lost, Is.EqualTo(1));
|
||||
Assert.That(restored, Is.EqualTo(disconnectedPeriod));
|
||||
Assert.That(resubscribeError, Is.SameAs(error));
|
||||
Assert.That(paused, Is.EqualTo(1));
|
||||
Assert.That(unpaused, Is.EqualTo(1));
|
||||
Assert.That(receivedException, Is.SameAs(exception));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void InvokeConnectionClosed_Should_CloseAndOnlyInvokeOnce()
|
||||
{
|
||||
var controller = new ManualUpdateSubscription();
|
||||
var closed = 0;
|
||||
controller.Subscription.ConnectionClosed += () => closed++;
|
||||
|
||||
controller.InvokeConnectionClosed();
|
||||
controller.InvokeConnectionClosed();
|
||||
|
||||
Assert.That(closed, Is.EqualTo(1));
|
||||
Assert.That(controller.Subscription.SocketStatus, Is.EqualTo(SocketStatus.Closed));
|
||||
Assert.That(controller.Subscription.SubscriptionStatus, Is.EqualTo(SubscriptionStatus.Closed));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task SubscriptionOperations_Should_InvokeCallbacks()
|
||||
{
|
||||
var closes = 0;
|
||||
var reconnects = 0;
|
||||
var resubscribes = 0;
|
||||
var controller = new ManualUpdateSubscription(
|
||||
closeAsync: () =>
|
||||
{
|
||||
closes++;
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
reconnectAsync: () =>
|
||||
{
|
||||
reconnects++;
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
resubscribeAsync: () =>
|
||||
{
|
||||
resubscribes++;
|
||||
return Task.FromResult(CallResult.Ok());
|
||||
});
|
||||
|
||||
await controller.Subscription.ReconnectAsync();
|
||||
var resubscribeResult = await controller.Subscription.ResubscribeAsync();
|
||||
await controller.Subscription.CloseAsync();
|
||||
await controller.Subscription.CloseAsync();
|
||||
|
||||
Assert.That(reconnects, Is.EqualTo(1));
|
||||
Assert.That(resubscribes, Is.EqualTo(1));
|
||||
Assert.That(resubscribeResult.Success, Is.True);
|
||||
Assert.That(closes, Is.EqualTo(1));
|
||||
Assert.That(controller.Subscription.SubscriptionStatus, Is.EqualTo(SubscriptionStatus.Closed));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,319 +12,248 @@ namespace CryptoExchange.Net.UnitTests
|
||||
[Test]
|
||||
public void AddingBasicValue_SetValueCorrectly()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
var parameters = new Parameters(new ParameterSerializationSettings());
|
||||
parameters.Add("test", "value");
|
||||
Assert.That(parameters["test"], Is.EqualTo("value"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingBasicNullValue_ThrowsException()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
Assert.Throws<ArgumentNullException>(() => parameters.Add("test", null!));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingOptionalBasicValue_SetValueCorrectly()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddOptional("test", "value");
|
||||
Assert.That(parameters["test"], Is.EqualTo("value"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingOptionalBasicNullValue_DoesntSetValue()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddOptional("test", null);
|
||||
var parameters = new Parameters(new ParameterSerializationSettings());
|
||||
parameters.Add("test", null);
|
||||
Assert.That(parameters.ContainsKey("test"), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingDecimalValueAsString_SetValueCorrectly()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddString("test", 0.1m);
|
||||
var parameters = new Parameters(new ParameterSerializationSettings());
|
||||
parameters.Add("test", 0.1m, DecimalSerialization.String);
|
||||
Assert.That(parameters["test"], Is.EqualTo("0.1"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingOptionalDecimalValueAsString_SetValueCorrectly()
|
||||
public void AddingDecimalValueAsString2_SetValueCorrectly()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddOptionalString("test", 0.1m);
|
||||
var parameters = new Parameters(new ParameterSerializationSettings()
|
||||
{
|
||||
Decimal = DecimalSerialization.String
|
||||
});
|
||||
parameters.Add("test", 0.1m);
|
||||
Assert.That(parameters["test"], Is.EqualTo("0.1"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingOptionalDecimalNullValueAsString_DoesntSetValue()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddOptionalString("test", (decimal?)null);
|
||||
Assert.That(parameters.ContainsKey("test"), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingIntValueAsString_SetValueCorrectly()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddString("test", 1);
|
||||
Assert.That(parameters["test"], Is.EqualTo("1"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingOptionalIntValueAsString_SetValueCorrectly()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddOptionalString("test", 1);
|
||||
Assert.That(parameters["test"], Is.EqualTo("1"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingOptionalIntNullValueAsString_DoesntSetValue()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddOptionalString("test", (int?)null);
|
||||
var parameters = new Parameters(new ParameterSerializationSettings());
|
||||
parameters.Add("test", (int?)null);
|
||||
Assert.That(parameters.ContainsKey("test"), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingLongValueAsString_SetValueCorrectly()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddString("test", 1L);
|
||||
var parameters = new Parameters(new ParameterSerializationSettings());
|
||||
parameters.Add("test", 1L, IntegerSerialization.String);
|
||||
Assert.That(parameters["test"], Is.EqualTo("1"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingOptionalLongValueAsString_SetValueCorrectly()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddOptionalString("test", 1L);
|
||||
var parameters = new Parameters(new ParameterSerializationSettings()
|
||||
{
|
||||
Integer = IntegerSerialization.String
|
||||
});
|
||||
parameters.Add("test", 1L);
|
||||
Assert.That(parameters["test"], Is.EqualTo("1"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingOptionalLongNullValueAsString_DoesntSetValue()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddOptionalString("test", (long?)null);
|
||||
var parameters = new Parameters(new ParameterSerializationSettings());
|
||||
parameters.Add("test", (long?)null);
|
||||
Assert.That(parameters.ContainsKey("test"), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingMillisecondTimestamp_SetValueCorrectly()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddMilliseconds("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc));
|
||||
var parameters = new Parameters(new ParameterSerializationSettings());
|
||||
parameters.Add("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc), DateTimeSerialization.MillisecondsNumber);
|
||||
Assert.That(parameters["test"], Is.EqualTo(1735689600000));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingOptionalMillisecondTimestamp_SetValueCorrectly()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddOptionalMilliseconds("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc));
|
||||
var parameters = new Parameters(new ParameterSerializationSettings()
|
||||
{
|
||||
DateTimes = DateTimeSerialization.MillisecondsNumber
|
||||
});
|
||||
parameters.Add("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc));
|
||||
Assert.That(parameters["test"], Is.EqualTo(1735689600000));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingOptionalMillisecondNullValue_DoesntSetValue()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddOptionalMilliseconds("test", null);
|
||||
var parameters = new Parameters(new ParameterSerializationSettings());
|
||||
parameters.Add("test", (DateTime?)null);
|
||||
Assert.That(parameters.ContainsKey("test"), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingMillisecondTimestampString_SetValueCorrectly()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddMillisecondsString("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc));
|
||||
var parameters = new Parameters(new ParameterSerializationSettings());
|
||||
parameters.Add("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc), DateTimeSerialization.MillisecondsString);
|
||||
Assert.That(parameters["test"], Is.EqualTo("1735689600000"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingOptionalMillisecondTimestampString_SetValueCorrectly()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddOptionalMillisecondsString("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc));
|
||||
var parameters = new Parameters(new ParameterSerializationSettings()
|
||||
{
|
||||
DateTimes = DateTimeSerialization.MillisecondsString
|
||||
});
|
||||
parameters.Add("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc));
|
||||
Assert.That(parameters["test"], Is.EqualTo("1735689600000"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingOptionalMillisecondStringNullValue_DoesntSetValue()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddOptionalMillisecondsString("test", null);
|
||||
Assert.That(parameters.ContainsKey("test"), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingSecondTimestamp_SetValueCorrectly()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddSeconds("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc));
|
||||
var parameters = new Parameters(new ParameterSerializationSettings());
|
||||
parameters.Add("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc), DateTimeSerialization.SecondsNumber);
|
||||
Assert.That(parameters["test"], Is.EqualTo(1735689600));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingOptionalSecondTimestamp_SetValueCorrectly()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddOptionalSeconds("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc));
|
||||
var parameters = new Parameters(new ParameterSerializationSettings()
|
||||
{
|
||||
DateTimes = DateTimeSerialization.SecondsNumber
|
||||
});
|
||||
parameters.Add("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc));
|
||||
Assert.That(parameters["test"], Is.EqualTo(1735689600));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingSecondNullValue_DoesntSetValue()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddOptionalSeconds("test", null);
|
||||
Assert.That(parameters.ContainsKey("test"), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingSecondTimestampString_SetValueCorrectly()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddSecondsString("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc));
|
||||
var parameters = new Parameters(new ParameterSerializationSettings());
|
||||
parameters.Add("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc), DateTimeSerialization.SecondsString);
|
||||
Assert.That(parameters["test"], Is.EqualTo("1735689600"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingOptionalSecondTimestampString_SetValueCorrectly()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddOptionalSecondsString("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc));
|
||||
var parameters = new Parameters(new ParameterSerializationSettings()
|
||||
{
|
||||
DateTimes = DateTimeSerialization.SecondsString
|
||||
});
|
||||
parameters.Add("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc));
|
||||
Assert.That(parameters["test"], Is.EqualTo("1735689600"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingSecondStringNullValue_DoesntSetValue()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddOptionalSecondsString("test", null);
|
||||
Assert.That(parameters.ContainsKey("test"), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingEnum_SetValueCorrectly()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddEnum("test", TestEnum.Two);
|
||||
var parameters = new Parameters(new ParameterSerializationSettings());
|
||||
parameters.Add("test", TestEnum.Two);
|
||||
Assert.That(parameters["test"], Is.EqualTo("2"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingOptionalEnum_SetValueCorrectly()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddOptionalEnum("test", (TestEnum?)TestEnum.Two);
|
||||
var parameters = new Parameters(new ParameterSerializationSettings());
|
||||
parameters.Add("test", (TestEnum?)TestEnum.Two);
|
||||
Assert.That(parameters["test"], Is.EqualTo("2"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingOptionalEnumNullValue_DoesntSetValue()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddOptionalEnum("test", (TestEnum?)null);
|
||||
var parameters = new Parameters(new ParameterSerializationSettings());
|
||||
parameters.Add("test", (TestEnum?)null);
|
||||
Assert.That(parameters.ContainsKey("test"), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingEnumAsInt_SetValueCorrectly()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddEnumAsInt("test", TestEnum.Two);
|
||||
var parameters = new Parameters(new ParameterSerializationSettings());
|
||||
parameters.Add("test", TestEnum.Two, EnumSerialization.Number);
|
||||
Assert.That(parameters["test"], Is.EqualTo(2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingOptionalEnumAsInt_SetValueCorrectly()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddOptionalEnumAsInt("test", (TestEnum?)TestEnum.Two);
|
||||
var parameters = new Parameters(new ParameterSerializationSettings()
|
||||
{
|
||||
Enum = EnumSerialization.Number
|
||||
});
|
||||
parameters.Add("test", TestEnum.Two);
|
||||
Assert.That(parameters["test"], Is.EqualTo(2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingOptionalEnumAsIntNullValue_DoesntSetValue()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddOptionalEnumAsInt("test", (TestEnum?)null);
|
||||
Assert.That(parameters.ContainsKey("test"), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingCommaSeparated_SetValueCorrectly()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
var parameters = new Parameters(new ParameterSerializationSettings());
|
||||
parameters.AddCommaSeparated("test", ["1", "2"]);
|
||||
Assert.That(parameters["test"], Is.EqualTo("1,2"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingOptionalCommaSeparated_SetValueCorrectly()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddOptionalCommaSeparated("test", ["1", "2"]);
|
||||
Assert.That(parameters["test"], Is.EqualTo("1,2"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingOptionalCommaSeparatedNullValue_DoesntSetValue()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddOptionalCommaSeparated("test", (string[]?)null);
|
||||
var parameters = new Parameters(new ParameterSerializationSettings());
|
||||
parameters.AddCommaSeparated("test", (string[]?)null);
|
||||
Assert.That(parameters.ContainsKey("test"), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingCommaSeparatedEnum_SetValueCorrectly()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
var parameters = new Parameters(new ParameterSerializationSettings());
|
||||
parameters.AddCommaSeparated("test", [TestEnum.Two, TestEnum.One]);
|
||||
Assert.That(parameters["test"], Is.EqualTo("2,1"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingOptionalCommaSeparatedEnum_SetValueCorrectly()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddOptionalCommaSeparated("test", [TestEnum.Two, TestEnum.One]);
|
||||
Assert.That(parameters["test"], Is.EqualTo("2,1"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingOptionalCommaSeparatedEnumNullValue_DoesntSetValue()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddOptionalCommaSeparated("test", (TestEnum[]?)null);
|
||||
Assert.That(parameters.ContainsKey("test"), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingBoolString_SetValueCorrectly()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddBoolString("test", true);
|
||||
var parameters = new Parameters(new ParameterSerializationSettings());
|
||||
parameters.Add("test", true, BoolSerialization.String);
|
||||
Assert.That(parameters["test"], Is.EqualTo("true"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingOptionalBoolString_SetValueCorrectly()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddOptionalBoolString("test", true);
|
||||
var parameters = new Parameters(new ParameterSerializationSettings()
|
||||
{
|
||||
Bool = BoolSerialization.String
|
||||
});
|
||||
parameters.Add("test", true);
|
||||
Assert.That(parameters["test"], Is.EqualTo("true"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddingOptionalBoolStringNullValue_DoesntSetValue()
|
||||
{
|
||||
var parameters = new ParameterCollection();
|
||||
parameters.AddOptionalBoolString("test", null);
|
||||
var parameters = new Parameters(new ParameterSerializationSettings());
|
||||
parameters.Add("test", null);
|
||||
Assert.That(parameters.ContainsKey("test"), Is.False);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,16 +29,16 @@ namespace CryptoExchange.Net.UnitTests
|
||||
|
||||
var triggered = false;
|
||||
rateLimiter.RateLimitTriggered += (x) => { triggered = true; };
|
||||
var requestDefinition = new RequestDefinition("/sapi/v1/system/status", HttpMethod.Get);
|
||||
var requestDefinition = new RequestDefinition("https://test.com", "/sapi/v1/system/status", HttpMethod.Get);
|
||||
|
||||
for (var i = 0; i < requests + 1; i++)
|
||||
{
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
Assert.That(i == requests ? triggered : !triggered);
|
||||
}
|
||||
triggered = false;
|
||||
await Task.Delay((int)Math.Round(perSeconds * 1000) + 10);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
Assert.That(!triggered);
|
||||
}
|
||||
|
||||
@@ -52,13 +52,13 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var rateLimiter = new RateLimitGate("Test");
|
||||
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new PathStartFilter("/sapi/"), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
|
||||
|
||||
var requestDefinition = new RequestDefinition(endpoint, HttpMethod.Get);
|
||||
var requestDefinition = new RequestDefinition("https://test.com", endpoint, HttpMethod.Get);
|
||||
|
||||
RateLimitEvent? evnt = null;
|
||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||
for (var i = 0; i < 2; i++)
|
||||
{
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
bool expected = i == 1 ? expectLimiting ? evnt?.DelayTime > TimeSpan.Zero : evnt == null : evnt == null;
|
||||
Assert.That(expected);
|
||||
}
|
||||
@@ -73,15 +73,15 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var rateLimiter = new RateLimitGate("Test");
|
||||
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerEndpoint, new PathStartFilter("/sapi/"), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
|
||||
|
||||
var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get);
|
||||
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get);
|
||||
var requestDefinition1 = new RequestDefinition("https://test.com", endpoint1, HttpMethod.Get);
|
||||
var requestDefinition2 = new RequestDefinition("https://test.com", endpoint2, HttpMethod.Get);
|
||||
|
||||
RateLimitEvent? evnt = null;
|
||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
Assert.That(evnt == null);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
Assert.That(expectLimiting ? evnt != null : evnt == null);
|
||||
}
|
||||
|
||||
@@ -96,16 +96,16 @@ namespace CryptoExchange.Net.UnitTests
|
||||
|
||||
bool triggered = false;
|
||||
rateLimiter.RateLimitTriggered += (x) => { triggered = true; };
|
||||
var requestDefinition = new RequestDefinition("/sapi/test", HttpMethod.Get);
|
||||
var requestDefinition = new RequestDefinition("https://test.com", "/sapi/test", HttpMethod.Get);
|
||||
|
||||
for (var i = 0; i < requests + 1; i++)
|
||||
{
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
Assert.That(i == requests ? triggered : !triggered);
|
||||
}
|
||||
triggered = false;
|
||||
await Task.Delay((int)Math.Round(perSeconds * 1000) + 10);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
Assert.That(!triggered);
|
||||
}
|
||||
|
||||
@@ -117,13 +117,13 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var rateLimiter = new RateLimitGate("Test");
|
||||
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerEndpoint, new ExactPathFilter("/sapi/test"), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
|
||||
|
||||
var requestDefinition = new RequestDefinition(endpoint, HttpMethod.Get);
|
||||
var requestDefinition = new RequestDefinition("https://test.com", endpoint, HttpMethod.Get);
|
||||
|
||||
RateLimitEvent? evnt = null;
|
||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||
for (var i = 0; i < 2; i++)
|
||||
{
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
bool expected = i == 1 ? expectLimited ? evnt?.DelayTime > TimeSpan.Zero : evnt == null : evnt == null;
|
||||
Assert.That(expected);
|
||||
}
|
||||
@@ -137,13 +137,13 @@ namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
var rateLimiter = new RateLimitGate("Test");
|
||||
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerEndpoint, new ExactPathsFilter(new[] { "/sapi/test", "/sapi/test2" }), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
|
||||
var requestDefinition = new RequestDefinition(endpoint, HttpMethod.Get);
|
||||
var requestDefinition = new RequestDefinition("https://test.com", endpoint, HttpMethod.Get);
|
||||
|
||||
RateLimitEvent? evnt = null;
|
||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||
for (var i = 0; i < 2; i++)
|
||||
{
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
bool expected = i == 1 ? expectLimited ? evnt?.DelayTime > TimeSpan.Zero : evnt == null : evnt == null;
|
||||
Assert.That(expected);
|
||||
}
|
||||
@@ -160,15 +160,15 @@ namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
var rateLimiter = new RateLimitGate("Test");
|
||||
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerApiKey, new AuthenticatedEndpointFilter(true), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Sliding));
|
||||
var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get) { Authenticated = key1 != null };
|
||||
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get) { Authenticated = key2 != null };
|
||||
var requestDefinition1 = new RequestDefinition("https://test.com", endpoint1, HttpMethod.Get) { Authenticated = key1 != null };
|
||||
var requestDefinition2 = new RequestDefinition("https://test.com", endpoint2, HttpMethod.Get) { Authenticated = key2 != null };
|
||||
|
||||
RateLimitEvent? evnt = null;
|
||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", key1, 1, RateLimitingBehaviour.Wait, null, default);
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, key1, 1, RateLimitingBehaviour.Wait, null, default);
|
||||
Assert.That(evnt == null);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", key2, 1, RateLimitingBehaviour.Wait, null, default);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, key2, 1, RateLimitingBehaviour.Wait, null, default);
|
||||
Assert.That(expectLimited ? evnt != null : evnt == null);
|
||||
}
|
||||
|
||||
@@ -179,15 +179,15 @@ namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
var rateLimiter = new RateLimitGate("Test");
|
||||
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, Array.Empty<IGuardFilter>(), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
|
||||
var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get);
|
||||
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get) { Authenticated = true };
|
||||
var requestDefinition1 = new RequestDefinition("https://test.com", endpoint1, HttpMethod.Get);
|
||||
var requestDefinition2 = new RequestDefinition("https://test.com", endpoint2, HttpMethod.Get) { Authenticated = true };
|
||||
|
||||
RateLimitEvent? evnt = null;
|
||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
Assert.That(evnt == null);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", null, 1, RateLimitingBehaviour.Wait, null, default);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, null, 1, RateLimitingBehaviour.Wait, null, default);
|
||||
Assert.That(expectLimited ? evnt != null : evnt == null);
|
||||
}
|
||||
|
||||
@@ -199,15 +199,15 @@ namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
var rateLimiter = new RateLimitGate("Test");
|
||||
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new HostFilter("https://test.com"), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
|
||||
var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get);
|
||||
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get) { Authenticated = true };
|
||||
var requestDefinition1 = new RequestDefinition(host1, endpoint1, HttpMethod.Get);
|
||||
var requestDefinition2 = new RequestDefinition(host2, endpoint2, HttpMethod.Get) { Authenticated = true };
|
||||
|
||||
RateLimitEvent? evnt = null;
|
||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, host1, "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
Assert.That(evnt == null);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, host2, "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
Assert.That(expectLimited ? evnt != null : evnt == null);
|
||||
}
|
||||
|
||||
@@ -222,9 +222,9 @@ namespace CryptoExchange.Net.UnitTests
|
||||
RateLimitEvent? evnt = null;
|
||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), host1, "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition(host1, "1", HttpMethod.Get), "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
Assert.That(evnt == null);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), host2, "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition(host2, "1", HttpMethod.Get), "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
Assert.That(expectLimited ? evnt != null : evnt == null);
|
||||
}
|
||||
|
||||
@@ -238,8 +238,8 @@ namespace CryptoExchange.Net.UnitTests
|
||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||
var ct = new CancellationTokenSource(TimeSpan.FromSeconds(0.2));
|
||||
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, ct.Token);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, ct.Token);
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("https://test.com", "1", HttpMethod.Get), "123", 1, RateLimitingBehaviour.Wait, null, ct.Token);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("https://test.com", "1", HttpMethod.Get), "123", 1, RateLimitingBehaviour.Wait, null, ct.Token);
|
||||
Assert.That(result2.Error, Is.TypeOf<CancellationRequestedError>());
|
||||
}
|
||||
|
||||
@@ -250,16 +250,16 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var rateLimiter = new RateLimitGate("Test");
|
||||
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerConnection, new LimitItemTypeFilter(RateLimitItemType.Request), 1, TimeSpan.FromSeconds(10), RateLimitWindowType.Fixed));
|
||||
|
||||
var definition = new RequestDefinition("1", HttpMethod.Get) { ConnectionId = 1 };
|
||||
var definition = new RequestDefinition("https://test.com", "1", HttpMethod.Get) { ConnectionId = 1 };
|
||||
|
||||
RateLimitEvent? evnt = null;
|
||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||
var ct = new CancellationTokenSource(TimeSpan.FromSeconds(0.2));
|
||||
|
||||
// act
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition, "https://test.com", null, 1, RateLimitingBehaviour.Fail, null, ct.Token);
|
||||
await rateLimiter.ResetAsync(RateLimitItemType.Request, definition, "https://test.com", null, null, default);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition, "https://test.com", null, 1, RateLimitingBehaviour.Fail, null, ct.Token);
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition, null, 1, RateLimitingBehaviour.Fail, null, ct.Token);
|
||||
await rateLimiter.ResetAsync(RateLimitItemType.Request, definition, null, null, null, default);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition, null, 1, RateLimitingBehaviour.Fail, null, ct.Token);
|
||||
|
||||
// assert
|
||||
Assert.That(evnt, Is.Null);
|
||||
@@ -272,17 +272,17 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var rateLimiter = new RateLimitGate("Test");
|
||||
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerConnection, new LimitItemTypeFilter(RateLimitItemType.Request), 1, TimeSpan.FromSeconds(10), RateLimitWindowType.Fixed));
|
||||
|
||||
var definition1 = new RequestDefinition("1", HttpMethod.Get) { ConnectionId = 1 };
|
||||
var definition2 = new RequestDefinition("2", HttpMethod.Get) { ConnectionId = 2 };
|
||||
var definition1 = new RequestDefinition("https://test.com", "1", HttpMethod.Get) { ConnectionId = 1 };
|
||||
var definition2 = new RequestDefinition("https://test.com", "2", HttpMethod.Get) { ConnectionId = 2 };
|
||||
|
||||
RateLimitEvent? evnt = null;
|
||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||
|
||||
// act
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition1, "https://test.com", null, 1, RateLimitingBehaviour.Fail, null, default);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition2, "https://test.com", null, 1, RateLimitingBehaviour.Fail, null, default);
|
||||
await rateLimiter.ResetAsync(RateLimitItemType.Request, definition1, "https://test.com", null, null, default);
|
||||
var result3 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition2, "https://test.com", null, 1, RateLimitingBehaviour.Fail, null, default);
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition1, null, 1, RateLimitingBehaviour.Fail, null, default);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition2, null, 1, RateLimitingBehaviour.Fail, null, default);
|
||||
await rateLimiter.ResetAsync(RateLimitItemType.Request, definition1, null, null, null, default);
|
||||
var result3 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition2, null, 1, RateLimitingBehaviour.Fail, null, default);
|
||||
|
||||
// assert
|
||||
Assert.That(evnt, Is.Not.Null);
|
||||
|
||||
@@ -16,9 +16,9 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
|
||||
// arrange
|
||||
var routes = new MessageRoute[]
|
||||
{
|
||||
MessageRoute<string>.CreateWithoutTopicFilter("type1", (_, _, _, _) => null),
|
||||
MessageRoute<string>.CreateWithTopicFilter("type1", "topic1", (_, _, _, _) => null),
|
||||
MessageRoute<int>.CreateWithTopicFilter("type2", "topic2", (_, _, _, _) => null)
|
||||
MessageRoute.CreateForEvent<string>("type1", (_, _, _, _) => null),
|
||||
MessageRoute.CreateForEvent<string>("type1", "topic1", (_, _, _, _) => null),
|
||||
MessageRoute.CreateForEvent<int>("type2", "topic2", (_, _, _, _) => null)
|
||||
};
|
||||
|
||||
var router = new QueryRouter(routes);
|
||||
@@ -46,10 +46,10 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
|
||||
var collection = new QueryRouteCollection(typeof(string));
|
||||
|
||||
// act
|
||||
collection.AddRoute(null, MessageRoute<string>.CreateWithoutTopicFilter("type", (_, _, _, _) => null));
|
||||
collection.AddRoute(null, MessageRoute.CreateForEvent<string>("type", (_, _, _, _) => null));
|
||||
var beforeMultipleReaders = collection.MultipleReaders;
|
||||
|
||||
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) => null, true));
|
||||
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) => null, true));
|
||||
var afterMultipleReaders = collection.MultipleReaders;
|
||||
|
||||
// assert
|
||||
@@ -63,12 +63,12 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
|
||||
// arrange
|
||||
var calls = new List<string>();
|
||||
var collection = new QueryRouteCollection(typeof(string));
|
||||
collection.AddRoute(null, MessageRoute<string>.CreateWithoutTopicFilter("type", (_, _, _, _) =>
|
||||
collection.AddRoute(null, MessageRoute.CreateForEvent<string>("type", (_, _, _, _) =>
|
||||
{
|
||||
calls.Add("no-topic");
|
||||
return null;
|
||||
}));
|
||||
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
|
||||
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
|
||||
{
|
||||
calls.Add("topic");
|
||||
return null;
|
||||
@@ -89,7 +89,7 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
|
||||
{
|
||||
// arrange
|
||||
var collection = new QueryRouteCollection(typeof(string));
|
||||
collection.AddRoute("other-topic", MessageRoute<string>.CreateWithTopicFilter("type", "other-topic", (_, _, _, _) => null));
|
||||
collection.AddRoute("other-topic", MessageRoute.CreateForEvent<string>("type", "other-topic", (_, _, _, _) => null));
|
||||
collection.Build();
|
||||
|
||||
// act
|
||||
@@ -106,12 +106,12 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
|
||||
// arrange
|
||||
var calls = new List<string>();
|
||||
var collection = new QueryRouteCollection(typeof(string));
|
||||
collection.AddRoute(null, MessageRoute<string>.CreateWithoutTopicFilter("type", (_, _, _, _) =>
|
||||
collection.AddRoute(null, MessageRoute.CreateForEvent<string>("type", (_, _, _, _) =>
|
||||
{
|
||||
calls.Add("no-topic");
|
||||
return null;
|
||||
}));
|
||||
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
|
||||
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
|
||||
{
|
||||
calls.Add("topic");
|
||||
return null;
|
||||
@@ -132,17 +132,17 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
|
||||
{
|
||||
// arrange
|
||||
var calls = new List<string>();
|
||||
var expectedResult = CallResult.SuccessResult;
|
||||
var expectedResult = CallResult.Ok();
|
||||
var collection = new QueryRouteCollection(typeof(string));
|
||||
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
|
||||
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
|
||||
{
|
||||
calls.Add("first");
|
||||
return expectedResult;
|
||||
}));
|
||||
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
|
||||
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
|
||||
{
|
||||
calls.Add("second");
|
||||
return new CallResult(null);
|
||||
return CallResult.Ok();
|
||||
}));
|
||||
collection.Build();
|
||||
|
||||
@@ -160,17 +160,17 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
|
||||
{
|
||||
// arrange
|
||||
var calls = new List<string>();
|
||||
var expectedResult = CallResult.SuccessResult;
|
||||
var expectedResult = CallResult.Ok();
|
||||
var collection = new QueryRouteCollection(typeof(string));
|
||||
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
|
||||
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
|
||||
{
|
||||
calls.Add("first");
|
||||
return expectedResult;
|
||||
}, true));
|
||||
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
|
||||
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
|
||||
{
|
||||
calls.Add("second");
|
||||
return new CallResult(null);
|
||||
return CallResult.Ok();
|
||||
}));
|
||||
collection.Build();
|
||||
|
||||
@@ -188,22 +188,22 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
|
||||
{
|
||||
// arrange
|
||||
var calls = new List<string>();
|
||||
var expectedResult = CallResult.SuccessResult;
|
||||
var expectedResult = CallResult.Ok();
|
||||
var collection = new QueryRouteCollection(typeof(string));
|
||||
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
|
||||
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
|
||||
{
|
||||
calls.Add("first");
|
||||
return null;
|
||||
}));
|
||||
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
|
||||
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
|
||||
{
|
||||
calls.Add("second");
|
||||
return expectedResult;
|
||||
}));
|
||||
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
|
||||
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
|
||||
{
|
||||
calls.Add("third");
|
||||
return new CallResult(null);
|
||||
return CallResult.Ok();
|
||||
}));
|
||||
collection.Build();
|
||||
|
||||
@@ -221,7 +221,7 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
|
||||
{
|
||||
// arrange
|
||||
var collection = new QueryRouteCollection(typeof(string));
|
||||
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) => null));
|
||||
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) => null));
|
||||
collection.Build();
|
||||
|
||||
// act
|
||||
|
||||
@@ -17,13 +17,13 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
|
||||
var processor1 = new TestMessageProcessor(
|
||||
1,
|
||||
MessageRouter.Create(
|
||||
MessageRoute<string>.CreateWithoutTopicFilter("type1", (_, _, _, _) => null),
|
||||
MessageRoute<string>.CreateWithTopicFilter("type1", "topic1", (_, _, _, _) => null)));
|
||||
MessageRoute.CreateForEvent<string>("type1", (_, _, _, _) => null),
|
||||
MessageRoute.CreateForEvent<string>("type1", "topic1", (_, _, _, _) => null)));
|
||||
|
||||
var processor2 = new TestMessageProcessor(
|
||||
2,
|
||||
MessageRouter.Create(
|
||||
MessageRoute<int>.CreateWithTopicFilter("type2", "topic2", (_, _, _, _) => null)));
|
||||
MessageRoute.CreateForEvent<int>("type2", "topic2", (_, _, _, _) => null)));
|
||||
|
||||
var table = new RoutingTable();
|
||||
|
||||
@@ -57,12 +57,12 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
|
||||
var processor1 = new TestMessageProcessor(
|
||||
1,
|
||||
MessageRouter.Create(
|
||||
MessageRoute<string>.CreateWithoutTopicFilter("type1", (_, _, _, _) => null)));
|
||||
MessageRoute.CreateForEvent<string>("type1", (_, _, _, _) => null)));
|
||||
|
||||
var processor2 = new TestMessageProcessor(
|
||||
2,
|
||||
MessageRouter.Create(
|
||||
MessageRoute<string>.CreateWithTopicFilter("type1", "topic1", (_, _, _, _) => null)));
|
||||
MessageRoute.CreateForEvent<string>("type1", "topic1", (_, _, _, _) => null)));
|
||||
|
||||
var table = new RoutingTable();
|
||||
|
||||
@@ -85,12 +85,12 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
|
||||
var initialProcessor = new TestMessageProcessor(
|
||||
1,
|
||||
MessageRouter.Create(
|
||||
MessageRoute<string>.CreateWithoutTopicFilter("type1", (_, _, _, _) => null)));
|
||||
MessageRoute.CreateForEvent<string>("type1", (_, _, _, _) => null)));
|
||||
|
||||
var replacementProcessor = new TestMessageProcessor(
|
||||
2,
|
||||
MessageRouter.Create(
|
||||
MessageRoute<int>.CreateWithoutTopicFilter("type2", (_, _, _, _) => null)));
|
||||
MessageRoute.CreateForEvent<int>("type2", (_, _, _, _) => null)));
|
||||
|
||||
var table = new RoutingTable();
|
||||
table.Update(new IMessageProcessor[] { initialProcessor });
|
||||
@@ -116,7 +116,7 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
|
||||
var processor = new TestMessageProcessor(
|
||||
1,
|
||||
MessageRouter.Create(
|
||||
MessageRoute<string>.CreateWithoutTopicFilter("type1", (_, _, _, _) => null)));
|
||||
MessageRoute.CreateForEvent<string>("type1", (_, _, _, _) => null)));
|
||||
|
||||
var table = new RoutingTable();
|
||||
table.Update(new IMessageProcessor[] { processor });
|
||||
@@ -156,8 +156,9 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
|
||||
MessageRouter = messageRouter;
|
||||
}
|
||||
|
||||
#pragma warning disable CS0067 // The event is never used, but it's required by the interface
|
||||
public event Action? OnMessageRouterUpdated;
|
||||
|
||||
#pragma warning restore CS0067
|
||||
public bool Handle(string typeIdentifier, string? topicFilter, SocketConnection socketConnection, DateTime receiveTime, string? originalData, object result)
|
||||
{
|
||||
return true;
|
||||
|
||||
@@ -15,9 +15,9 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
|
||||
// arrange
|
||||
var routes = new MessageRoute[]
|
||||
{
|
||||
MessageRoute<string>.CreateWithoutTopicFilter("type1", (_, _, _, _) => null),
|
||||
MessageRoute<string>.CreateWithTopicFilter("type1", "topic1", (_, _, _, _) => null),
|
||||
MessageRoute<int>.CreateWithTopicFilter("type2", "topic2", (_, _, _, _) => null)
|
||||
MessageRoute.CreateForEvent<string>("type1", (_, _, _, _) => null),
|
||||
MessageRoute.CreateForEvent<string>("type1", "topic1", (_, _, _, _) => null),
|
||||
MessageRoute.CreateForEvent<int>("type2", "topic2", (_, _, _, _) => null)
|
||||
};
|
||||
|
||||
var router = new SubscriptionRouter(routes);
|
||||
@@ -44,12 +44,12 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
|
||||
// arrange
|
||||
var calls = new List<string>();
|
||||
var collection = new SubscriptionRouteCollection(typeof(string));
|
||||
collection.AddRoute(null, MessageRoute<string>.CreateWithoutTopicFilter("type", (_, _, _, _) =>
|
||||
collection.AddRoute(null, MessageRoute.CreateForEvent<string>("type", (_, _, _, _) =>
|
||||
{
|
||||
calls.Add("no-topic");
|
||||
return null;
|
||||
}));
|
||||
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
|
||||
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
|
||||
{
|
||||
calls.Add("topic");
|
||||
return null;
|
||||
@@ -61,7 +61,7 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
|
||||
|
||||
// assert
|
||||
Assert.That(handled, Is.True);
|
||||
Assert.That(result, Is.SameAs(CallResult.SuccessResult));
|
||||
Assert.That(result, Is.SameAs(CallResult.Ok()));
|
||||
Assert.That(calls, Is.EqualTo(new[] { "no-topic" }));
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
|
||||
{
|
||||
// arrange
|
||||
var collection = new SubscriptionRouteCollection(typeof(string));
|
||||
collection.AddRoute("other-topic", MessageRoute<string>.CreateWithTopicFilter("type", "other-topic", (_, _, _, _) => null));
|
||||
collection.AddRoute("other-topic", MessageRoute.CreateForEvent<string>("type", "other-topic", (_, _, _, _) => null));
|
||||
collection.Build();
|
||||
|
||||
// act
|
||||
@@ -78,7 +78,7 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
|
||||
|
||||
// assert
|
||||
Assert.That(handled, Is.False);
|
||||
Assert.That(result, Is.SameAs(CallResult.SuccessResult));
|
||||
Assert.That(result, Is.SameAs(CallResult.Ok()));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -87,12 +87,12 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
|
||||
// arrange
|
||||
var calls = new List<string>();
|
||||
var collection = new SubscriptionRouteCollection(typeof(string));
|
||||
collection.AddRoute(null, MessageRoute<string>.CreateWithoutTopicFilter("type", (_, _, _, _) =>
|
||||
collection.AddRoute(null, MessageRoute.CreateForEvent<string>("type", (_, _, _, _) =>
|
||||
{
|
||||
calls.Add("no-topic");
|
||||
return null;
|
||||
}));
|
||||
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
|
||||
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
|
||||
{
|
||||
calls.Add("topic");
|
||||
return null;
|
||||
@@ -104,7 +104,7 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
|
||||
|
||||
// assert
|
||||
Assert.That(handled, Is.True);
|
||||
Assert.That(result, Is.SameAs(CallResult.SuccessResult));
|
||||
Assert.That(result, Is.SameAs(CallResult.Ok()));
|
||||
Assert.That(calls, Is.EqualTo(new[] { "no-topic", "topic" }));
|
||||
}
|
||||
|
||||
@@ -114,12 +114,12 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
|
||||
// arrange
|
||||
var calls = new List<string>();
|
||||
var collection = new SubscriptionRouteCollection(typeof(string));
|
||||
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
|
||||
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
|
||||
{
|
||||
calls.Add("first");
|
||||
return CallResult.SuccessResult;
|
||||
return CallResult.Ok();
|
||||
}));
|
||||
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
|
||||
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
|
||||
{
|
||||
calls.Add("second");
|
||||
return null;
|
||||
@@ -131,7 +131,7 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
|
||||
|
||||
// assert
|
||||
Assert.That(handled, Is.True);
|
||||
Assert.That(result, Is.SameAs(CallResult.SuccessResult));
|
||||
Assert.That(result, Is.SameAs(CallResult.Ok()));
|
||||
Assert.That(calls, Is.EqualTo(new[] { "first", "second" }));
|
||||
}
|
||||
|
||||
@@ -141,7 +141,7 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
|
||||
// arrange
|
||||
var calls = new List<string>();
|
||||
var collection = new SubscriptionRouteCollection(typeof(string));
|
||||
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
|
||||
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
|
||||
{
|
||||
calls.Add("topic");
|
||||
return null;
|
||||
@@ -153,7 +153,7 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
|
||||
|
||||
// assert
|
||||
Assert.That(handled, Is.False);
|
||||
Assert.That(result, Is.SameAs(CallResult.SuccessResult));
|
||||
Assert.That(result, Is.SameAs(CallResult.Ok()));
|
||||
Assert.That(calls, Is.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
}
|
||||
|
||||
|
||||
protected override Task<CallResult<bool>> DoResyncAsync(CancellationToken ct)
|
||||
protected override Task<CallResult> DoResyncAsync(CancellationToken ct)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,347 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Errors;
|
||||
using CryptoExchange.Net.Sockets;
|
||||
using CryptoExchange.Net.Sockets.Default;
|
||||
using CryptoExchange.Net.TokenManagement;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using NUnit.Framework;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
[TestFixture]
|
||||
public class TokenManagementTests
|
||||
{
|
||||
private static readonly TimeSpan TestMaintenanceInterval = TimeSpan.FromMilliseconds(5);
|
||||
|
||||
[Test]
|
||||
public async Task AcquireWithoutApiKeyReturnsCredentialsError()
|
||||
{
|
||||
var starts = 0;
|
||||
var manager = CreateManager(
|
||||
(_, _) =>
|
||||
{
|
||||
starts++;
|
||||
return Task.FromResult(CallResult.Ok("token"));
|
||||
});
|
||||
|
||||
var result = await manager.AcquireAsync(new TokenScope("Test", "Test", "Test", ""));
|
||||
|
||||
Assert.That(result.Success, Is.False);
|
||||
Assert.That(result.Error, Is.TypeOf<NoApiCredentialsError>());
|
||||
Assert.That(starts, Is.EqualTo(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task StartTokenFailureIsReturned()
|
||||
{
|
||||
var error = new ServerError(ErrorType.Unknown, "start failed");
|
||||
var manager = CreateManager((_, _) => Task.FromResult(CallResult.Fail<string>(error)));
|
||||
|
||||
var result = await manager.AcquireAsync(CreateScope());
|
||||
|
||||
Assert.That(result.Success, Is.False);
|
||||
Assert.That(result.Error, Is.SameAs(error));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ActiveTokenIsSharedWhileLeasedAndStoppedAfterLastRelease()
|
||||
{
|
||||
var starts = 0;
|
||||
var stops = 0;
|
||||
var manager = CreateManager(
|
||||
(_, _) => Task.FromResult(CallResult.Ok("token-" + ++starts)),
|
||||
stopToken: (_, _) =>
|
||||
{
|
||||
stops++;
|
||||
return Task.FromResult(CallResult.Ok());
|
||||
});
|
||||
var scope = CreateScope();
|
||||
|
||||
var first = await manager.AcquireAsync(scope);
|
||||
var second = await manager.AcquireAsync(scope);
|
||||
AssertSuccess(first);
|
||||
AssertSuccess(second);
|
||||
|
||||
Assert.That(second.Data!.Token.Token, Is.EqualTo(first.Data!.Token.Token));
|
||||
Assert.That(starts, Is.EqualTo(1));
|
||||
|
||||
await first.Data!.ReleaseAsync();
|
||||
Assert.That(stops, Is.EqualTo(0));
|
||||
|
||||
await second.Data!.ReleaseAsync();
|
||||
Assert.That(stops, Is.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ActiveTokenStartsNewTokenAfterLeaseRelease()
|
||||
{
|
||||
var starts = 0;
|
||||
var stops = 0;
|
||||
var manager = CreateManager(
|
||||
(_, _) => Task.FromResult(CallResult.Ok("token-" + ++starts)),
|
||||
stopToken: (_, _) =>
|
||||
{
|
||||
stops++;
|
||||
return Task.FromResult(CallResult.Ok());
|
||||
});
|
||||
var scope = CreateScope();
|
||||
|
||||
var first = await manager.AcquireAsync(scope);
|
||||
AssertSuccess(first);
|
||||
await first.Data!.ReleaseAsync();
|
||||
var second = await manager.AcquireAsync(scope);
|
||||
AssertSuccess(second);
|
||||
|
||||
Assert.That(second.Data!.Token.Token, Is.Not.EqualTo(first.Data!.Token.Token));
|
||||
Assert.That(starts, Is.EqualTo(2));
|
||||
Assert.That(stops, Is.EqualTo(1));
|
||||
await second.Data!.ReleaseAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ReleasingLeaseTwiceOnlyStopsActiveTokenOnce()
|
||||
{
|
||||
var stops = 0;
|
||||
var manager = CreateManager(
|
||||
(_, _) => Task.FromResult(CallResult.Ok("token")),
|
||||
stopToken: (_, _) =>
|
||||
{
|
||||
stops++;
|
||||
return Task.FromResult(CallResult.Ok());
|
||||
});
|
||||
|
||||
var leaseResult = await manager.AcquireAsync(CreateScope());
|
||||
AssertSuccess(leaseResult);
|
||||
|
||||
await leaseResult.Data!.ReleaseAsync();
|
||||
await leaseResult.Data!.ReleaseAsync();
|
||||
|
||||
Assert.That(stops, Is.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task CachedTokenIsReusedAfterLeaseRelease()
|
||||
{
|
||||
var starts = 0;
|
||||
var manager = CreateManager(
|
||||
(_, _) => Task.FromResult(CallResult.Ok("token-" + ++starts)),
|
||||
managementType: TokenManagementType.Cached);
|
||||
var scope = CreateScope();
|
||||
|
||||
var first = await manager.AcquireAsync(scope);
|
||||
AssertSuccess(first);
|
||||
await first.Data!.ReleaseAsync();
|
||||
var second = await manager.AcquireAsync(scope);
|
||||
AssertSuccess(second);
|
||||
|
||||
Assert.That(second.Data!.Token.Token, Is.EqualTo(first.Data!.Token.Token));
|
||||
Assert.That(starts, Is.EqualTo(1));
|
||||
await second.Data!.ReleaseAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task CachedTokensAreScopedIndependently()
|
||||
{
|
||||
var starts = 0;
|
||||
var manager = CreateManager(
|
||||
(_, _) => Task.FromResult(CallResult.Ok("token-" + ++starts)),
|
||||
managementType: TokenManagementType.Cached);
|
||||
|
||||
var firstScope = CreateScope(additionalIdentifier: "one");
|
||||
var secondScope = CreateScope(additionalIdentifier: "two");
|
||||
|
||||
var first = await manager.AcquireAsync(firstScope);
|
||||
var second = await manager.AcquireAsync(secondScope);
|
||||
AssertSuccess(first);
|
||||
AssertSuccess(second);
|
||||
await first.Data!.ReleaseAsync();
|
||||
await second.Data!.ReleaseAsync();
|
||||
|
||||
var firstAgain = await manager.AcquireAsync(firstScope);
|
||||
AssertSuccess(firstAgain);
|
||||
|
||||
Assert.That(firstAgain.Data!.Token.Token, Is.EqualTo(first.Data!.Token.Token));
|
||||
Assert.That(second.Data!.Token.Token, Is.Not.EqualTo(first.Data!.Token.Token));
|
||||
Assert.That(starts, Is.EqualTo(2));
|
||||
await firstAgain.Data!.ReleaseAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ExpiredCachedTokenIsNotReused()
|
||||
{
|
||||
var starts = 0;
|
||||
var manager = CreateManager(
|
||||
(_, _) => Task.FromResult(CallResult.Ok("token-" + ++starts)),
|
||||
timeValid: TimeSpan.FromMilliseconds(20),
|
||||
managementType: TokenManagementType.Cached);
|
||||
var scope = CreateScope();
|
||||
|
||||
var first = await manager.AcquireAsync(scope);
|
||||
AssertSuccess(first);
|
||||
await first.Data!.ReleaseAsync();
|
||||
await Task.Delay(50);
|
||||
var second = await manager.AcquireAsync(scope);
|
||||
AssertSuccess(second);
|
||||
|
||||
Assert.That(first.Data!.Token.Status, Is.EqualTo(TokenStatus.Expired));
|
||||
Assert.That(second.Data!.Token.Token, Is.Not.EqualTo(first.Data!.Token.Token));
|
||||
Assert.That(starts, Is.EqualTo(2));
|
||||
await second.Data!.ReleaseAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task CachedTokenDoesNotRunKeepAliveLoop()
|
||||
{
|
||||
var keepAlives = 0;
|
||||
var manager = CreateManager(
|
||||
(_, _) => Task.FromResult(CallResult.Ok("token")),
|
||||
refreshInterval: TimeSpan.FromMilliseconds(1),
|
||||
keepAliveToken: (_, _) =>
|
||||
{
|
||||
keepAlives++;
|
||||
return Task.FromResult(CallResult.Ok());
|
||||
},
|
||||
managementType: TokenManagementType.Cached);
|
||||
|
||||
var leaseResult = await manager.AcquireAsync(CreateScope());
|
||||
AssertSuccess(leaseResult);
|
||||
await Task.Delay(50);
|
||||
|
||||
Assert.That(keepAlives, Is.EqualTo(0));
|
||||
await leaseResult.Data!.ReleaseAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ActiveTokenKeepAliveRefreshesValidity()
|
||||
{
|
||||
var keepAlives = 0;
|
||||
var manager = CreateManager(
|
||||
(_, _) => Task.FromResult(CallResult.Ok("token")),
|
||||
refreshInterval: TimeSpan.FromMilliseconds(1),
|
||||
timeValid: TimeSpan.FromSeconds(1),
|
||||
keepAliveToken: (_, _) =>
|
||||
{
|
||||
keepAlives++;
|
||||
return Task.FromResult(CallResult.Ok());
|
||||
});
|
||||
|
||||
var leaseResult = await manager.AcquireAsync(CreateScope());
|
||||
AssertSuccess(leaseResult);
|
||||
var originalValidUntil = leaseResult.Data!.Token.ValidUntil;
|
||||
|
||||
await WaitUntilAsync(() => keepAlives > 0);
|
||||
|
||||
Assert.That(leaseResult.Data!.Token.ValidUntil, Is.GreaterThan(originalValidUntil));
|
||||
await leaseResult.Data!.ReleaseAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ActiveTokenKeepAliveFailureExpiresTokenWhenValidityPassed()
|
||||
{
|
||||
var starts = 0;
|
||||
var expired = false;
|
||||
var manager = CreateManager(
|
||||
(_, _) => Task.FromResult(CallResult.Ok("token-" + ++starts)),
|
||||
refreshInterval: TimeSpan.FromMilliseconds(1),
|
||||
timeValid: TimeSpan.FromMilliseconds(25),
|
||||
keepAliveToken: (_, _) => Task.FromResult(CallResult.Fail(new ServerError(ErrorType.Unknown, "keep alive failed"))));
|
||||
|
||||
var leaseResult = await manager.AcquireAsync(CreateScope());
|
||||
AssertSuccess(leaseResult);
|
||||
leaseResult.Data!.Token.Expired += _ => expired = true;
|
||||
|
||||
await WaitUntilAsync(() => expired);
|
||||
|
||||
Assert.That(leaseResult.Data!.Token.Status, Is.EqualTo(TokenStatus.Expired));
|
||||
|
||||
var nextLease = await manager.AcquireAsync(CreateScope());
|
||||
AssertSuccess(nextLease);
|
||||
Assert.That(nextLease.Data!.Token.Token, Is.Not.EqualTo(leaseResult.Data!.Token.Token));
|
||||
Assert.That(starts, Is.EqualTo(2));
|
||||
|
||||
await leaseResult.Data!.ReleaseAsync();
|
||||
await nextLease.Data!.ReleaseAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task AcquireAndReplaceReleasesPreviousSubscriptionLease()
|
||||
{
|
||||
var starts = 0;
|
||||
var stops = 0;
|
||||
var manager = CreateManager(
|
||||
(_, _) => Task.FromResult(CallResult.Ok("token-" + ++starts)),
|
||||
stopToken: (_, _) =>
|
||||
{
|
||||
stops++;
|
||||
return Task.FromResult(CallResult.Ok());
|
||||
});
|
||||
var subscription = new TestSubscription();
|
||||
|
||||
var first = await manager.AcquireAndReplaceAsync(subscription, CreateScope(additionalIdentifier: "one"));
|
||||
AssertSuccess(first);
|
||||
var second = await manager.AcquireAndReplaceAsync(subscription, CreateScope(additionalIdentifier: "two"));
|
||||
AssertSuccess(second);
|
||||
|
||||
Assert.That(subscription.TokenLease, Is.SameAs(second.Data));
|
||||
Assert.That(second.Data!.Token.Token, Is.Not.EqualTo(first.Data!.Token.Token));
|
||||
Assert.That(stops, Is.EqualTo(1));
|
||||
|
||||
await subscription.TokenLease!.ReleaseAsync();
|
||||
}
|
||||
|
||||
private static TokenManager CreateManager(
|
||||
Func<TokenScope, System.Threading.CancellationToken, Task<CallResult<string>>> startToken,
|
||||
TimeSpan? refreshInterval = null,
|
||||
TimeSpan? timeValid = null,
|
||||
Func<TokenInfo, System.Threading.CancellationToken, Task<CallResult>>? keepAliveToken = null,
|
||||
Func<TokenInfo, System.Threading.CancellationToken, Task<CallResult>>? stopToken = null,
|
||||
TokenManagementType managementType = TokenManagementType.Active)
|
||||
{
|
||||
return new TokenManager(
|
||||
Guid.NewGuid().ToString(),
|
||||
null,
|
||||
refreshInterval ?? TimeSpan.FromMinutes(1),
|
||||
timeValid ?? TimeSpan.FromMinutes(1),
|
||||
startToken,
|
||||
keepAliveToken,
|
||||
stopToken,
|
||||
managementType,
|
||||
TestMaintenanceInterval);
|
||||
}
|
||||
|
||||
private static TokenScope CreateScope(string apiKey = "apiKey", string? additionalIdentifier = null)
|
||||
=> new TokenScope("Test", "Test", "Test", apiKey, additionalIdentifier);
|
||||
|
||||
private static void AssertSuccess(CallResult<TokenLease> result)
|
||||
{
|
||||
Assert.That(result.Success, Is.True, result.Error?.ToString());
|
||||
Assert.That(result.Data, Is.Not.Null);
|
||||
}
|
||||
|
||||
private static async Task WaitUntilAsync(Func<bool> condition)
|
||||
{
|
||||
var timeout = DateTime.UtcNow.AddSeconds(2);
|
||||
while (!condition())
|
||||
{
|
||||
if (DateTime.UtcNow > timeout)
|
||||
Assert.Fail("Condition was not met within the timeout");
|
||||
|
||||
await Task.Delay(10);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TestSubscription : Subscription
|
||||
{
|
||||
public TestSubscription() : base(NullLogger.Instance, true)
|
||||
{
|
||||
}
|
||||
|
||||
protected override Query? GetSubQuery(SocketConnection connection) => null;
|
||||
|
||||
protected override Query? GetUnsubQuery(SocketConnection connection) => null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,8 +15,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleClient", "Examples\C
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharedClients", "Examples\SharedClients\SharedClients.csproj", "{988A87EF-EAEA-4313-A6CF-FA869813D5AB}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CryptoExchange.Net.Protobuf", "CryptoExchange.Net.Protobuf\CryptoExchange.Net.Protobuf.csproj", "{CC6A807A-9183-6F41-8EF1-8A70172B0E83}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -43,10 +41,6 @@ Global
|
||||
{988A87EF-EAEA-4313-A6CF-FA869813D5AB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{988A87EF-EAEA-4313-A6CF-FA869813D5AB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{988A87EF-EAEA-4313-A6CF-FA869813D5AB}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{CC6A807A-9183-6F41-8EF1-8A70172B0E83}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{CC6A807A-9183-6F41-8EF1-8A70172B0E83}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{CC6A807A-9183-6F41-8EF1-8A70172B0E83}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{CC6A807A-9183-6F41-8EF1-8A70172B0E83}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#if NETSTANDARD2_0
|
||||
namespace System.Diagnostics.CodeAnalysis
|
||||
namespace System.Diagnostics.CodeAnalysis
|
||||
{
|
||||
using System;
|
||||
#if NETSTANDARD2_0
|
||||
|
||||
/// <summary>
|
||||
/// Specifies that <see langword="null"/> is allowed as an input even if the
|
||||
@@ -206,5 +206,26 @@ namespace System.Diagnostics.CodeAnalysis
|
||||
ReturnValue = returnValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#if NETSTANDARD2_0 || NETSTANDARD2_1
|
||||
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, AllowMultiple = true, Inherited = false)]
|
||||
[ExcludeFromCodeCoverage]
|
||||
internal sealed class MemberNotNullWhenAttribute : Attribute
|
||||
{
|
||||
public MemberNotNullWhenAttribute(bool returnValue, string member)
|
||||
{
|
||||
ReturnValue = returnValue;
|
||||
Members = [member];
|
||||
}
|
||||
|
||||
public MemberNotNullWhenAttribute(bool returnValue, params string[] members)
|
||||
{
|
||||
ReturnValue = returnValue;
|
||||
Members = members;
|
||||
}
|
||||
|
||||
public bool ReturnValue { get; }
|
||||
public string[] Members { get; }
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -439,13 +439,13 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// <param name="serializer"></param>
|
||||
/// <param name="parameters"></param>
|
||||
/// <returns></returns>
|
||||
protected static string GetSerializedBody(IMessageSerializer serializer, IDictionary<string, object> parameters)
|
||||
protected static string GetSerializedBody(IMessageSerializer serializer, Parameters? parameters)
|
||||
{
|
||||
if (serializer is not IStringMessageSerializer stringSerializer)
|
||||
throw new InvalidOperationException("Non-string message serializer can't get serialized request body");
|
||||
|
||||
if (parameters?.Count == 1 && parameters.TryGetValue(Constants.BodyPlaceHolderKey, out object? value))
|
||||
return stringSerializer.Serialize(value);
|
||||
if (parameters?.BodyValue != null)
|
||||
return stringSerializer.Serialize(parameters.BodyValue);
|
||||
else
|
||||
return stringSerializer.Serialize(parameters);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ using CryptoExchange.Net.Objects.Errors;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
namespace CryptoExchange.Net.Clients
|
||||
{
|
||||
@@ -25,7 +26,7 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <summary>
|
||||
/// If we are disposing
|
||||
/// </summary>
|
||||
protected bool _disposing;
|
||||
protected bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Whether a proxy is configured
|
||||
@@ -47,6 +48,11 @@ namespace CryptoExchange.Net.Clients
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The name of the exchange this client is for
|
||||
/// </summary>
|
||||
public string Exchange { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The environment this client communicates to
|
||||
/// </summary>
|
||||
@@ -75,20 +81,26 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger</param>
|
||||
/// <param name="loggerFactory">Logger factory</param>
|
||||
/// <param name="exchange">The exchange name</param>
|
||||
/// <param name="outputOriginalData">Should data from this client include the original data in the call result</param>
|
||||
/// <param name="baseAddress">Base address for this API client</param>
|
||||
/// <param name="clientOptions">Client options</param>
|
||||
/// <param name="apiOptions">Api options</param>
|
||||
protected BaseApiClient(
|
||||
ILogger logger,
|
||||
ILoggerFactory? loggerFactory,
|
||||
string exchange,
|
||||
bool outputOriginalData,
|
||||
string baseAddress,
|
||||
ExchangeOptions clientOptions,
|
||||
ApiOptions apiOptions)
|
||||
{
|
||||
_logger = logger;
|
||||
var loggerName = ClientName.StartsWith(exchange, StringComparison.OrdinalIgnoreCase)
|
||||
? exchange + "." + ClientName.Substring(exchange.Length).TrimStart('.')
|
||||
: exchange + "." + ClientName;
|
||||
_logger = loggerFactory?.CreateLogger(loggerName) ?? NullLogger.Instance;
|
||||
|
||||
Exchange = exchange;
|
||||
ClientOptions = clientOptions;
|
||||
ApiOptions = apiOptions;
|
||||
OutputOriginalData = outputOriginalData;
|
||||
@@ -113,9 +125,18 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <summary>
|
||||
/// Dispose
|
||||
/// </summary>
|
||||
public virtual void Dispose()
|
||||
public void Dispose()
|
||||
{
|
||||
_disposing = true;
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose
|
||||
/// </summary>
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
@@ -89,7 +90,6 @@ namespace CryptoExchange.Net.Clients
|
||||
throw new ArgumentNullException(nameof(options));
|
||||
|
||||
ClientOptions = options;
|
||||
_logger.Log(LogLevel.Trace, $"Client configuration: {options}, CryptoExchange.Net: v{CryptoExchangeLibVersion}, {Exchange}.Net: v{ExchangeLibVersion}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -115,6 +115,12 @@ namespace CryptoExchange.Net.Clients
|
||||
return opts;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{GetType().Name}, CryptoExchange.Net: v{CryptoExchangeLibVersion}, {Exchange}.Net: v{ExchangeLibVersion}, configuration: {ClientOptions}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose
|
||||
/// </summary>
|
||||
|
||||
@@ -72,7 +72,7 @@ namespace CryptoExchange.Net.Clients
|
||||
/// Set the API credentials for this client. All Api clients in this client will use the new credentials, regardless of earlier set options.
|
||||
/// </summary>
|
||||
/// <param name="credentials">The credentials to set</param>
|
||||
public void SetApiCredentials(TApiCredentials credentials)
|
||||
public virtual void SetApiCredentials(TApiCredentials credentials)
|
||||
{
|
||||
foreach (var apiClient in ApiClients)
|
||||
apiClient.SetApiCredentials(credentials);
|
||||
|
||||
@@ -41,11 +41,6 @@ namespace CryptoExchange.Net.Clients
|
||||
/// </summary>
|
||||
protected internal RequestBodyFormat RequestBodyFormat = RequestBodyFormat.Json;
|
||||
|
||||
/// <summary>
|
||||
/// How to serialize array parameters when making requests
|
||||
/// </summary>
|
||||
protected internal ArrayParametersSerialization ArraySerialization = ArrayParametersSerialization.Array;
|
||||
|
||||
/// <summary>
|
||||
/// What request body should be set when no data is send (only used in combination with postParametersPosition.InBody)
|
||||
/// </summary>
|
||||
@@ -56,16 +51,6 @@ namespace CryptoExchange.Net.Clients
|
||||
/// </summary>
|
||||
protected Dictionary<string, string> StandardRequestHeaders { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Whether parameters need to be ordered
|
||||
/// </summary>
|
||||
protected internal bool OrderParameters { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Parameter order comparer
|
||||
/// </summary>
|
||||
protected IComparer<string> ParameterOrderComparer { get; } = new OrderedStringComparer();
|
||||
|
||||
/// <summary>
|
||||
/// Where to put the parameters for requests with different Http methods
|
||||
/// </summary>
|
||||
@@ -117,17 +102,20 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger</param>
|
||||
/// <param name="loggerFactory">Logger factory</param>
|
||||
/// <param name="exchangeName">The exchange name</param>
|
||||
/// <param name="httpClient">HttpClient to use</param>
|
||||
/// <param name="baseAddress">Base address for this API client</param>
|
||||
/// <param name="options">The base client options</param>
|
||||
/// <param name="apiOptions">The Api client options</param>
|
||||
public RestApiClient(ILogger logger,
|
||||
public RestApiClient(ILoggerFactory? loggerFactory,
|
||||
string exchangeName,
|
||||
HttpClient? httpClient,
|
||||
string baseAddress,
|
||||
RestExchangeOptions options,
|
||||
RestApiOptions apiOptions)
|
||||
: base(logger,
|
||||
: base(loggerFactory,
|
||||
exchangeName,
|
||||
apiOptions.OutputOriginalData ?? options.OutputOriginalData,
|
||||
baseAddress,
|
||||
options,
|
||||
@@ -144,33 +132,10 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <returns></returns>
|
||||
protected abstract IMessageSerializer CreateSerializer();
|
||||
|
||||
/// <summary>
|
||||
/// Send a request to the base address based on the request definition
|
||||
/// </summary>
|
||||
/// <param name="baseAddress">Host and schema</param>
|
||||
/// <param name="definition">Request definition</param>
|
||||
/// <param name="parameters">Request parameters</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <param name="additionalHeaders">Additional headers for this request</param>
|
||||
/// <param name="weight">Override the request weight for this request definition, for example when the weight depends on the parameters</param>
|
||||
/// <returns></returns>
|
||||
protected virtual async Task<WebCallResult> SendAsync(
|
||||
string baseAddress,
|
||||
RequestDefinition definition,
|
||||
ParameterCollection? parameters,
|
||||
CancellationToken cancellationToken,
|
||||
Dictionary<string, string>? additionalHeaders = null,
|
||||
int? weight = null)
|
||||
{
|
||||
var result = await SendAsync<object>(baseAddress, definition, parameters, cancellationToken, additionalHeaders, weight).ConfigureAwait(false);
|
||||
return result.AsDataless();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send a request to the base address based on the request definition
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Response type</typeparam>
|
||||
/// <param name="baseAddress">Host and schema</param>
|
||||
/// <param name="definition">Request definition</param>
|
||||
/// <param name="parameters">Request parameters</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
@@ -179,10 +144,9 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <param name="weightSingleLimiter">Specify the weight to apply to the individual rate limit guard for this request</param>
|
||||
/// <param name="rateLimitKeySuffix">An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters.</param>
|
||||
/// <returns></returns>
|
||||
protected virtual Task<WebCallResult<T>> SendAsync<T>(
|
||||
string baseAddress,
|
||||
protected virtual Task<HttpResult<T>> SendAsync<T>(
|
||||
RequestDefinition definition,
|
||||
ParameterCollection? parameters,
|
||||
Parameters? parameters,
|
||||
CancellationToken cancellationToken,
|
||||
Dictionary<string, string>? additionalHeaders = null,
|
||||
int? weight = null,
|
||||
@@ -191,7 +155,6 @@ namespace CryptoExchange.Net.Clients
|
||||
{
|
||||
var parameterPosition = definition.ParameterPosition ?? ParameterPositions[definition.Method];
|
||||
return SendAsync<T>(
|
||||
baseAddress,
|
||||
definition,
|
||||
parameterPosition == HttpMethodParameterPosition.InUri ? parameters : null,
|
||||
parameterPosition == HttpMethodParameterPosition.InBody ? parameters : null,
|
||||
@@ -206,7 +169,6 @@ namespace CryptoExchange.Net.Clients
|
||||
/// Send a request to the base address based on the request definition
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Response type</typeparam>
|
||||
/// <param name="baseAddress">Host and schema</param>
|
||||
/// <param name="definition">Request definition</param>
|
||||
/// <param name="uriParameters">Request query parameters</param>
|
||||
/// <param name="bodyParameters">Request body parameters</param>
|
||||
@@ -216,11 +178,10 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <param name="weightSingleLimiter">Specify the weight to apply to the individual rate limit guard for this request</param>
|
||||
/// <param name="rateLimitKeySuffix">An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters.</param>
|
||||
/// <returns></returns>
|
||||
protected virtual async Task<WebCallResult<T>> SendAsync<T>(
|
||||
string baseAddress,
|
||||
protected virtual async Task<HttpResult<T>> SendAsync<T>(
|
||||
RequestDefinition definition,
|
||||
ParameterCollection? uriParameters,
|
||||
ParameterCollection? bodyParameters,
|
||||
Parameters? uriParameters,
|
||||
Parameters? bodyParameters,
|
||||
CancellationToken cancellationToken,
|
||||
Dictionary<string, string>? additionalHeaders = null,
|
||||
int? weight = null,
|
||||
@@ -231,20 +192,20 @@ namespace CryptoExchange.Net.Clients
|
||||
if (definition.Authenticated && GetAuthenticationProvider() == null)
|
||||
{
|
||||
_logger.RestApiNoApiCredentials(requestId, definition.Path);
|
||||
return new WebCallResult<T>(new NoApiCredentialsError());
|
||||
return HttpResult.Fail<T>(Exchange, new NoApiCredentialsError());
|
||||
}
|
||||
|
||||
string? cacheKey = null;
|
||||
if (ShouldCache(definition))
|
||||
{
|
||||
cacheKey = baseAddress + definition + uriParameters?.ToFormData();
|
||||
cacheKey = definition.FullUrl + definition + uriParameters?.ToFormData();
|
||||
_logger.CheckingCache(cacheKey);
|
||||
var cachedValue = _cache.Get(cacheKey, ClientOptions.CachingMaxAge);
|
||||
if (cachedValue != null)
|
||||
{
|
||||
_logger.CacheHit(cacheKey);
|
||||
var original = (WebCallResult<T>)cachedValue;
|
||||
return original.Cached();
|
||||
var original = (HttpResult<T>)cachedValue;
|
||||
return original with { DataSource = ResultDataSource.Cache };
|
||||
}
|
||||
|
||||
_logger.CacheNotHit(cacheKey);
|
||||
@@ -258,7 +219,6 @@ namespace CryptoExchange.Net.Clients
|
||||
await CheckTimeSync(requestId, definition).ConfigureAwait(false);
|
||||
|
||||
var error = await RateLimitAsync(
|
||||
baseAddress,
|
||||
requestId,
|
||||
definition,
|
||||
weight ?? definition.Weight,
|
||||
@@ -266,11 +226,10 @@ namespace CryptoExchange.Net.Clients
|
||||
weightSingleLimiter,
|
||||
rateLimitKeySuffix).ConfigureAwait(false);
|
||||
if (error != null)
|
||||
return new WebCallResult<T>(error);
|
||||
return HttpResult.Fail<T>(Exchange, error);
|
||||
|
||||
var request = CreateRequest(
|
||||
requestId,
|
||||
baseAddress,
|
||||
definition,
|
||||
uriParameters,
|
||||
bodyParameters,
|
||||
@@ -284,7 +243,7 @@ namespace CryptoExchange.Net.Clients
|
||||
if (result.Error is not CancellationRequestedError)
|
||||
{
|
||||
var originalData = OutputOriginalData ? result.OriginalData : "[Data only available when OutputOriginal = true]";
|
||||
if (!result)
|
||||
if (!result.Success)
|
||||
{
|
||||
_logger.RestApiErrorReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), result.Error?.ToString(), originalData, result.Error?.Exception);
|
||||
}
|
||||
@@ -316,7 +275,6 @@ namespace CryptoExchange.Net.Clients
|
||||
/// Check rate limits for the request
|
||||
/// </summary>
|
||||
protected virtual async ValueTask<Error?> RateLimitAsync(
|
||||
string host,
|
||||
int requestId,
|
||||
RequestDefinition definition,
|
||||
int weight,
|
||||
@@ -338,13 +296,12 @@ namespace CryptoExchange.Net.Clients
|
||||
requestId,
|
||||
RateLimitItemType.Request,
|
||||
definition,
|
||||
host,
|
||||
GetAuthenticationProvider()?.Key,
|
||||
requestWeight,
|
||||
ClientOptions.RateLimitingBehaviour,
|
||||
rateLimitKeySuffix + ClientOptions.RateLimitGroup,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
if (!limitResult)
|
||||
if (!limitResult.Success)
|
||||
return limitResult.Error!;
|
||||
}
|
||||
}
|
||||
@@ -364,13 +321,12 @@ namespace CryptoExchange.Net.Clients
|
||||
definition.LimitGuard,
|
||||
RateLimitItemType.Request,
|
||||
definition,
|
||||
host,
|
||||
GetAuthenticationProvider()?.Key,
|
||||
singleRequestWeight,
|
||||
ClientOptions.RateLimitingBehaviour,
|
||||
rateLimitKeySuffix,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
if (!limitResult)
|
||||
if (!limitResult.Success)
|
||||
return limitResult.Error!;
|
||||
}
|
||||
}
|
||||
@@ -382,7 +338,6 @@ namespace CryptoExchange.Net.Clients
|
||||
/// Creates a request object
|
||||
/// </summary>
|
||||
/// <param name="requestId">Id of the request</param>
|
||||
/// <param name="baseAddress">Host and schema</param>
|
||||
/// <param name="definition">Request definition</param>
|
||||
/// <param name="uriParameters">The query parameters of the request</param>
|
||||
/// <param name="bodyParameters">The body parameters of the request</param>
|
||||
@@ -390,19 +345,16 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <returns></returns>
|
||||
protected virtual IRequest CreateRequest(
|
||||
int requestId,
|
||||
string baseAddress,
|
||||
RequestDefinition definition,
|
||||
ParameterCollection? uriParameters,
|
||||
ParameterCollection? bodyParameters,
|
||||
Parameters? uriParameters,
|
||||
Parameters? bodyParameters,
|
||||
Dictionary<string, string>? additionalHeaders)
|
||||
{
|
||||
var requestConfiguration = new RestRequestConfiguration(
|
||||
definition,
|
||||
baseAddress,
|
||||
uriParameters == null ? null : CreateParameterDictionary(uriParameters),
|
||||
bodyParameters == null ? null : CreateParameterDictionary(bodyParameters),
|
||||
uriParameters,
|
||||
bodyParameters,
|
||||
additionalHeaders,
|
||||
definition.ArraySerialization ?? ArraySerialization,
|
||||
definition.ParameterPosition ?? ParameterPositions[definition.Method],
|
||||
definition.RequestBodyFormat ?? RequestBodyFormat);
|
||||
|
||||
@@ -419,11 +371,7 @@ namespace CryptoExchange.Net.Clients
|
||||
if (!string.IsNullOrEmpty(queryString) && !queryString.StartsWith("?"))
|
||||
queryString = $"?{queryString}";
|
||||
|
||||
var path = baseAddress.AppendPath(definition.Path);
|
||||
if (definition.ForcePathEndWithSlash == true && !path.EndsWith("/"))
|
||||
path += "/";
|
||||
|
||||
var uri = new Uri(path + queryString);
|
||||
var uri = new Uri(definition.FullUrl + queryString);
|
||||
var request = RequestFactory.Create(ClientOptions.HttpVersion, definition.Method, uri, requestId);
|
||||
request.Accept = MessageHandler.AcceptHeader;
|
||||
|
||||
@@ -436,9 +384,11 @@ namespace CryptoExchange.Net.Clients
|
||||
foreach (var header in StandardRequestHeaders)
|
||||
{
|
||||
// Only add it if it isn't overwritten
|
||||
requestConfiguration.Headers ??= new Dictionary<string, string>();
|
||||
if (!requestConfiguration.Headers.ContainsKey(header.Key))
|
||||
if (requestConfiguration.Headers == null
|
||||
|| !requestConfiguration.Headers.ContainsKey(header.Key))
|
||||
{
|
||||
request.AddHeader(header.Key, header.Value);
|
||||
}
|
||||
}
|
||||
|
||||
if (requestConfiguration.ParameterPosition == HttpMethodParameterPosition.InBody)
|
||||
@@ -451,7 +401,7 @@ namespace CryptoExchange.Net.Clients
|
||||
}
|
||||
else
|
||||
{
|
||||
if (requestConfiguration.BodyParameters != null && requestConfiguration.BodyParameters.Count != 0)
|
||||
if (requestConfiguration.BodyParameters != null && !requestConfiguration.BodyParameters.Empty)
|
||||
WriteParamBody(request, requestConfiguration.BodyParameters, contentType);
|
||||
else if (OmitContentTypeHeaderWithoutContent != true)
|
||||
request.SetContent(RequestBodyEmptyContent, RequestBodyContentEncoding, contentType);
|
||||
@@ -469,7 +419,7 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <param name="gate">The ratelimit gate used</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns></returns>
|
||||
protected virtual async Task<WebCallResult<T>> GetResponseAsync2<T>(
|
||||
protected virtual async Task<HttpResult<T>> GetResponseAsync2<T>(
|
||||
RequestDefinition requestDefinition,
|
||||
IRequest request,
|
||||
IRateLimitGate? gate,
|
||||
@@ -535,16 +485,16 @@ namespace CryptoExchange.Net.Clients
|
||||
{
|
||||
_logger.LogError(ex, "Unhandled exception when parsing error response: {Message}", ex.Message);
|
||||
var errorResult = new ServerError(ErrorInfo.Unknown with { Message = ex.Message });
|
||||
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, errorResult);
|
||||
return FailHttpRequest<T>(request, response, sw.Elapsed, originalData, errorResult);
|
||||
}
|
||||
}
|
||||
|
||||
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
|
||||
return FailHttpRequest<T>(request, response, sw.Elapsed, originalData, error);
|
||||
}
|
||||
|
||||
if (typeof(T) == typeof(object))
|
||||
if (typeof(T) == Unit.Type)
|
||||
// Success status code and expected empty response, assume it's correct
|
||||
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, 0, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, null);
|
||||
return OkHttpRequest<T>(request, response, sw.Elapsed, originalData, default!);
|
||||
|
||||
// Data response received, inspect the message and check if it is an error or not
|
||||
var parsedError = await MessageHandler.CheckForErrorResponse(
|
||||
@@ -563,7 +513,7 @@ namespace CryptoExchange.Net.Clients
|
||||
}
|
||||
|
||||
// Success status code, but TryParseError determined it was an error response
|
||||
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, parsedError);
|
||||
return FailHttpRequest<T>(request, response, sw.Elapsed, originalData, parsedError);
|
||||
}
|
||||
|
||||
if (MessageHandler.RequiresSeekableStream)
|
||||
@@ -573,43 +523,43 @@ namespace CryptoExchange.Net.Clients
|
||||
// Try deserialization into the expected type
|
||||
var (deserializeResult, deserializeError) = await MessageHandler.TryDeserializeAsync<T>(responseStream, cancellationToken).ConfigureAwait(false);
|
||||
if (deserializeError != null)
|
||||
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult, deserializeError); ;
|
||||
return FailHttpRequest<T>(request, response, sw.Elapsed, originalData, deserializeError, deserializeResult);
|
||||
|
||||
try
|
||||
{
|
||||
// Check the deserialized response to see if it's an error or not
|
||||
var responseError = MessageHandler.CheckDeserializedResponse(response.ResponseHeaders, deserializeResult);
|
||||
if (responseError != null)
|
||||
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult, responseError);
|
||||
return FailHttpRequest<T>(request, response, sw.Elapsed, originalData, responseError, deserializeResult);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Unhandled exception when checking deserialized response: {Message}", ex.Message);
|
||||
var error = new ServerError(ErrorInfo.Unknown with { Message = ex.Message });
|
||||
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult, error);
|
||||
return FailHttpRequest<T>(request, response, sw.Elapsed, originalData, error, deserializeResult);
|
||||
}
|
||||
|
||||
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult, null);
|
||||
return OkHttpRequest<T>(request, response, sw.Elapsed, originalData, deserializeResult!);
|
||||
}
|
||||
catch (HttpRequestException requestException)
|
||||
{
|
||||
// Request exception, can't reach server for instance
|
||||
var error = new WebError(requestException.Message, requestException);
|
||||
return new WebCallResult<T>(null, null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
|
||||
return FailHttpRequest<T>(request, response, sw.Elapsed, null, error);
|
||||
}
|
||||
catch (OperationCanceledException canceledException)
|
||||
{
|
||||
if (cancellationToken != default && canceledException.CancellationToken == cancellationToken)
|
||||
{
|
||||
// Cancellation token canceled by caller
|
||||
return new WebCallResult<T>(null, null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new CancellationRequestedError(canceledException));
|
||||
return FailHttpRequest<T>(request, null, sw.Elapsed, null, new CancellationRequestedError(canceledException));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Request timed out
|
||||
var error = new WebError($"Request timed out", exception: canceledException);
|
||||
error.ErrorType = ErrorType.Timeout;
|
||||
return new WebCallResult<T>(null, null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
|
||||
return FailHttpRequest<T>(request, null, sw.Elapsed, null, error);
|
||||
}
|
||||
}
|
||||
catch (ArgumentException argumentException)
|
||||
@@ -618,7 +568,7 @@ namespace CryptoExchange.Net.Clients
|
||||
{
|
||||
// Unsupported HTTP version error .net framework
|
||||
var error = ArgumentError.Invalid(nameof(RestExchangeOptions.HttpVersion), $"Invalid HTTP version {request.HttpVersion}: " + argumentException.Message);
|
||||
return new WebCallResult<T>(null, null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
|
||||
return FailHttpRequest<T>(request, null, sw.Elapsed, null, error);
|
||||
}
|
||||
|
||||
throw;
|
||||
@@ -629,7 +579,7 @@ namespace CryptoExchange.Net.Clients
|
||||
{
|
||||
// Unsupported HTTP version error dotnet code
|
||||
var error = ArgumentError.Invalid(nameof(RestExchangeOptions.HttpVersion), $"Invalid HTTP version {request.HttpVersion}: " + notSupportedException.Message);
|
||||
return new WebCallResult<T>(null, null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
|
||||
return FailHttpRequest<T>(request, null, sw.Elapsed, null, error);
|
||||
}
|
||||
|
||||
throw;
|
||||
@@ -641,16 +591,55 @@ namespace CryptoExchange.Net.Clients
|
||||
}
|
||||
}
|
||||
|
||||
private HttpResult<T> OkHttpRequest<T>(IRequest request, IResponse response, TimeSpan elapsed, string? originalData, T result)
|
||||
{
|
||||
return HttpResult.Ok(
|
||||
Exchange,
|
||||
response.StatusCode,
|
||||
response.HttpVersion,
|
||||
response.ResponseHeaders,
|
||||
elapsed,
|
||||
response.ContentLength,
|
||||
originalData,
|
||||
request.RequestId,
|
||||
request.Uri.ToString(),
|
||||
request.Content,
|
||||
request.Method,
|
||||
request.GetHeaders(),
|
||||
ResultDataSource.Server,
|
||||
result);
|
||||
}
|
||||
|
||||
private HttpResult<T> FailHttpRequest<T>(IRequest request, IResponse? response, TimeSpan elapsed, string? originalData, Error error, T? result = default)
|
||||
{
|
||||
return HttpResult.Fail<T>(
|
||||
Exchange,
|
||||
response?.StatusCode,
|
||||
response?.HttpVersion,
|
||||
response?.ResponseHeaders,
|
||||
elapsed,
|
||||
response?.ContentLength,
|
||||
originalData,
|
||||
request.RequestId,
|
||||
request.Uri.ToString(),
|
||||
request.Content,
|
||||
request.Method,
|
||||
request.GetHeaders(),
|
||||
ResultDataSource.Server,
|
||||
error,
|
||||
result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Can be used to indicate that a request should be retried. Defaults to false. Make sure to retry a max number of times (based on the the tries parameter) or the request will retry forever.
|
||||
/// Note that this is always called; even when the request might be successful
|
||||
/// </summary>
|
||||
/// <typeparam name="T">WebCallResult type parameter</typeparam>
|
||||
/// <typeparam name="T">HttpResult type parameter</typeparam>
|
||||
/// <param name="gate">The rate limit gate the call used</param>
|
||||
/// <param name="callResult">The result of the call</param>
|
||||
/// <param name="tries">The current try number</param>
|
||||
/// <returns>True if call should retry, false if the call should return</returns>
|
||||
protected virtual async ValueTask<bool> ShouldRetryRequestAsync<T>(IRateLimitGate? gate, WebCallResult<T> callResult, int tries)
|
||||
protected virtual async ValueTask<bool> ShouldRetryRequestAsync<T>(IRateLimitGate? gate, HttpResult<T> callResult, int tries)
|
||||
{
|
||||
if (tries >= 2)
|
||||
// Only retry once
|
||||
@@ -681,7 +670,7 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <param name="request">The request to set the parameters on</param>
|
||||
/// <param name="parameters">The parameters to set</param>
|
||||
/// <param name="contentType">The content type of the data</param>
|
||||
protected virtual void WriteParamBody(IRequest request, IDictionary<string, object> parameters, string contentType)
|
||||
protected virtual void WriteParamBody(IRequest request, Parameters parameters, string contentType)
|
||||
{
|
||||
if (contentType == Constants.JsonContentHeader)
|
||||
{
|
||||
@@ -691,8 +680,13 @@ namespace CryptoExchange.Net.Clients
|
||||
|
||||
// Write the parameters as json in the body
|
||||
string stringData;
|
||||
if (parameters.Count == 1 && parameters.TryGetValue(Constants.BodyPlaceHolderKey, out object? value))
|
||||
stringData = stringSerializer.Serialize(value);
|
||||
if (parameters.BodyValue != null)
|
||||
{
|
||||
if (parameters.BodyValue is string bodyString)
|
||||
stringData = bodyString;
|
||||
else
|
||||
stringData = stringSerializer.Serialize(parameters.BodyValue);
|
||||
}
|
||||
else
|
||||
stringData = stringSerializer.Serialize(parameters);
|
||||
request.SetContent(stringData, RequestBodyContentEncoding, contentType);
|
||||
@@ -705,24 +699,11 @@ namespace CryptoExchange.Net.Clients
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create the parameter IDictionary
|
||||
/// </summary>
|
||||
/// <param name="parameters"></param>
|
||||
/// <returns></returns>
|
||||
protected internal IDictionary<string, object> CreateParameterDictionary(IDictionary<string, object> parameters)
|
||||
{
|
||||
if (!OrderParameters)
|
||||
return parameters;
|
||||
|
||||
return new SortedDictionary<string, object>(parameters, ParameterOrderComparer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve the server time for the purpose of syncing time between client and server to prevent authentication issues
|
||||
/// </summary>
|
||||
/// <returns>Server time</returns>
|
||||
protected virtual Task<WebCallResult<DateTime>> GetServerTimestampAsync() => throw new NotImplementedException();
|
||||
protected virtual Task<HttpResult<DateTime>> GetServerTimestampAsync() => throw new NotImplementedException();
|
||||
|
||||
private async ValueTask CheckTimeSync(int requestId, RequestDefinition definition)
|
||||
{
|
||||
@@ -757,7 +738,7 @@ namespace CryptoExchange.Net.Clients
|
||||
return;
|
||||
|
||||
var localTime = DateTime.UtcNow;
|
||||
WebCallResult<DateTime> result;
|
||||
HttpResult<DateTime> result;
|
||||
try
|
||||
{
|
||||
result = await GetServerTimestampAsync().ConfigureAwait(false);
|
||||
@@ -767,7 +748,7 @@ namespace CryptoExchange.Net.Clients
|
||||
throw new ArgumentException("AutoTimestamp is not available for this API");
|
||||
}
|
||||
|
||||
if (!result)
|
||||
if (!result.Success)
|
||||
{
|
||||
_logger.LogWarning("Failed to determine time offset between client and server, timestamping might fail");
|
||||
return;
|
||||
@@ -778,7 +759,7 @@ namespace CryptoExchange.Net.Clients
|
||||
// If this was the first request make another one to calculate the offset since the first one can be slower
|
||||
localTime = DateTime.UtcNow;
|
||||
result = await GetServerTimestampAsync().ConfigureAwait(false);
|
||||
if (!result)
|
||||
if (!result.Success)
|
||||
{
|
||||
_logger.LogWarning("Failed to determine time offset between client and server, timestamping might fail");
|
||||
return;
|
||||
@@ -845,12 +826,14 @@ namespace CryptoExchange.Net.Clients
|
||||
/// ctor
|
||||
/// </summary>
|
||||
protected RestApiClient(
|
||||
ILogger logger,
|
||||
ILoggerFactory? loggerFactory,
|
||||
string exchangeName,
|
||||
HttpClient? httpClient,
|
||||
string baseAddress,
|
||||
RestExchangeOptions options,
|
||||
RestApiOptions apiOptions) : base(
|
||||
logger,
|
||||
loggerFactory,
|
||||
exchangeName,
|
||||
httpClient,
|
||||
baseAddress,
|
||||
options,
|
||||
@@ -877,12 +860,14 @@ namespace CryptoExchange.Net.Clients
|
||||
/// ctor
|
||||
/// </summary>
|
||||
protected RestApiClient(
|
||||
ILogger logger,
|
||||
ILoggerFactory? loggerFactory,
|
||||
string exchangeName,
|
||||
HttpClient? httpClient,
|
||||
string baseAddress,
|
||||
RestExchangeOptions<TEnvironment, TApiCredentials> options,
|
||||
RestApiOptions apiOptions) : base(
|
||||
logger,
|
||||
loggerFactory,
|
||||
exchangeName,
|
||||
httpClient,
|
||||
baseAddress,
|
||||
options,
|
||||
@@ -912,13 +897,18 @@ namespace CryptoExchange.Net.Clients
|
||||
where TAuthenticationProvider : AuthenticationProvider<TApiCredentials>
|
||||
where TEnvironment : TradeEnvironment
|
||||
{
|
||||
|
||||
private bool _authProviderInitialized = false;
|
||||
private TAuthenticationProvider? _authenticationProvider;
|
||||
/// <summary>
|
||||
/// Auth provider initialized field
|
||||
/// </summary>
|
||||
protected bool _authProviderInitialized = false;
|
||||
/// <summary>
|
||||
/// Auth provider field
|
||||
/// </summary>
|
||||
protected TAuthenticationProvider? _authenticationProvider;
|
||||
/// <summary>
|
||||
/// The authentication provider for this API client. (null if no credentials are set)
|
||||
/// </summary>
|
||||
public TAuthenticationProvider? AuthenticationProvider
|
||||
public virtual TAuthenticationProvider? AuthenticationProvider
|
||||
{
|
||||
get
|
||||
{
|
||||
@@ -932,7 +922,7 @@ namespace CryptoExchange.Net.Clients
|
||||
|
||||
return _authenticationProvider;
|
||||
}
|
||||
internal set => _authenticationProvider = value;
|
||||
protected internal set => _authenticationProvider = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -942,12 +932,14 @@ namespace CryptoExchange.Net.Clients
|
||||
/// ctor
|
||||
/// </summary>
|
||||
protected RestApiClient(
|
||||
ILogger logger,
|
||||
ILoggerFactory? loggerFactory,
|
||||
string exchangeName,
|
||||
HttpClient? httpClient,
|
||||
string baseAddress,
|
||||
RestExchangeOptions<TEnvironment, TApiCredentials> options,
|
||||
RestApiOptions apiOptions) : base(
|
||||
logger,
|
||||
loggerFactory,
|
||||
exchangeName,
|
||||
httpClient,
|
||||
baseAddress,
|
||||
options,
|
||||
|
||||
@@ -15,6 +15,7 @@ using CryptoExchange.Net.Sockets.Default.Interfaces;
|
||||
using CryptoExchange.Net.Sockets.HighPerf;
|
||||
using CryptoExchange.Net.Sockets.HighPerf.Interfaces;
|
||||
using CryptoExchange.Net.Sockets.Interfaces;
|
||||
using CryptoExchange.Net.TokenManagement;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
@@ -71,11 +72,6 @@ namespace CryptoExchange.Net.Clients
|
||||
/// </summary>
|
||||
protected List<SystemSubscription> systemSubscriptions = new();
|
||||
|
||||
/// <summary>
|
||||
/// If a message is received on the socket which is not handled by a handler this boolean determines whether this logs an error message
|
||||
/// </summary>
|
||||
protected internal bool UnhandledMessageExpected { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The rate limiters
|
||||
/// </summary>
|
||||
@@ -153,21 +149,26 @@ namespace CryptoExchange.Net.Clients
|
||||
/// Configured environment name
|
||||
/// </summary>
|
||||
public abstract string EnvironmentName { get; }
|
||||
|
||||
private int _isDisposed;
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="logger">log</param>
|
||||
/// <param name="loggerFactory">Logger factory</param>
|
||||
/// <param name="exchangeName">Exchange name</param>
|
||||
/// <param name="options">Client options</param>
|
||||
/// <param name="baseAddress">Base address for this API client</param>
|
||||
/// <param name="apiOptions">The Api client options</param>
|
||||
public SocketApiClient(
|
||||
ILogger logger,
|
||||
ILoggerFactory? loggerFactory,
|
||||
string exchangeName,
|
||||
string baseAddress,
|
||||
SocketExchangeOptions options,
|
||||
SocketApiOptions apiOptions)
|
||||
: base(logger,
|
||||
: base(loggerFactory,
|
||||
exchangeName,
|
||||
apiOptions.OutputOriginalData ?? options.OutputOriginalData,
|
||||
baseAddress,
|
||||
options,
|
||||
@@ -216,7 +217,7 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <param name="interval"></param>
|
||||
/// <param name="queryDelegate"></param>
|
||||
/// <param name="callback"></param>
|
||||
protected virtual void RegisterPeriodicQuery(string identifier, TimeSpan interval, Func<ISocketConnection, Query> queryDelegate, Action<SocketConnection, CallResult>? callback)
|
||||
protected virtual void RegisterPeriodicQuery(string identifier, TimeSpan interval, Func<ISocketConnection, Query> queryDelegate, Action<SocketConnection, WebSocketResult>? callback)
|
||||
{
|
||||
PeriodicTaskRegistrations.Add(new PeriodicTaskRegistration
|
||||
{
|
||||
@@ -233,7 +234,7 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <param name="subscription">The subscription</param>
|
||||
/// <param name="ct">Cancellation token for closing this subscription</param>
|
||||
/// <returns></returns>
|
||||
protected virtual Task<CallResult<UpdateSubscription>> SubscribeAsync(Subscription subscription, CancellationToken ct)
|
||||
protected virtual Task<WebSocketResult<UpdateSubscription>> SubscribeAsync(Subscription subscription, CancellationToken ct)
|
||||
{
|
||||
return SubscribeAsync(BaseAddress, subscription, ct);
|
||||
}
|
||||
@@ -245,86 +246,102 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <param name="subscription">The subscription</param>
|
||||
/// <param name="ct">Cancellation token for closing this subscription</param>
|
||||
/// <returns></returns>
|
||||
protected virtual async Task<CallResult<UpdateSubscription>> SubscribeAsync(string url, Subscription subscription, CancellationToken ct)
|
||||
protected virtual async Task<WebSocketResult<UpdateSubscription>> SubscribeAsync(string url, Subscription subscription, CancellationToken ct)
|
||||
{
|
||||
if (_disposing)
|
||||
return new CallResult<UpdateSubscription>(new InvalidOperationError("Client disposed, can't subscribe"));
|
||||
|
||||
if (subscription.Authenticated && GetAuthenticationProvider() == null)
|
||||
{
|
||||
_logger.LogWarning("Failed to subscribe, private subscription but no API credentials set");
|
||||
return new CallResult<UpdateSubscription>(new NoApiCredentialsError());
|
||||
}
|
||||
|
||||
if (subscription.IndividualSubscriptionCount > MaxIndividualSubscriptionsPerConnection)
|
||||
return new CallResult<UpdateSubscription>(ArgumentError.Invalid("subscriptions", $"Max number of subscriptions in a single call is {MaxIndividualSubscriptionsPerConnection}"));
|
||||
|
||||
SocketConnection socketConnection;
|
||||
var released = false;
|
||||
// Wait for a semaphore here, so we only connect 1 socket at a time.
|
||||
// This is necessary for being able to see if connections can be combined
|
||||
bool successResult = false;
|
||||
try
|
||||
{
|
||||
await semaphoreSlim.WaitAsync(ct).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException tce)
|
||||
{
|
||||
return new CallResult<UpdateSubscription>(new CancellationRequestedError(tce));
|
||||
}
|
||||
if (_disposed)
|
||||
return WebSocketResult.Fail<UpdateSubscription>(Exchange, new InvalidOperationError("Client disposed, can't subscribe"));
|
||||
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
if (subscription.Authenticated && GetAuthenticationProvider() == null)
|
||||
{
|
||||
// Get a new or existing socket connection
|
||||
var socketResult = await GetSocketConnection(url, subscription.Authenticated, false, ct, subscription.Topic, subscription.IndividualSubscriptionCount).ConfigureAwait(false);
|
||||
if (!socketResult)
|
||||
return socketResult.As<UpdateSubscription>(null);
|
||||
|
||||
socketConnection = socketResult.Data;
|
||||
|
||||
// Add a subscription on the socket connection
|
||||
var success = socketConnection.AddSubscription(subscription);
|
||||
if (!success)
|
||||
{
|
||||
_logger.FailedToAddSubscriptionRetryOnDifferentConnection(socketConnection.SocketId);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ClientOptions.SocketSubscriptionsCombineTarget == 1)
|
||||
{
|
||||
// Only 1 subscription per connection, so no need to wait for connection since a new subscription will create a new connection anyway
|
||||
semaphoreSlim.Release();
|
||||
released = true;
|
||||
}
|
||||
|
||||
var needsConnecting = !socketConnection.Connected;
|
||||
|
||||
var connectResult = await ConnectIfNeededAsync(socketConnection, subscription.Authenticated, ct).ConfigureAwait(false);
|
||||
if (!connectResult)
|
||||
return new CallResult<UpdateSubscription>(connectResult.Error!);
|
||||
|
||||
break;
|
||||
_logger.LogWarning("Failed to subscribe, private subscription but no API credentials set");
|
||||
return WebSocketResult.Fail<UpdateSubscription>(Exchange, new NoApiCredentialsError());
|
||||
}
|
||||
|
||||
if (subscription.IndividualSubscriptionCount > MaxIndividualSubscriptionsPerConnection)
|
||||
return WebSocketResult.Fail<UpdateSubscription>(Exchange, ArgumentError.Invalid("subscriptions", $"Max number of subscriptions in a single call is {MaxIndividualSubscriptionsPerConnection}"));
|
||||
|
||||
SocketConnection socketConnection;
|
||||
var released = false;
|
||||
// Wait for a semaphore here, so we only connect 1 socket at a time.
|
||||
// This is necessary for being able to see if connections can be combined
|
||||
try
|
||||
{
|
||||
await semaphoreSlim.WaitAsync(ct).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException tce)
|
||||
{
|
||||
return WebSocketResult.Fail<UpdateSubscription>(Exchange, new CancellationRequestedError(tce));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
// Get a new or existing socket connection
|
||||
var socketResult = await GetSocketConnection(url, subscription.Authenticated, false, ct, subscription.Topic, subscription.IndividualSubscriptionCount).ConfigureAwait(false);
|
||||
if (!socketResult.Success)
|
||||
return WebSocketResult.Fail<UpdateSubscription>(Exchange, socketResult.Error);
|
||||
|
||||
socketConnection = socketResult.Data;
|
||||
|
||||
// Add a subscription on the socket connection
|
||||
var success = socketConnection.AddSubscription(subscription);
|
||||
if (!success)
|
||||
{
|
||||
_logger.FailedToAddSubscriptionRetryOnDifferentConnection(socketConnection.SocketId);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ClientOptions.SocketSubscriptionsCombineTarget == 1)
|
||||
{
|
||||
// Only 1 subscription per connection, so no need to wait for connection since a new subscription will create a new connection anyway
|
||||
semaphoreSlim.Release();
|
||||
released = true;
|
||||
}
|
||||
|
||||
var needsConnecting = !socketConnection.Connected;
|
||||
|
||||
var connectResult = await ConnectIfNeededAsync(socketConnection, subscription.Authenticated, ct).ConfigureAwait(false);
|
||||
if (!connectResult.Success)
|
||||
return WebSocketResult.Fail<UpdateSubscription>(Exchange, connectResult.Error!);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (!released)
|
||||
semaphoreSlim.Release();
|
||||
}
|
||||
|
||||
if (socketConnection.PausedActivity)
|
||||
{
|
||||
_logger.HasBeenPausedCantSubscribeAtThisMoment(socketConnection.SocketId);
|
||||
return WebSocketResult.Fail<UpdateSubscription>(Exchange, new ServerError(new ErrorInfo(ErrorType.WebsocketPaused, "Socket is paused")));
|
||||
}
|
||||
|
||||
var subscribeResult = await socketConnection.TrySubscribeAsync(subscription, true, ct).ConfigureAwait(false);
|
||||
if (!subscribeResult.Success)
|
||||
return WebSocketResult.Fail<UpdateSubscription>(Exchange, subscribeResult.Error!);
|
||||
|
||||
successResult = true;
|
||||
_logger.SubscriptionCompletedSuccessfully(socketConnection.SocketId, subscription.Id);
|
||||
return WebSocketResult.Ok(
|
||||
Exchange,
|
||||
socketConnection.SocketId,
|
||||
subscribeResult.ResponseTime!.Value,
|
||||
subscribeResult.RequestId!.Value,
|
||||
subscribeResult.Url,
|
||||
new UpdateSubscription(socketConnection, subscription));
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (!released)
|
||||
semaphoreSlim.Release();
|
||||
if (!successResult && subscription.TokenLease != null)
|
||||
_ = subscription.TokenLease.ReleaseAsync();
|
||||
}
|
||||
|
||||
if (socketConnection.PausedActivity)
|
||||
{
|
||||
_logger.HasBeenPausedCantSubscribeAtThisMoment(socketConnection.SocketId);
|
||||
return new CallResult<UpdateSubscription>(new ServerError(new ErrorInfo(ErrorType.WebsocketPaused, "Socket is paused")));
|
||||
}
|
||||
|
||||
var subscribeResult = await socketConnection.TrySubscribeAsync(subscription, true, ct).ConfigureAwait(false);
|
||||
if (!subscribeResult)
|
||||
return new CallResult<UpdateSubscription>(subscribeResult.Error!);
|
||||
|
||||
_logger.SubscriptionCompletedSuccessfully(socketConnection.SocketId, subscription.Id);
|
||||
return new CallResult<UpdateSubscription>(new UpdateSubscription(socketConnection, subscription));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -335,14 +352,14 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <param name="connectionFactory">The factory for creating a socket connection</param>
|
||||
/// <param name="ct">Cancellation token for closing this subscription</param>
|
||||
/// <returns></returns>
|
||||
protected virtual async Task<CallResult<HighPerfUpdateSubscription>> SubscribeHighPerfAsync<TUpdateType>(
|
||||
protected virtual async Task<WebSocketResult<HighPerfUpdateSubscription>> SubscribeHighPerfAsync<TUpdateType>(
|
||||
string url,
|
||||
HighPerfSubscription<TUpdateType> subscription,
|
||||
IHighPerfConnectionFactory connectionFactory,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (_disposing)
|
||||
return new CallResult<HighPerfUpdateSubscription>(new InvalidOperationError("Client disposed, can't subscribe"));
|
||||
if (_disposed)
|
||||
return WebSocketResult.Fail<HighPerfUpdateSubscription>(Exchange, new InvalidOperationError("Client disposed, can't subscribe"));
|
||||
|
||||
HighPerfSocketConnection<TUpdateType> socketConnection;
|
||||
var released = false;
|
||||
@@ -354,7 +371,7 @@ namespace CryptoExchange.Net.Clients
|
||||
}
|
||||
catch (OperationCanceledException tce)
|
||||
{
|
||||
return new CallResult<HighPerfUpdateSubscription>(new CancellationRequestedError(tce));
|
||||
return WebSocketResult.Fail<HighPerfUpdateSubscription>(Exchange, new CancellationRequestedError(tce));
|
||||
}
|
||||
|
||||
try
|
||||
@@ -363,8 +380,8 @@ namespace CryptoExchange.Net.Clients
|
||||
{
|
||||
// Get a new or existing socket connection
|
||||
var socketResult = await GetHighPerfSocketConnection<TUpdateType>(url, connectionFactory, ct).ConfigureAwait(false);
|
||||
if (!socketResult)
|
||||
return socketResult.As<HighPerfUpdateSubscription>(null);
|
||||
if (!socketResult.Success)
|
||||
return WebSocketResult.Fail<HighPerfUpdateSubscription>(Exchange, socketResult.Error);
|
||||
|
||||
socketConnection = socketResult.Data;
|
||||
|
||||
@@ -384,8 +401,8 @@ namespace CryptoExchange.Net.Clients
|
||||
}
|
||||
|
||||
var connectResult = await ConnectIfNeededAsync(socketConnection, false, ct).ConfigureAwait(false);
|
||||
if (!connectResult)
|
||||
return new CallResult<HighPerfUpdateSubscription>(connectResult.Error!);
|
||||
if (!connectResult.Success)
|
||||
return WebSocketResult.Fail<HighPerfUpdateSubscription>(Exchange, connectResult.Error!);
|
||||
|
||||
break;
|
||||
}
|
||||
@@ -401,10 +418,10 @@ namespace CryptoExchange.Net.Clients
|
||||
{
|
||||
// Send the request and wait for answer
|
||||
var sendResult = await socketConnection.SendAsync(subRequest).ConfigureAwait(false);
|
||||
if (!sendResult)
|
||||
if (!sendResult.Success)
|
||||
{
|
||||
await socketConnection.CloseAsync().ConfigureAwait(false);
|
||||
return new CallResult<HighPerfUpdateSubscription>(sendResult.Error!);
|
||||
return WebSocketResult.Fail<HighPerfUpdateSubscription>(Exchange, sendResult.Error!);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -418,7 +435,13 @@ namespace CryptoExchange.Net.Clients
|
||||
}
|
||||
|
||||
_logger.SubscriptionCompletedSuccessfully(socketConnection.SocketId, subscription.Id);
|
||||
return new CallResult<HighPerfUpdateSubscription>(new HighPerfUpdateSubscription(socketConnection, subscription));
|
||||
return WebSocketResult.Ok(
|
||||
Exchange,
|
||||
socketConnection.SocketId,
|
||||
default,
|
||||
default,
|
||||
socketConnection.ConnectionUri.ToString(),
|
||||
new HighPerfUpdateSubscription(socketConnection, subscription));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -428,7 +451,7 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <param name="query">The query</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
/// <returns></returns>
|
||||
protected virtual Task<CallResult<THandlerResponse>> QueryAsync<THandlerResponse>(Query<THandlerResponse> query, CancellationToken ct = default)
|
||||
protected virtual Task<QueryResult<THandlerResponse>> QueryAsync<THandlerResponse>(Query<THandlerResponse> query, CancellationToken ct = default)
|
||||
{
|
||||
return QueryAsync(BaseAddress, query, ct);
|
||||
}
|
||||
@@ -441,13 +464,13 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <param name="query">The query</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
/// <returns></returns>
|
||||
protected virtual async Task<CallResult<THandlerResponse>> QueryAsync<THandlerResponse>(string url, Query<THandlerResponse> query, CancellationToken ct = default)
|
||||
protected virtual async Task<QueryResult<THandlerResponse>> QueryAsync<THandlerResponse>(string url, Query<THandlerResponse> query, CancellationToken ct = default)
|
||||
{
|
||||
if (_disposing)
|
||||
return new CallResult<THandlerResponse>(new InvalidOperationError("Client disposed, can't query"));
|
||||
if (_disposed)
|
||||
return QueryResult.Fail<THandlerResponse>(Exchange, new InvalidOperationError("Client disposed, can't query"));
|
||||
|
||||
if (ct.IsCancellationRequested)
|
||||
return new CallResult<THandlerResponse>(new CancellationRequestedError());
|
||||
return QueryResult.Fail<THandlerResponse>(Exchange, new CancellationRequestedError());
|
||||
|
||||
SocketConnection socketConnection;
|
||||
var released = false;
|
||||
@@ -455,8 +478,8 @@ namespace CryptoExchange.Net.Clients
|
||||
try
|
||||
{
|
||||
var socketResult = await GetSocketConnection(url, query.Authenticated, true, ct).ConfigureAwait(false);
|
||||
if (!socketResult)
|
||||
return socketResult.As<THandlerResponse>(default);
|
||||
if (!socketResult.Success)
|
||||
return QueryResult.Fail<THandlerResponse>(Exchange, socketResult.Error);
|
||||
|
||||
socketConnection = socketResult.Data;
|
||||
|
||||
@@ -468,8 +491,8 @@ namespace CryptoExchange.Net.Clients
|
||||
}
|
||||
|
||||
var connectResult = await ConnectIfNeededAsync(socketConnection, query.Authenticated, ct).ConfigureAwait(false);
|
||||
if (!connectResult)
|
||||
return new CallResult<THandlerResponse>(connectResult.Error!);
|
||||
if (!connectResult.Success)
|
||||
return QueryResult.Fail<THandlerResponse>(Exchange, connectResult.Error!);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -480,11 +503,11 @@ namespace CryptoExchange.Net.Clients
|
||||
if (socketConnection.PausedActivity)
|
||||
{
|
||||
_logger.HasBeenPausedCantSendQueryAtThisMoment(socketConnection.SocketId);
|
||||
return new CallResult<THandlerResponse>(new ServerError(new ErrorInfo(ErrorType.WebsocketPaused, "Socket is paused")));
|
||||
return QueryResult.Fail<THandlerResponse>(Exchange, new ServerError(new ErrorInfo(ErrorType.WebsocketPaused, "Socket is paused")));
|
||||
}
|
||||
|
||||
if (ct.IsCancellationRequested)
|
||||
return new CallResult<THandlerResponse>(new CancellationRequestedError());
|
||||
return QueryResult.Fail<THandlerResponse>(Exchange, new CancellationRequestedError());
|
||||
|
||||
return await socketConnection.SendAndWaitQueryAsync(query, ct).ConfigureAwait(false);
|
||||
}
|
||||
@@ -499,23 +522,23 @@ namespace CryptoExchange.Net.Clients
|
||||
protected virtual async Task<CallResult> ConnectIfNeededAsync(ISocketConnection socket, bool authenticated, CancellationToken ct)
|
||||
{
|
||||
if (socket.Connected)
|
||||
return CallResult.SuccessResult;
|
||||
return CallResult.Ok();
|
||||
|
||||
var connectResult = await ConnectSocketAsync(socket, ct).ConfigureAwait(false);
|
||||
if (!connectResult)
|
||||
if (!connectResult.Success)
|
||||
return connectResult;
|
||||
|
||||
if (ClientOptions.DelayAfterConnect != TimeSpan.Zero)
|
||||
await Task.Delay(ClientOptions.DelayAfterConnect).ConfigureAwait(false);
|
||||
|
||||
if (!authenticated || socket.Authenticated)
|
||||
return CallResult.SuccessResult;
|
||||
return CallResult.Ok();
|
||||
|
||||
if (socket is not SocketConnection sc)
|
||||
throw new InvalidOperationException("HighPerfSocketConnection not supported for authentication");
|
||||
|
||||
var result = await AuthenticateSocketAsync(sc).ConfigureAwait(false);
|
||||
if (!result)
|
||||
if (!result.Success)
|
||||
await socket.CloseAsync().ConfigureAwait(false);
|
||||
|
||||
return result;
|
||||
@@ -529,29 +552,28 @@ namespace CryptoExchange.Net.Clients
|
||||
public virtual async Task<CallResult> AuthenticateSocketAsync(SocketConnection socket)
|
||||
{
|
||||
if (GetAuthenticationProvider() == null)
|
||||
return new CallResult(new NoApiCredentialsError());
|
||||
return CallResult.Fail(new NoApiCredentialsError());
|
||||
|
||||
_logger.AttemptingToAuthenticate(socket.SocketId);
|
||||
var authRequest = await GetAuthenticationRequestAsync(socket).ConfigureAwait(false);
|
||||
if (authRequest != null)
|
||||
{
|
||||
var result = await socket.SendAndWaitQueryAsync(authRequest).ConfigureAwait(false);
|
||||
|
||||
if (!result)
|
||||
if (!result.Success)
|
||||
{
|
||||
_logger.AuthenticationFailed(socket.SocketId);
|
||||
if (socket.Connected)
|
||||
await socket.CloseAsync().ConfigureAwait(false);
|
||||
|
||||
result.Error!.Message = "Authentication failed: " + result.Error.Message;
|
||||
return new CallResult(result.Error)!;
|
||||
return CallResult.Fail(result.Error)!;
|
||||
}
|
||||
|
||||
_logger.Authenticated(socket.SocketId);
|
||||
}
|
||||
|
||||
socket.Authenticated = true;
|
||||
return CallResult.SuccessResult;
|
||||
return CallResult.Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -580,7 +602,7 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <returns></returns>
|
||||
protected virtual Task<CallResult<string?>> GetConnectionUrlAsync(string address, bool authentication)
|
||||
{
|
||||
return Task.FromResult(new CallResult<string?>(address));
|
||||
return Task.FromResult(CallResult.Ok<string?>(address));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -600,7 +622,22 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <returns></returns>
|
||||
protected internal virtual Task<CallResult> RevitalizeRequestAsync(Subscription subscription)
|
||||
{
|
||||
return Task.FromResult(CallResult.SuccessResult);
|
||||
return Task.FromResult(CallResult.Ok());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether the connection can be used for a new subscription or query with the provided parameters
|
||||
/// </summary>
|
||||
/// <param name="connection">The connection to check</param>
|
||||
/// <param name="address">The address set by the request</param>
|
||||
/// <param name="authenticated">Whether the request needs an authenticated connection</param>
|
||||
/// <param name="topic">Topic of the request</param>
|
||||
/// <returns>True if connection can be used</returns>
|
||||
protected virtual bool ConnectionCanBeUsedFor(SocketConnection connection, string address, bool authenticated, string? topic = null)
|
||||
{
|
||||
return connection.ConnectionUriString.Equals(address.TrimEnd('/'), StringComparison.Ordinal)
|
||||
&& connection.ApiClient.ClientName.Equals(ClientName, StringComparison.Ordinal)
|
||||
&& (AllowTopicsOnTheSameConnection || !connection.Topics.Contains(topic));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -621,24 +658,20 @@ namespace CryptoExchange.Net.Clients
|
||||
string? topic = null,
|
||||
int individualSubscriptionCount = 1)
|
||||
{
|
||||
var socketQuery = _socketConnections.Where(s => s.Value.Tag.TrimEnd('/') == address.TrimEnd('/')
|
||||
&& s.Value.ApiClient.GetType() == GetType()
|
||||
&& (AllowTopicsOnTheSameConnection || !s.Value.Topics.Contains(topic)))
|
||||
.Select(x => x.Value)
|
||||
.ToList();
|
||||
var socketQuery = _socketConnections.Where(s => ConnectionCanBeUsedFor(s.Value, address, authenticated, topic)).Select(x => x.Value); // Don't ToList this so the query is executed again when called
|
||||
|
||||
// If all current socket connections are reconnecting or resubscribing wait for that to finish as we can probably use the existing connection
|
||||
var delayStart = DateTime.UtcNow;
|
||||
var delayed = false;
|
||||
while (socketQuery.Count >= 1 && socketQuery.All(x => x.Status == SocketStatus.Reconnecting || x.Status == SocketStatus.Resubscribing))
|
||||
while (socketQuery.Count() >= 1 && socketQuery.All(x => x.Status == SocketStatus.Reconnecting || x.Status == SocketStatus.Resubscribing))
|
||||
{
|
||||
if (DateTime.UtcNow - delayStart > TimeSpan.FromSeconds(10))
|
||||
{
|
||||
if (socketQuery.Count >= 1 && socketQuery.All(x => x.Status == SocketStatus.Reconnecting || x.Status == SocketStatus.Resubscribing))
|
||||
if (socketQuery.Count() >= 1 && socketQuery.All(x => x.Status == SocketStatus.Reconnecting || x.Status == SocketStatus.Resubscribing))
|
||||
{
|
||||
// If after this time we still trying to reconnect/reprocess there is some issue in the connection
|
||||
_logger.TimeoutWaitingForReconnectingSocket();
|
||||
return new CallResult<SocketConnection>(new CantConnectError());
|
||||
return CallResult.Fail<SocketConnection>(new CantConnectError());
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -648,7 +681,7 @@ namespace CryptoExchange.Net.Clients
|
||||
try { await Task.Delay(50, ct).ConfigureAwait(false); } catch (Exception) { }
|
||||
|
||||
if (ct.IsCancellationRequested)
|
||||
return new CallResult<SocketConnection>(new CancellationRequestedError());
|
||||
return CallResult.Fail<SocketConnection>(new CancellationRequestedError());
|
||||
}
|
||||
|
||||
if (delayed)
|
||||
@@ -658,58 +691,42 @@ namespace CryptoExchange.Net.Clients
|
||||
&& (s.Authenticated == authenticated || !authenticated)
|
||||
&& s.Connected).ToList();
|
||||
|
||||
SocketConnection? connection;
|
||||
if (!dedicatedRequestConnection)
|
||||
{
|
||||
connection = socketQuery.Where(s => !s.DedicatedRequestConnection.IsDedicatedRequestConnection).OrderBy(s => s.UserSubscriptionCount).FirstOrDefault();
|
||||
}
|
||||
else
|
||||
bool maxConnectionsReached = _socketConnections.Count >= (ApiOptions.MaxSocketConnections ?? ClientOptions.MaxSocketConnections);
|
||||
SocketConnection? connection = null;
|
||||
if (dedicatedRequestConnection)
|
||||
{
|
||||
connection = socketQuery.Where(s => s.DedicatedRequestConnection.IsDedicatedRequestConnection).FirstOrDefault();
|
||||
if (connection != null && !connection.DedicatedRequestConnection.Authenticated)
|
||||
// Mark dedicated request connection as authenticated if the request is authenticated
|
||||
connection.DedicatedRequestConnection.Authenticated = authenticated;
|
||||
|
||||
if (connection == null)
|
||||
// Fall back to an existing connection if there is no dedicated request connection available
|
||||
connection = socketQuery.OrderBy(s => s.UserSubscriptionCount).FirstOrDefault();
|
||||
}
|
||||
|
||||
bool maxConnectionsReached = _socketConnections.Count >= (ApiOptions.MaxSocketConnections ?? ClientOptions.MaxSocketConnections);
|
||||
if (connection == null)
|
||||
// Use an eligible non-dedicated connection for subscriptions, or as fallback when no dedicated request connection is available
|
||||
connection = socketQuery
|
||||
.Where(s => !s.DedicatedRequestConnection.IsDedicatedRequestConnection)
|
||||
.Where(s => IsConnectionEligible(s, individualSubscriptionCount, maxConnectionsReached))
|
||||
.OrderBy(s => s.UserSubscriptionCount)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (connection != null)
|
||||
{
|
||||
bool lessThanBatchSubCombineTarget = connection.UserSubscriptionCount < ClientOptions.SocketSubscriptionsCombineTarget;
|
||||
bool lessThanIndividualSubCombineTarget = connection.Subscriptions.Sum(x => x.IndividualSubscriptionCount) < ClientOptions.SocketIndividualSubscriptionCombineTarget;
|
||||
|
||||
if ((lessThanBatchSubCombineTarget && lessThanIndividualSubCombineTarget)
|
||||
|| maxConnectionsReached)
|
||||
{
|
||||
// Use existing socket if it has less than target connections OR it has the least connections and we can't make new
|
||||
// If there is a max subscriptions per connection limit also only use existing if the new subscription doesn't go over the limit
|
||||
if (MaxIndividualSubscriptionsPerConnection == null)
|
||||
return new CallResult<SocketConnection>(connection);
|
||||
|
||||
var currentCount = connection.Subscriptions.Sum(x => x.IndividualSubscriptionCount);
|
||||
if (currentCount + individualSubscriptionCount <= MaxIndividualSubscriptionsPerConnection)
|
||||
return new CallResult<SocketConnection>(connection);
|
||||
}
|
||||
}
|
||||
return CallResult.Ok(connection);
|
||||
|
||||
if (maxConnectionsReached)
|
||||
return new CallResult<SocketConnection>(new InvalidOperationError("Max amount of socket connections reached"));
|
||||
return CallResult.Fail<SocketConnection>(new InvalidOperationError("Max amount of socket connections reached"));
|
||||
|
||||
var connectionAddress = await GetConnectionUrlAsync(address, authenticated).ConfigureAwait(false);
|
||||
if (!connectionAddress)
|
||||
if (!connectionAddress.Success)
|
||||
{
|
||||
_logger.FailedToDetermineConnectionUrl(connectionAddress.Error?.ToString());
|
||||
return connectionAddress.As<SocketConnection>(null);
|
||||
_logger.FailedToDetermineConnectionUrl(connectionAddress.Error.ToString());
|
||||
return CallResult.Fail<SocketConnection>(connectionAddress.Error);
|
||||
}
|
||||
|
||||
if (connectionAddress.Data != address)
|
||||
_logger.ConnectionAddressSetTo(connectionAddress.Data!);
|
||||
|
||||
// Create new socket connection
|
||||
var socketConnection = new SocketConnection(_logger, SocketFactory, GetWebSocketParameters(connectionAddress.Data!), this, address);
|
||||
var socketConnection = new SocketConnection(_logger, SocketFactory, GetWebSocketParameters(connectionAddress.Data!), this);
|
||||
socketConnection.ConnectRateLimitedAsync += HandleConnectRateLimitedAsync;
|
||||
if (dedicatedRequestConnection)
|
||||
{
|
||||
@@ -726,7 +743,7 @@ namespace CryptoExchange.Net.Clients
|
||||
foreach (var systemSubscription in systemSubscriptions)
|
||||
socketConnection.AddSubscription(systemSubscription);
|
||||
|
||||
return new CallResult<SocketConnection>(socketConnection);
|
||||
return CallResult.Ok(socketConnection);
|
||||
}
|
||||
|
||||
|
||||
@@ -743,21 +760,36 @@ namespace CryptoExchange.Net.Clients
|
||||
CancellationToken ct)
|
||||
{
|
||||
var connectionAddress = await GetConnectionUrlAsync(address, false).ConfigureAwait(false);
|
||||
if (!connectionAddress)
|
||||
if (!connectionAddress.Success)
|
||||
{
|
||||
_logger.FailedToDetermineConnectionUrl(connectionAddress.Error?.ToString());
|
||||
return connectionAddress.As<HighPerfSocketConnection<TUpdateType>>(null);
|
||||
_logger.FailedToDetermineConnectionUrl(connectionAddress.Error.ToString());
|
||||
return CallResult.Fail<HighPerfSocketConnection<TUpdateType>>(connectionAddress.Error);
|
||||
}
|
||||
|
||||
if (connectionAddress.Data != address)
|
||||
_logger.ConnectionAddressSetTo(connectionAddress.Data!);
|
||||
|
||||
// Create new socket connection
|
||||
var socketConnection = connectionFactory.CreateHighPerfConnection<TUpdateType>(_logger, SocketFactory, GetWebSocketParameters(connectionAddress.Data!), this, address);
|
||||
var socketConnection = connectionFactory.CreateHighPerfConnection<TUpdateType>(_logger, SocketFactory, GetWebSocketParameters(connectionAddress.Data!), this);
|
||||
foreach (var ptg in PeriodicTaskRegistrations)
|
||||
socketConnection.QueryPeriodic(ptg.Identifier, ptg.Interval, (con) => ptg.QueryDelegate(con).Request);
|
||||
|
||||
return new CallResult<HighPerfSocketConnection<TUpdateType>>(socketConnection);
|
||||
return CallResult.Ok(socketConnection);
|
||||
}
|
||||
|
||||
private bool IsConnectionEligible(SocketConnection socketConnection, int individualSubscriptionCount, bool maxConnectionsReached)
|
||||
{
|
||||
var currentIndividualSubscriptionCount = socketConnection.Subscriptions.Sum(x => x.IndividualSubscriptionCount);
|
||||
bool lessThanBatchSubCombineTarget = socketConnection.UserSubscriptionCount < ClientOptions.SocketSubscriptionsCombineTarget;
|
||||
// Include the incoming batch so batched subscriptions cannot overshoot the configured socket target.
|
||||
bool lessThanIndividualSubCombineTarget = currentIndividualSubscriptionCount + individualSubscriptionCount <= ClientOptions.SocketIndividualSubscriptionCombineTarget;
|
||||
|
||||
if ((!lessThanBatchSubCombineTarget || !lessThanIndividualSubCombineTarget)
|
||||
&& !maxConnectionsReached)
|
||||
return false;
|
||||
|
||||
return MaxIndividualSubscriptionsPerConnection == null
|
||||
|| currentIndividualSubscriptionCount + individualSubscriptionCount <= MaxIndividualSubscriptionsPerConnection;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -791,7 +823,7 @@ namespace CryptoExchange.Net.Clients
|
||||
protected virtual async Task<CallResult> ConnectSocketAsync(ISocketConnection socketConnection, CancellationToken ct)
|
||||
{
|
||||
var connectResult = await socketConnection.ConnectAsync(ct).ConfigureAwait(false);
|
||||
if (connectResult)
|
||||
if (connectResult.Success)
|
||||
{
|
||||
if (socketConnection is SocketConnection sc)
|
||||
_socketConnections.TryAdd(socketConnection.SocketId, sc);
|
||||
@@ -914,15 +946,15 @@ namespace CryptoExchange.Net.Clients
|
||||
foreach (var item in DedicatedConnectionConfigs)
|
||||
{
|
||||
var socketResult = await GetSocketConnection(item.SocketAddress, item.Authenticated, true, CancellationToken.None).ConfigureAwait(false);
|
||||
if (!socketResult)
|
||||
return socketResult.AsDataless();
|
||||
if (!socketResult.Success)
|
||||
return CallResult.Fail(socketResult.Error);
|
||||
|
||||
var connectResult = await ConnectIfNeededAsync(socketResult.Data, item.Authenticated, default).ConfigureAwait(false);
|
||||
if (!connectResult)
|
||||
return new CallResult(connectResult.Error!);
|
||||
if (!connectResult.Success)
|
||||
return CallResult.Fail(connectResult.Error!);
|
||||
}
|
||||
|
||||
return CallResult.SuccessResult;
|
||||
return CallResult.Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1004,23 +1036,28 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <summary>
|
||||
/// Dispose the client
|
||||
/// </summary>
|
||||
public override void Dispose()
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
_disposing = true;
|
||||
var tasks = new List<Task>();
|
||||
if (Interlocked.CompareExchange(ref _isDisposed, 1, 0) == 0)
|
||||
{
|
||||
var socketList = _socketConnections.Values.Where(x => x.UserSubscriptionCount > 0 || x.Connected);
|
||||
if (socketList.Any())
|
||||
_logger.DisposingSocketClient();
|
||||
if (!disposing)
|
||||
return;
|
||||
|
||||
foreach (var connection in socketList)
|
||||
var tasks = new List<Task>();
|
||||
{
|
||||
tasks.Add(connection.CloseAsync());
|
||||
}
|
||||
}
|
||||
var socketList = _socketConnections.Values.Where(x => x.UserSubscriptionCount > 0 || x.Connected);
|
||||
if (socketList.Any())
|
||||
_logger.DisposingSocketClient();
|
||||
|
||||
semaphoreSlim?.Dispose();
|
||||
base.Dispose();
|
||||
foreach (var connection in socketList)
|
||||
{
|
||||
tasks.Add(connection.CloseAsync());
|
||||
}
|
||||
}
|
||||
|
||||
semaphoreSlim?.Dispose();
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1071,11 +1108,13 @@ namespace CryptoExchange.Net.Clients
|
||||
/// ctor
|
||||
/// </summary>
|
||||
protected SocketApiClient(
|
||||
ILogger logger,
|
||||
ILoggerFactory? loggerFactory,
|
||||
string exchangeName,
|
||||
string baseAddress,
|
||||
SocketExchangeOptions<TEnvironment> options,
|
||||
SocketApiOptions apiOptions) : base(
|
||||
logger,
|
||||
loggerFactory,
|
||||
exchangeName,
|
||||
baseAddress,
|
||||
options,
|
||||
apiOptions)
|
||||
@@ -1101,11 +1140,13 @@ namespace CryptoExchange.Net.Clients
|
||||
/// ctor
|
||||
/// </summary>
|
||||
protected SocketApiClient(
|
||||
ILogger logger,
|
||||
ILoggerFactory? loggerFactory,
|
||||
string exchangeName,
|
||||
string baseAddress,
|
||||
SocketExchangeOptions<TEnvironment, TApiCredentials> options,
|
||||
SocketApiOptions apiOptions) : base(
|
||||
logger,
|
||||
loggerFactory,
|
||||
exchangeName,
|
||||
baseAddress,
|
||||
options,
|
||||
apiOptions)
|
||||
@@ -1132,13 +1173,18 @@ namespace CryptoExchange.Net.Clients
|
||||
where TApiCredentials : ApiCredentials
|
||||
where TEnvironment : TradeEnvironment
|
||||
{
|
||||
|
||||
private bool _authProviderInitialized = false;
|
||||
private TAuthenticationProvider? _authenticationProvider;
|
||||
/// <summary>
|
||||
/// Auth provider initialized field
|
||||
/// </summary>
|
||||
protected bool _authProviderInitialized = false;
|
||||
/// <summary>
|
||||
/// Auth provider field
|
||||
/// </summary>
|
||||
protected TAuthenticationProvider? _authenticationProvider;
|
||||
/// <summary>
|
||||
/// The authentication provider for this API client. (null if no credentials are set)
|
||||
/// </summary>
|
||||
public TAuthenticationProvider? AuthenticationProvider
|
||||
public virtual TAuthenticationProvider? AuthenticationProvider
|
||||
{
|
||||
get
|
||||
{
|
||||
@@ -1152,7 +1198,7 @@ namespace CryptoExchange.Net.Clients
|
||||
|
||||
return _authenticationProvider;
|
||||
}
|
||||
internal set => _authenticationProvider = value;
|
||||
protected internal set => _authenticationProvider = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -1162,11 +1208,13 @@ namespace CryptoExchange.Net.Clients
|
||||
/// ctor
|
||||
/// </summary>
|
||||
protected SocketApiClient(
|
||||
ILogger logger,
|
||||
ILoggerFactory? loggerFactory,
|
||||
string exchangeName,
|
||||
string baseAddress,
|
||||
SocketExchangeOptions<TEnvironment, TApiCredentials> options,
|
||||
SocketApiOptions apiOptions) : base(
|
||||
logger,
|
||||
loggerFactory,
|
||||
exchangeName,
|
||||
baseAddress,
|
||||
options,
|
||||
apiOptions)
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Interfaces.Clients;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.Clients
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public abstract class UserClientProvider<TRestClient, TSocketClient, TRestOptions, TSocketOptions, TCredentials, TEnvironment>
|
||||
where TRestClient : IRestClient<TCredentials>
|
||||
where TSocketClient : ISocketClient<TCredentials>
|
||||
where TRestOptions : RestExchangeOptions<TEnvironment, TCredentials>, new()
|
||||
where TSocketOptions : SocketExchangeOptions<TEnvironment, TCredentials>, new()
|
||||
where TCredentials : ApiCredentials
|
||||
where TEnvironment : TradeEnvironment
|
||||
{
|
||||
private ConcurrentDictionary<string, TRestClient> _restClients = new ConcurrentDictionary<string, TRestClient>();
|
||||
private ConcurrentDictionary<string, TSocketClient> _socketClients = new ConcurrentDictionary<string, TSocketClient>();
|
||||
|
||||
private readonly IOptions<TRestOptions> _restOptions;
|
||||
private readonly IOptions<TSocketOptions> _socketOptions;
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly ILoggerFactory? _loggerFactory;
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract string ExchangeName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public UserClientProvider(
|
||||
HttpClient? httpClient,
|
||||
ILoggerFactory? loggerFactory,
|
||||
IOptions<TRestOptions> restOptions,
|
||||
IOptions<TSocketOptions> socketOptions)
|
||||
{
|
||||
_httpClient = httpClient ?? new HttpClient();
|
||||
_httpClient.Timeout = restOptions.Value.RequestTimeout;
|
||||
_loggerFactory = loggerFactory;
|
||||
_restOptions = restOptions;
|
||||
_socketOptions = socketOptions;
|
||||
}
|
||||
|
||||
|
||||
private IOptions<TRestOptions> SetRestEnvironment(IOptions<TRestOptions> options, TEnvironment? environment)
|
||||
{
|
||||
if (environment == null)
|
||||
return options;
|
||||
|
||||
var newRestClientOptions = new TRestOptions();
|
||||
options.Value.Set(newRestClientOptions);
|
||||
newRestClientOptions.Environment = environment;
|
||||
return Options.Create(newRestClientOptions);
|
||||
}
|
||||
|
||||
private IOptions<TSocketOptions> SetSocketEnvironment(IOptions<TSocketOptions> options, TEnvironment? environment)
|
||||
{
|
||||
if (environment == null)
|
||||
return options;
|
||||
|
||||
var newSocketClientOptions = new TSocketOptions();
|
||||
options.Value.Set(newSocketClientOptions);
|
||||
newSocketClientOptions.Environment = environment;
|
||||
return Options.Create(newSocketClientOptions);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void InitializeUserClient(string userIdentifier, TCredentials credentials, TEnvironment? environment = null)
|
||||
{
|
||||
CreateRestClient(userIdentifier, credentials, environment);
|
||||
CreateSocketClient(userIdentifier, credentials, environment);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public TRestClient GetRestClient(string userIdentifier, TCredentials? credentials = null, TEnvironment? environment = null)
|
||||
{
|
||||
if (!_restClients.TryGetValue(userIdentifier, out var client) || client.Disposed)
|
||||
client = CreateRestClient(userIdentifier, credentials, environment);
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public TSocketClient GetSocketClient(string userIdentifier, TCredentials? credentials = null, TEnvironment? environment = null)
|
||||
{
|
||||
if (!_socketClients.TryGetValue(userIdentifier, out var client) || client.Disposed)
|
||||
client = CreateSocketClient(userIdentifier, credentials, environment);
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
private TRestClient CreateRestClient(string userIdentifier, TCredentials? credentials, TEnvironment? environment)
|
||||
{
|
||||
var clientRestOptions = SetRestEnvironment(_restOptions, environment);
|
||||
var client = ConstructRestClient(_httpClient, _loggerFactory, clientRestOptions);
|
||||
if (credentials != null)
|
||||
{
|
||||
_restClients[userIdentifier] = client;
|
||||
client.SetApiCredentials(credentials);
|
||||
}
|
||||
return client;
|
||||
}
|
||||
|
||||
private TSocketClient CreateSocketClient(string userIdentifier, TCredentials? credentials, TEnvironment? environment)
|
||||
{
|
||||
var clientSocketOptions = SetSocketEnvironment(_socketOptions, environment);
|
||||
var client = ConstructSocketClient(_loggerFactory, clientSocketOptions);
|
||||
if (credentials != null)
|
||||
{
|
||||
_socketClients[userIdentifier] = client;
|
||||
client.SetApiCredentials(credentials);
|
||||
}
|
||||
return client;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new instance of the rest client
|
||||
/// </summary>
|
||||
protected abstract TRestClient ConstructRestClient(
|
||||
HttpClient client,
|
||||
ILoggerFactory? loggerFactory,
|
||||
IOptions<TRestOptions> options);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new instance of the socket client
|
||||
/// </summary>
|
||||
protected abstract TSocketClient ConstructSocketClient(
|
||||
ILoggerFactory? loggerFactory,
|
||||
IOptions<TSocketOptions> options);
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ClearUserClients(string userIdentifier)
|
||||
{
|
||||
_restClients.TryRemove(userIdentifier, out var restClient);
|
||||
_socketClients.TryRemove(userIdentifier, out var socketClient);
|
||||
restClient?.Dispose();
|
||||
socketClient?.Dispose();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Clear()
|
||||
{
|
||||
foreach (var client in _restClients.Values)
|
||||
client.Dispose();
|
||||
_restClients.Clear();
|
||||
|
||||
foreach (var client in _socketClients.Values)
|
||||
client.Dispose();
|
||||
_socketClients.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the provided options delegate to a new instance of the specified type.
|
||||
/// </summary>
|
||||
protected static T ApplyOptionsDelegate<T>(Action<T>? del) where T : new()
|
||||
{
|
||||
var opts = new T();
|
||||
del?.Invoke(opts);
|
||||
return opts;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -56,14 +56,26 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
if (reader.TokenType == JsonTokenType.False)
|
||||
return false;
|
||||
|
||||
var value = reader.TokenType switch
|
||||
if (reader.TokenType == JsonTokenType.Number)
|
||||
{
|
||||
JsonTokenType.String => reader.GetString(),
|
||||
JsonTokenType.Number => reader.GetInt16().ToString(),
|
||||
_ => null
|
||||
};
|
||||
var number = reader.GetInt16();
|
||||
if (number >= 1)
|
||||
return true;
|
||||
|
||||
value = value?.ToLowerInvariant().Trim();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (reader.TokenType == JsonTokenType.Null)
|
||||
{
|
||||
if (typeToConvert == typeof(bool))
|
||||
LibraryHelpers.StaticLogger?.LogWarning("Received null bool value, but property type is not a nullable bool. Resolver: {Resolver}", options.TypeInfoResolver?.GetType()?.Name);
|
||||
return default;
|
||||
}
|
||||
|
||||
if (reader.TokenType != JsonTokenType.String)
|
||||
throw new SerializationException($"Can't convert bool value for token type {reader.TokenType}");
|
||||
|
||||
var value = reader.GetString()?.ToLowerInvariant().Trim();
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
if (typeToConvert == typeof(bool))
|
||||
@@ -73,12 +85,14 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
|
||||
switch (value)
|
||||
{
|
||||
case "enabled":
|
||||
case "true":
|
||||
case "yes":
|
||||
case "y":
|
||||
case "1":
|
||||
case "on":
|
||||
return true;
|
||||
case "disabled":
|
||||
case "false":
|
||||
case "no":
|
||||
case "n":
|
||||
@@ -88,7 +102,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
return false;
|
||||
}
|
||||
|
||||
throw new SerializationException($"Can't convert bool value {value}");
|
||||
throw new SerializationException($"Can't convert bool value, unknown string value: {value}");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -16,17 +16,19 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
private const long _ticksPerSecond = TimeSpan.TicksPerMillisecond * 1000;
|
||||
private const decimal _ticksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000m;
|
||||
private const decimal _ticksPerNanosecond = TimeSpan.TicksPerMillisecond / 1000m / 1000;
|
||||
private static Type _dateTimeType = typeof(DateTime);
|
||||
private static Type _nullableDateTimeType = typeof(DateTime?);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanConvert(Type typeToConvert)
|
||||
{
|
||||
return typeToConvert == typeof(DateTime) || typeToConvert == typeof(DateTime?);
|
||||
return typeToConvert == _dateTimeType || typeToConvert == _nullableDateTimeType;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
return typeToConvert == typeof(DateTime) ? new DateTimeConverterInner() : new NullableDateTimeConverterInner();
|
||||
return typeToConvert == _dateTimeType ? new DateTimeConverterInner() : new NullableDateTimeConverterInner();
|
||||
}
|
||||
|
||||
private class NullableDateTimeConverterInner : JsonConverter<DateTime?>
|
||||
@@ -68,7 +70,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.Null)
|
||||
{
|
||||
if (typeToConvert == typeof(DateTime))
|
||||
if (typeToConvert == _dateTimeType)
|
||||
LibraryHelpers.StaticLogger?.LogWarning("DateTime value of null, but property is not nullable. Resolver: {Resolver}", options.TypeInfoResolver?.GetType()?.Name);
|
||||
return default;
|
||||
}
|
||||
@@ -76,7 +78,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
if (reader.TokenType is JsonTokenType.Number)
|
||||
{
|
||||
var decValue = reader.GetDecimal();
|
||||
if (decValue == 0 || decValue < 0)
|
||||
if (decValue <= 0)
|
||||
return default;
|
||||
|
||||
return ParseFromDecimal(decValue);
|
||||
@@ -86,8 +88,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
var stringValue = reader.GetString();
|
||||
if (string.IsNullOrWhiteSpace(stringValue)
|
||||
|| stringValue!.Equals("-1", StringComparison.Ordinal)
|
||||
|| stringValue!.Equals("0001-01-01T00:00:00Z", StringComparison.OrdinalIgnoreCase)
|
||||
|| decimal.TryParse(stringValue, out var decVal) && decVal == 0)
|
||||
|| stringValue!.Equals("0001-01-01T00:00:00Z", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
@@ -124,7 +125,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
/// <summary>
|
||||
/// Parse a string value to datetime
|
||||
/// </summary>
|
||||
public static DateTime ParseFromString(string stringValue, string? resolverName)
|
||||
public static DateTime? ParseFromString(string stringValue, string? resolverName)
|
||||
{
|
||||
if (stringValue!.Length == 12 && stringValue.StartsWith("202", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
@@ -197,6 +198,9 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc);
|
||||
}
|
||||
|
||||
if (stringValue.EndsWith("+00:00:00"))
|
||||
return DateTime.Parse(stringValue.Substring(0, stringValue.Length - 9), CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal);
|
||||
|
||||
return DateTime.Parse(stringValue, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -88,7 +88,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers
|
||||
stream.Seek(0, SeekOrigin.Begin);
|
||||
var written = new StreamReader(stream).ReadBlock(dataSnippet, 0, _errorResponseSnippetLimit);
|
||||
var data = new string(dataSnippet, 0, written);
|
||||
errorMsg += $": {data}";
|
||||
errorMsg += $": {(string.IsNullOrEmpty(data) ? "(empty)" : data)}";
|
||||
if (data.Length == _errorResponseSnippetLimit)
|
||||
errorMsg += " (truncated)";
|
||||
}
|
||||
|
||||
@@ -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.2.0</PackageVersion>
|
||||
<AssemblyVersion>11.2.0</AssemblyVersion>
|
||||
<FileVersion>11.2.0</FileVersion>
|
||||
<PackageVersion>12.4.0</PackageVersion>
|
||||
<AssemblyVersion>12.4.0</AssemblyVersion>
|
||||
<FileVersion>12.4.0</FileVersion>
|
||||
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
||||
<PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange;CryptoExchange.Net</PackageTags>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
|
||||
@@ -311,16 +311,16 @@ namespace CryptoExchange.Net
|
||||
/// <param name="request">The request parameters</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
/// <returns></returns>
|
||||
public static async IAsyncEnumerable<ExchangeWebResult<T[]>> ExecutePages<T, U>(Func<U, PageRequest?, CancellationToken, Task<ExchangeWebResult<T[]>>> paginatedFunc, U request, [EnumeratorCancellation]CancellationToken ct = default)
|
||||
public static async IAsyncEnumerable<HttpResult<T[]>> ExecutePages<T, U>(Func<U, PageRequest?, CancellationToken, Task<HttpResult<T[]>>> paginatedFunc, U request, [EnumeratorCancellation]CancellationToken ct = default)
|
||||
{
|
||||
var result = new List<T>();
|
||||
ExchangeWebResult<T[]> batch;
|
||||
HttpResult<T[]> batch;
|
||||
PageRequest? nextPageToken = null;
|
||||
while (true)
|
||||
{
|
||||
batch = await paginatedFunc(request, nextPageToken, ct).ConfigureAwait(false);
|
||||
yield return batch;
|
||||
if (!batch || ct.IsCancellationRequested)
|
||||
if (!batch.Success || ct.IsCancellationRequested)
|
||||
break;
|
||||
|
||||
result.AddRange(batch.Data);
|
||||
@@ -399,8 +399,8 @@ namespace CryptoExchange.Net
|
||||
/// <param name="asyncHandler">The async update handler</param>
|
||||
/// <param name="maxQueuedItems">The max number of updates to be queued up. When happens when the queue is full and a new write is attempted can be specified with <see>fullMode</see></param>
|
||||
/// <param name="fullBehavior">What should happen if the queue contains <see>maxQueuedItems</see> pending updates. If no max is set this setting is ignored</param>
|
||||
public static async Task<CallResult<UpdateSubscription>> ProcessQueuedAsync<T>(
|
||||
Func<Action<DataEvent<T>>, Task<CallResult<UpdateSubscription>>> subscribeCall,
|
||||
public static async Task<WebSocketResult<UpdateSubscription>> ProcessQueuedAsync<T>(
|
||||
Func<Action<DataEvent<T>>, Task<WebSocketResult<UpdateSubscription>>> subscribeCall,
|
||||
Func<DataEvent<T>, Task> asyncHandler,
|
||||
int? maxQueuedItems = null,
|
||||
QueueFullBehavior? fullBehavior = null)
|
||||
@@ -408,7 +408,7 @@ namespace CryptoExchange.Net
|
||||
var processor = new ProcessQueue<DataEvent<T>>(asyncHandler, maxQueuedItems, fullBehavior);
|
||||
await processor.StartAsync().ConfigureAwait(false);
|
||||
var result = await subscribeCall(upd => processor.Write(upd)).ConfigureAwait(false);
|
||||
if (!result)
|
||||
if (!result.Success)
|
||||
{
|
||||
await processor.StopAsync().ConfigureAwait(false);
|
||||
return result;
|
||||
@@ -473,7 +473,7 @@ namespace CryptoExchange.Net
|
||||
}, maxQueuedItems, fullBehavior);
|
||||
await processor.StartAsync().ConfigureAwait(false);
|
||||
var result = await subscribeCall(processor).ConfigureAwait(false);
|
||||
if (!result)
|
||||
if (!result.Success)
|
||||
{
|
||||
await processor.StopAsync().ConfigureAwait(false);
|
||||
return result;
|
||||
@@ -499,7 +499,7 @@ namespace CryptoExchange.Net
|
||||
return null;
|
||||
|
||||
// Try parse, only fails for these reasons:
|
||||
// 1. string is null or empty
|
||||
// 1. string is null or empty (already covered)
|
||||
// 2. value is larger or smaller than decimal max/min
|
||||
// 3. unparsable format
|
||||
if (decimal.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var decValue))
|
||||
@@ -516,7 +516,7 @@ namespace CryptoExchange.Net
|
||||
if (string.Equals("Infinity", value, StringComparison.OrdinalIgnoreCase))
|
||||
return decimal.MaxValue;
|
||||
else if(string.Equals("-Infinity", value, StringComparison.OrdinalIgnoreCase))
|
||||
return decimal.MinValue;
|
||||
return decimal.MinValue;
|
||||
|
||||
if (value!.Length > 27 && decimal.TryParse(value.Substring(0, 27), out var overflowValue))
|
||||
{
|
||||
|
||||
@@ -11,85 +11,92 @@ namespace CryptoExchange.Net
|
||||
/// </summary>
|
||||
public static class ExchangeSymbolCache
|
||||
{
|
||||
private static ConcurrentDictionary<string, ExchangeInfo> _symbolInfos = new ConcurrentDictionary<string, ExchangeInfo>();
|
||||
private static ConcurrentDictionary<string, ExchangeKeyedCache> _symbolInfos = new ConcurrentDictionary<string, ExchangeKeyedCache>();
|
||||
|
||||
/// <summary>
|
||||
/// Update the cached symbol data for an exchange
|
||||
/// </summary>
|
||||
/// <param name="topicId">Id for the provided data</param>
|
||||
/// <param name="environment">Trading environment</param>
|
||||
/// <param name="key">Optional data set key</param>
|
||||
/// <param name="updateData">Symbol data</param>
|
||||
public static void UpdateSymbolInfo(string topicId, SharedSpotSymbol[] updateData)
|
||||
public static void UpdateSymbolInfo(string topicId, string environment, string? key, SharedSpotSymbol[] updateData)
|
||||
{
|
||||
if(!_symbolInfos.TryGetValue(topicId, out var exchangeInfo))
|
||||
var id = topicId + environment;
|
||||
if(!_symbolInfos.TryGetValue(id, out var exchangeInfo))
|
||||
{
|
||||
exchangeInfo = new ExchangeInfo(DateTime.UtcNow, updateData.ToDictionary(x => x.Name, x => x.SharedSymbol));
|
||||
_symbolInfos.TryAdd(topicId, exchangeInfo);
|
||||
exchangeInfo = new ExchangeKeyedCache();
|
||||
_symbolInfos.TryAdd(id, exchangeInfo);
|
||||
}
|
||||
|
||||
if (DateTime.UtcNow - exchangeInfo.UpdateTime < TimeSpan.FromMinutes(60))
|
||||
var keyedCache = exchangeInfo.Get(key);
|
||||
if (keyedCache != null && DateTime.UtcNow - keyedCache.UpdateTime < TimeSpan.FromMinutes(60))
|
||||
return;
|
||||
|
||||
_symbolInfos[topicId] = new ExchangeInfo(DateTime.UtcNow, updateData.ToDictionary(x => x.Name, x => x.SharedSymbol));
|
||||
exchangeInfo.Set(key, new ExchangeInfo(DateTime.UtcNow, updateData.ToDictionary(x => x.Name, x => x)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether the specific topic has been cached
|
||||
/// </summary>
|
||||
/// <param name="topicId">Id</param>
|
||||
public static bool HasCached(string topicId)
|
||||
/// <param name="environment">Trading environment</param>
|
||||
/// <param name="key">Optional data set key</param>
|
||||
public static bool HasCached(string topicId, string environment, string? key)
|
||||
{
|
||||
if (!_symbolInfos.TryGetValue(topicId, out var exchangeInfo))
|
||||
var id = topicId + environment;
|
||||
if (!_symbolInfos.TryGetValue(id, out var exchangeInfo))
|
||||
return false;
|
||||
|
||||
return exchangeInfo.Symbols.Count > 0;
|
||||
return exchangeInfo.HasCached(key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether a specific exchange(topic) support the provided symbol
|
||||
/// </summary>
|
||||
/// <param name="topicId">Id for the provided data</param>
|
||||
/// <param name="environment">Trading environment</param>
|
||||
/// <param name="key">Optional data set key</param>
|
||||
/// <param name="symbolName">The symbol name</param>
|
||||
public static bool SupportsSymbol(string topicId, string symbolName)
|
||||
public static bool SupportsSymbol(string topicId, string environment, string? key, string symbolName)
|
||||
{
|
||||
if (!_symbolInfos.TryGetValue(topicId, out var exchangeInfo))
|
||||
var id = topicId + environment;
|
||||
if (!_symbolInfos.TryGetValue(id, out var exchangeInfo))
|
||||
return false;
|
||||
|
||||
if (!exchangeInfo.Symbols.TryGetValue(symbolName, out var symbolInfo))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
return exchangeInfo.SupportsSymbol(key, symbolName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether a specific exchange(topic) support the provided symbol
|
||||
/// </summary>
|
||||
/// <param name="topicId">Id for the provided data</param>
|
||||
/// <param name="environment">Trading environment</param>
|
||||
/// <param name="key">Optional data set key</param>
|
||||
/// <param name="symbol">The symbol info</param>
|
||||
public static bool SupportsSymbol(string topicId, SharedSymbol symbol)
|
||||
public static bool SupportsSymbol(string topicId, string environment, string? key, SharedSymbol symbol)
|
||||
{
|
||||
if (!_symbolInfos.TryGetValue(topicId, out var exchangeInfo))
|
||||
var id = topicId + environment;
|
||||
if (!_symbolInfos.TryGetValue(id, out var exchangeInfo))
|
||||
return false;
|
||||
|
||||
return exchangeInfo.Symbols.Any(x =>
|
||||
x.Value.TradingMode == symbol.TradingMode
|
||||
&& x.Value.BaseAsset == symbol.BaseAsset
|
||||
&& x.Value.QuoteAsset == symbol.QuoteAsset);
|
||||
return exchangeInfo.SupportsSymbol(key, symbol);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all symbols for a specific base asset
|
||||
/// </summary>
|
||||
/// <param name="topicId">Id for the provided data</param>
|
||||
/// <param name="environment">Trading environment</param>
|
||||
/// <param name="key">Optional data set key</param>
|
||||
/// <param name="baseAsset">Base asset name</param>
|
||||
public static SharedSymbol[] GetSymbolsForBaseAsset(string topicId, string baseAsset)
|
||||
public static SharedSymbol[] GetSymbolsForBaseAsset(string topicId, string environment, string? key, string baseAsset)
|
||||
{
|
||||
if (!_symbolInfos.TryGetValue(topicId, out var exchangeInfo))
|
||||
var id = topicId + environment;
|
||||
if (!_symbolInfos.TryGetValue(id, out var exchangeInfo))
|
||||
return [];
|
||||
|
||||
return exchangeInfo.Symbols
|
||||
.Where(x => x.Value.BaseAsset.Equals(baseAsset, StringComparison.InvariantCultureIgnoreCase))
|
||||
.Select(x => x.Value)
|
||||
.ToArray();
|
||||
return exchangeInfo.GetSymbolsForBaseAsset(key, baseAsset);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -97,29 +104,261 @@ namespace CryptoExchange.Net
|
||||
/// </summary>
|
||||
/// <param name="topicId">Id for the provided data</param>
|
||||
/// <param name="symbolName">Symbol name</param>
|
||||
public static SharedSymbol? ParseSymbol(string topicId, string? symbolName)
|
||||
/// <param name="environment">Trade environment</param>
|
||||
/// <param name="key">Additional data set identification key</param>
|
||||
public static SharedSymbol? ParseSymbol(string topicId, string environment, string? key, string? symbolName)
|
||||
{
|
||||
if (symbolName == null)
|
||||
return null;
|
||||
|
||||
if (!_symbolInfos.TryGetValue(topicId, out var exchangeInfo))
|
||||
var id = topicId + environment;
|
||||
if (!_symbolInfos.TryGetValue(id, out var exchangeInfo))
|
||||
return null;
|
||||
|
||||
if (!exchangeInfo.Symbols.TryGetValue(symbolName, out var symbolInfo))
|
||||
return null;
|
||||
|
||||
return new SharedSymbol(symbolInfo.TradingMode, symbolInfo.BaseAsset, symbolInfo.QuoteAsset, symbolName)
|
||||
{
|
||||
DeliverTime = symbolInfo.DeliverTime
|
||||
};
|
||||
return exchangeInfo.ParseSymbol(key, symbolName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a symbol catalog for a specific exchange(topic) and environment. Only available if <see cref="UpdateSymbolInfo(string, string, string?, SharedSpotSymbol[])"/> has been called previously.
|
||||
/// </summary>
|
||||
/// <param name="exchange">Exchange name</param>
|
||||
/// <param name="topicId">Id for the provided data</param>
|
||||
/// <param name="environmentName">Trade environment</param>
|
||||
/// <param name="key">Additional data set identification key</param>
|
||||
public static SharedSymbolCatalog? GetSymbolCatalog(string exchange, string topicId, string environmentName, string? key)
|
||||
{
|
||||
var id = topicId + environmentName;
|
||||
if (!_symbolInfos.TryGetValue(id, out var exchangeInfo))
|
||||
return null;
|
||||
|
||||
return exchangeInfo.GetSymbolCatalog(exchange, key);
|
||||
}
|
||||
|
||||
class ExchangeKeyedCache
|
||||
{
|
||||
private ExchangeInfo? _noKeyCache;
|
||||
private ConcurrentDictionary<string, ExchangeInfo> _keyedCache = new ConcurrentDictionary<string, ExchangeInfo>();
|
||||
|
||||
public ExchangeInfo? Get(string? key)
|
||||
{
|
||||
if (key == null)
|
||||
return _noKeyCache;
|
||||
|
||||
if (_keyedCache.TryGetValue(key, out var exchangeInfo))
|
||||
return exchangeInfo;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void Set(string? key, ExchangeInfo exchangeInfo)
|
||||
{
|
||||
if (key == null)
|
||||
_noKeyCache = exchangeInfo;
|
||||
else
|
||||
_keyedCache[key] = exchangeInfo;
|
||||
}
|
||||
|
||||
public bool HasCached(string? key)
|
||||
{
|
||||
if (key == null)
|
||||
{
|
||||
if (_noKeyCache?.Symbols.Count > 0)
|
||||
return true;
|
||||
|
||||
foreach (var cache in _keyedCache.Values)
|
||||
{
|
||||
if (cache.Symbols.Count > 0)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return _keyedCache.TryGetValue(key, out var exchangeInfo) && exchangeInfo.Symbols.Count > 0;
|
||||
}
|
||||
|
||||
public SharedSymbol? ParseSymbol(string? key, string symbolName)
|
||||
{
|
||||
SharedSpotSymbol? symbolInfo = null;
|
||||
if (key == null)
|
||||
{
|
||||
if (_noKeyCache != null)
|
||||
{
|
||||
if (!_noKeyCache.Symbols.TryGetValue(symbolName, out symbolInfo))
|
||||
return null;
|
||||
|
||||
return new SharedSymbol(symbolInfo.TradingMode, symbolInfo.BaseAsset, symbolInfo.QuoteAsset, symbolName)
|
||||
{
|
||||
DeliverTime = (symbolInfo as SharedFuturesSymbol)?.DeliveryTime
|
||||
};
|
||||
}
|
||||
|
||||
foreach(var cache in _keyedCache.Values)
|
||||
{
|
||||
if (cache.Symbols.TryGetValue(symbolName, out symbolInfo))
|
||||
{
|
||||
return new SharedSymbol(symbolInfo.TradingMode, symbolInfo.BaseAsset, symbolInfo.QuoteAsset, symbolName)
|
||||
{
|
||||
DeliverTime = (symbolInfo as SharedFuturesSymbol)?.DeliveryTime
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
var hasKeyedSet = _keyedCache.TryGetValue(key, out var exchangeInfo);
|
||||
if (!hasKeyedSet || exchangeInfo == null)
|
||||
return null;
|
||||
|
||||
if (exchangeInfo.Symbols.TryGetValue(symbolName, out symbolInfo))
|
||||
{
|
||||
return new SharedSymbol(symbolInfo.TradingMode, symbolInfo.BaseAsset, symbolInfo.QuoteAsset, symbolName)
|
||||
{
|
||||
DeliverTime = (symbolInfo as SharedFuturesSymbol)?.DeliveryTime
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public bool SupportsSymbol(string? key, string symbolName)
|
||||
{
|
||||
if (key == null)
|
||||
{
|
||||
if (_noKeyCache?.Symbols.ContainsKey(symbolName) == true)
|
||||
return true;
|
||||
|
||||
foreach(var cache in _keyedCache.Values)
|
||||
{
|
||||
if (cache.Symbols.ContainsKey(symbolName))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return _keyedCache.TryGetValue(key, out var exchangeInfo) && exchangeInfo.Symbols.ContainsKey(symbolName);
|
||||
}
|
||||
|
||||
public bool SupportsSymbol(string? key, SharedSymbol symbol)
|
||||
{
|
||||
if (key == null)
|
||||
{
|
||||
if (_noKeyCache?.Symbols.Any(x =>
|
||||
x.Value.TradingMode == symbol.TradingMode
|
||||
&& x.Value.BaseAsset == symbol.BaseAsset
|
||||
&& x.Value.QuoteAsset == symbol.QuoteAsset) == true)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach (var cache in _keyedCache.Values)
|
||||
{
|
||||
if (cache.Symbols.Any(x =>
|
||||
x.Value.TradingMode == symbol.TradingMode
|
||||
&& x.Value.BaseAsset == symbol.BaseAsset
|
||||
&& x.Value.QuoteAsset == symbol.QuoteAsset))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return _keyedCache.TryGetValue(key, out var exchangeInfo) && exchangeInfo.Symbols.Any(x =>
|
||||
x.Value.TradingMode == symbol.TradingMode
|
||||
&& x.Value.BaseAsset == symbol.BaseAsset
|
||||
&& x.Value.QuoteAsset == symbol.QuoteAsset);
|
||||
}
|
||||
|
||||
public SharedSymbol[] GetSymbolsForBaseAsset(string? key, string baseAsset)
|
||||
{
|
||||
if (key == null)
|
||||
{
|
||||
if (_noKeyCache != null)
|
||||
{
|
||||
return _noKeyCache.Symbols
|
||||
.Where(x => x.Value.BaseAsset.Equals(baseAsset, StringComparison.InvariantCultureIgnoreCase))
|
||||
.Select(x => x.Value.SharedSymbol)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
var result = new List<SharedSymbol>();
|
||||
foreach(var cache in _keyedCache.Values)
|
||||
{
|
||||
result.AddRange(cache.Symbols
|
||||
.Where(x => x.Value.BaseAsset.Equals(baseAsset, StringComparison.InvariantCultureIgnoreCase))
|
||||
.Select(x => x.Value.SharedSymbol));
|
||||
}
|
||||
|
||||
return result.ToArray();
|
||||
}
|
||||
|
||||
var hasKeyedSet = _keyedCache.TryGetValue(key, out var exchangeInfo);
|
||||
if (!hasKeyedSet || exchangeInfo == null)
|
||||
return [];
|
||||
|
||||
return exchangeInfo.Symbols
|
||||
.Where(x => x.Value.BaseAsset.Equals(baseAsset, StringComparison.InvariantCultureIgnoreCase))
|
||||
.Select(x => x.Value.SharedSymbol)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
internal SharedSymbolCatalog? GetSymbolCatalog(string exchange, string? key)
|
||||
{
|
||||
IEnumerable<SharedSpotSymbol> cachedSymbols;
|
||||
if (key == null)
|
||||
{
|
||||
if (_noKeyCache != null)
|
||||
cachedSymbols = _noKeyCache.Symbols.Values;
|
||||
else
|
||||
cachedSymbols = _keyedCache.Values.SelectMany(x => x.Symbols.Values);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!_keyedCache.TryGetValue(key, out var exchangeInfo) || exchangeInfo == null)
|
||||
return null;
|
||||
|
||||
cachedSymbols = exchangeInfo.Symbols.Values;
|
||||
}
|
||||
|
||||
var assets = new Dictionary<string, SharedAssetInfo>();
|
||||
var symbols = new Dictionary<string, SharedSpotSymbol>();
|
||||
foreach (var symbol in cachedSymbols)
|
||||
{
|
||||
if (!assets.TryGetValue(symbol.BaseAsset, out var baseAssetInfo))
|
||||
{
|
||||
baseAssetInfo = new SharedAssetInfo(symbol.BaseAsset, symbol.BaseAssetType, symbol.BaseAssetSubType);
|
||||
assets.Add(symbol.BaseAsset, baseAssetInfo);
|
||||
}
|
||||
|
||||
if (!assets.TryGetValue(symbol.QuoteAsset, out var quoteAssetInfo))
|
||||
{
|
||||
quoteAssetInfo = new SharedAssetInfo(symbol.QuoteAsset, symbol.QuoteAssetType, symbol.QuoteAssetSubType);
|
||||
assets.Add(symbol.QuoteAsset, quoteAssetInfo);
|
||||
}
|
||||
|
||||
symbols.Add(symbol.Name, symbol);
|
||||
}
|
||||
|
||||
return new SharedSymbolCatalog
|
||||
{
|
||||
Exchange = exchange,
|
||||
Assets = assets,
|
||||
Symbols = symbols
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class ExchangeInfo
|
||||
{
|
||||
public DateTime UpdateTime { get; set; }
|
||||
public Dictionary<string, SharedSymbol> Symbols { get; set; }
|
||||
public Dictionary<string, SharedSpotSymbol> Symbols { get; set; }
|
||||
|
||||
public ExchangeInfo(DateTime updateTime, Dictionary<string, SharedSymbol> symbols)
|
||||
public ExchangeInfo(DateTime updateTime, Dictionary<string, SharedSpotSymbol> symbols)
|
||||
{
|
||||
UpdateTime = updateTime;
|
||||
Symbols = symbols;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System;
|
||||
@@ -24,7 +25,7 @@ namespace CryptoExchange.Net
|
||||
/// <param name="parameters"></param>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
public static void AddParameter(this Dictionary<string, object> parameters, string key, string value)
|
||||
public static void AddParameter(this IDictionary<string, object> parameters, string key, string value)
|
||||
{
|
||||
parameters.Add(key, value);
|
||||
}
|
||||
@@ -35,7 +36,7 @@ namespace CryptoExchange.Net
|
||||
/// <param name="parameters"></param>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
public static void AddParameter(this Dictionary<string, object> parameters, string key, object value)
|
||||
public static void AddParameter(this IDictionary<string, object> parameters, string key, object value)
|
||||
{
|
||||
parameters.Add(key, value);
|
||||
}
|
||||
@@ -46,7 +47,7 @@ namespace CryptoExchange.Net
|
||||
/// <param name="parameters"></param>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
public static void AddOptionalParameter(this Dictionary<string, object> parameters, string key, object? value)
|
||||
public static void AddOptionalParameter(this IDictionary<string, object> parameters, string key, object? value)
|
||||
{
|
||||
if (value != null)
|
||||
parameters.Add(key, value);
|
||||
@@ -378,8 +379,6 @@ namespace CryptoExchange.Net
|
||||
services.AddTransient(x => (IDepositRestClient)client(x)!);
|
||||
if (typeof(IKlineRestClient).IsAssignableFrom(typeof(T)))
|
||||
services.AddTransient(x => (IKlineRestClient)client(x)!);
|
||||
if (typeof(IListenKeyRestClient).IsAssignableFrom(typeof(T)))
|
||||
services.AddTransient(x => (IListenKeyRestClient)client(x)!);
|
||||
if (typeof(IOrderBookRestClient).IsAssignableFrom(typeof(T)))
|
||||
services.AddTransient(x => (IOrderBookRestClient)client(x)!);
|
||||
if (typeof(IRecentTradeRestClient).IsAssignableFrom(typeof(T)))
|
||||
|
||||
@@ -10,6 +10,10 @@ namespace CryptoExchange.Net.Interfaces.Clients
|
||||
/// </summary>
|
||||
public interface IBaseApiClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Exchange name
|
||||
/// </summary>
|
||||
string Exchange { get; }
|
||||
/// <summary>
|
||||
/// Base address
|
||||
/// </summary>
|
||||
|
||||
@@ -28,6 +28,11 @@ namespace CryptoExchange.Net.Interfaces.Clients
|
||||
/// </summary>
|
||||
bool Authenticated { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Configured credentials
|
||||
/// </summary>
|
||||
TApiCredentials? ApiCredentials { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Set the API credentials for this API client
|
||||
/// </summary>
|
||||
|
||||
@@ -84,6 +84,12 @@ namespace CryptoExchange.Net.Interfaces.Clients
|
||||
/// Whether or not API credentials have been configured for this client. Does not check the credentials are actually valid.
|
||||
/// </summary>
|
||||
bool Authenticated { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Configured credentials
|
||||
/// </summary>
|
||||
TApiCredentials? ApiCredentials { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Set the API credentials for this API client
|
||||
/// </summary>
|
||||
|
||||
@@ -107,7 +107,7 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// </summary>
|
||||
/// <param name="ct">A cancellation token to stop the order book when canceled</param>
|
||||
/// <returns></returns>
|
||||
Task<CallResult<bool>> StartAsync(CancellationToken? ct = null);
|
||||
Task<CallResult> StartAsync(CancellationToken? ct = null);
|
||||
|
||||
/// <summary>
|
||||
/// Stop syncing the order book
|
||||
|
||||
@@ -24,8 +24,9 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// <param name="interval">Kline interval</param>
|
||||
/// <param name="limit">The max amount of klines to retain</param>
|
||||
/// <param name="period">The max period the data should be retained</param>
|
||||
/// <param name="exchangeParameters">Exchange parameters</param>
|
||||
/// <returns></returns>
|
||||
IKlineTracker CreateKlineTracker(SharedSymbol symbol, SharedKlineInterval interval, int? limit = null, TimeSpan? period = null);
|
||||
IKlineTracker CreateKlineTracker(SharedSymbol symbol, SharedKlineInterval interval, int? limit = null, TimeSpan? period = null, ExchangeParameters? exchangeParameters = null);
|
||||
|
||||
/// <summary>
|
||||
/// Whether the factory supports creating a TradeTracker instance for this symbol
|
||||
@@ -39,7 +40,8 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// <param name="symbol">The symbol</param>
|
||||
/// <param name="limit">The max amount of trades to retain</param>
|
||||
/// <param name="period">The max period the data should be retained</param>
|
||||
/// <param name="exchangeParameters">Exchange parameters</param>
|
||||
/// <returns></returns>
|
||||
ITradeTracker CreateTradeTracker(SharedSymbol symbol, int? limit = null, TimeSpan? period = null);
|
||||
ITradeTracker CreateTradeTracker(SharedSymbol symbol, int? limit = null, TimeSpan? period = null, ExchangeParameters? exchangeParameters = null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ using CryptoExchange.Net.Objects.Options;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO.Pipelines;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
|
||||
@@ -14,6 +16,61 @@ namespace CryptoExchange.Net
|
||||
/// </summary>
|
||||
public static class LibraryHelpers
|
||||
{
|
||||
private static readonly HashSet<string> _stableCoins = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
// USD
|
||||
"USDT", "USDC", "DAI", "FDUSD", "USDE", "TUSD", "USDP", "PYUSD", "GUSD",
|
||||
"USDD", "LUSD", "USDJ", "SUSD", "ZUSD", "BUSD", "USTC", "USDX", "USDK",
|
||||
"CUSD", "USD1", "USD0", "XUSD", "BFUSD", "USDS", "RLUSD", "OUSD", "USDH",
|
||||
"APXUSD", "USDQ", "USDPT", "FIDD", "AUSD",
|
||||
// EUR
|
||||
"EURS", "EURC", "EURI", "EURT", "AGEUR", "CEUR", "AEUR", "EURQ", "EUROP",
|
||||
// Other
|
||||
"CNYT", // CNY
|
||||
"CREAL", "BRL1", // BRL
|
||||
"XSGD", // SGD
|
||||
"GYEN", // JPY
|
||||
"KGST", // KGS
|
||||
"QCAD", // CAD
|
||||
"TGBP", // GBP
|
||||
"AUDX", // AUD
|
||||
"MXNB", // MXN
|
||||
};
|
||||
|
||||
private static readonly HashSet<string> _commodities = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
// Metals
|
||||
"XAU", "XAUT", "XAG", "XPT", "XPD", "COPPER", "PAXG", "XNI", "XCU", "XAL", "GOLD", "SILVER",
|
||||
// Energy
|
||||
"BZ", "NATGAS", "NGAS", "CL", "XTI", "UKOIL", "USOIL", "BRENTOIL"
|
||||
};
|
||||
|
||||
private static readonly HashSet<string> _stocks = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
// Top stocks, will need to update periodically
|
||||
"AAAU", "AADR", "AAPL", "ACWI", "ACWX", "AGG", "AMD", "AMLP", "AMZN", "ARKF",
|
||||
"ARKG", "ARKK", "ARKQ", "ARKW", "AVGO", "BA", "BABA", "BND", "BNDX", "BOTZ",
|
||||
"CIBR", "COIN", "DIA", "DIVB", "DVY", "EEM", "EFA", "EFAV", "ESGU", "EWG",
|
||||
"EWJ", "EWT", "EWU", "EWW", "EWY", "EWZ", "FDN", "FEZ", "GLDM", "GOOGL",
|
||||
"HDV", "HOOD", "HYG", "IAU", "IBB", "ICLN", "IEFA", "IEMG", "IGSB", "IJH",
|
||||
"IJR", "INTC", "ITOT", "IUSB", "IUSG", "IUSV", "IWM", "IWO", "IWR", "IYR",
|
||||
"JETS", "JPM", "LIT", "MCHI", "META", "MGK", "MSTR", "MTUM", "MU", "NET",
|
||||
"NFLX", "NOBL", "NVDA", "OIH", "ORCL", "PAVE", "PBW", "PLTR", "QQQ", "QQQM",
|
||||
"SCHB", "SCHD", "SCHF", "SCHG", "SCHH", "SCHV", "SCHX", "SKHY", "SPCX", "SPCXD",
|
||||
"SPLG", "SPY", "SPYG", "SPYV", "SQQQ", "TSLA", "TSM", "TQQQ", "USMV", "VBR",
|
||||
"VCIT", "VCSH", "VEA", "VEU", "VGIT", "VGK", "VGT", "VHT", "VIG", "VNQ",
|
||||
"VOO", "VOT", "VTI", "VTV", "VUG", "VXUS", "XBI", "XLC", "XLE", "XLF",
|
||||
"XLI", "XLK", "XLP", "XLU", "XLV", "XLY", "CSCO", "UBER", "MRVL", "RKLB",
|
||||
"COHR", "SOXL", "HD", "DIS", "CBRS", "V", "BRKB", "FLNC", "LLY", "COST",
|
||||
"ARM", "BMNR", "NBIS", "ASML", "AAOI", "GLW", "SHLD", "BE", "QNTX", "IBM",
|
||||
"AMAT", "NOK", "ASTS", "BBX", "SLX", "SKHYNIX", "SAMSUNG", "HYUNDAI", "NVO",
|
||||
"IREN", "ONDS", "CRM" , "VRT", "ZEST", "BTW", "HPE", "AXTI", "BX", "CRWD",
|
||||
"CRDO", "NOW", "ZM", "DKNG", "RIVN", "URNM", "EBAY", "ADBE", "UVXY", "RDW",
|
||||
"CIEN","PANW", "WIN", "PAYP", "HIMS", "CRWV", "QCOM", "LITE", "DRAM", "ANTHROPIC",
|
||||
"OPENAI", "USAR", "BILL", "SNDK", "NASDAQ100", "SPX500", "BSB", "CRCL", "STRC",
|
||||
"MSFT", "WDC"
|
||||
};
|
||||
|
||||
private static ILogger? _staticLogger;
|
||||
/// <summary>
|
||||
/// Static logger
|
||||
@@ -105,6 +162,67 @@ namespace CryptoExchange.Net
|
||||
return _defaultClientReferences.TryGetValue(key, out var id) ? id : throw new KeyNotFoundException($"{exchange} not found in configuration");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check whether an asset is a known stablecoin. Note that this is not definitive, only large known stocks are checked
|
||||
/// </summary>
|
||||
/// <param name="asset">Asset name</param>
|
||||
/// <param name="additionalStableCoins">Additional stablecoin names for the specific exchange</param>
|
||||
public static bool IsStableCoin(string asset, params HashSet<string> additionalStableCoins)
|
||||
{
|
||||
if (string.IsNullOrEmpty(asset))
|
||||
return false;
|
||||
|
||||
return _stableCoins.Contains(asset) || (additionalStableCoins != null && additionalStableCoins.Contains(asset, StringComparer.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check whether an asset is a known commodity. Note that this is not definitive, only large known stocks are checked
|
||||
/// </summary>
|
||||
/// <param name="asset">Asset name</param>
|
||||
/// <param name="additionalCommodities">Additional commodity names for the specific exchange</param>
|
||||
public static bool IsCommodity(string asset, params HashSet<string> additionalCommodities)
|
||||
{
|
||||
if (string.IsNullOrEmpty(asset))
|
||||
return false;
|
||||
|
||||
return _commodities.Contains(asset) || (additionalCommodities != null && additionalCommodities.Contains(asset, StringComparer.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check whether an asset is a known stock. Note that this is not definitive, only large known stocks are checked
|
||||
/// </summary>
|
||||
/// <param name="asset">Asset name</param>
|
||||
/// <param name="additionalStocks">Additional stock names for the specific exchange</param>
|
||||
public static bool IsEquity(string asset, params HashSet<string> additionalStocks)
|
||||
=> IsEquity(asset, [], additionalStocks);
|
||||
|
||||
/// <summary>
|
||||
/// Check whether an asset is a known stock.
|
||||
/// </summary>
|
||||
/// <param name="asset">Asset name</param>
|
||||
/// <param name="potentialSuffixes">Suffixes to check, for example when `X` is a potential suffix both `TSLA` and `TSLAX` will be checked</param>
|
||||
/// <param name="additionalStocks">Additional stock names for the specific exchange</param>
|
||||
public static bool IsEquity(string asset, string[] potentialSuffixes, params HashSet<string> additionalStocks)
|
||||
{
|
||||
if (string.IsNullOrEmpty(asset))
|
||||
return false;
|
||||
|
||||
if (_stocks.Contains(asset) || (additionalStocks != null && additionalStocks.Contains(asset, StringComparer.OrdinalIgnoreCase)))
|
||||
return true;
|
||||
|
||||
foreach (var suffix in potentialSuffixes)
|
||||
{
|
||||
if (!asset.EndsWith(suffix))
|
||||
continue;
|
||||
|
||||
var suffixAsset = asset.Substring(0, asset.Length - suffix.Length);
|
||||
if (_stocks.Contains(suffixAsset) || (additionalStocks != null && additionalStocks.Contains(suffixAsset, StringComparer.OrdinalIgnoreCase)))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new HttpMessageHandler instance
|
||||
/// </summary>
|
||||
|
||||
@@ -18,32 +18,32 @@ namespace CryptoExchange.Net.Logging.Extensions
|
||||
_rateLimitRequestFailed = LoggerMessage.Define<int, string, string, string>(
|
||||
LogLevel.Warning,
|
||||
new EventId(6000, "RateLimitRequestFailed"),
|
||||
"[Req {Id}] Call to {Path} failed because of ratelimit guard {Guard}; {Limit}");
|
||||
"[Req {Id}] call to {Path} failed because of ratelimit guard {Guard}; {Limit}");
|
||||
|
||||
_rateLimitConnectionFailed = LoggerMessage.Define<int, string, string>(
|
||||
LogLevel.Warning,
|
||||
new EventId(6001, "RateLimitConnectionFailed"),
|
||||
"[Sckt {Id}] Connection failed because of ratelimit guard {Guard}; {Limit}");
|
||||
"[Sckt {Id}] connection failed because of ratelimit guard {Guard}; {Limit}");
|
||||
|
||||
_rateLimitDelayingRequest = LoggerMessage.Define<int, string, TimeSpan, string, string>(
|
||||
LogLevel.Warning,
|
||||
new EventId(6002, "RateLimitDelayingRequest"),
|
||||
"[Req {Id}] Delaying call to {Path} by {Delay} because of ratelimit guard {Guard}; {Limit}");
|
||||
"[Req {Id}] delaying call to {Path} by {Delay} because of ratelimit guard {Guard}; {Limit}");
|
||||
|
||||
_rateLimitDelayingConnection = LoggerMessage.Define<int, TimeSpan, string, string>(
|
||||
LogLevel.Warning,
|
||||
new EventId(6003, "RateLimitDelayingConnection"),
|
||||
"[Sckt {Id}] Delaying connection by {Delay} because of ratelimit guard {Guard}; {Limit}");
|
||||
"[Sckt {Id}] delaying connection by {Delay} because of ratelimit guard {Guard}; {Limit}");
|
||||
|
||||
_rateLimitAppliedConnection = LoggerMessage.Define<int, string, string, int>(
|
||||
LogLevel.Trace,
|
||||
new EventId(6004, "RateLimitDelayingConnection"),
|
||||
"[Sckt {Id}] Connection passed ratelimit guard {Guard}; {Limit}, New count: {Current}");
|
||||
"[Sckt {Id}] connection passed ratelimit guard {Guard}; {Limit}, New count: {Current}");
|
||||
|
||||
_rateLimitAppliedRequest = LoggerMessage.Define<int, string, string, string, int>(
|
||||
LogLevel.Trace,
|
||||
new EventId(6005, "RateLimitAppliedRequest"),
|
||||
"[Req {Id}] Call to {Path} passed ratelimit guard {Guard}; {Limit}, New count: {Current}");
|
||||
"[Req {Id}] call to {Path} passed ratelimit guard {Guard}; {Limit}, New count: {Current}");
|
||||
}
|
||||
|
||||
public static void RateLimitRequestFailed(this ILogger logger, int requestId, string path, string guard, string limit)
|
||||
|
||||
@@ -28,67 +28,67 @@ namespace CryptoExchange.Net.Logging.Extensions
|
||||
_restApiErrorReceived = LoggerMessage.Define<int?, int?, long, string?, string?>(
|
||||
LogLevel.Warning,
|
||||
new EventId(4000, "RestApiErrorReceived"),
|
||||
"[Req {RequestId}] {ResponseStatusCode} - Error received in {ResponseTime}ms: {ErrorMessage}, Data: {OriginalData}");
|
||||
"[Req {RequestId}] {ResponseStatusCode} - error received in {ResponseTime}ms: {ErrorMessage}, Data: {OriginalData}");
|
||||
|
||||
_restApiResponseReceived = LoggerMessage.Define<int?, int?, long, string?>(
|
||||
LogLevel.Debug,
|
||||
new EventId(4001, "RestApiResponseReceived"),
|
||||
"[Req {RequestId}] {ResponseStatusCode} - Response received in {ResponseTime}ms: {OriginalData}");
|
||||
"[Req {RequestId}] {ResponseStatusCode} - response received in {ResponseTime}ms: {OriginalData}");
|
||||
|
||||
_restApiFailedToSyncTime = LoggerMessage.Define<int, string>(
|
||||
LogLevel.Debug,
|
||||
new EventId(4002, "RestApiFailedToSyncTime"),
|
||||
"[Req {RequestId}] Failed to sync time, aborting request: {ErrorMessage}");
|
||||
"[Req {RequestId}] failed to sync time, aborting request: {ErrorMessage}");
|
||||
|
||||
_restApiNoApiCredentials = LoggerMessage.Define<int, string>(
|
||||
LogLevel.Warning,
|
||||
new EventId(4003, "RestApiNoApiCredentials"),
|
||||
"[Req {RequestId}] Request {RestApiUri} failed because no ApiCredentials were provided");
|
||||
"[Req {RequestId}] request {RestApiUri} failed because no ApiCredentials were provided");
|
||||
|
||||
_restApiCreatingRequest = LoggerMessage.Define<int, Uri>(
|
||||
LogLevel.Information,
|
||||
new EventId(4004, "RestApiCreatingRequest"),
|
||||
"[Req {RequestId}] Creating request for {RestApiUri}");
|
||||
"[Req {RequestId}] creating request for {RestApiUri}");
|
||||
|
||||
_restApiSendingRequest = LoggerMessage.Define<int, HttpMethod, string, Uri, string>(
|
||||
LogLevel.Trace,
|
||||
new EventId(4005, "RestApiSendingRequest"),
|
||||
"[Req {RequestId}] Sending {Method} {Signed} request to {RestApiUri}{Query}");
|
||||
"[Req {RequestId}] sending {Method} {Signed} request to {RestApiUri}{Query}");
|
||||
|
||||
_restApiRateLimitRetry = LoggerMessage.Define<int, DateTime>(
|
||||
LogLevel.Warning,
|
||||
new EventId(4006, "RestApiRateLimitRetry"),
|
||||
"[Req {RequestId}] Received ratelimit error, retrying after {Timestamp}");
|
||||
"[Req {RequestId}] received ratelimit error, retrying after {Timestamp}");
|
||||
|
||||
_restApiRateLimitPauseUntil = LoggerMessage.Define<int, DateTime>(
|
||||
LogLevel.Warning,
|
||||
new EventId(4007, "RestApiRateLimitPauseUntil"),
|
||||
"[Req {RequestId}] Ratelimit error from server, pausing requests until {Until}");
|
||||
"[Req {RequestId}] ratelimit error from server, pausing requests until {Until}");
|
||||
|
||||
_restApiSendRequest = LoggerMessage.Define<int, RequestDefinition, string?, string, string>(
|
||||
LogLevel.Debug,
|
||||
new EventId(4008, "RestApiSendRequest"),
|
||||
"[Req {RequestId}] Sending {Definition} request with body {Body}, query parameters {Query} and headers {Headers}");
|
||||
"[Req {RequestId}] sending {Definition} request with body {Body}, query parameters {Query} and headers {Headers}");
|
||||
|
||||
_restApiCheckingCache = LoggerMessage.Define<string>(
|
||||
LogLevel.Trace,
|
||||
new EventId(4009, "RestApiCheckingCache"),
|
||||
"Checking cache for key {Key}");
|
||||
"checking cache for key {Key}");
|
||||
|
||||
_restApiCacheHit = LoggerMessage.Define<string>(
|
||||
LogLevel.Trace,
|
||||
new EventId(4010, "RestApiCacheHit"),
|
||||
"Cache hit for key {Key}");
|
||||
"cache hit for key {Key}");
|
||||
|
||||
_restApiCacheNotHit = LoggerMessage.Define<string>(
|
||||
LogLevel.Trace,
|
||||
new EventId(4011, "RestApiCacheNotHit"),
|
||||
"Cache not hit for key {Key}");
|
||||
"cache not hit for key {Key}");
|
||||
|
||||
_restApiCancellationRequested = LoggerMessage.Define<int?>(
|
||||
LogLevel.Debug,
|
||||
new EventId(4012, "RestApiCancellationRequested"),
|
||||
"[Req {RequestId}] Request cancelled by user");
|
||||
"[Req {RequestId}] request cancelled by user");
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ namespace CryptoExchange.Net.Logging.Extensions
|
||||
_attemptingToAuthenticate = LoggerMessage.Define<int>(
|
||||
LogLevel.Debug,
|
||||
new EventId(3006, "AttemptingToAuthenticate"),
|
||||
"[Sckt {SocketId}] Attempting to authenticate");
|
||||
"[Sckt {SocketId}] attempting to authenticate");
|
||||
|
||||
_authenticationFailed = LoggerMessage.Define<int>(
|
||||
LogLevel.Warning,
|
||||
@@ -76,12 +76,12 @@ namespace CryptoExchange.Net.Logging.Extensions
|
||||
_failedToDetermineConnectionUrl = LoggerMessage.Define<string?>(
|
||||
LogLevel.Warning,
|
||||
new EventId(3009, "FailedToDetermineConnectionUrl"),
|
||||
"Failed to determine connection url: {ErrorMessage}");
|
||||
"failed to determine connection url: {ErrorMessage}");
|
||||
|
||||
_connectionAddressSetTo = LoggerMessage.Define<string>(
|
||||
LogLevel.Debug,
|
||||
new EventId(3010, "ConnectionAddressSetTo"),
|
||||
"Connection address set to {ConnectionAddress}");
|
||||
"connection address set to {ConnectionAddress}");
|
||||
|
||||
_socketCreatedForAddress = LoggerMessage.Define<int, string>(
|
||||
LogLevel.Debug,
|
||||
@@ -91,37 +91,37 @@ namespace CryptoExchange.Net.Logging.Extensions
|
||||
_unsubscribingAll = LoggerMessage.Define<int>(
|
||||
LogLevel.Information,
|
||||
new EventId(3013, "UnsubscribingAll"),
|
||||
"Unsubscribing all {SubscriptionCount} subscriptions");
|
||||
"unsubscribing all {SubscriptionCount} subscriptions");
|
||||
|
||||
_disposingSocketClient = LoggerMessage.Define(
|
||||
LogLevel.Debug,
|
||||
new EventId(3015, "DisposingSocketClient"),
|
||||
"Disposing socket client, closing all subscriptions");
|
||||
"disposing socket client, closing all subscriptions");
|
||||
|
||||
_unsubscribingSubscription = LoggerMessage.Define<int, int>(
|
||||
LogLevel.Information,
|
||||
new EventId(3016, "UnsubscribingSubscription"),
|
||||
"[Sckt {SocketId}] Unsubscribing subscription {SubscriptionId}");
|
||||
"[Sckt {SocketId}] unsubscribing subscription {SubscriptionId}");
|
||||
|
||||
_reconnectingAllConnections = LoggerMessage.Define<int>(
|
||||
LogLevel.Information,
|
||||
new EventId(3017, "ReconnectingAll"),
|
||||
"Reconnecting all {ConnectionCount} connections");
|
||||
"reconnecting all {ConnectionCount} connections");
|
||||
|
||||
_addingRetryAfterGuard = LoggerMessage.Define<DateTime>(
|
||||
LogLevel.Warning,
|
||||
new EventId(3018, "AddRetryAfterGuard"),
|
||||
"Adding RetryAfterGuard ({RetryAfter}) because the connection attempt was rate limited");
|
||||
"adding RetryAfterGuard ({RetryAfter}) because the connection attempt was rate limited");
|
||||
|
||||
_timeoutWaitingForReconnectingSocket = LoggerMessage.Define(
|
||||
LogLevel.Debug,
|
||||
new EventId(3019, "TimeoutWaitingForReconnectingSocket"),
|
||||
"Timeout while waiting for existing socket reconnection, failing request");
|
||||
"timeout while waiting for existing socket reconnection, failing request");
|
||||
|
||||
_waitedForReconnectingSocket = LoggerMessage.Define<long>(
|
||||
LogLevel.Trace,
|
||||
new EventId(3020, "WaitedForReconnectingSocket"),
|
||||
"Waited for reconnecting socket for {Timespan}ms");
|
||||
"waited for reconnecting socket for {Timespan}ms");
|
||||
}
|
||||
|
||||
public static void FailedToAddSubscriptionRetryOnDifferentConnection(this ILogger logger, int socketId)
|
||||
|
||||
@@ -60,7 +60,7 @@ namespace CryptoExchange.Net.Logging.Extensions
|
||||
_unknownExceptionWhileProcessingReconnection = LoggerMessage.Define<int>(
|
||||
LogLevel.Warning,
|
||||
new EventId(2003, "UnknownExceptionWhileProcessingReconnection"),
|
||||
"[Sckt {SocketId}] Unknown exception while processing reconnection, reconnecting again");
|
||||
"[Sckt {SocketId}] unknown exception while processing reconnection, reconnecting again");
|
||||
|
||||
_webSocketErrorCodeAndDetails = LoggerMessage.Define<int, WebSocketError, string?>(
|
||||
LogLevel.Warning,
|
||||
@@ -180,7 +180,7 @@ namespace CryptoExchange.Net.Logging.Extensions
|
||||
_receivedMessageNotMatchedToAnyListener = LoggerMessage.Define<int, string, string, string>(
|
||||
LogLevel.Warning,
|
||||
new EventId(2029, "ReceivedMessageNotMatchedToAnyListener"),
|
||||
"[Sckt {SocketId}] received message not matched to any listener. TypeIdentifier: {TypeIdentifier}, ListenId: {ListenId}, current listeners: [{ListenIds}]");
|
||||
"[Sckt {SocketId}] received message not matched to any listener. TypeIdentifier: {TypeIdentifier}, TopicFilter: {ListenId}, registered TopicFilters for type: [{TopicFilters}]");
|
||||
|
||||
_failedToParse = LoggerMessage.Define<int, string>(
|
||||
LogLevel.Warning,
|
||||
@@ -326,9 +326,9 @@ namespace CryptoExchange.Net.Logging.Extensions
|
||||
_sendingData(logger, socketId, requestId, data, null);
|
||||
}
|
||||
|
||||
public static void ReceivedMessageNotMatchedToAnyListener(this ILogger logger, int socketId, string typeIdentifier, string listenId, string listenIds)
|
||||
public static void ReceivedMessageNotMatchedToAnyListener(this ILogger logger, int socketId, string typeIdentifier, string topicFilter, string topicFilters)
|
||||
{
|
||||
_receivedMessageNotMatchedToAnyListener(logger, socketId, typeIdentifier, listenId, listenIds, null);
|
||||
_receivedMessageNotMatchedToAnyListener(logger, socketId, typeIdentifier, topicFilter, topicFilters, null);
|
||||
}
|
||||
|
||||
public static void SendingByteData(this ILogger logger, int socketId, int requestId, int length)
|
||||
|
||||
@@ -1,592 +0,0 @@
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.Objects
|
||||
{
|
||||
/// <summary>
|
||||
/// The result of an operation
|
||||
/// </summary>
|
||||
public class CallResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Static success result
|
||||
/// </summary>
|
||||
public static CallResult SuccessResult { get; } = new CallResult(null);
|
||||
|
||||
/// <summary>
|
||||
/// An error if the call didn't succeed, will always be filled if Success = false
|
||||
/// </summary>
|
||||
public Error? Error { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the call was successful
|
||||
/// </summary>
|
||||
public bool Success => Error == null;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="error"></param>
|
||||
public CallResult(Error? error)
|
||||
{
|
||||
Error = error;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Overwrite bool check so we can use if(callResult) instead of if(callResult.Success)
|
||||
/// </summary>
|
||||
/// <param name="obj"></param>
|
||||
public static implicit operator bool(CallResult obj)
|
||||
{
|
||||
return obj?.Success == true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return Success ? $"Success" : $"Error: {Error}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The result of an operation
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
public class CallResult<T>: CallResult
|
||||
{
|
||||
/// <summary>
|
||||
/// The data returned by the call, only available when Success = true
|
||||
/// </summary>
|
||||
public T Data { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// The original data returned by the call, only available when `OutputOriginalData` is set to `true` in the client options
|
||||
/// </summary>
|
||||
public string? OriginalData { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="data"></param>
|
||||
/// <param name="originalData"></param>
|
||||
/// <param name="error"></param>
|
||||
#pragma warning disable 8618
|
||||
public CallResult([AllowNull]T data, string? originalData, Error? error): base(error)
|
||||
#pragma warning restore 8618
|
||||
{
|
||||
OriginalData = originalData;
|
||||
#pragma warning disable 8601
|
||||
Data = data;
|
||||
#pragma warning restore 8601
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new data result
|
||||
/// </summary>
|
||||
/// <param name="data">The data to return</param>
|
||||
public CallResult(T data) : this(data, null, null) { }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new error result
|
||||
/// </summary>
|
||||
/// <param name="error">The error to return</param>
|
||||
public CallResult(Error error) : this(default, null, error) { }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new error result
|
||||
/// </summary>
|
||||
/// <param name="error">The error to return</param>
|
||||
/// <param name="originalData">The original response data</param>
|
||||
public CallResult(Error error, string? originalData) : this(default, originalData, error) { }
|
||||
|
||||
/// <summary>
|
||||
/// Overwrite bool check so we can use if(callResult) instead of if(callResult.Success)
|
||||
/// </summary>
|
||||
/// <param name="obj"></param>
|
||||
public static implicit operator bool(CallResult<T> obj)
|
||||
{
|
||||
return obj?.Success == true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether the call was successful or not. Useful for nullability checking.
|
||||
/// </summary>
|
||||
/// <param name="data">The data returned by the call.</param>
|
||||
/// <param name="error"><see cref="Error"/> on failure.</param>
|
||||
/// <returns><c>true</c> when <see cref="CallResult{T}"/> succeeded, <c>false</c> otherwise.</returns>
|
||||
public bool GetResultOrError([MaybeNullWhen(false)] out T data, [NotNullWhen(false)] out Error? error)
|
||||
{
|
||||
if (Success)
|
||||
{
|
||||
data = Data!;
|
||||
error = null;
|
||||
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
data = default;
|
||||
error = Error!;
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy the WebCallResult to a new data type
|
||||
/// </summary>
|
||||
/// <typeparam name="K">The new type</typeparam>
|
||||
/// <param name="data">The data of the new type</param>
|
||||
/// <returns></returns>
|
||||
public CallResult<K> As<K>([AllowNull] K data)
|
||||
{
|
||||
return new CallResult<K>(data, OriginalData, Error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy as a dataless result
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public CallResult AsDataless()
|
||||
{
|
||||
if (Error != null )
|
||||
return new CallResult(Error);
|
||||
|
||||
return SuccessResult;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy as a dataless result
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public CallResult AsDatalessError(Error error)
|
||||
{
|
||||
return new CallResult(error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy the CallResult to a new data type
|
||||
/// </summary>
|
||||
/// <typeparam name="K">The new type</typeparam>
|
||||
/// <param name="data">The data</param>
|
||||
/// <param name="error">The error returned</param>
|
||||
/// <returns></returns>
|
||||
public CallResult<K> AsErrorWithData<K>(Error error, K data)
|
||||
{
|
||||
return new CallResult<K>(data, OriginalData, error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy the WebCallResult to a new data type
|
||||
/// </summary>
|
||||
/// <typeparam name="K">The new type</typeparam>
|
||||
/// <param name="error">The error to return</param>
|
||||
/// <returns></returns>
|
||||
public CallResult<K> AsError<K>(Error error)
|
||||
{
|
||||
return new CallResult<K>(default, OriginalData, error);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return Success ? $"Success" : $"Error: {Error}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The result of a request
|
||||
/// </summary>
|
||||
public class WebCallResult : CallResult
|
||||
{
|
||||
/// <summary>
|
||||
/// The request http method
|
||||
/// </summary>
|
||||
public HttpMethod? RequestMethod { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// HTTP protocol version
|
||||
/// </summary>
|
||||
public Version? HttpVersion { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The headers sent with the request
|
||||
/// </summary>
|
||||
public HttpRequestHeaders? RequestHeaders { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The request id
|
||||
/// </summary>
|
||||
public int? RequestId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The url which was requested
|
||||
/// </summary>
|
||||
public string? RequestUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The body of the request
|
||||
/// </summary>
|
||||
public string? RequestBody { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The original data returned by the call, only available when `OutputOriginalData` is set to `true` in the client options
|
||||
/// </summary>
|
||||
public string? OriginalData { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// The status code of the response. Note that a OK status does not always indicate success, check the Success parameter for this.
|
||||
/// </summary>
|
||||
public HttpStatusCode? ResponseStatusCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The response headers
|
||||
/// </summary>
|
||||
public HttpResponseHeaders? ResponseHeaders { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The time between sending the request and receiving the response
|
||||
/// </summary>
|
||||
public TimeSpan? ResponseTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public WebCallResult(
|
||||
HttpStatusCode? code,
|
||||
Version? httpVersion,
|
||||
HttpResponseHeaders? responseHeaders,
|
||||
TimeSpan? responseTime,
|
||||
string? originalData,
|
||||
int? requestId,
|
||||
string? requestUrl,
|
||||
string? requestBody,
|
||||
HttpMethod? requestMethod,
|
||||
HttpRequestHeaders? requestHeaders,
|
||||
Error? error) : base(error)
|
||||
{
|
||||
ResponseStatusCode = code;
|
||||
HttpVersion = httpVersion;
|
||||
ResponseHeaders = responseHeaders;
|
||||
ResponseTime = responseTime;
|
||||
RequestId = requestId;
|
||||
OriginalData = originalData;
|
||||
|
||||
RequestUrl = requestUrl;
|
||||
RequestBody = requestBody;
|
||||
RequestHeaders = requestHeaders;
|
||||
RequestMethod = requestMethod;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="error"></param>
|
||||
public WebCallResult(Error error): base(error) { }
|
||||
|
||||
/// <summary>
|
||||
/// Return the result as an error result
|
||||
/// </summary>
|
||||
/// <param name="error">The error returned</param>
|
||||
/// <returns></returns>
|
||||
public WebCallResult AsError(Error error)
|
||||
{
|
||||
return new WebCallResult(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy the WebCallResult to a new data type
|
||||
/// </summary>
|
||||
/// <typeparam name="K">The new type</typeparam>
|
||||
/// <param name="data">The data of the new type</param>
|
||||
/// <returns></returns>
|
||||
public WebCallResult<K> As<K>([AllowNull] K data)
|
||||
{
|
||||
return new WebCallResult<K>(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, 0, null, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, ResultDataSource.Server, data, Error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy the WebCallResult to an ExchangeWebResult of a new data type
|
||||
/// </summary>
|
||||
/// <typeparam name="K">The new type</typeparam>
|
||||
/// <param name="exchange">The exchange</param>
|
||||
/// <param name="tradeMode">Trade mode the result applies to</param>
|
||||
/// <param name="data">The data</param>
|
||||
/// <returns></returns>
|
||||
public ExchangeWebResult<K> AsExchangeResult<K>(string exchange, TradingMode tradeMode, [AllowNull] K data)
|
||||
{
|
||||
return new ExchangeWebResult<K>(exchange, tradeMode, this.As<K>(data));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy the WebCallResult to an ExchangeWebResult of a new data type
|
||||
/// </summary>
|
||||
/// <typeparam name="K">The new type</typeparam>
|
||||
/// <param name="exchange">The exchange</param>
|
||||
/// <param name="tradeModes">Trade modes the result applies to</param>
|
||||
/// <param name="data">The data</param>
|
||||
/// <returns></returns>
|
||||
public ExchangeWebResult<K> AsExchangeResult<K>(string exchange, TradingMode[]? tradeModes, [AllowNull] K data)
|
||||
{
|
||||
return new ExchangeWebResult<K>(exchange, tradeModes, this.As<K>(data));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy the WebCallResult to a new data type
|
||||
/// </summary>
|
||||
/// <typeparam name="K">The new type</typeparam>
|
||||
/// <param name="error">The error returned</param>
|
||||
/// <returns></returns>
|
||||
public WebCallResult<K> AsError<K>(Error error)
|
||||
{
|
||||
return new WebCallResult<K>(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, 0, null, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, ResultDataSource.Server, default, error);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return (Success ? $"Success" : $"Error: {Error}") + $" in {ResponseTime}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The result of a request
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
public class WebCallResult<T>: CallResult<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// The request http method
|
||||
/// </summary>
|
||||
public HttpMethod? RequestMethod { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// HTTP protocol version
|
||||
/// </summary>
|
||||
public Version? HttpVersion { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The headers sent with the request
|
||||
/// </summary>
|
||||
public HttpRequestHeaders? RequestHeaders { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The request id
|
||||
/// </summary>
|
||||
public int? RequestId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The url which was requested
|
||||
/// </summary>
|
||||
public string? RequestUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The body of the request
|
||||
/// </summary>
|
||||
public string? RequestBody { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The status code of the response. Note that a OK status does not always indicate success, check the Success parameter for this.
|
||||
/// </summary>
|
||||
public HttpStatusCode? ResponseStatusCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Length in bytes of the response
|
||||
/// </summary>
|
||||
public long? ResponseLength { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The response headers
|
||||
/// </summary>
|
||||
public HttpResponseHeaders? ResponseHeaders { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The time between sending the request and receiving the response
|
||||
/// </summary>
|
||||
public TimeSpan? ResponseTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The data source of this result
|
||||
/// </summary>
|
||||
public ResultDataSource DataSource { get; set; } = ResultDataSource.Server;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new result
|
||||
/// </summary>
|
||||
public WebCallResult(
|
||||
HttpStatusCode? code,
|
||||
Version? httpVersion,
|
||||
HttpResponseHeaders? responseHeaders,
|
||||
TimeSpan? responseTime,
|
||||
long? responseLength,
|
||||
string? originalData,
|
||||
int? requestId,
|
||||
string? requestUrl,
|
||||
string? requestBody,
|
||||
HttpMethod? requestMethod,
|
||||
HttpRequestHeaders? requestHeaders,
|
||||
ResultDataSource dataSource,
|
||||
[AllowNull] T data,
|
||||
Error? error) : base(data, originalData, error)
|
||||
{
|
||||
HttpVersion = httpVersion;
|
||||
ResponseStatusCode = code;
|
||||
ResponseHeaders = responseHeaders;
|
||||
ResponseTime = responseTime;
|
||||
ResponseLength = responseLength;
|
||||
|
||||
RequestId = requestId;
|
||||
RequestUrl = requestUrl;
|
||||
RequestBody = requestBody;
|
||||
RequestHeaders = requestHeaders;
|
||||
RequestMethod = requestMethod;
|
||||
DataSource = dataSource;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy as a dataless result
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public new WebCallResult AsDataless()
|
||||
{
|
||||
return new WebCallResult(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, Error);
|
||||
}
|
||||
/// <summary>
|
||||
/// Copy as a dataless result
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public new WebCallResult AsDatalessError(Error error)
|
||||
{
|
||||
return new WebCallResult(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new error result
|
||||
/// </summary>
|
||||
/// <param name="error">The error</param>
|
||||
public WebCallResult(Error? error) : this(null, null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, default, error) { }
|
||||
|
||||
/// <summary>
|
||||
/// Copy the WebCallResult to a new data type
|
||||
/// </summary>
|
||||
/// <typeparam name="K">The new type</typeparam>
|
||||
/// <param name="data">The data of the new type</param>
|
||||
/// <returns></returns>
|
||||
public new WebCallResult<K> As<K>([AllowNull] K data)
|
||||
{
|
||||
return new WebCallResult<K>(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, data, Error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy the WebCallResult to a new data type
|
||||
/// </summary>
|
||||
/// <typeparam name="K">The new type</typeparam>
|
||||
/// <param name="error">The error returned</param>
|
||||
/// <returns></returns>
|
||||
public new WebCallResult<K> AsError<K>(Error error)
|
||||
{
|
||||
return new WebCallResult<K>(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, default, error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy the WebCallResult to a new data type
|
||||
/// </summary>
|
||||
/// <typeparam name="K">The new type</typeparam>
|
||||
/// <param name="data">The data</param>
|
||||
/// <param name="error">The error returned</param>
|
||||
/// <returns></returns>
|
||||
public new WebCallResult<K> AsErrorWithData<K>(Error error, K data)
|
||||
{
|
||||
return new WebCallResult<K>(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, data, error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy the WebCallResult to an ExchangeWebResult of a new data type
|
||||
/// </summary>
|
||||
/// <param name="exchange">The exchange</param>
|
||||
/// <param name="tradeMode">Trade mode the result applies to</param>
|
||||
/// <returns></returns>
|
||||
public ExchangeWebResult<T> AsExchangeResult(string exchange, TradingMode tradeMode)
|
||||
{
|
||||
return new ExchangeWebResult<T>(exchange, tradeMode, this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy the WebCallResult to an ExchangeWebResult of a new data type
|
||||
/// </summary>
|
||||
/// <param name="exchange">The exchange</param>
|
||||
/// <param name="tradeModes">Trade modes the result applies to</param>
|
||||
/// <returns></returns>
|
||||
public ExchangeWebResult<T> AsExchangeResult(string exchange, TradingMode[] tradeModes)
|
||||
{
|
||||
return new ExchangeWebResult<T>(exchange, tradeModes, this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy the WebCallResult to an ExchangeWebResult of a new data type
|
||||
/// </summary>
|
||||
/// <typeparam name="K">The new type</typeparam>
|
||||
/// <param name="exchange">The exchange</param>
|
||||
/// <param name="tradeMode">Trade mode the result applies to</param>
|
||||
/// <param name="data">Data</param>
|
||||
/// <param name="nextPageRequest">Next page request</param>
|
||||
/// <returns></returns>
|
||||
public ExchangeWebResult<K> AsExchangeResult<K>(string exchange, TradingMode tradeMode, [AllowNull] K data, PageRequest? nextPageRequest = null)
|
||||
{
|
||||
return new ExchangeWebResult<K>(exchange, tradeMode, As<K>(data), nextPageRequest);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy the WebCallResult to an ExchangeWebResult of a new data type
|
||||
/// </summary>
|
||||
/// <typeparam name="K">The new type</typeparam>
|
||||
/// <param name="exchange">The exchange</param>
|
||||
/// <param name="tradeModes">Trade modes the result applies to</param>
|
||||
/// <param name="data">Data</param>
|
||||
/// <param name="nextPageRequest">Next page token</param>
|
||||
/// <returns></returns>
|
||||
public ExchangeWebResult<K> AsExchangeResult<K>(string exchange, TradingMode[]? tradeModes, [AllowNull] K data, PageRequest? nextPageRequest = null)
|
||||
{
|
||||
return new ExchangeWebResult<K>(exchange, tradeModes, As<K>(data), nextPageRequest);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy the WebCallResult to an ExchangeWebResult with a specific error
|
||||
/// </summary>
|
||||
/// <typeparam name="K">The new type</typeparam>
|
||||
/// <param name="exchange">The exchange</param>
|
||||
/// <param name="error">The error returned</param>
|
||||
/// <returns></returns>
|
||||
public ExchangeWebResult<K> AsExchangeError<K>(string exchange, Error error)
|
||||
{
|
||||
return new ExchangeWebResult<K>(exchange, null, AsError<K>(error));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return a copy of this result with data source set to cache
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
internal WebCallResult<T> Cached()
|
||||
{
|
||||
return new WebCallResult<T>(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, ResultDataSource.Cache, Data, Error);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.Append(Success ? $"Success response" : $"Error response: {Error}");
|
||||
if (ResponseLength != null)
|
||||
sb.Append($", {ResponseLength} bytes");
|
||||
if (ResponseTime != null)
|
||||
sb.Append($", received in {Math.Round(ResponseTime?.TotalMilliseconds ?? 0)}ms");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -149,6 +149,14 @@ namespace CryptoExchange.Net.Objects
|
||||
/// </summary>
|
||||
public class ServerError : Error
|
||||
{
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public ServerError(ErrorType type, string message, Exception? exception = null)
|
||||
: base(null, new ErrorInfo(type, message), exception)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
|
||||
@@ -44,7 +44,7 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return $"RequestTimeout: {RequestTimeout}, Proxy: {(Proxy == null ? "-" : "set")}";
|
||||
return $"Proxy: {(Proxy == null ? "-" : "set")}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,6 +105,12 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
target.Environment = Environment;
|
||||
return target;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{base.ToString()} | Environment: {Environment.Name}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -131,7 +137,7 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{base.ToString()}, ApiCredentials: {(ApiCredentials == null ? "-" : "set")}";
|
||||
return $"{base.ToString()} | ApiCredentials: {(ApiCredentials == null ? "-" : "set")}";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -132,6 +132,12 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
target.Environment = Environment;
|
||||
return target;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{base.ToString()} | Environment: {Environment.Name}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -159,7 +165,7 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{base.ToString()}, ApiCredentials: {(ApiCredentials == null ? "-" : "set")}";
|
||||
return $"{base.ToString()} | ApiCredentials: {(ApiCredentials == null ? "-" : "set")}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,343 +0,0 @@
|
||||
using CryptoExchange.Net.Attributes;
|
||||
using CryptoExchange.Net.Converters.SystemTextJson;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
|
||||
namespace CryptoExchange.Net.Objects
|
||||
{
|
||||
/// <summary>
|
||||
/// Parameters collection
|
||||
/// </summary>
|
||||
public class ParameterCollection : Dictionary<string, object>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public new void Add(string key, object value)
|
||||
{
|
||||
if (value == null)
|
||||
throw new ArgumentNullException(key);
|
||||
|
||||
base.Add(key, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add an optional parameter. Not added if value is null
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
public void AddOptional(string key, object? value)
|
||||
{
|
||||
if (value != null)
|
||||
base.Add(key, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a decimal value as string
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
public void AddString(string key, decimal value)
|
||||
{
|
||||
base.Add(key, value.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a decimal value as string. Not added if value is null
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
public void AddOptionalString(string key, decimal? value)
|
||||
{
|
||||
if (value != null)
|
||||
base.Add(key, value.Value.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a int value as string
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
public void AddString(string key, int value)
|
||||
{
|
||||
base.Add(key, value.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a int value as string. Not added if value is null
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
public void AddOptionalString(string key, int? value)
|
||||
{
|
||||
if (value != null)
|
||||
base.Add(key, value.Value.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a long value as string
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
public void AddString(string key, long value)
|
||||
{
|
||||
base.Add(key, value.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a long value as string. Not added if value is null
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
public void AddOptionalString(string key, long? value)
|
||||
{
|
||||
if (value != null)
|
||||
base.Add(key, value.Value.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a DateTime value as string
|
||||
/// </summary>
|
||||
public void AddString(string key, DateTime value)
|
||||
{
|
||||
base.Add(key, value.ToString("yyyy-MM-ddTHH:mm:ssZ"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a DateTime value as string. Not added if value is null
|
||||
/// </summary>
|
||||
public void AddOptionalString(string key, DateTime? value)
|
||||
{
|
||||
if (value != null)
|
||||
base.Add(key, value.Value.ToString("yyyy-MM-ddTHH:mm:ssZ"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a datetime value as milliseconds timestamp
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
public void AddMilliseconds(string key, DateTime value)
|
||||
{
|
||||
base.Add(key, DateTimeConverter.ConvertToMilliseconds(value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a datetime value as milliseconds timestamp. Not added if value is null
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
public void AddOptionalMilliseconds(string key, DateTime? value)
|
||||
{
|
||||
if (value != null)
|
||||
base.Add(key, DateTimeConverter.ConvertToMilliseconds(value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a datetime value as milliseconds timestamp
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
public void AddMillisecondsString(string key, DateTime value)
|
||||
{
|
||||
base.Add(key, DateTimeConverter.ConvertToMilliseconds(value).Value.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a datetime value as milliseconds timestamp. Not added if value is null
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
public void AddOptionalMillisecondsString(string key, DateTime? value)
|
||||
{
|
||||
if (value != null)
|
||||
base.Add(key, DateTimeConverter.ConvertToMilliseconds(value).Value.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a datetime value as seconds timestamp
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
public void AddSeconds(string key, DateTime value)
|
||||
{
|
||||
base.Add(key, DateTimeConverter.ConvertToSeconds(value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a datetime value as seconds timestamp. Not added if value is null
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
public void AddOptionalSeconds(string key, DateTime? value)
|
||||
{
|
||||
if (value != null)
|
||||
base.Add(key, DateTimeConverter.ConvertToSeconds(value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a datetime value as string seconds timestamp
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
public void AddSecondsString(string key, DateTime value)
|
||||
{
|
||||
base.Add(key, DateTimeConverter.ConvertToSeconds(value).ToString()!);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a datetime value as string seconds timestamp. Not added if value is null
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
public void AddOptionalSecondsString(string key, DateTime? value)
|
||||
{
|
||||
if (value != null)
|
||||
base.Add(key, DateTimeConverter.ConvertToSeconds(value).ToString()!);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add an enum value as the string value as mapped using the <see cref="MapAttribute" />
|
||||
/// </summary>
|
||||
#if NET5_0_OR_GREATER
|
||||
public void AddEnum<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string key, T value)
|
||||
#else
|
||||
public void AddEnum<T>(string key, T value)
|
||||
#endif
|
||||
where T : struct, Enum
|
||||
{
|
||||
base.Add(key, EnumConverter<T>.GetString(value)!);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add an enum value as the string value as mapped using the <see cref="MapAttribute" />
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
#if NET5_0_OR_GREATER
|
||||
public void AddEnumAsInt<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string key, T value)
|
||||
#else
|
||||
public void AddEnumAsInt<T>(string key, T value)
|
||||
#endif
|
||||
where T : struct, Enum
|
||||
{
|
||||
var stringVal = EnumConverter<T>.GetString(value)!;
|
||||
base.Add(key, int.Parse(stringVal)!);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add an enum value as the string value as mapped using the <see cref="MapAttribute" />. Not added if value is null
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
#if NET5_0_OR_GREATER
|
||||
public void AddOptionalEnum<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string key, T? value)
|
||||
#else
|
||||
public void AddOptionalEnum<T>(string key, T? value)
|
||||
#endif
|
||||
where T : struct, Enum
|
||||
{
|
||||
if (value != null)
|
||||
base.Add(key, EnumConverter<T>.GetString(value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add an enum value as the string value as mapped using the <see cref="MapAttribute" />. Not added if value is null
|
||||
/// </summary>
|
||||
#if NET5_0_OR_GREATER
|
||||
public void AddOptionalEnumAsInt<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string key, T? value)
|
||||
#else
|
||||
public void AddOptionalEnumAsInt<T>(string key, T? value)
|
||||
#endif
|
||||
where T : struct, Enum
|
||||
{
|
||||
if (value != null)
|
||||
{
|
||||
var stringVal = EnumConverter<T>.GetString(value);
|
||||
base.Add(key, int.Parse(stringVal));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add key as comma separated values
|
||||
/// </summary>
|
||||
public void AddCommaSeparated(string key, IEnumerable<string> values)
|
||||
{
|
||||
base.Add(key, string.Join(",", values));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add key as comma separated values if there are values provided
|
||||
/// </summary>
|
||||
public void AddOptionalCommaSeparated(string key, IEnumerable<string>? values)
|
||||
{
|
||||
if (values == null || !values.Any())
|
||||
return;
|
||||
|
||||
base.Add(key, string.Join(",", values));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add key as comma separated values
|
||||
/// </summary>
|
||||
#if NET5_0_OR_GREATER
|
||||
public void AddCommaSeparated<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string key, IEnumerable<T> values)
|
||||
#else
|
||||
public void AddCommaSeparated<T>(string key, IEnumerable<T> values)
|
||||
#endif
|
||||
where T : struct, Enum
|
||||
{
|
||||
base.Add(key, string.Join(",", values.Select(x => EnumConverter.GetString(x))));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add key as comma separated values if there are values provided
|
||||
/// </summary>
|
||||
#if NET5_0_OR_GREATER
|
||||
public void AddOptionalCommaSeparated<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string key, IEnumerable<T>? values)
|
||||
#else
|
||||
public void AddOptionalCommaSeparated<T>(string key, IEnumerable<T>? values)
|
||||
#endif
|
||||
where T : struct, Enum
|
||||
{
|
||||
if (values == null || !values.Any())
|
||||
return;
|
||||
|
||||
base.Add(key, string.Join(",", values.Select(x => EnumConverter.GetString(x))));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add key as boolean lower case value
|
||||
/// </summary>
|
||||
public void AddBoolString(string key, bool value)
|
||||
{
|
||||
base.Add(key, value.ToString().ToLower());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add key as boolean lower case value if it's not null
|
||||
/// </summary>
|
||||
public void AddOptionalBoolString(string key, bool? value)
|
||||
{
|
||||
if (value == null)
|
||||
return;
|
||||
|
||||
base.Add(key, value.ToString()!.ToLower());
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Set the request body. Can be used to specify a simple value or array as the body instead of an object
|
||||
/// </summary>
|
||||
/// <param name="body">Body to set</param>
|
||||
/// <exception cref="InvalidOperationException"></exception>
|
||||
public void SetBody(object body)
|
||||
{
|
||||
if (this.Any())
|
||||
throw new InvalidOperationException("Can't set body when other parameters already specified");
|
||||
|
||||
base.Add(Constants.BodyPlaceHolderKey, body);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.Objects
|
||||
{
|
||||
/// <summary>
|
||||
/// Settings for parameter serialization
|
||||
/// </summary>
|
||||
public class ParameterSerializationSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// Default serialization settings
|
||||
/// </summary>
|
||||
public static ParameterSerializationSettings Default { get; } = new ParameterSerializationSettings();
|
||||
|
||||
/// <summary>
|
||||
/// Whether to sort the parameters
|
||||
/// </summary>
|
||||
public bool Sort { get; set; } = true;
|
||||
/// <summary>
|
||||
/// The parameter comparer when sorting
|
||||
/// </summary>
|
||||
public IComparer<string>? SortComparer { get; set; }
|
||||
/// <summary>
|
||||
/// Decimal serialization type
|
||||
/// </summary>
|
||||
public DecimalSerialization Decimal { get; set; } = DecimalSerialization.Number;
|
||||
/// <summary>
|
||||
/// DateTime serialization type
|
||||
/// </summary>
|
||||
public DateTimeSerialization DateTimes { get; set; } = DateTimeSerialization.MillisecondsNumber;
|
||||
/// <summary>
|
||||
/// Boolean serialization type
|
||||
/// </summary>
|
||||
public BoolSerialization Bool { get; set; } = BoolSerialization.Bool;
|
||||
/// <summary>
|
||||
/// Integer serialization type
|
||||
/// </summary>
|
||||
public IntegerSerialization Integer { get; set; } = IntegerSerialization.Number;
|
||||
/// <summary>
|
||||
/// Enum serialization type
|
||||
/// </summary>
|
||||
public EnumSerialization Enum { get; set; } = EnumSerialization.String;
|
||||
/// <summary>
|
||||
/// Array serialization type
|
||||
/// </summary>
|
||||
public ArrayParametersSerialization Array { get; set; } = ArrayParametersSerialization.Array;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Type of decimal value serialization
|
||||
/// </summary>
|
||||
public enum DecimalSerialization
|
||||
{
|
||||
/// <summary>
|
||||
/// Decimals should be serialized as numbers
|
||||
/// </summary>
|
||||
Number,
|
||||
/// <summary>
|
||||
/// Decimals should be strings
|
||||
/// </summary>
|
||||
String
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Type of DateTime value serialization
|
||||
/// </summary>
|
||||
public enum DateTimeSerialization
|
||||
{
|
||||
/// <summary>
|
||||
/// DateTimes should be serialized as milliseconds number
|
||||
/// </summary>
|
||||
MillisecondsNumber,
|
||||
/// <summary>
|
||||
/// DateTimes should be serialized as milliseconds string
|
||||
/// </summary>
|
||||
MillisecondsString,
|
||||
/// <summary>
|
||||
/// DateTimes should be serialized as seconds number
|
||||
/// </summary>
|
||||
SecondsNumber,
|
||||
/// <summary>
|
||||
/// DateTimes should be serialized as seconds string
|
||||
/// </summary>
|
||||
SecondsString,
|
||||
/// <summary>
|
||||
/// DateTimes should be serialized as microseconds number
|
||||
/// </summary>
|
||||
MicrosecondsNumber,
|
||||
/// <summary>
|
||||
/// DateTimes should be serialized as microseconds string
|
||||
/// </summary>
|
||||
MicrosecondsString,
|
||||
/// <summary>
|
||||
/// DateTimes should be serialized as ISO 8601 string
|
||||
/// </summary>
|
||||
Rfc3339String
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Type of boolean value serialization
|
||||
/// </summary>
|
||||
public enum BoolSerialization
|
||||
{
|
||||
/// <summary>
|
||||
/// Booleans should be serialized as bool values
|
||||
/// </summary>
|
||||
Bool,
|
||||
/// <summary>
|
||||
/// Booleans should be serialized as strings
|
||||
/// </summary>
|
||||
String
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Type of integer value serialization
|
||||
/// </summary>
|
||||
public enum IntegerSerialization
|
||||
{
|
||||
/// <summary>
|
||||
/// Integers should be serialized as integer values
|
||||
/// </summary>
|
||||
Number,
|
||||
/// <summary>
|
||||
/// Integers should be serialized as strings
|
||||
/// </summary>
|
||||
String
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Type of enum value serialization
|
||||
/// </summary>
|
||||
public enum EnumSerialization
|
||||
{
|
||||
/// <summary>
|
||||
/// Enums should be serialized as integer values
|
||||
/// </summary>
|
||||
Number,
|
||||
/// <summary>
|
||||
/// Enums should be serialized as strings
|
||||
/// </summary>
|
||||
String
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,375 @@
|
||||
using CryptoExchange.Net.Attributes;
|
||||
using CryptoExchange.Net.Converters.SystemTextJson;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
|
||||
namespace CryptoExchange.Net.Objects
|
||||
{
|
||||
/// <summary>
|
||||
/// Set of parameters
|
||||
/// </summary>
|
||||
public class Parameters : IDictionary<string, object>
|
||||
{
|
||||
private readonly ParameterSerializationSettings _serializationSettings;
|
||||
private IDictionary<string, object> _parameters;
|
||||
private object? _value;
|
||||
|
||||
/// <inheritdoc />
|
||||
public object? BodyValue => _value;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ICollection<string> Keys => _parameters.Keys;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ICollection<object> Values => _parameters.Values;
|
||||
|
||||
/// <inheritdoc />
|
||||
public int Count => _parameters.Count;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsReadOnly => _parameters.IsReadOnly;
|
||||
|
||||
/// <summary>
|
||||
/// Whether any parameters are defined
|
||||
/// </summary>
|
||||
public bool Empty => _parameters.Count == 0 && _value == null;
|
||||
|
||||
/// <inheritdoc />
|
||||
public object this[string key] { get => _parameters[key]; set => _parameters[key] = value; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="serializationSettings">Serialization settings</param>
|
||||
public Parameters(ParameterSerializationSettings serializationSettings)
|
||||
{
|
||||
_serializationSettings = serializationSettings;
|
||||
if (_serializationSettings.Sort)
|
||||
_parameters = new SortedDictionary<string, object>(_serializationSettings.SortComparer);
|
||||
else
|
||||
_parameters = new Dictionary<string, object>();
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="serializationSettings">Serialization settings</param>
|
||||
/// <param name="value">Body value</param>
|
||||
public Parameters(object value, ParameterSerializationSettings serializationSettings)
|
||||
{
|
||||
_parameters = new Dictionary<string, object>();
|
||||
_serializationSettings = serializationSettings;
|
||||
_value = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a short value if it is not null
|
||||
/// </summary>
|
||||
public void Add(string key, short? value, IntegerSerialization? serialization = null)
|
||||
{
|
||||
if (value == null)
|
||||
return;
|
||||
|
||||
Add(key, value.Value, serialization);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a short value
|
||||
/// </summary>
|
||||
public void Add(string key, short value, IntegerSerialization? serialization = null)
|
||||
{
|
||||
var serializationToUse = serialization ?? _serializationSettings.Integer;
|
||||
if (serializationToUse == IntegerSerialization.String)
|
||||
_parameters.Add(key, value.ToString(CultureInfo.InvariantCulture));
|
||||
else if (serializationToUse == IntegerSerialization.Number)
|
||||
_parameters.Add(key, value);
|
||||
else
|
||||
throw new ArgumentException("Unknown Integer serialization setting");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add an int value if it is not null
|
||||
/// </summary>
|
||||
public void Add(string key, int? value, IntegerSerialization? serialization = null)
|
||||
{
|
||||
if (value == null)
|
||||
return;
|
||||
|
||||
Add(key, value.Value, serialization);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add an int value
|
||||
/// </summary>
|
||||
public void Add(string key, int value, IntegerSerialization? serialization = null)
|
||||
{
|
||||
var serializationToUse = serialization ?? _serializationSettings.Integer;
|
||||
if (serializationToUse == IntegerSerialization.String)
|
||||
_parameters.Add(key, value.ToString(CultureInfo.InvariantCulture));
|
||||
else if (serializationToUse == IntegerSerialization.Number)
|
||||
_parameters.Add(key, value);
|
||||
else
|
||||
throw new ArgumentException("Unknown Integer serialization setting");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a long value if it is not null
|
||||
/// </summary>
|
||||
public void Add(string key, long? value, IntegerSerialization? serialization = null)
|
||||
{
|
||||
if (value == null)
|
||||
return;
|
||||
|
||||
Add(key, value.Value, serialization);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a long value
|
||||
/// </summary>
|
||||
public void Add(string key, long value, IntegerSerialization? serialization = null)
|
||||
{
|
||||
var serializationToUse = serialization ?? _serializationSettings.Integer;
|
||||
if (serializationToUse == IntegerSerialization.String)
|
||||
_parameters.Add(key, value.ToString(CultureInfo.InvariantCulture));
|
||||
else if (serializationToUse == IntegerSerialization.Number)
|
||||
_parameters.Add(key, value);
|
||||
else
|
||||
throw new ArgumentException("Unknown Integer serialization setting");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a decimal value if it is not null
|
||||
/// </summary>
|
||||
public void Add(string key, decimal? value, DecimalSerialization? serialization = null)
|
||||
{
|
||||
if (value == null)
|
||||
return;
|
||||
|
||||
Add(key, value.Value, serialization);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a decimal value
|
||||
/// </summary>
|
||||
public void Add(string key, decimal value, DecimalSerialization? serialization = null)
|
||||
{
|
||||
var serializationToUse = serialization ?? _serializationSettings.Decimal;
|
||||
if (serializationToUse == DecimalSerialization.String)
|
||||
_parameters.Add(key, value.ToString(CultureInfo.InvariantCulture));
|
||||
else if (serializationToUse == DecimalSerialization.Number)
|
||||
_parameters.Add(key, value);
|
||||
else
|
||||
throw new ArgumentException("Unknown Decimal serialization setting");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a double value if it is not null
|
||||
/// </summary>
|
||||
public void Add(string key, double? value, DecimalSerialization? serialization = null)
|
||||
{
|
||||
if (value == null)
|
||||
return;
|
||||
|
||||
Add(key, value.Value, serialization);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a double value
|
||||
/// </summary>
|
||||
public void Add(string key, double value, DecimalSerialization? serialization = null)
|
||||
{
|
||||
var serializationToUse = serialization ?? _serializationSettings.Decimal;
|
||||
if (serializationToUse == DecimalSerialization.String)
|
||||
_parameters.Add(key, value.ToString(CultureInfo.InvariantCulture));
|
||||
else if (serializationToUse == DecimalSerialization.Number)
|
||||
_parameters.Add(key, value);
|
||||
else
|
||||
throw new ArgumentException("Unknown Decimal serialization setting");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a bool value if it is not null
|
||||
/// </summary>
|
||||
public void Add(string key, bool? value, BoolSerialization? serialization = null)
|
||||
{
|
||||
if (value == null)
|
||||
return;
|
||||
|
||||
Add(key, value.Value, serialization);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a bool value
|
||||
/// </summary>
|
||||
public void Add(string key, bool value, BoolSerialization? serialization = null)
|
||||
{
|
||||
var serializationToUse = serialization ?? _serializationSettings.Bool;
|
||||
if (serializationToUse == BoolSerialization.String)
|
||||
_parameters.Add(key, value.ToString(CultureInfo.InvariantCulture).ToLowerInvariant());
|
||||
else if (serializationToUse == BoolSerialization.Bool)
|
||||
_parameters.Add(key, value);
|
||||
else
|
||||
throw new ArgumentException("Unknown Bool serialization setting");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add key as comma separated values if there are values provided
|
||||
/// </summary>
|
||||
public void AddCommaSeparated(string key, IEnumerable<string>? values)
|
||||
{
|
||||
if (values == null || !values.Any())
|
||||
return;
|
||||
|
||||
_parameters.Add(key, string.Join(",", values));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add key as comma separated values
|
||||
/// </summary>
|
||||
#if NET5_0_OR_GREATER
|
||||
public void AddCommaSeparated<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string key, IEnumerable<T>? values)
|
||||
#else
|
||||
public void AddCommaSeparated<T>(string key, IEnumerable<T>? values)
|
||||
#endif
|
||||
where T : struct, Enum
|
||||
{
|
||||
if (values == null || !values.Any())
|
||||
return;
|
||||
|
||||
_parameters.Add(key, string.Join(",", values.Select(x => EnumConverter.GetString(x))));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add an enum value if it is not null
|
||||
/// </summary>
|
||||
public void Add<
|
||||
#if NET5_0_OR_GREATER
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)]
|
||||
# endif
|
||||
T>(string key, T? value, EnumSerialization? serialization = null)
|
||||
where T : struct, Enum
|
||||
|
||||
{
|
||||
if (value == null)
|
||||
return;
|
||||
|
||||
Add(key, value.Value, serialization);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a enum value
|
||||
/// </summary>
|
||||
public void Add<
|
||||
#if NET5_0_OR_GREATER
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)]
|
||||
#endif
|
||||
T>(string key, T value, EnumSerialization? serialization = null)
|
||||
where T : struct, Enum
|
||||
{
|
||||
var serializationToUse = serialization ?? _serializationSettings.Enum;
|
||||
if (serializationToUse == EnumSerialization.String)
|
||||
_parameters.Add(key, EnumConverter<T>.GetString(value));
|
||||
else if (serializationToUse == EnumSerialization.Number)
|
||||
_parameters.Add(key, int.Parse(EnumConverter<T>.GetString(value), CultureInfo.InvariantCulture));
|
||||
else
|
||||
throw new ArgumentException("Unknown Integer serialization setting");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a DateTime value if it is not null
|
||||
/// </summary>
|
||||
public void Add(string key, DateTime? value, DateTimeSerialization? serialization = null)
|
||||
{
|
||||
if (value == null)
|
||||
return;
|
||||
|
||||
Add(key, value.Value, serialization);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a DateTime value
|
||||
/// </summary>
|
||||
public void Add(string key, DateTime value, DateTimeSerialization? serialization = null)
|
||||
{
|
||||
var serializationToUse = serialization ?? _serializationSettings.DateTimes;
|
||||
if (serializationToUse == DateTimeSerialization.MillisecondsNumber)
|
||||
_parameters.Add(key, DateTimeConverter.ConvertToMilliseconds(value));
|
||||
else if (serializationToUse == DateTimeSerialization.MillisecondsString)
|
||||
_parameters.Add(key, DateTimeConverter.ConvertToMilliseconds(value).Value.ToString(CultureInfo.InvariantCulture));
|
||||
else if (serializationToUse == DateTimeSerialization.SecondsNumber)
|
||||
_parameters.Add(key, DateTimeConverter.ConvertToSeconds(value));
|
||||
else if (serializationToUse == DateTimeSerialization.SecondsString)
|
||||
_parameters.Add(key, DateTimeConverter.ConvertToSeconds(value).Value.ToString(CultureInfo.InvariantCulture));
|
||||
else if (serializationToUse == DateTimeSerialization.MicrosecondsNumber)
|
||||
_parameters.Add(key, DateTimeConverter.ConvertToMicroseconds(value));
|
||||
else if (serializationToUse == DateTimeSerialization.MicrosecondsString)
|
||||
_parameters.Add(key, DateTimeConverter.ConvertToMicroseconds(value).Value.ToString(CultureInfo.InvariantCulture));
|
||||
else if (serializationToUse == DateTimeSerialization.Rfc3339String)
|
||||
_parameters.Add(key, value.ToRfc3339String());
|
||||
else
|
||||
throw new ArgumentException("Unknown DateTime serialization setting");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a string value if it is not null
|
||||
/// </summary>
|
||||
public void Add(string key, string? value)
|
||||
{
|
||||
if (value == null)
|
||||
return;
|
||||
|
||||
_parameters.Add(key, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add an array of values if there are values provided
|
||||
/// </summary>
|
||||
public void AddArray<T>(string key, IEnumerable<T>? values)
|
||||
{
|
||||
if (values == null || !values.Any())
|
||||
return;
|
||||
|
||||
_parameters.Add(key, values is T[] arr ? arr : values.ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a raw object value if it is not null
|
||||
/// </summary>
|
||||
public void AddRaw(string key, object? value)
|
||||
{
|
||||
if (value == null)
|
||||
return;
|
||||
|
||||
_parameters.Add(key, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Add(string key, object value) => _parameters.Add(key, value);
|
||||
/// <inheritdoc />
|
||||
public bool ContainsKey(string key) => _parameters.ContainsKey(key);
|
||||
/// <inheritdoc />
|
||||
public bool Remove(string key) => _parameters.Remove(key);
|
||||
/// <inheritdoc />
|
||||
public bool TryGetValue(string key, out object value) => _parameters.TryGetValue(key, out value!);
|
||||
/// <inheritdoc />
|
||||
public void Add(KeyValuePair<string, object> item) => _parameters.Add(item.Key, item.Value);
|
||||
/// <inheritdoc />
|
||||
public void Clear() => _parameters.Clear();
|
||||
/// <inheritdoc />
|
||||
public bool Contains(KeyValuePair<string, object> item) => _parameters.ContainsKey(item.Key) && _parameters[item.Key] == item.Value;
|
||||
/// <inheritdoc />
|
||||
public void CopyTo(KeyValuePair<string, object>[] array, int arrayIndex) => _parameters.CopyTo(array, arrayIndex);
|
||||
/// <inheritdoc />
|
||||
public bool Remove(KeyValuePair<string, object> item) => _parameters.Remove(item.Key);
|
||||
/// <inheritdoc />
|
||||
public IEnumerator<KeyValuePair<string, object>> GetEnumerator() => _parameters.GetEnumerator();
|
||||
/// <inheritdoc />
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
}
|
||||
}
|
||||
@@ -33,11 +33,23 @@
|
||||
/// Centralization type
|
||||
/// </summary>
|
||||
public CentralizationType CentralizationType { get; }
|
||||
/// <summary>
|
||||
/// Supported environments
|
||||
/// </summary>
|
||||
public string[] SupportedEnvironments { get; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public PlatformInfo(string id, string displayName, string logo, string url, string[] apiDocsUrl, PlatformType platformType, CentralizationType centralizationType)
|
||||
public PlatformInfo(
|
||||
string id,
|
||||
string displayName,
|
||||
string logo,
|
||||
string url,
|
||||
string[] apiDocsUrl,
|
||||
PlatformType platformType,
|
||||
CentralizationType centralizationType,
|
||||
string[] supportedEnvironments)
|
||||
{
|
||||
Id = id;
|
||||
DisplayName = displayName;
|
||||
@@ -46,6 +58,7 @@
|
||||
ApiDocsUrl = apiDocsUrl;
|
||||
PlatformType = platformType;
|
||||
CentralizationType = centralizationType;
|
||||
SupportedEnvironments = supportedEnvironments;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,9 +9,14 @@ namespace CryptoExchange.Net.Objects
|
||||
public class RequestDefinition
|
||||
{
|
||||
private string? _stringRep;
|
||||
private string? _fullUrl;
|
||||
|
||||
// Basics
|
||||
|
||||
/// <summary>
|
||||
/// Base address of the request
|
||||
/// </summary>
|
||||
public string BaseAddress { get; set; }
|
||||
/// <summary>
|
||||
/// Path of the request
|
||||
/// </summary>
|
||||
@@ -77,13 +82,31 @@ namespace CryptoExchange.Net.Objects
|
||||
/// </summary>
|
||||
public bool? ForcePathEndWithSlash { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Full url, host + path
|
||||
/// </summary>
|
||||
public string FullUrl
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_fullUrl != null)
|
||||
return _fullUrl;
|
||||
|
||||
var result = BaseAddress.AppendPath(Path);
|
||||
if (ForcePathEndWithSlash == true && !result.EndsWith("/"))
|
||||
result += "/";
|
||||
|
||||
_fullUrl = result;
|
||||
return _fullUrl;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="path"></param>
|
||||
/// <param name="method"></param>
|
||||
public RequestDefinition(string path, HttpMethod method)
|
||||
public RequestDefinition(string baseAddress, string path, HttpMethod method)
|
||||
{
|
||||
BaseAddress = baseAddress;
|
||||
Path = path;
|
||||
Method = method;
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using CryptoExchange.Net.RateLimiting.Interfaces;
|
||||
using System.Collections.Concurrent;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace CryptoExchange.Net.Objects
|
||||
@@ -15,27 +16,30 @@ namespace CryptoExchange.Net.Objects
|
||||
/// Get a definition if it is already in the cache or create a new definition and add it to the cache
|
||||
/// </summary>
|
||||
/// <param name="method">The HttpMethod</param>
|
||||
/// <param name="baseAddress">The base address/host</param>
|
||||
/// <param name="path">Endpoint path</param>
|
||||
/// <param name="authenticated">Endpoint is authenticated</param>
|
||||
/// <returns></returns>
|
||||
public RequestDefinition GetOrCreate(HttpMethod method, string path, bool authenticated = false)
|
||||
=> GetOrCreate(method, path, null, 0, authenticated, null, null, null, null, null);
|
||||
public RequestDefinition GetOrCreate(HttpMethod method, string baseAddress, string path, bool authenticated = false)
|
||||
=> GetOrCreate(method, baseAddress, path, null, 0, authenticated, null, null, null, null, null, null, null);
|
||||
|
||||
/// <summary>
|
||||
/// Get a definition if it is already in the cache or create a new definition and add it to the cache
|
||||
/// </summary>
|
||||
/// <param name="method">The HttpMethod</param>
|
||||
/// <param name="baseAddress">The base address/host</param>
|
||||
/// <param name="path">Endpoint path</param>
|
||||
/// <param name="rateLimitGate">The rate limit gate</param>
|
||||
/// <param name="weight">Request weight</param>
|
||||
/// <param name="authenticated">Endpoint is authenticated</param>
|
||||
/// <returns></returns>
|
||||
public RequestDefinition GetOrCreate(HttpMethod method, string path, IRateLimitGate rateLimitGate, int weight = 1, bool authenticated = false)
|
||||
=> GetOrCreate(method, path, rateLimitGate, weight, authenticated, null, null, null, null, null);
|
||||
public RequestDefinition GetOrCreate(HttpMethod method, string baseAddress, string path, IRateLimitGate rateLimitGate, int weight = 1, bool authenticated = false)
|
||||
=> GetOrCreate(method, baseAddress, path, rateLimitGate, weight, authenticated, null, null, null, null, null, null, null);
|
||||
|
||||
/// <summary>
|
||||
/// Get a definition if it is already in the cache or create a new definition and add it to the cache
|
||||
/// </summary>
|
||||
/// <param name="baseAddress">The base address/host</param>
|
||||
/// <param name="method">The HttpMethod</param>
|
||||
/// <param name="path">Endpoint path</param>
|
||||
/// <param name="rateLimitGate">The rate limit gate</param>
|
||||
@@ -48,9 +52,11 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <param name="preventCaching">Prevent request caching</param>
|
||||
/// <param name="tryParseOnNonSuccess">Try parse the response even when status is not success</param>
|
||||
/// <param name="forcePathEndWithSlash">Force trailing `/`</param>
|
||||
/// <param name="identifier">Optional request identifier override</param>
|
||||
/// <returns></returns>
|
||||
public RequestDefinition GetOrCreate(
|
||||
HttpMethod method,
|
||||
string baseAddress,
|
||||
string path,
|
||||
IRateLimitGate? rateLimitGate,
|
||||
int weight,
|
||||
@@ -61,45 +67,13 @@ namespace CryptoExchange.Net.Objects
|
||||
ArrayParametersSerialization? arraySerialization = null,
|
||||
bool? preventCaching = null,
|
||||
bool? tryParseOnNonSuccess = null,
|
||||
bool? forcePathEndWithSlash = null)
|
||||
=> GetOrCreate(method + path, method, path, rateLimitGate, weight, authenticated, limitGuard, requestBodyFormat, parameterPosition, arraySerialization, preventCaching, tryParseOnNonSuccess, forcePathEndWithSlash);
|
||||
|
||||
/// <summary>
|
||||
/// Get a definition if it is already in the cache or create a new definition and add it to the cache
|
||||
/// </summary>
|
||||
/// <param name="identifier">Request identifier</param>
|
||||
/// <param name="method">The HttpMethod</param>
|
||||
/// <param name="path">Endpoint path</param>
|
||||
/// <param name="rateLimitGate">The rate limit gate</param>
|
||||
/// <param name="limitGuard">The rate limit guard for this specific endpoint</param>
|
||||
/// <param name="weight">Request weight</param>
|
||||
/// <param name="authenticated">Endpoint is authenticated</param>
|
||||
/// <param name="requestBodyFormat">Request body format</param>
|
||||
/// <param name="parameterPosition">Parameter position</param>
|
||||
/// <param name="arraySerialization">Array serialization type</param>
|
||||
/// <param name="preventCaching">Prevent request caching</param>
|
||||
/// <param name="tryParseOnNonSuccess">Try parse the response even when status is not success</param>
|
||||
/// <param name="forcePathEndWithSlash">Force trailing `/`</param>
|
||||
/// <returns></returns>
|
||||
public RequestDefinition GetOrCreate(
|
||||
string identifier,
|
||||
HttpMethod method,
|
||||
string path,
|
||||
IRateLimitGate? rateLimitGate,
|
||||
int weight,
|
||||
bool authenticated,
|
||||
IRateLimitGuard? limitGuard = null,
|
||||
RequestBodyFormat? requestBodyFormat = null,
|
||||
HttpMethodParameterPosition? parameterPosition = null,
|
||||
ArrayParametersSerialization? arraySerialization = null,
|
||||
bool? preventCaching = null,
|
||||
bool? tryParseOnNonSuccess = null,
|
||||
bool? forcePathEndWithSlash = null)
|
||||
bool? forcePathEndWithSlash = null,
|
||||
string? identifier = null)
|
||||
{
|
||||
|
||||
if (!_definitions.TryGetValue(identifier, out var def))
|
||||
var identifierToUse = identifier ?? $"{path}{method.Method}{baseAddress}";
|
||||
if (!_definitions.TryGetValue(identifierToUse, out var def))
|
||||
{
|
||||
def = new RequestDefinition(path, method)
|
||||
def = new RequestDefinition(baseAddress, path, method)
|
||||
{
|
||||
Authenticated = authenticated,
|
||||
LimitGuard = limitGuard,
|
||||
@@ -110,9 +84,9 @@ namespace CryptoExchange.Net.Objects
|
||||
ParameterPosition = parameterPosition,
|
||||
PreventCaching = preventCaching ?? false,
|
||||
TryParseOnNonSuccess = tryParseOnNonSuccess ?? false,
|
||||
ForcePathEndWithSlash = forcePathEndWithSlash ?? false
|
||||
ForcePathEndWithSlash = forcePathEndWithSlash ?? false,
|
||||
};
|
||||
_definitions.TryAdd(identifier, def);
|
||||
_definitions.TryAdd(identifierToUse, def);
|
||||
}
|
||||
|
||||
return def;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace CryptoExchange.Net.Objects
|
||||
@@ -12,29 +13,17 @@ namespace CryptoExchange.Net.Objects
|
||||
private string? _queryString;
|
||||
|
||||
/// <summary>
|
||||
/// Http method
|
||||
/// The request definition for the request
|
||||
/// </summary>
|
||||
public HttpMethod Method { get; set; }
|
||||
/// <summary>
|
||||
/// Whether the request needs authentication
|
||||
/// </summary>
|
||||
public bool Authenticated { get; set; }
|
||||
/// <summary>
|
||||
/// Base address for the request
|
||||
/// </summary>
|
||||
public string BaseAddress { get; set; }
|
||||
/// <summary>
|
||||
/// The request path
|
||||
/// </summary>
|
||||
public string Path { get; set; }
|
||||
public RequestDefinition RequestDefinition { get; set; }
|
||||
/// <summary>
|
||||
/// Query parameters
|
||||
/// </summary>
|
||||
public IDictionary<string, object>? QueryParameters { get; set; }
|
||||
public Parameters? QueryParameters { get; set; }
|
||||
/// <summary>
|
||||
/// Body parameters
|
||||
/// </summary>
|
||||
public IDictionary<string, object>? BodyParameters { get; set; }
|
||||
public Parameters? BodyParameters { get; set; }
|
||||
/// <summary>
|
||||
/// Request headers
|
||||
/// </summary>
|
||||
@@ -57,22 +46,16 @@ namespace CryptoExchange.Net.Objects
|
||||
/// </summary>
|
||||
public RestRequestConfiguration(
|
||||
RequestDefinition requestDefinition,
|
||||
string baseAddress,
|
||||
IDictionary<string, object>? queryParams,
|
||||
IDictionary<string, object>? bodyParams,
|
||||
Parameters? queryParams,
|
||||
Parameters? bodyParams,
|
||||
IDictionary<string, string>? headers,
|
||||
ArrayParametersSerialization arraySerialization,
|
||||
HttpMethodParameterPosition parametersPosition,
|
||||
RequestBodyFormat bodyFormat)
|
||||
{
|
||||
Method = requestDefinition.Method;
|
||||
Authenticated = requestDefinition.Authenticated;
|
||||
Path = requestDefinition.Path;
|
||||
BaseAddress = baseAddress;
|
||||
RequestDefinition = requestDefinition;
|
||||
QueryParameters = queryParams;
|
||||
BodyParameters = bodyParams;
|
||||
Headers = headers;
|
||||
ArraySerialization = arraySerialization;
|
||||
ParameterPosition = parametersPosition;
|
||||
BodyFormat = bodyFormat;
|
||||
}
|
||||
@@ -80,15 +63,15 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <summary>
|
||||
/// Get the parameter collection based on the ParameterPosition
|
||||
/// </summary>
|
||||
public IDictionary<string, object> GetPositionParameters()
|
||||
public Parameters GetPositionParameters()
|
||||
{
|
||||
if (ParameterPosition == HttpMethodParameterPosition.InBody)
|
||||
{
|
||||
BodyParameters ??= new Dictionary<string, object>();
|
||||
BodyParameters ??= new Parameters(ParameterSerializationSettings.Default);
|
||||
return BodyParameters;
|
||||
}
|
||||
|
||||
QueryParameters ??= new Dictionary<string, object>();
|
||||
QueryParameters ??= new Parameters(ParameterSerializationSettings.Default);
|
||||
return QueryParameters;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.Objects;
|
||||
|
||||
/// <summary>
|
||||
/// Call result
|
||||
/// </summary>
|
||||
[DebuggerDisplay("{DebugView,nq}")]
|
||||
public record CallResult : ICallResult
|
||||
{
|
||||
private string DebugView => Success ? "Success" : $"Error: {Error}";
|
||||
|
||||
private static CallResult _successResult = new CallResult();
|
||||
|
||||
/// <inheritdoc />
|
||||
public Error? Error { get; init; }
|
||||
/// <inheritdoc />
|
||||
[MemberNotNullWhen(false, nameof(Error))]
|
||||
public bool Success => Error == null;
|
||||
|
||||
/// <summary>
|
||||
/// Create an error response
|
||||
/// </summary>
|
||||
/// <param name="error">The error</param>
|
||||
public static CallResult Fail(Error error) => new CallResult { Error = error };
|
||||
/// <summary>
|
||||
/// Create a success result
|
||||
/// </summary>
|
||||
public static CallResult Ok() => _successResult;
|
||||
/// <summary>
|
||||
/// Create a success result
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Result type</typeparam>
|
||||
/// <param name="originalData">The original string data</param>
|
||||
/// <param name="data">Data type</param>
|
||||
public static CallResult<T> Ok<T>(T data, string? originalData = null) => new CallResult<T> { Data = data, OriginalData = originalData };
|
||||
/// <summary>
|
||||
/// Create an error response
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Result type</typeparam>
|
||||
/// <param name="originalData">The original string data</param>
|
||||
/// <param name="error">The error</param>
|
||||
public static CallResult<T> Fail<T>(Error error, string? originalData = null) => new CallResult<T> { Error = error, OriginalData = originalData };
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return Success ? $"Success" : $"Error: {Error}";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public record CallResult<T> : CallResult, ICallResult<T>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public new Error? Error
|
||||
{
|
||||
get => base.Error;
|
||||
init => base.Error = value;
|
||||
}
|
||||
/// <inheritdoc />
|
||||
[MemberNotNullWhen(false, nameof(Error))]
|
||||
[MemberNotNullWhen(true, nameof(Data))]
|
||||
public new bool Success => Error == null;
|
||||
|
||||
/// <summary>
|
||||
/// The data returned by the call, only available when Success = true
|
||||
/// </summary>
|
||||
public T? Data { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The original data returned by the call, only available when `OutputOriginalData` is set to `true` in the client options
|
||||
/// </summary>
|
||||
public string? OriginalData { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Create an error response
|
||||
/// </summary>
|
||||
/// <param name="error">The error</param>
|
||||
/// <param name="originalData">The original string data</param>
|
||||
public static CallResult<T> Fail(Error error, string? originalData = null) => new CallResult<T> { Error = error, OriginalData = originalData };
|
||||
/// <summary>
|
||||
/// Create a success result
|
||||
/// </summary>
|
||||
/// <param name="data">The data</param>
|
||||
/// <param name="originalData">The original string data</param>
|
||||
/// <returns></returns>
|
||||
public static CallResult<T> Ok(T data, string? originalData = null) => new CallResult<T> { Data = data, OriginalData = originalData };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Call result for an exchange
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Data type</typeparam>
|
||||
public record ExchangeCallResult<T> : CallResult<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// Exchange name
|
||||
/// </summary>
|
||||
public string Exchange { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Create an error response
|
||||
/// </summary>
|
||||
/// <param name="exchange">The exchange name</param>
|
||||
/// <param name="error">The error</param>
|
||||
/// <param name="originalData">The original string data</param>
|
||||
public static ExchangeCallResult<T> Fail(string exchange, Error error, string? originalData = null) => new ExchangeCallResult<T> { Exchange = exchange, Error = error };
|
||||
/// <summary>
|
||||
/// Create a success result
|
||||
/// </summary>
|
||||
/// <param name="exchange">The exchange name</param>
|
||||
/// <param name="data">The data</param>
|
||||
/// <param name="originalData">The original string data</param>
|
||||
/// <returns></returns>
|
||||
public static ExchangeCallResult<T> Ok(string exchange, T data, string? originalData = null) => new ExchangeCallResult<T> { Exchange = exchange, Data = data };
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.Objects;
|
||||
|
||||
/// <summary>
|
||||
/// HTTP call result
|
||||
/// </summary>
|
||||
[DebuggerDisplay("{DebugView,nq}")]
|
||||
public record HttpResult : IHttpResult
|
||||
{
|
||||
private string DebugView => $"[Req {RequestId}] " + (Success ? "Success" : $"Error: {Error}");
|
||||
|
||||
/// <summary>
|
||||
/// Create a new success HTTP result
|
||||
/// </summary>
|
||||
public static HttpResult<T> Ok<T>(
|
||||
string exchange,
|
||||
HttpStatusCode code,
|
||||
Version version,
|
||||
HttpResponseHeaders responseHeaders,
|
||||
TimeSpan elapsed,
|
||||
long? contentLength,
|
||||
string? originalData,
|
||||
int requestId,
|
||||
string uri,
|
||||
string? content,
|
||||
HttpMethod method,
|
||||
HttpRequestHeaders requestHeaders,
|
||||
ResultDataSource source,
|
||||
T data) =>
|
||||
new HttpResult<T>(exchange, data, null)
|
||||
{
|
||||
ResponseStatusCode = code,
|
||||
HttpVersion = version,
|
||||
ResponseHeaders = responseHeaders,
|
||||
ResponseTime = elapsed,
|
||||
ResponseLength = contentLength,
|
||||
OriginalData = originalData,
|
||||
RequestId = requestId,
|
||||
RequestUrl = uri,
|
||||
RequestBody = content,
|
||||
RequestMethod = method,
|
||||
RequestHeaders = requestHeaders,
|
||||
DataSource = source,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Create a new success HTTP result
|
||||
/// </summary>
|
||||
public static HttpResult<T> Ok<T>(IHttpResult result, T data, PageRequest? pageRequest = null) =>
|
||||
new HttpResult<T>(result.Exchange, data, null)
|
||||
{
|
||||
ResponseStatusCode = result.ResponseStatusCode,
|
||||
HttpVersion = result.HttpVersion,
|
||||
ResponseHeaders = result.ResponseHeaders,
|
||||
ResponseTime = result.ResponseTime,
|
||||
ResponseLength = result.ResponseLength,
|
||||
OriginalData = result.OriginalData,
|
||||
RequestId = result.RequestId,
|
||||
RequestUrl = result.RequestUrl,
|
||||
RequestBody = result.RequestBody,
|
||||
RequestMethod = result.RequestMethod,
|
||||
RequestHeaders = result.RequestHeaders,
|
||||
DataSource = result.DataSource,
|
||||
Error = result.Error,
|
||||
Data = data,
|
||||
NextPageRequest = pageRequest
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Create a new error HTTP result
|
||||
/// </summary>
|
||||
public static HttpResult<T> Fail<T>(string exchange, Error error) => new HttpResult<T>(exchange, default, error);
|
||||
|
||||
/// <summary>
|
||||
/// Create a new error HTTP result
|
||||
/// </summary>
|
||||
public static HttpResult<T> Fail<T>(IHttpResult result, Error? error = null, T? data = default)
|
||||
=> new HttpResult<T>(result.Exchange, data, error ?? result.Error)
|
||||
{
|
||||
ResponseStatusCode = result.ResponseStatusCode,
|
||||
HttpVersion = result.HttpVersion,
|
||||
ResponseHeaders = result.ResponseHeaders,
|
||||
ResponseTime = result.ResponseTime,
|
||||
ResponseLength = result.ResponseLength,
|
||||
OriginalData = result.OriginalData,
|
||||
RequestId = result.RequestId,
|
||||
RequestUrl = result.RequestUrl,
|
||||
RequestBody = result.RequestBody,
|
||||
RequestMethod = result.RequestMethod,
|
||||
RequestHeaders = result.RequestHeaders,
|
||||
DataSource = result.DataSource,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Create a new error HTTP result
|
||||
/// </summary>
|
||||
public static HttpResult<T> Fail<T>(
|
||||
string exchange,
|
||||
HttpStatusCode? code,
|
||||
Version? version,
|
||||
HttpResponseHeaders? responseHeaders,
|
||||
TimeSpan elapsed,
|
||||
long? contentLength,
|
||||
string? originalData,
|
||||
int requestId,
|
||||
string uri,
|
||||
string? content,
|
||||
HttpMethod method,
|
||||
HttpRequestHeaders requestHeaders,
|
||||
ResultDataSource source,
|
||||
Error error,
|
||||
T? result = default) =>
|
||||
new HttpResult<T>(exchange, result, error)
|
||||
{
|
||||
ResponseStatusCode = code,
|
||||
HttpVersion = version,
|
||||
ResponseHeaders = responseHeaders,
|
||||
ResponseTime = elapsed,
|
||||
ResponseLength = contentLength,
|
||||
OriginalData = originalData,
|
||||
RequestId = requestId,
|
||||
RequestUrl = uri,
|
||||
RequestBody = content,
|
||||
RequestMethod = method,
|
||||
RequestHeaders = requestHeaders,
|
||||
DataSource = source,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Create a new error HTTP result
|
||||
/// </summary>
|
||||
public static HttpResult Fail(string exchange, Error error) => new HttpResult() { Exchange = exchange, Error = error };
|
||||
|
||||
/// <summary>
|
||||
/// Create a new error HTTP result
|
||||
/// </summary>
|
||||
public static HttpResult Fail(IHttpResult result, Error? error = null)
|
||||
=> new HttpResult()
|
||||
{
|
||||
ResponseStatusCode = result.ResponseStatusCode,
|
||||
HttpVersion = result.HttpVersion,
|
||||
ResponseHeaders = result.ResponseHeaders,
|
||||
ResponseTime = result.ResponseTime,
|
||||
ResponseLength = result.ResponseLength,
|
||||
OriginalData = result.OriginalData,
|
||||
RequestId = result.RequestId,
|
||||
RequestUrl = result.RequestUrl,
|
||||
RequestBody = result.RequestBody,
|
||||
RequestMethod = result.RequestMethod,
|
||||
RequestHeaders = result.RequestHeaders,
|
||||
DataSource = result.DataSource,
|
||||
Exchange = result.Exchange,
|
||||
Error = error ?? result.Error
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Create a new success HTTP result
|
||||
/// </summary>
|
||||
public static HttpResult Ok(IHttpResult result)
|
||||
=> new HttpResult()
|
||||
{
|
||||
ResponseStatusCode = result.ResponseStatusCode,
|
||||
HttpVersion = result.HttpVersion,
|
||||
ResponseHeaders = result.ResponseHeaders,
|
||||
ResponseTime = result.ResponseTime,
|
||||
ResponseLength = result.ResponseLength,
|
||||
OriginalData = result.OriginalData,
|
||||
RequestId = result.RequestId,
|
||||
RequestUrl = result.RequestUrl,
|
||||
RequestBody = result.RequestBody,
|
||||
RequestMethod = result.RequestMethod,
|
||||
RequestHeaders = result.RequestHeaders,
|
||||
DataSource = result.DataSource,
|
||||
Exchange = result.Exchange,
|
||||
};
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Exchange name
|
||||
/// </summary>
|
||||
public string Exchange { get; init; } = string.Empty;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Error? Error { get; internal set; }
|
||||
/// <inheritdoc />
|
||||
[MemberNotNullWhen(false, nameof(Error))]
|
||||
public bool Success => Error == null;
|
||||
/// <summary>
|
||||
/// The original data returned by the call, only available when `OutputOriginalData` is set to `true` in the client options
|
||||
/// </summary>
|
||||
public string? OriginalData { get; init; }
|
||||
/// <summary>
|
||||
/// The request http method
|
||||
/// </summary>
|
||||
public HttpMethod? RequestMethod { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// HTTP protocol version
|
||||
/// </summary>
|
||||
public Version? HttpVersion { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The headers sent with the request
|
||||
/// </summary>
|
||||
public HttpRequestHeaders? RequestHeaders { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The request id
|
||||
/// </summary>
|
||||
public int? RequestId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The url which was requested
|
||||
/// </summary>
|
||||
public string? RequestUrl { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The body of the request
|
||||
/// </summary>
|
||||
public string? RequestBody { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Length in bytes of the response
|
||||
/// </summary>
|
||||
public long? ResponseLength { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The status code of the response. Note that a OK status does not always indicate success, check the Success parameter for this.
|
||||
/// </summary>
|
||||
public HttpStatusCode? ResponseStatusCode { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The response headers
|
||||
/// </summary>
|
||||
public HttpResponseHeaders? ResponseHeaders { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The time between sending the request and receiving the response
|
||||
/// </summary>
|
||||
public TimeSpan? ResponseTime { get; init; }
|
||||
/// <summary>
|
||||
/// The data source of this result
|
||||
/// </summary>
|
||||
public ResultDataSource DataSource { get; init; } = ResultDataSource.Server;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
[DebuggerDisplay("{DebugView,nq}")]
|
||||
public record HttpResult<T> : HttpResult, IHttpResult<T>
|
||||
{
|
||||
private string DebugView
|
||||
{
|
||||
get
|
||||
{
|
||||
var result = new StringBuilder($"[Req {RequestId}] " + (Success ? "Success" : $"Error: {Error}"));
|
||||
if (Data != null)
|
||||
{
|
||||
result.Append(", ");
|
||||
var typeName = typeof(T).Name;
|
||||
if (Data is Array ar)
|
||||
{
|
||||
result.Append($"{ar.Length} {typeName.Substring(0, typeName.Length - 2)}");
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Append(typeName);
|
||||
}
|
||||
}
|
||||
|
||||
return result.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public HttpResult(string exchange, T? value, Error? error)
|
||||
{
|
||||
Exchange = exchange;
|
||||
Data = value;
|
||||
Error = error;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public new Error? Error
|
||||
{
|
||||
get => base.Error;
|
||||
internal set => base.Error = value;
|
||||
}
|
||||
/// <inheritdoc />
|
||||
[MemberNotNullWhen(false, nameof(Error))]
|
||||
[MemberNotNullWhen(true, nameof(Data))]
|
||||
public new bool Success => Error == null;
|
||||
|
||||
/// <summary>
|
||||
/// The data returned by the call, only available when Success = true
|
||||
/// </summary>
|
||||
public T? Data { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Next page request, only potentially available when using Shared API's
|
||||
/// </summary>
|
||||
public PageRequest? NextPageRequest { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.Objects;
|
||||
|
||||
/// <summary>
|
||||
/// Call result
|
||||
/// </summary>
|
||||
public interface ICallResult
|
||||
{
|
||||
/// <summary>
|
||||
/// An error if the call didn't succeed, will always be filled if Success = false
|
||||
/// </summary>
|
||||
Error? Error { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the call was successful
|
||||
/// </summary>
|
||||
[MemberNotNullWhen(false, nameof(Error))]
|
||||
bool Success { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Call result
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Result data type</typeparam>
|
||||
public interface ICallResult<T> : ICallResult
|
||||
{
|
||||
/// <inheritdoc />
|
||||
new Error? Error { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
[MemberNotNullWhen(false, nameof(Error))]
|
||||
[MemberNotNullWhen(true, nameof(Data))]
|
||||
new bool Success { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The result data, only available when Success = true
|
||||
/// </summary>
|
||||
T? Data { get; }
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.Objects
|
||||
{
|
||||
/// <summary>
|
||||
/// HTTP call result
|
||||
/// </summary>
|
||||
public interface IHttpResult : ICallResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Exchange name
|
||||
/// </summary>
|
||||
string Exchange { get; init; }
|
||||
/// <summary>
|
||||
/// The original data returned by the call, only available when `OutputOriginalData` is set to `true` in the client options
|
||||
/// </summary>
|
||||
string? OriginalData { get; init; }
|
||||
/// <summary>
|
||||
/// The request http method
|
||||
/// </summary>
|
||||
HttpMethod? RequestMethod { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// HTTP protocol version
|
||||
/// </summary>
|
||||
Version? HttpVersion { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The headers sent with the request
|
||||
/// </summary>
|
||||
HttpRequestHeaders? RequestHeaders { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The request id
|
||||
/// </summary>
|
||||
int? RequestId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The url which was requested
|
||||
/// </summary>
|
||||
string? RequestUrl { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The body of the request
|
||||
/// </summary>
|
||||
string? RequestBody { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Length in bytes of the response
|
||||
/// </summary>
|
||||
long? ResponseLength { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The status code of the response. Note that a OK status does not always indicate success, check the Success parameter for this.
|
||||
/// </summary>
|
||||
HttpStatusCode? ResponseStatusCode { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The response headers
|
||||
/// </summary>
|
||||
HttpResponseHeaders? ResponseHeaders { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The time between sending the request and receiving the response
|
||||
/// </summary>
|
||||
TimeSpan? ResponseTime { get; init; }
|
||||
/// <summary>
|
||||
/// The data source of this result
|
||||
/// </summary>
|
||||
ResultDataSource DataSource { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HTTP call result
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Result data type</typeparam>
|
||||
public interface IHttpResult<T> : IHttpResult, ICallResult<T>
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.Objects
|
||||
{
|
||||
/// <summary>
|
||||
/// WebSocket call result
|
||||
/// </summary>
|
||||
public interface IWebSocketResult : ICallResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Exchange name
|
||||
/// </summary>
|
||||
string Exchange { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The request id
|
||||
/// </summary>
|
||||
public int? RequestId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The url which was requested
|
||||
/// </summary>
|
||||
public int? ConnectionId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The websocket url
|
||||
/// </summary>
|
||||
public string? Url { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The time between sending the request and receiving the response
|
||||
/// </summary>
|
||||
public TimeSpan? ResponseTime { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// WebSocket call result
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Data result type</typeparam>
|
||||
public interface IWebSocketResult<T> : IWebSocketResult, ICallResult<T>
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Query result
|
||||
/// </summary>
|
||||
public interface IQueryResult : IWebSocketResult
|
||||
{
|
||||
/// <summary>
|
||||
/// The original returned data, only available when OutputOriginalData is set to true in the client options
|
||||
/// </summary>
|
||||
public string? OriginalData { get; init; }
|
||||
/// <summary>
|
||||
/// The query request body
|
||||
/// </summary>
|
||||
public string? RequestBody { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Query result
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
public interface IQueryResult<T> : IQueryResult, IWebSocketResult<T>
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.Objects;
|
||||
|
||||
/// <summary>
|
||||
/// Void result
|
||||
/// </summary>
|
||||
public readonly struct Unit
|
||||
{
|
||||
/// <summary>
|
||||
/// Void value
|
||||
/// </summary>
|
||||
public static readonly Unit Value = default;
|
||||
/// <summary>
|
||||
/// Type
|
||||
/// </summary>
|
||||
public static Type Type { get; } = typeof(Unit);
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.Objects;
|
||||
|
||||
/// <summary>
|
||||
/// WebSocket call result
|
||||
/// </summary>
|
||||
[DebuggerDisplay("{DebugView,nq}")]
|
||||
public record WebSocketResult : IWebSocketResult
|
||||
{
|
||||
private string DebugView => $"[Sckt {ConnectionId}] " + (RequestId == null ? "" : $"[Req {RequestId}] ") + (Success ? "Success" : $"Error: {Error}");
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public WebSocketResult(string exchange, Error? error)
|
||||
{
|
||||
Exchange = exchange;
|
||||
Error = error;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new success WebSocket result
|
||||
/// </summary>
|
||||
public static WebSocketResult<T> Ok<T>(IWebSocketResult result, T data) =>
|
||||
new WebSocketResult<T>(result.Exchange, data, null)
|
||||
{
|
||||
ConnectionId = result.ConnectionId,
|
||||
Url = result.Url,
|
||||
RequestId = result.RequestId,
|
||||
ResponseTime = result.ResponseTime,
|
||||
Error = result.Error,
|
||||
Data = data
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Create a new success WebSocket result
|
||||
/// </summary>
|
||||
public static WebSocketResult<T> Ok<T>(
|
||||
string exchange,
|
||||
int connectionId,
|
||||
TimeSpan elapsed,
|
||||
int requestId,
|
||||
string? url,
|
||||
T data) =>
|
||||
new WebSocketResult<T>(exchange, data, null)
|
||||
{
|
||||
ResponseTime = elapsed,
|
||||
RequestId = requestId,
|
||||
ConnectionId = connectionId,
|
||||
Url = url
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Create a new success WebSocket result
|
||||
/// </summary>
|
||||
public static WebSocketResult Ok(
|
||||
string exchange,
|
||||
int connectionId,
|
||||
TimeSpan elapsed,
|
||||
int requestId,
|
||||
string? url) =>
|
||||
new WebSocketResult(exchange, null)
|
||||
{
|
||||
ResponseTime = elapsed,
|
||||
RequestId = requestId,
|
||||
ConnectionId = connectionId,
|
||||
Url = url
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Create a new error WebSocket result
|
||||
/// </summary>
|
||||
public static WebSocketResult Fail(string exchange, Error error) => new WebSocketResult(exchange, error);
|
||||
/// <summary>
|
||||
/// Create a new error WebSocket result
|
||||
/// </summary>
|
||||
public static WebSocketResult Fail(
|
||||
string exchange,
|
||||
int connectionId,
|
||||
TimeSpan elapsed,
|
||||
int requestId,
|
||||
string? url,
|
||||
Error error) =>
|
||||
new WebSocketResult(exchange, error)
|
||||
{
|
||||
ResponseTime = elapsed,
|
||||
RequestId = requestId,
|
||||
ConnectionId = connectionId,
|
||||
Url = url
|
||||
};
|
||||
/// <summary>
|
||||
/// Create a new error WebSocket result
|
||||
/// </summary>
|
||||
public static WebSocketResult<T> Fail<T>(IWebSocketResult result, Error? error = null, T? data = default)
|
||||
=> new WebSocketResult<T>(result.Exchange, data, error ?? result.Error)
|
||||
{
|
||||
ConnectionId = result.ConnectionId,
|
||||
Url = result.Url,
|
||||
RequestId = result.RequestId,
|
||||
ResponseTime = result.ResponseTime,
|
||||
};
|
||||
/// <summary>
|
||||
/// Create a new error WebSocket result
|
||||
/// </summary>
|
||||
public static WebSocketResult<T> Fail<T>(string exchange, Error error) => new WebSocketResult<T>(exchange, default, error);
|
||||
|
||||
/// <summary>
|
||||
/// Create a new error WebSocket result
|
||||
/// </summary>
|
||||
public static WebSocketResult<T> Fail<T>(
|
||||
string exchange,
|
||||
int connectionId,
|
||||
TimeSpan elapsed,
|
||||
int requestId,
|
||||
string? url,
|
||||
Error error) =>
|
||||
new WebSocketResult<T>(exchange, default, error)
|
||||
{
|
||||
ResponseTime = elapsed,
|
||||
RequestId = requestId,
|
||||
ConnectionId = connectionId,
|
||||
Url = url
|
||||
};
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Exchange name
|
||||
/// </summary>
|
||||
public string Exchange { get; init; }
|
||||
/// <inheritdoc />
|
||||
public Error? Error { get; init; }
|
||||
/// <inheritdoc />
|
||||
[MemberNotNullWhen(false, nameof(Error))]
|
||||
public bool Success => Error == null;
|
||||
|
||||
/// <summary>
|
||||
/// The request id
|
||||
/// </summary>
|
||||
public int? RequestId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The url which was requested
|
||||
/// </summary>
|
||||
public int? ConnectionId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The websocket url
|
||||
/// </summary>
|
||||
public string? Url { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The time between sending the request and receiving the response
|
||||
/// </summary>
|
||||
public TimeSpan? ResponseTime { get; init; }
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public record WebSocketResult<T> : WebSocketResult, IWebSocketResult<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public WebSocketResult(string exchange, T? value, Error? error): base(exchange, error)
|
||||
{
|
||||
Data = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public new Error? Error
|
||||
{
|
||||
get => base.Error;
|
||||
init => base.Error = value;
|
||||
}
|
||||
/// <inheritdoc />
|
||||
[MemberNotNullWhen(false, nameof(Error))]
|
||||
[MemberNotNullWhen(true, nameof(Data))]
|
||||
public new bool Success => Error == null;
|
||||
/// <summary>
|
||||
/// The data returned by the call, only available when Success = true
|
||||
/// </summary>
|
||||
public T? Data { get; init; }
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public record QueryResult : WebSocketResult
|
||||
{
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public QueryResult(string exchange, Error? error) : base(exchange, error)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new error Query result
|
||||
/// </summary>
|
||||
public new static QueryResult Fail(string exchange, Error error) => new QueryResult(exchange, error);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Create a new error WebSocket result
|
||||
/// </summary>
|
||||
public static QueryResult Fail(IQueryResult result, Error? error = null)
|
||||
=> new QueryResult(result.Exchange, error ?? result.Error)
|
||||
{
|
||||
ConnectionId = result.ConnectionId,
|
||||
Url = result.Url,
|
||||
RequestId = result.RequestId,
|
||||
ResponseTime = result.ResponseTime,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Create a new success query result
|
||||
/// </summary>
|
||||
public static QueryResult<T> Ok<T>(
|
||||
string exchange,
|
||||
int connectionId,
|
||||
TimeSpan elapsed,
|
||||
int requestId,
|
||||
string? requestBody,
|
||||
string? url,
|
||||
string? originalData,
|
||||
T data) =>
|
||||
new QueryResult<T>(exchange, data, null)
|
||||
{
|
||||
ResponseTime = elapsed,
|
||||
RequestId = requestId,
|
||||
RequestBody = requestBody,
|
||||
ConnectionId = connectionId,
|
||||
Url = url,
|
||||
OriginalData = originalData,
|
||||
};
|
||||
/// <summary>
|
||||
/// Create a new success WebSocket result
|
||||
/// </summary>
|
||||
public static QueryResult<T> Ok<T>(IQueryResult result, T data) =>
|
||||
new QueryResult<T>(result.Exchange, data, null)
|
||||
{
|
||||
ConnectionId = result.ConnectionId,
|
||||
Url = result.Url,
|
||||
RequestId = result.RequestId,
|
||||
RequestBody = result.RequestBody,
|
||||
ResponseTime = result.ResponseTime,
|
||||
Error = result.Error,
|
||||
OriginalData = result.OriginalData,
|
||||
Data = data
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Create a new error WebSocket result
|
||||
/// </summary>
|
||||
public static QueryResult<T> Fail<T>(
|
||||
string exchange,
|
||||
int connectionId,
|
||||
TimeSpan elapsed,
|
||||
int requestId,
|
||||
string? requestBody,
|
||||
string? url,
|
||||
string? originalData,
|
||||
Error error) =>
|
||||
new QueryResult<T>(exchange, default, error)
|
||||
{
|
||||
ResponseTime = elapsed,
|
||||
RequestId = requestId,
|
||||
RequestBody = requestBody,
|
||||
ConnectionId = connectionId,
|
||||
OriginalData = originalData,
|
||||
Url = url
|
||||
};
|
||||
/// <summary>
|
||||
/// Create a new error WebSocket result
|
||||
/// </summary>
|
||||
public static QueryResult<T> Fail<T>(IQueryResult result, Error? error = null, T? data = default)
|
||||
=> new QueryResult<T>(result.Exchange, data, error ?? result.Error)
|
||||
{
|
||||
ConnectionId = result.ConnectionId,
|
||||
Url = result.Url,
|
||||
RequestId = result.RequestId,
|
||||
RequestBody = result.RequestBody,
|
||||
OriginalData = result.OriginalData,
|
||||
ResponseTime = result.ResponseTime,
|
||||
};
|
||||
/// <summary>
|
||||
/// Create a new error WebSocket result
|
||||
/// </summary>
|
||||
public new static QueryResult<T> Fail<T>(string exchange, Error error) => new QueryResult<T>(exchange, default, error);
|
||||
|
||||
/// <inheritdoc />
|
||||
public string? RequestBody { get; init; }
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public record QueryResult<T> : QueryResult, IQueryResult<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public QueryResult(string exchange, T? value, Error? error) : base(exchange, error)
|
||||
{
|
||||
Data = value;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public new Error? Error
|
||||
{
|
||||
get => base.Error;
|
||||
init => base.Error = value;
|
||||
}
|
||||
/// <inheritdoc />
|
||||
[MemberNotNullWhen(false, nameof(Error))]
|
||||
[MemberNotNullWhen(true, nameof(Data))]
|
||||
public new bool Success => Error == null;
|
||||
/// <inheritdoc />
|
||||
public T? Data { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string? OriginalData { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
using CryptoExchange.Net.Sockets;
|
||||
using CryptoExchange.Net.Sockets.Default;
|
||||
using CryptoExchange.Net.Sockets.Default.Routing;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.Objects.Sockets
|
||||
{
|
||||
/// <summary>
|
||||
/// Controller for an update subscription which isn't backed by a websocket connection. Can be used for testing.
|
||||
/// </summary>
|
||||
public class ManualUpdateSubscription
|
||||
{
|
||||
private readonly Func<Task> _closeAsync;
|
||||
private readonly Func<Task> _reconnectAsync;
|
||||
private readonly Func<Task<CallResult>> _resubscribeAsync;
|
||||
private readonly ManualSubscription _manualSubscription;
|
||||
private int _closedEventInvoked;
|
||||
|
||||
/// <summary>
|
||||
/// The update subscription
|
||||
/// </summary>
|
||||
public UpdateSubscription Subscription { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The virtual socket id
|
||||
/// </summary>
|
||||
public int SocketId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The last timestamp anything was received by the subscription
|
||||
/// </summary>
|
||||
public DateTime? LastReceiveTime { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The current virtual websocket status
|
||||
/// </summary>
|
||||
public SocketStatus SocketStatus { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a manually controlled update subscription
|
||||
/// </summary>
|
||||
/// <param name="socketId">The virtual socket id</param>
|
||||
/// <param name="closeAsync">Callback when the subscription is closed</param>
|
||||
/// <param name="reconnectAsync">Callback when a reconnect is requested</param>
|
||||
/// <param name="resubscribeAsync">Callback when a resubscribe is requested</param>
|
||||
public ManualUpdateSubscription(
|
||||
int socketId = 0,
|
||||
Func<Task>? closeAsync = null,
|
||||
Func<Task>? reconnectAsync = null,
|
||||
Func<Task<CallResult>>? resubscribeAsync = null)
|
||||
{
|
||||
SocketId = socketId;
|
||||
SocketStatus = SocketStatus.Connected;
|
||||
_closeAsync = closeAsync ?? (() => Task.CompletedTask);
|
||||
_reconnectAsync = reconnectAsync ?? (() => Task.CompletedTask);
|
||||
_resubscribeAsync = resubscribeAsync ?? (() => Task.FromResult(CallResult.Ok()));
|
||||
|
||||
_manualSubscription = new ManualSubscription();
|
||||
_manualSubscription.Status = SubscriptionStatus.Subscribed;
|
||||
Subscription = new UpdateSubscription(this, _manualSubscription);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the last timestamp anything was received by the subscription
|
||||
/// </summary>
|
||||
/// <param name="timestamp">The receive timestamp</param>
|
||||
public void SetLastReceiveTime(DateTime? timestamp)
|
||||
{
|
||||
LastReceiveTime = timestamp;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the virtual websocket status
|
||||
/// </summary>
|
||||
/// <param name="status">The status</param>
|
||||
public void SetSocketStatus(SocketStatus status)
|
||||
{
|
||||
SocketStatus = status;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the subscription status
|
||||
/// </summary>
|
||||
/// <param name="status">The status</param>
|
||||
public void SetSubscriptionStatus(SubscriptionStatus status)
|
||||
{
|
||||
_manualSubscription.Status = status;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoke the connection lost event
|
||||
/// </summary>
|
||||
public void InvokeConnectionLost()
|
||||
{
|
||||
Subscription.HandleConnectionLostEvent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoke the connection restored event
|
||||
/// </summary>
|
||||
/// <param name="disconnectedPeriod">The period the connection was disconnected</param>
|
||||
public void InvokeConnectionRestored(TimeSpan disconnectedPeriod)
|
||||
{
|
||||
Subscription.HandleConnectionRestoredEvent(disconnectedPeriod);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoke the connection closed event
|
||||
/// </summary>
|
||||
public void InvokeConnectionClosed()
|
||||
{
|
||||
if (Interlocked.Exchange(ref _closedEventInvoked, 1) != 0)
|
||||
return;
|
||||
|
||||
SocketStatus = SocketStatus.Closed;
|
||||
_manualSubscription.Status = SubscriptionStatus.Closed;
|
||||
Subscription.HandleConnectionClosedEvent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoke the resubscribing failed event
|
||||
/// </summary>
|
||||
/// <param name="error">The resubscribe error</param>
|
||||
public void InvokeResubscribingFailed(Error error)
|
||||
{
|
||||
if (error == null)
|
||||
throw new ArgumentNullException(nameof(error));
|
||||
|
||||
Subscription.HandleResubscribeFailedEvent(error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoke the activity paused event
|
||||
/// </summary>
|
||||
public void InvokeActivityPaused()
|
||||
{
|
||||
Subscription.HandlePausedEvent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoke the activity unpaused event
|
||||
/// </summary>
|
||||
public void InvokeActivityUnpaused()
|
||||
{
|
||||
Subscription.HandleUnpausedEvent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoke the exception event
|
||||
/// </summary>
|
||||
/// <param name="exception">The exception</param>
|
||||
public void InvokeException(Exception exception)
|
||||
{
|
||||
if (exception == null)
|
||||
throw new ArgumentNullException(nameof(exception));
|
||||
|
||||
_manualSubscription.InvokeExceptionHandler(exception);
|
||||
}
|
||||
|
||||
internal async Task CloseAsync()
|
||||
{
|
||||
if (_manualSubscription.Status == SubscriptionStatus.Closed
|
||||
|| _manualSubscription.Status == SubscriptionStatus.Closing)
|
||||
return;
|
||||
|
||||
_manualSubscription.Status = SubscriptionStatus.Closing;
|
||||
try
|
||||
{
|
||||
await _closeAsync().ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_manualSubscription.Status = SubscriptionStatus.Closed;
|
||||
}
|
||||
}
|
||||
|
||||
internal Task ReconnectAsync()
|
||||
{
|
||||
return _reconnectAsync();
|
||||
}
|
||||
|
||||
internal Task<CallResult> ResubscribeAsync()
|
||||
{
|
||||
return _resubscribeAsync();
|
||||
}
|
||||
|
||||
private class ManualSubscription : Subscription
|
||||
{
|
||||
public ManualSubscription()
|
||||
: base(NullLogger.Instance, false)
|
||||
{
|
||||
MessageRouter = MessageRouter.Create();
|
||||
}
|
||||
|
||||
protected override Query? GetSubQuery(SocketConnection connection)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
protected override Query? GetUnsubQuery(SocketConnection connection)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,8 @@ namespace CryptoExchange.Net.Objects.Sockets
|
||||
/// </summary>
|
||||
public class UpdateSubscription
|
||||
{
|
||||
private readonly SocketConnection _connection;
|
||||
private readonly SocketConnection? _connection;
|
||||
private readonly ManualUpdateSubscription? _manualSubscription;
|
||||
internal readonly Subscription _subscription;
|
||||
|
||||
#if NET9_0_OR_GREATER
|
||||
@@ -102,7 +103,7 @@ namespace CryptoExchange.Net.Objects.Sockets
|
||||
/// <summary>
|
||||
/// The id of the socket
|
||||
/// </summary>
|
||||
public int SocketId => _connection.SocketId;
|
||||
public int SocketId => _connection?.SocketId ?? _manualSubscription!.SocketId;
|
||||
|
||||
/// <summary>
|
||||
/// The id of the subscription
|
||||
@@ -112,12 +113,12 @@ namespace CryptoExchange.Net.Objects.Sockets
|
||||
/// <summary>
|
||||
/// The last timestamp anything was received from the server
|
||||
/// </summary>
|
||||
public DateTime? LastReceiveTime => _connection.LastReceiveTime;
|
||||
public DateTime? LastReceiveTime => _connection?.LastReceiveTime ?? _manualSubscription!.LastReceiveTime;
|
||||
|
||||
/// <summary>
|
||||
/// The current websocket status
|
||||
/// </summary>
|
||||
public SocketStatus SocketStatus => _connection.Status;
|
||||
public SocketStatus SocketStatus => _connection?.Status ?? _manualSubscription!.SocketStatus;
|
||||
|
||||
/// <summary>
|
||||
/// The current subscription status
|
||||
@@ -143,6 +144,18 @@ namespace CryptoExchange.Net.Objects.Sockets
|
||||
_subscription.StatusChanged += (x) => SubscriptionStatusChanged?.Invoke(x);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="manualSubscription">The manual subscription for controlling events and data</param>
|
||||
/// <param name="subscription">The subscription</param>
|
||||
internal UpdateSubscription(ManualUpdateSubscription manualSubscription, Subscription subscription)
|
||||
{
|
||||
_manualSubscription = manualSubscription;
|
||||
_subscription = subscription;
|
||||
_subscription.StatusChanged += (x) => SubscriptionStatusChanged?.Invoke(x);
|
||||
}
|
||||
|
||||
private void UnsubscribeConnectionEvents()
|
||||
{
|
||||
lock (_eventLock)
|
||||
@@ -150,22 +163,26 @@ namespace CryptoExchange.Net.Objects.Sockets
|
||||
if (!_connectionEventsSubscribed)
|
||||
return;
|
||||
|
||||
_connection.ConnectionClosed -= HandleConnectionClosedEvent;
|
||||
_connection.ConnectionLost -= HandleConnectionLostEvent;
|
||||
_connection.ConnectionRestored -= HandleConnectionRestoredEvent;
|
||||
_connection.ResubscribingFailed -= HandleResubscribeFailedEvent;
|
||||
_connection.ActivityPaused -= HandlePausedEvent;
|
||||
_connection.ActivityUnpaused -= HandleUnpausedEvent;
|
||||
if (_connection != null)
|
||||
{
|
||||
_connection.ConnectionClosed -= HandleConnectionClosedEvent;
|
||||
_connection.ConnectionLost -= HandleConnectionLostEvent;
|
||||
_connection.ConnectionRestored -= HandleConnectionRestoredEvent;
|
||||
_connection.ResubscribingFailed -= HandleResubscribeFailedEvent;
|
||||
_connection.ActivityPaused -= HandlePausedEvent;
|
||||
_connection.ActivityUnpaused -= HandleUnpausedEvent;
|
||||
}
|
||||
|
||||
_connectionEventsSubscribed = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleConnectionClosedEvent()
|
||||
internal void HandleConnectionClosedEvent()
|
||||
{
|
||||
UnsubscribeConnectionEvents();
|
||||
|
||||
// If we're not the subscription closing this connection don't bother emitting
|
||||
if (!_subscription.IsClosingConnection)
|
||||
if (_connection != null && !_subscription.IsClosingConnection)
|
||||
return;
|
||||
|
||||
List<Action> handlers;
|
||||
@@ -176,7 +193,7 @@ namespace CryptoExchange.Net.Objects.Sockets
|
||||
callback();
|
||||
}
|
||||
|
||||
private void HandleConnectionLostEvent()
|
||||
internal void HandleConnectionLostEvent()
|
||||
{
|
||||
if (!_subscription.Active)
|
||||
{
|
||||
@@ -192,7 +209,7 @@ namespace CryptoExchange.Net.Objects.Sockets
|
||||
callback();
|
||||
}
|
||||
|
||||
private void HandleConnectionRestoredEvent(TimeSpan period)
|
||||
internal void HandleConnectionRestoredEvent(TimeSpan period)
|
||||
{
|
||||
if (!_subscription.Active)
|
||||
{
|
||||
@@ -208,7 +225,7 @@ namespace CryptoExchange.Net.Objects.Sockets
|
||||
callback(period);
|
||||
}
|
||||
|
||||
private void HandleResubscribeFailedEvent(Error error)
|
||||
internal void HandleResubscribeFailedEvent(Error error)
|
||||
{
|
||||
if (!_subscription.Active)
|
||||
{
|
||||
@@ -224,7 +241,7 @@ namespace CryptoExchange.Net.Objects.Sockets
|
||||
callback(error);
|
||||
}
|
||||
|
||||
private void HandlePausedEvent()
|
||||
internal void HandlePausedEvent()
|
||||
{
|
||||
if (!_subscription.Active)
|
||||
{
|
||||
@@ -240,7 +257,7 @@ namespace CryptoExchange.Net.Objects.Sockets
|
||||
callback();
|
||||
}
|
||||
|
||||
private void HandleUnpausedEvent()
|
||||
internal void HandleUnpausedEvent()
|
||||
{
|
||||
if (!_subscription.Active)
|
||||
{
|
||||
@@ -262,7 +279,10 @@ namespace CryptoExchange.Net.Objects.Sockets
|
||||
/// <returns></returns>
|
||||
public Task CloseAsync()
|
||||
{
|
||||
return _connection.CloseAsync(_subscription);
|
||||
if (_connection != null)
|
||||
return _connection.CloseAsync(_subscription);
|
||||
|
||||
return _manualSubscription!.CloseAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -271,7 +291,10 @@ namespace CryptoExchange.Net.Objects.Sockets
|
||||
/// <returns></returns>
|
||||
public Task ReconnectAsync()
|
||||
{
|
||||
return _connection.TriggerReconnectAsync();
|
||||
if (_connection != null)
|
||||
return _connection.TriggerReconnectAsync();
|
||||
|
||||
return _manualSubscription!.ReconnectAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -280,7 +303,13 @@ namespace CryptoExchange.Net.Objects.Sockets
|
||||
/// <returns></returns>
|
||||
internal async Task UnsubscribeAsync()
|
||||
{
|
||||
await _connection.UnsubscribeAsync(_subscription).ConfigureAwait(false);
|
||||
if (_connection != null)
|
||||
{
|
||||
await _connection.UnsubscribeAsync(_subscription).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
await _manualSubscription!.CloseAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -289,7 +318,10 @@ namespace CryptoExchange.Net.Objects.Sockets
|
||||
/// <returns></returns>
|
||||
internal async Task<CallResult> ResubscribeAsync()
|
||||
{
|
||||
return await _connection.ResubscribeAsync(_subscription).ConfigureAwait(false);
|
||||
if (_connection != null)
|
||||
return await _connection.ResubscribeAsync(_subscription).ConfigureAwait(false);
|
||||
|
||||
return await _manualSubscription!.ResubscribeAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,7 +262,7 @@ namespace CryptoExchange.Net.OrderBook
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<CallResult<bool>> StartAsync(CancellationToken? ct = null)
|
||||
public async Task<CallResult> StartAsync(CancellationToken? ct = null)
|
||||
{
|
||||
if (Status != OrderBookStatus.Disconnected)
|
||||
throw new InvalidOperationException($"Can't start book unless state is {OrderBookStatus.Disconnected}. Current state: {Status}");
|
||||
@@ -286,10 +286,10 @@ namespace CryptoExchange.Net.OrderBook
|
||||
_processTask = Task.Factory.StartNew(ProcessQueue, TaskCreationOptions.LongRunning);
|
||||
|
||||
var startResult = await DoStartAsync(_cts.Token).ConfigureAwait(false);
|
||||
if (!startResult)
|
||||
if (!startResult.Success)
|
||||
{
|
||||
Status = OrderBookStatus.Disconnected;
|
||||
return new CallResult<bool>(startResult.Error!);
|
||||
return CallResult.Fail(startResult.Error!);
|
||||
}
|
||||
|
||||
if (_cts.IsCancellationRequested)
|
||||
@@ -297,7 +297,7 @@ namespace CryptoExchange.Net.OrderBook
|
||||
_logger.OrderBookStoppedStarting(Api, Symbol);
|
||||
await startResult.Data.CloseAsync().ConfigureAwait(false);
|
||||
Status = OrderBookStatus.Disconnected;
|
||||
return new CallResult<bool>(new CancellationRequestedError());
|
||||
return CallResult.Fail(new CancellationRequestedError());
|
||||
}
|
||||
|
||||
_subscription = startResult.Data;
|
||||
@@ -306,7 +306,7 @@ namespace CryptoExchange.Net.OrderBook
|
||||
_subscription.ConnectionRestored += HandleConnectionRestored;
|
||||
|
||||
Status = OrderBookStatus.Synced;
|
||||
return new CallResult<bool>(true);
|
||||
return CallResult.Ok();
|
||||
}
|
||||
|
||||
private void HandleConnectionLost()
|
||||
@@ -354,7 +354,7 @@ namespace CryptoExchange.Net.OrderBook
|
||||
public CallResult<decimal> CalculateAverageFillPrice(decimal baseQuantity, OrderBookEntryType type)
|
||||
{
|
||||
if (Status != OrderBookStatus.Synced)
|
||||
return new CallResult<decimal>(new InvalidOperationError($"{nameof(CalculateAverageFillPrice)} is not available when book is not in Synced state"));
|
||||
return CallResult<decimal>.Fail(new InvalidOperationError($"{nameof(CalculateAverageFillPrice)} is not available when book is not in Synced state"));
|
||||
|
||||
var totalCost = 0m;
|
||||
var totalAmount = 0m;
|
||||
@@ -367,7 +367,7 @@ namespace CryptoExchange.Net.OrderBook
|
||||
while (amountLeft > 0)
|
||||
{
|
||||
if (step == list.Count)
|
||||
return new CallResult<decimal>(new InvalidOperationError("Quantity is larger than order in the order book"));
|
||||
return CallResult<decimal>.Fail(new InvalidOperationError("Quantity is larger than order in the order book"));
|
||||
|
||||
var element = list.ElementAt(step);
|
||||
var stepAmount = Math.Min(element.Value.Quantity, amountLeft);
|
||||
@@ -378,14 +378,14 @@ namespace CryptoExchange.Net.OrderBook
|
||||
}
|
||||
}
|
||||
|
||||
return new CallResult<decimal>(Math.Round(totalCost / totalAmount, 8));
|
||||
return CallResult<decimal>.Ok(Math.Round(totalCost / totalAmount, 8));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public CallResult<decimal> CalculateTradableAmount(decimal quoteQuantity, OrderBookEntryType type)
|
||||
{
|
||||
if (Status != OrderBookStatus.Synced)
|
||||
return new CallResult<decimal>(new InvalidOperationError($"{nameof(CalculateTradableAmount)} is not available when book is not in Synced state"));
|
||||
return CallResult<decimal>.Fail(new InvalidOperationError($"{nameof(CalculateTradableAmount)} is not available when book is not in Synced state"));
|
||||
|
||||
var quoteQuantityLeft = quoteQuantity;
|
||||
var totalBaseQuantity = 0m;
|
||||
@@ -397,7 +397,7 @@ namespace CryptoExchange.Net.OrderBook
|
||||
while (quoteQuantityLeft > 0)
|
||||
{
|
||||
if (step == list.Count)
|
||||
return new CallResult<decimal>(new InvalidOperationError("Quantity is larger than order in the order book"));
|
||||
return CallResult<decimal>.Fail(new InvalidOperationError("Quantity is larger than order in the order book"));
|
||||
|
||||
var element = list.ElementAt(step);
|
||||
var stepAmount = Math.Min(element.Value.Quantity * element.Value.Price, quoteQuantityLeft);
|
||||
@@ -407,7 +407,7 @@ namespace CryptoExchange.Net.OrderBook
|
||||
}
|
||||
}
|
||||
|
||||
return new CallResult<decimal>(Math.Round(totalBaseQuantity, 8));
|
||||
return CallResult<decimal>.Ok(Math.Round(totalBaseQuantity, 8));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -426,7 +426,7 @@ namespace CryptoExchange.Net.OrderBook
|
||||
/// Resync the order book
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected abstract Task<CallResult<bool>> DoResyncAsync(CancellationToken ct);
|
||||
protected abstract Task<CallResult> DoResyncAsync(CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Implementation for validating a checksum value with the current order book. If checksum validation fails (returns false)
|
||||
@@ -605,10 +605,9 @@ namespace CryptoExchange.Net.OrderBook
|
||||
var listToChange = type == OrderBookEntryType.Ask ? _asks : _bids;
|
||||
if (entry.Quantity == 0)
|
||||
{
|
||||
if (!listToChange.ContainsKey(entry.Price))
|
||||
if (!listToChange.Remove(entry.Price))
|
||||
return true;
|
||||
|
||||
listToChange.Remove(entry.Price);
|
||||
if (type == OrderBookEntryType.Ask) AskCount--;
|
||||
else BidCount--;
|
||||
}
|
||||
@@ -635,16 +634,16 @@ namespace CryptoExchange.Net.OrderBook
|
||||
/// <param name="timeout">Max wait time</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
/// <returns></returns>
|
||||
protected async Task<CallResult<bool>> WaitForSetOrderBookAsync(TimeSpan timeout, CancellationToken ct)
|
||||
protected async Task<CallResult> WaitForSetOrderBookAsync(TimeSpan timeout, CancellationToken ct)
|
||||
{
|
||||
var startWait = DateTime.UtcNow;
|
||||
while (!_bookSet && Status == OrderBookStatus.Syncing)
|
||||
{
|
||||
if(ct.IsCancellationRequested)
|
||||
return new CallResult<bool>(new CancellationRequestedError());
|
||||
return CallResult.Fail(new CancellationRequestedError());
|
||||
|
||||
if (DateTime.UtcNow - startWait > timeout)
|
||||
return new CallResult<bool>(new ServerError(new ErrorInfo(ErrorType.OrderBookTimeout, "Timeout while waiting for data")));
|
||||
return CallResult.Fail(new ServerError(new ErrorInfo(ErrorType.OrderBookTimeout, "Timeout while waiting for data")));
|
||||
|
||||
try
|
||||
{
|
||||
@@ -654,7 +653,7 @@ namespace CryptoExchange.Net.OrderBook
|
||||
{ }
|
||||
}
|
||||
|
||||
return new CallResult<bool>(true);
|
||||
return CallResult.Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -670,10 +669,10 @@ namespace CryptoExchange.Net.OrderBook
|
||||
while (_processBuffer.Count == 0)
|
||||
{
|
||||
if (ct.IsCancellationRequested)
|
||||
return new CallResult(new CancellationRequestedError());
|
||||
return CallResult.Fail(new CancellationRequestedError());
|
||||
|
||||
if (DateTime.UtcNow - startWait > maxWait)
|
||||
return new CallResult(new ServerError(new ErrorInfo(ErrorType.OrderBookTimeout, "Timeout while waiting for data")));
|
||||
return CallResult.Fail(new ServerError(new ErrorInfo(ErrorType.OrderBookTimeout, "Timeout while waiting for data")));
|
||||
|
||||
try
|
||||
{
|
||||
@@ -690,7 +689,7 @@ namespace CryptoExchange.Net.OrderBook
|
||||
await Task.Delay(minWait.Value - dif).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return CallResult.SuccessResult;
|
||||
return CallResult.Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -809,7 +808,7 @@ namespace CryptoExchange.Net.OrderBook
|
||||
return;
|
||||
|
||||
var resyncResult = await DoResyncAsync(_cts!.Token).ConfigureAwait(false);
|
||||
success = resyncResult;
|
||||
success = resyncResult.Success;
|
||||
}
|
||||
|
||||
_logger.OrderBookResynced(Api, Symbol);
|
||||
@@ -835,7 +834,7 @@ namespace CryptoExchange.Net.OrderBook
|
||||
|
||||
if (item is OrderBookSnapshot snapshot)
|
||||
ProcessOrderBookSnapshot(snapshot);
|
||||
if (item is OrderBookUpdate update)
|
||||
else if (item is OrderBookUpdate update)
|
||||
ProcessQueueItem(update);
|
||||
else if (item is OrderBookChecksum checksum)
|
||||
ProcessChecksum(checksum);
|
||||
@@ -963,7 +962,8 @@ namespace CryptoExchange.Net.OrderBook
|
||||
await _subscription!.UnsubscribeAsync().ConfigureAwait(false);
|
||||
Reset();
|
||||
_stopProcessing = false;
|
||||
if (!await _subscription!.ResubscribeAsync().ConfigureAwait(false))
|
||||
var resubResult = await _subscription!.ResubscribeAsync().ConfigureAwait(false);
|
||||
if (!resubResult.Success)
|
||||
{
|
||||
// Resubscribing failed, reconnect the socket
|
||||
_logger.OrderBookResyncFailed(Api, Symbol);
|
||||
@@ -1057,8 +1057,10 @@ namespace CryptoExchange.Net.OrderBook
|
||||
{
|
||||
if (sequenceNumber < LastSequenceNumber
|
||||
&& (_firstUpdateAfterSnapshotDone || !_skipSequenceCheckFirstUpdateAfterSnapshotSet))
|
||||
{
|
||||
// Update is somehow from before the current state
|
||||
return SequenceNumberResult.OutOfSync;
|
||||
}
|
||||
|
||||
if (_sequencesAreConsecutive
|
||||
&& LastSequenceNumber != 0
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace CryptoExchange.Net.RateLimiting.Filters
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey)
|
||||
public bool Passes(RateLimitItemType type, RequestDefinition definition, string? apiKey)
|
||||
=> definition.Authenticated == _authenticated;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace CryptoExchange.Net.RateLimiting.Filters
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey)
|
||||
public bool Passes(RateLimitItemType type, RequestDefinition definition, string? apiKey)
|
||||
=> string.Equals(definition.Path, _path, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace CryptoExchange.Net.RateLimiting.Filters
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey)
|
||||
public bool Passes(RateLimitItemType type, RequestDefinition definition, string? apiKey)
|
||||
=> _paths.Contains(definition.Path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,8 +20,8 @@ namespace CryptoExchange.Net.RateLimiting.Filters
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey)
|
||||
=> host.Equals(_host, System.StringComparison.InvariantCulture);
|
||||
public bool Passes(RateLimitItemType type, RequestDefinition definition, string? apiKey)
|
||||
=> definition.BaseAddress.Equals(_host, System.StringComparison.InvariantCulture);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace CryptoExchange.Net.RateLimiting.Filters
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey)
|
||||
public bool Passes(RateLimitItemType type, RequestDefinition definition, string? apiKey)
|
||||
=> type == _type;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace CryptoExchange.Net.RateLimiting.Filters
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey)
|
||||
public bool Passes(RateLimitItemType type, RequestDefinition definition, string? apiKey)
|
||||
=> definition.Path.TrimStart('/').StartsWith(_path, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,30 +14,30 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
/// <summary>
|
||||
/// Apply guard per host
|
||||
/// </summary>
|
||||
public static Func<RequestDefinition, string, string?, string> PerHost { get; } = new Func<RequestDefinition, string, string?, string>((def, host, key) => host);
|
||||
public static Func<RequestDefinition, string?, string> PerHost { get; } = new Func<RequestDefinition, string?, string>((def, key) => def.BaseAddress);
|
||||
/// <summary>
|
||||
/// Apply guard per endpoint
|
||||
/// </summary>
|
||||
public static Func<RequestDefinition, string, string?, string> PerEndpoint { get; } = new Func<RequestDefinition, string, string?, string>((def, host, key) => def.Path + def.Method);
|
||||
public static Func<RequestDefinition, string?, string> PerEndpoint { get; } = new Func<RequestDefinition, string?, string>((def, key) => def.Path + def.Method);
|
||||
/// <summary>
|
||||
/// Apply guard per connection
|
||||
/// </summary>
|
||||
public static Func<RequestDefinition, string, string?, string> PerConnection { get; } = new Func<RequestDefinition, string, string?, string>((def, host, key) => def.ConnectionId.ToString()!);
|
||||
public static Func<RequestDefinition, string?, string> PerConnection { get; } = new Func<RequestDefinition, string?, string>((def, key) => def.ConnectionId.ToString()!);
|
||||
/// <summary>
|
||||
/// Apply guard per API key
|
||||
/// </summary>
|
||||
public static Func<RequestDefinition, string, string?, string> PerApiKey { get; } = new Func<RequestDefinition, string, string?, string>((def, host, key) => key!);
|
||||
public static Func<RequestDefinition, string?, string> PerApiKey { get; } = new Func<RequestDefinition, string?, string>((def, key) => key!);
|
||||
/// <summary>
|
||||
/// Apply guard per API key per endpoint
|
||||
/// </summary>
|
||||
public static Func<RequestDefinition, string, string?, string> PerApiKeyPerEndpoint { get; } = new Func<RequestDefinition, string, string?, string>((def, host, key) => key! + def.Path + def.Method);
|
||||
public static Func<RequestDefinition, string?, string> PerApiKeyPerEndpoint { get; } = new Func<RequestDefinition, string?, string>((def, key) => key! + def.Path + def.Method);
|
||||
|
||||
private readonly IEnumerable<IGuardFilter> _filters;
|
||||
private readonly Dictionary<string, IWindowTracker> _trackers;
|
||||
private readonly RateLimitWindowType _windowType;
|
||||
private readonly double? _decayRate;
|
||||
private readonly int? _connectionWeight;
|
||||
private readonly Func<RequestDefinition, string, string?, string> _keySelector;
|
||||
private readonly Func<RequestDefinition, string?, string> _keySelector;
|
||||
private readonly SemaphoreSlim? _sharedGuardSemaphore;
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -71,7 +71,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
/// <param name="decayPerTimeSpan">The decay per timespan if windowType is DecayWindowTracker</param>
|
||||
/// <param name="connectionWeight">The weight of a new connection</param>
|
||||
/// <param name="shared">Whether this guard is shared between multiple gates</param>
|
||||
public RateLimitGuard(Func<RequestDefinition, string, string?, string> keySelector, IGuardFilter filter, int limit, TimeSpan timeSpan, RateLimitWindowType windowType, double? decayPerTimeSpan = null, int? connectionWeight = null, bool shared = false)
|
||||
public RateLimitGuard(Func<RequestDefinition, string?, string> keySelector, IGuardFilter filter, int limit, TimeSpan timeSpan, RateLimitWindowType windowType, double? decayPerTimeSpan = null, int? connectionWeight = null, bool shared = false)
|
||||
: this(keySelector, new[] { filter }, limit, timeSpan, windowType, decayPerTimeSpan, connectionWeight, shared)
|
||||
{
|
||||
}
|
||||
@@ -87,7 +87,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
/// <param name="decayPerTimeSpan">The decay per timespan if windowType is DecayWindowTracker</param>
|
||||
/// <param name="connectionWeight">The weight of a new connection</param>
|
||||
/// <param name="shared">Whether this guard is shared between multiple gates</param>
|
||||
public RateLimitGuard(Func<RequestDefinition, string, string?, string> keySelector, IEnumerable<IGuardFilter> filters, int limit, TimeSpan timeSpan, RateLimitWindowType windowType, double? decayPerTimeSpan = null, int? connectionWeight = null, bool shared = false)
|
||||
public RateLimitGuard(Func<RequestDefinition, string?, string> keySelector, IEnumerable<IGuardFilter> filters, int limit, TimeSpan timeSpan, RateLimitWindowType windowType, double? decayPerTimeSpan = null, int? connectionWeight = null, bool shared = false)
|
||||
{
|
||||
_filters = filters;
|
||||
_trackers = new Dictionary<string, IWindowTracker>();
|
||||
@@ -104,11 +104,11 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix)
|
||||
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, string? keySuffix)
|
||||
{
|
||||
foreach (var filter in _filters)
|
||||
{
|
||||
if (!filter.Passes(type, definition, host, apiKey))
|
||||
if (!filter.Passes(type, definition, apiKey))
|
||||
return LimitCheck.NotApplicable;
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
|
||||
try
|
||||
{
|
||||
var key = _keySelector(definition, host, apiKey) + keySuffix;
|
||||
var key = _keySelector(definition, apiKey) + keySuffix;
|
||||
if (!_trackers.TryGetValue(key, out var tracker))
|
||||
{
|
||||
tracker = CreateTracker();
|
||||
@@ -141,11 +141,11 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix)
|
||||
public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, string? keySuffix)
|
||||
{
|
||||
foreach (var filter in _filters)
|
||||
{
|
||||
if (!filter.Passes(type, definition, host, apiKey))
|
||||
if (!filter.Passes(type, definition, apiKey))
|
||||
return RateLimitState.NotApplied;
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
requestWeight = _connectionWeight ?? requestWeight;
|
||||
|
||||
|
||||
var key = _keySelector(definition, host, apiKey) + keySuffix;
|
||||
var key = _keySelector(definition, apiKey) + keySuffix;
|
||||
var tracker = _trackers[key];
|
||||
|
||||
if (SharedGuard)
|
||||
@@ -173,11 +173,11 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Reset(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, string? keySuffix)
|
||||
public void Reset(RateLimitItemType type, RequestDefinition definition, string? apiKey, string? keySuffix, int? amount)
|
||||
{
|
||||
foreach (var filter in _filters)
|
||||
{
|
||||
if (!filter.Passes(type, definition, host, apiKey))
|
||||
if (!filter.Passes(type, definition, apiKey))
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -186,11 +186,11 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
|
||||
try
|
||||
{
|
||||
var key = _keySelector(definition, host, apiKey) + keySuffix;
|
||||
var key = _keySelector(definition, apiKey) + keySuffix;
|
||||
if (!_trackers.TryGetValue(key, out var tracker))
|
||||
return;
|
||||
|
||||
tracker.Reset();
|
||||
tracker.Reset(amount);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
@@ -42,7 +42,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix)
|
||||
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, string? keySuffix)
|
||||
{
|
||||
if (type != Type)
|
||||
return LimitCheck.NotApplicable;
|
||||
@@ -55,7 +55,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix)
|
||||
public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, string? keySuffix)
|
||||
{
|
||||
return RateLimitState.NotApplied;
|
||||
}
|
||||
@@ -67,7 +67,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
public void UpdateAfter(DateTime after) => After = after;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Reset(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, string? keySuffix)
|
||||
public void Reset(RateLimitItemType type, RequestDefinition definition, string? apiKey, string? keySuffix, int? amount)
|
||||
{
|
||||
After = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
@@ -14,19 +14,19 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
/// <summary>
|
||||
/// Default endpoint limit
|
||||
/// </summary>
|
||||
public static Func<RequestDefinition, string, string?, string> Default { get; } = new Func<RequestDefinition, string, string?, string>((def, host, key) => def.Path + def.Method);
|
||||
public static Func<RequestDefinition, string?, string> Default { get; } = new Func<RequestDefinition, string?, string>((def, key) => def.Path + def.Method);
|
||||
|
||||
/// <summary>
|
||||
/// Endpoint limit per API key
|
||||
/// </summary>
|
||||
public static Func<RequestDefinition, string, string?, string> PerApiKey { get; } = new Func<RequestDefinition, string, string?, string>((def, host, key) => def.Path + def.Method + key);
|
||||
public static Func<RequestDefinition, string?, string> PerApiKey { get; } = new Func<RequestDefinition, string?, string>((def, key) => def.Path + def.Method + key);
|
||||
|
||||
private readonly Dictionary<string, IWindowTracker> _trackers;
|
||||
private readonly RateLimitWindowType _windowType;
|
||||
private readonly double? _decayRate;
|
||||
private readonly int _limit;
|
||||
private readonly TimeSpan _period;
|
||||
private readonly Func<RequestDefinition, string, string?, string> _keySelector;
|
||||
private readonly Func<RequestDefinition, string?, string> _keySelector;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => "EndpointLimitGuard";
|
||||
@@ -42,7 +42,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
TimeSpan period,
|
||||
RateLimitWindowType windowType,
|
||||
double? decayRate = null,
|
||||
Func<RequestDefinition, string, string?, string>? keySelector = null)
|
||||
Func<RequestDefinition, string?, string>? keySelector = null)
|
||||
{
|
||||
_limit = limit;
|
||||
_period = period;
|
||||
@@ -53,9 +53,9 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix)
|
||||
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, string? keySuffix)
|
||||
{
|
||||
var key = _keySelector(definition, host, apiKey) + keySuffix;
|
||||
var key = _keySelector(definition, apiKey) + keySuffix;
|
||||
if (!_trackers.TryGetValue(key, out var tracker))
|
||||
{
|
||||
tracker = CreateTracker();
|
||||
@@ -70,9 +70,9 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix)
|
||||
public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, string? keySuffix)
|
||||
{
|
||||
var key = _keySelector(definition, host, apiKey) + keySuffix;
|
||||
var key = _keySelector(definition, apiKey) + keySuffix;
|
||||
var tracker = _trackers[key];
|
||||
tracker.ApplyWeight(requestWeight);
|
||||
return RateLimitState.Applied(_limit, _period, tracker.Current);
|
||||
@@ -90,13 +90,13 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Reset(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, string? keySuffix)
|
||||
public void Reset(RateLimitItemType type, RequestDefinition definition, string? apiKey, string? keySuffix, int? amount)
|
||||
{
|
||||
var key = _keySelector(definition, host, apiKey) + keySuffix;
|
||||
var key = _keySelector(definition, apiKey) + keySuffix;
|
||||
if (!_trackers.TryGetValue(key, out var tracker))
|
||||
return;
|
||||
|
||||
tracker.Reset();
|
||||
tracker.Reset(amount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,9 +12,8 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
|
||||
/// </summary>
|
||||
/// <param name="type">The type of item</param>
|
||||
/// <param name="definition">The request definition</param>
|
||||
/// <param name="host">The host address</param>
|
||||
/// <param name="apiKey">The API key</param>
|
||||
/// <returns>True if passed</returns>
|
||||
bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey);
|
||||
bool Passes(RateLimitItemType type, RequestDefinition definition, string? apiKey);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,14 +49,13 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
|
||||
/// <param name="itemId">Id of the item to check</param>
|
||||
/// <param name="type">The rate limit item type</param>
|
||||
/// <param name="definition">The request definition</param>
|
||||
/// <param name="baseAddress">The host address</param>
|
||||
/// <param name="apiKey">The API key</param>
|
||||
/// <param name="requestWeight">Request weight</param>
|
||||
/// <param name="behaviour">Behaviour when rate limit is hit</param>
|
||||
/// <param name="keySuffix">An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters.</param>
|
||||
/// <param name="ct">Cancelation token</param>
|
||||
/// <returns>Error if RateLimitingBehaviour is Fail and rate limit is hit</returns>
|
||||
ValueTask<CallResult> ProcessAsync(ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string baseAddress, string? apiKey, int requestWeight, RateLimitingBehaviour behaviour, string? keySuffix, CancellationToken ct);
|
||||
ValueTask<CallResult> ProcessAsync(ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, RateLimitingBehaviour behaviour, string? keySuffix, CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Enforces the rate limit as defined in the request definition. When a rate limit is hit will wait for the rate limit to pass if RateLimitingBehaviour is Wait, or return an error if it is set to Fail
|
||||
@@ -66,30 +65,29 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
|
||||
/// <param name="guard">The guard</param>
|
||||
/// <param name="type">The rate limit item type</param>
|
||||
/// <param name="definition">The request definition</param>
|
||||
/// <param name="baseAddress">The host address</param>
|
||||
/// <param name="apiKey">The API key</param>
|
||||
/// <param name="behaviour">Behaviour when rate limit is hit</param>
|
||||
/// <param name="requestWeight">The weight to apply to the limit guard</param>
|
||||
/// <param name="keySuffix">An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters.</param>
|
||||
/// <param name="ct">Cancelation token</param>
|
||||
/// <returns>Error if RateLimitingBehaviour is Fail and rate limit is hit</returns>
|
||||
ValueTask<CallResult> ProcessSingleAsync(ILogger logger, int itemId, IRateLimitGuard guard, RateLimitItemType type, RequestDefinition definition, string baseAddress, string? apiKey, int requestWeight, RateLimitingBehaviour behaviour, string? keySuffix, CancellationToken ct);
|
||||
ValueTask<CallResult> ProcessSingleAsync(ILogger logger, int itemId, IRateLimitGuard guard, RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, RateLimitingBehaviour behaviour, string? keySuffix, CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Reset the limit for the specified parameters
|
||||
/// </summary>
|
||||
/// <param name="type">The rate limit item type</param>
|
||||
/// <param name="definition">The request definition</param>
|
||||
/// <param name="host">The host address</param>
|
||||
/// <param name="apiKey">The API key</param>
|
||||
/// <param name="keySuffix">An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters.</param>
|
||||
/// <param name="amount">Amount in weight to reset by, or null to set used rate limit to 0</param>
|
||||
/// <param name="ct">Cancelation token</param>
|
||||
Task ResetAsync(
|
||||
RateLimitItemType type,
|
||||
RequestDefinition definition,
|
||||
string host,
|
||||
string? apiKey,
|
||||
string? keySuffix,
|
||||
int? amount,
|
||||
CancellationToken ct);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,33 +22,31 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
|
||||
/// </summary>
|
||||
/// <param name="type">The rate limit item type</param>
|
||||
/// <param name="definition">The request definition</param>
|
||||
/// <param name="host">The host address</param>
|
||||
/// <param name="apiKey">The API key</param>
|
||||
/// <param name="requestWeight">The request weight</param>
|
||||
/// <param name="keySuffix">An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters.</param>
|
||||
/// <returns></returns>
|
||||
LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix);
|
||||
LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, string? keySuffix);
|
||||
|
||||
/// <summary>
|
||||
/// Apply the request to this guard with the specified weight
|
||||
/// </summary>
|
||||
/// <param name="type">The rate limit item type</param>
|
||||
/// <param name="definition">The request definition</param>
|
||||
/// <param name="host">The host address</param>
|
||||
/// <param name="apiKey">The API key</param>
|
||||
/// <param name="requestWeight">The request weight</param>
|
||||
/// <param name="keySuffix">An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters.</param>
|
||||
/// <returns></returns>
|
||||
RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix);
|
||||
RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, string? keySuffix);
|
||||
|
||||
/// <summary>
|
||||
/// Reset the limit for the specified parameters
|
||||
/// </summary>
|
||||
/// <param name="type">The rate limit item type</param>
|
||||
/// <param name="definition">The request definition</param>
|
||||
/// <param name="host">The host address</param>
|
||||
/// <param name="apiKey">The API key</param>
|
||||
/// <param name="keySuffix">An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters.</param>
|
||||
void Reset(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, string? keySuffix);
|
||||
/// <param name="amount">Amount in weight to reset by, or null to set used rate limit to 0</param>
|
||||
void Reset(RateLimitItemType type, RequestDefinition definition, string? apiKey, string? keySuffix, int? amount);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,6 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
|
||||
/// <summary>
|
||||
/// Reset the limit counter for this tracker
|
||||
/// </summary>
|
||||
void Reset();
|
||||
void Reset(int? amount);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,10 +25,6 @@ namespace CryptoExchange.Net.RateLimiting
|
||||
/// </summary>
|
||||
public RequestDefinition RequestDefinition { get; set; }
|
||||
/// <summary>
|
||||
/// The host the request is for
|
||||
/// </summary>
|
||||
public string Host { get; set; } = default!;
|
||||
/// <summary>
|
||||
/// The current counter value
|
||||
/// </summary>
|
||||
public int Current { get; set; }
|
||||
@@ -56,13 +52,12 @@ namespace CryptoExchange.Net.RateLimiting
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public RateLimitEvent(int itemId, string apiLimit, string limitDescription, RequestDefinition definition, string host, int current, int requestWeight, int? limit, TimeSpan? timePeriod, TimeSpan? delayTime, RateLimitingBehaviour behaviour)
|
||||
public RateLimitEvent(int itemId, string apiLimit, string limitDescription, RequestDefinition definition, int current, int requestWeight, int? limit, TimeSpan? timePeriod, TimeSpan? delayTime, RateLimitingBehaviour behaviour)
|
||||
{
|
||||
ItemId = itemId;
|
||||
ApiLimit = apiLimit;
|
||||
LimitDescription = limitDescription;
|
||||
RequestDefinition = definition;
|
||||
Host = host;
|
||||
Current = current;
|
||||
RequestWeight = requestWeight;
|
||||
Limit = limit;
|
||||
|
||||
@@ -37,20 +37,20 @@ namespace CryptoExchange.Net.RateLimiting
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<CallResult> ProcessAsync(ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, RateLimitingBehaviour rateLimitingBehaviour, string? keySuffix, CancellationToken ct)
|
||||
public async ValueTask<CallResult> ProcessAsync(ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, RateLimitingBehaviour rateLimitingBehaviour, string? keySuffix, CancellationToken ct)
|
||||
{
|
||||
await _semaphore.WaitAsync(ct).ConfigureAwait(false);
|
||||
bool release = true;
|
||||
_waitingCount++;
|
||||
try
|
||||
{
|
||||
return await CheckGuardsAsync(_guards, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, keySuffix, ct).ConfigureAwait(false);
|
||||
return await CheckGuardsAsync(_guards, logger, itemId, type, definition, apiKey, requestWeight, rateLimitingBehaviour, keySuffix, ct).ConfigureAwait(false);
|
||||
}
|
||||
catch (TaskCanceledException tce)
|
||||
{
|
||||
// The semaphore has already been released if the task was cancelled
|
||||
release = false;
|
||||
return new CallResult(new CancellationRequestedError(tce));
|
||||
return CallResult.Fail(new CancellationRequestedError(tce));
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -67,7 +67,6 @@ namespace CryptoExchange.Net.RateLimiting
|
||||
IRateLimitGuard guard,
|
||||
RateLimitItemType type,
|
||||
RequestDefinition definition,
|
||||
string host,
|
||||
string? apiKey,
|
||||
int requestWeight,
|
||||
RateLimitingBehaviour rateLimitingBehaviour,
|
||||
@@ -79,13 +78,13 @@ namespace CryptoExchange.Net.RateLimiting
|
||||
_waitingCount++;
|
||||
try
|
||||
{
|
||||
return await CheckGuardsAsync(new IRateLimitGuard[] { guard }, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, keySuffix, ct).ConfigureAwait(false);
|
||||
return await CheckGuardsAsync(new IRateLimitGuard[] { guard }, logger, itemId, type, definition, apiKey, requestWeight, rateLimitingBehaviour, keySuffix, ct).ConfigureAwait(false);
|
||||
}
|
||||
catch (TaskCanceledException tce)
|
||||
{
|
||||
// The semaphore has already been released if the task was cancelled
|
||||
release = false;
|
||||
return new CallResult(new CancellationRequestedError(tce));
|
||||
return CallResult.Fail(new CancellationRequestedError(tce));
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -95,12 +94,12 @@ namespace CryptoExchange.Net.RateLimiting
|
||||
}
|
||||
}
|
||||
|
||||
private async ValueTask<CallResult> CheckGuardsAsync(IEnumerable<IRateLimitGuard> guards, ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, RateLimitingBehaviour rateLimitingBehaviour, string? keySuffix, CancellationToken ct)
|
||||
private async ValueTask<CallResult> CheckGuardsAsync(IEnumerable<IRateLimitGuard> guards, ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, RateLimitingBehaviour rateLimitingBehaviour, string? keySuffix, CancellationToken ct)
|
||||
{
|
||||
foreach (var guard in guards)
|
||||
{
|
||||
// Check if a wait is needed for this guard
|
||||
var result = guard.Check(type, definition, host, apiKey, requestWeight, keySuffix);
|
||||
var result = guard.Check(type, definition, apiKey, requestWeight, keySuffix);
|
||||
if (result.Delay != TimeSpan.Zero && rateLimitingBehaviour == RateLimitingBehaviour.Fail)
|
||||
{
|
||||
// Delay is needed and limit behaviour is to fail the request
|
||||
@@ -109,8 +108,8 @@ namespace CryptoExchange.Net.RateLimiting
|
||||
else
|
||||
logger.RateLimitRequestFailed(itemId, definition.Path, guard.Name, guard.Description);
|
||||
|
||||
RateLimitTriggered?.Invoke(new RateLimitEvent(itemId, _name, guard.Description, definition, host, result.Current, requestWeight, result.Limit, result.Period, result.Delay, rateLimitingBehaviour));
|
||||
return new CallResult(new ClientRateLimitError($"Rate limit check failed on guard {guard.Name}; {guard.Description}"));
|
||||
RateLimitTriggered?.Invoke(new RateLimitEvent(itemId, _name, guard.Description, definition, result.Current, requestWeight, result.Limit, result.Period, result.Delay, rateLimitingBehaviour));
|
||||
return CallResult.Fail(new ClientRateLimitError($"Rate limit check failed on guard {guard.Name}; {guard.Description}"));
|
||||
}
|
||||
|
||||
if (result.Delay != TimeSpan.Zero)
|
||||
@@ -124,17 +123,17 @@ namespace CryptoExchange.Net.RateLimiting
|
||||
else
|
||||
logger.RateLimitDelayingRequest(itemId, definition.Path, result.Delay, guard.Name, description);
|
||||
|
||||
RateLimitTriggered?.Invoke(new RateLimitEvent(itemId, _name, guard.Description, definition, host, result.Current, requestWeight, result.Limit, result.Period, result.Delay, rateLimitingBehaviour));
|
||||
RateLimitTriggered?.Invoke(new RateLimitEvent(itemId, _name, guard.Description, definition, result.Current, requestWeight, result.Limit, result.Period, result.Delay, rateLimitingBehaviour));
|
||||
await Task.Delay((int)result.Delay.TotalMilliseconds + 1, ct).ConfigureAwait(false);
|
||||
await _semaphore.WaitAsync(ct).ConfigureAwait(false);
|
||||
return await CheckGuardsAsync(guards, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, keySuffix, ct).ConfigureAwait(false);
|
||||
return await CheckGuardsAsync(guards, logger, itemId, type, definition, apiKey, requestWeight, rateLimitingBehaviour, keySuffix, ct).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply the weight on each guard
|
||||
foreach (var guard in guards)
|
||||
{
|
||||
var result = guard.ApplyWeight(type, definition, host, apiKey, requestWeight, keySuffix);
|
||||
var result = guard.ApplyWeight(type, definition, apiKey, requestWeight, keySuffix);
|
||||
if (result.IsApplied)
|
||||
{
|
||||
RateLimitUpdated?.Invoke(new RateLimitUpdateEvent(itemId, _name, guard.Description, result.Current, result.Limit, result.Period));
|
||||
@@ -149,7 +148,7 @@ namespace CryptoExchange.Net.RateLimiting
|
||||
}
|
||||
}
|
||||
|
||||
return CallResult.SuccessResult;
|
||||
return CallResult.Ok();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -198,16 +197,16 @@ namespace CryptoExchange.Net.RateLimiting
|
||||
public async Task ResetAsync(
|
||||
RateLimitItemType type,
|
||||
RequestDefinition definition,
|
||||
string host,
|
||||
string? apiKey,
|
||||
string? keySuffix,
|
||||
int? amount,
|
||||
CancellationToken ct)
|
||||
{
|
||||
await _semaphore.WaitAsync(ct).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
foreach (var guard in _guards)
|
||||
guard.Reset(type, definition, host, apiKey, keySuffix);
|
||||
guard.Reset(type, definition, apiKey, keySuffix, amount);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
@@ -27,10 +27,17 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Reset()
|
||||
public void Reset(int? amount)
|
||||
{
|
||||
_currentWeight = 0;
|
||||
_lastDecrease = DateTime.UtcNow;
|
||||
if (amount == null)
|
||||
{
|
||||
_lastDecrease = DateTime.UtcNow;
|
||||
_currentWeight = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
_currentWeight = Math.Max(0, _currentWeight - amount.Value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -30,11 +30,27 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Reset()
|
||||
public void Reset(int? amount)
|
||||
{
|
||||
_entries.Clear();
|
||||
_currentWeight = 0;
|
||||
_nextReset = null;
|
||||
if (amount == null)
|
||||
{
|
||||
_entries.Clear();
|
||||
_currentWeight = 0;
|
||||
_nextReset = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
_currentWeight = Math.Max(0, _currentWeight - amount.Value);
|
||||
var removedWeight = 0;
|
||||
while (true)
|
||||
{
|
||||
if (removedWeight >= amount.Value || _entries.Count == 0)
|
||||
break;
|
||||
|
||||
var lastEntry = _entries.Dequeue();
|
||||
removedWeight += lastEntry.Weight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public TimeSpan GetWaitTime(int weight)
|
||||
|
||||
@@ -29,10 +29,26 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Reset()
|
||||
public void Reset(int? amount)
|
||||
{
|
||||
_entries.Clear();
|
||||
_currentWeight = 0;
|
||||
if (amount == null)
|
||||
{
|
||||
_entries.Clear();
|
||||
_currentWeight = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
_currentWeight = Math.Max(0, _currentWeight - amount.Value);
|
||||
var removedWeight = 0;
|
||||
while (true)
|
||||
{
|
||||
if (removedWeight >= amount.Value || _entries.Count == 0)
|
||||
break;
|
||||
|
||||
var lastEntry = _entries.Dequeue();
|
||||
removedWeight += lastEntry.Weight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -29,10 +29,27 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Reset()
|
||||
public void Reset(int? amount)
|
||||
{
|
||||
_entries.Clear();
|
||||
_currentWeight = 0;
|
||||
if (amount == null)
|
||||
{
|
||||
_entries.Clear();
|
||||
_currentWeight = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
_currentWeight = Math.Max(0, _currentWeight - amount.Value);
|
||||
var removedWeight = 0;
|
||||
while (true)
|
||||
{
|
||||
if (removedWeight >= amount.Value || _entries.Count == 0)
|
||||
break;
|
||||
|
||||
var lastEntry = _entries[_entries.Count - 1];
|
||||
removedWeight += lastEntry.Weight;
|
||||
_entries.Remove(lastEntry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Asset type
|
||||
/// </summary>
|
||||
public enum SharedAssetType
|
||||
{
|
||||
/// <summary>
|
||||
/// Unknown or unspecified asset type
|
||||
/// </summary>
|
||||
Unspecified,
|
||||
/// <summary>
|
||||
/// Cryptocurrency asset type
|
||||
/// </summary>
|
||||
Crypto,
|
||||
/// <summary>
|
||||
/// Fiat currency asset type
|
||||
/// </summary>
|
||||
Fiat,
|
||||
/// <summary>
|
||||
/// Traditional finance asset type
|
||||
/// </summary>
|
||||
TradFi
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asset sub type
|
||||
/// </summary>
|
||||
public enum SharedAssetSubType
|
||||
{
|
||||
// --- Crypto sub types ---
|
||||
/// <summary>
|
||||
/// Stable coin, can be for different fiat currencies
|
||||
/// </summary>
|
||||
StableCoin,
|
||||
|
||||
// --- TradFi sub types ---
|
||||
/// <summary>
|
||||
/// Equity, can be stocks, ETFs, or indices
|
||||
/// </summary>
|
||||
Equity,
|
||||
/// <summary>
|
||||
/// Commodity, can be oil, gas, metals, etc.
|
||||
/// </summary>
|
||||
Commodity
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,11 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// </summary>
|
||||
bool Authenticated { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Get info on the client and supported features
|
||||
/// </summary>
|
||||
SharedClientInfo Discover();
|
||||
|
||||
/// <summary>
|
||||
/// Format a base and quote asset to an exchange accepted symbol
|
||||
/// </summary>
|
||||
@@ -33,14 +38,15 @@ namespace CryptoExchange.Net.SharedApis
|
||||
string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverDate = null);
|
||||
|
||||
/// <summary>
|
||||
/// Set a default exchange parameter. This can be used instead of passing in an ExchangeParameters object which each request.
|
||||
/// Set a default exchange parameter which will be statically set with each request. This can be used instead of passing it in an ExchangeParameters object with each request.<br />
|
||||
/// Default exchange parameters can still be overridden by passing the parameter in the ExchangeParameters of a request.
|
||||
/// </summary>
|
||||
/// <param name="name">Parameter name</param>
|
||||
/// <param name="value">Parameter value</param>
|
||||
void SetDefaultExchangeParameter(string name, object value);
|
||||
|
||||
/// <summary>
|
||||
/// Reset the default exchange parameters, resets parameters for all exchanges
|
||||
/// Reset previously set default exchange parameters for the exchange.
|
||||
/// </summary>
|
||||
void ResetDefaultExchangeParameters();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Threading;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
@@ -9,15 +10,18 @@ namespace CryptoExchange.Net.SharedApis
|
||||
public interface IFundingRateRestClient : ISharedClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Funding rate request options
|
||||
/// Funding rate request options.<br />
|
||||
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
|
||||
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
|
||||
/// </summary>
|
||||
GetFundingRateHistoryOptions GetFundingRateHistoryOptions { get; }
|
||||
/// <summary>
|
||||
/// Get funding rate records
|
||||
/// Get funding rate records, see <see cref="GetFundingRateHistoryOptions"/> for request options and exchange specific required/optional parameters. <br />
|
||||
/// The result is paginated, if there are more results to be retrieved, the `NextPageRequest` property of the result will contain the pagination request to be used for the next request to continue pagination.
|
||||
/// </summary>
|
||||
/// <param name="request">Request info</param>
|
||||
/// <param name="nextPageToken">The pagination request from the previous request result `NextPageRequest` property to continue pagination</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
Task<ExchangeWebResult<SharedFundingRate[]>> GetFundingRateHistoryAsync(GetFundingRateHistoryRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
|
||||
Task<HttpResult<SharedFundingRate[]>> GetFundingRateHistoryAsync(GetFundingRateHistoryRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user