mirror of
https://github.com/JKorf/CryptoExchange.Net.git
synced 2026-08-14 01:42:55 +00:00
Compare commits
86 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f5f4d50cc9 | |||
| f87506b490 | |||
| f6f9a53ce5 | |||
| 61130ef54e | |||
| e8bcbd59be | |||
| d433ff7475 | |||
| 71957037d0 | |||
| bcdcdbbd4e | |||
| 1ece13f5bc | |||
| da70ba6ec7 | |||
| a832f0e4d4 | |||
| 6ab4d005a0 | |||
| 18dc935038 | |||
| bb3a534f75 | |||
| 649ba370c6 | |||
| 51732c5ce6 | |||
| 0ba7b46680 | |||
| 94dfbb7b9e | |||
| b8b7512b35 | |||
| aba6b773ce | |||
| d88fb0d356 | |||
| b8c6d55156 | |||
| d9a5481db2 | |||
| 6a8bb42c0e | |||
| 2445f001ab | |||
| c84fa9ac32 | |||
| d44a11c44e | |||
| b215cccda4 | |||
| 3eda488361 | |||
| 993a44de35 | |||
| 99465f99a1 | |||
| d42de1fe90 | |||
| d0284c62c0 | |||
| d92f3b7904 | |||
| 3e1b5ada69 | |||
| 6156fb8154 | |||
| f2753aed1e | |||
| e33d826381 | |||
| 364aa4d324 | |||
| 455b332757 | |||
| 876b895645 | |||
| daf7ed9fe6 | |||
| 3e365f83c9 | |||
| 40977ebdbe | |||
| 4a9058fc1c | |||
| dab9a21608 | |||
| a89c222399 | |||
| 1e356d2a45 | |||
| eed794c2cf | |||
| 2f82e2015b | |||
| ad599badb2 | |||
| 1e45c73f1d | |||
| 49c1fda2c1 | |||
| 32a31e464b | |||
| cddb4167e4 | |||
| 65457d8df2 | |||
| 122a6cad43 | |||
| 4c0e841425 | |||
| 92f5839aec | |||
| 30475dae67 | |||
| 3d942bd503 | |||
| f739520e52 | |||
| 0152603ddb | |||
| aa06e0eead | |||
| 2fde9a285e | |||
| b9f6eb6abb | |||
| d77c4354a6 | |||
| 21860ddf85 | |||
| 2cffa22cc2 | |||
| 985ba9bb29 | |||
| 96f23f163d | |||
| 0e7d49991a | |||
| 3e635cf0fe | |||
| 1425c66c69 | |||
| fc3b7cc75b | |||
| 2cc2dc6ceb | |||
| 7da8cedf66 | |||
| 2cf10668dd | |||
| f1342b5ff2 | |||
| a04b636a11 | |||
| e4637ad295 | |||
| 3a1e43dabe | |||
| 10da1a7bfe | |||
| 37320ca862 | |||
| 2074a5e26f | |||
| 6b14cdbf06 |
@@ -0,0 +1,530 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFrameworks>netstandard2.0;netstandard2.1;net8.0;net9.0</TargetFrameworks>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup>
|
||||||
|
<PackageId>CryptoExchange.Net.Protobuf</PackageId>
|
||||||
|
<Authors>JKorf</Authors>
|
||||||
|
<Description>Protobuf support for CryptoExchange.Net</Description>
|
||||||
|
<PackageVersion>9.9.0</PackageVersion>
|
||||||
|
<AssemblyVersion>9.9.0</AssemblyVersion>
|
||||||
|
<FileVersion>9.9.0</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="9.9.0" />
|
||||||
|
<PackageReference Include="protobuf-net" Version="3.2.56" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
<?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>
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
#  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 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
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.Objects.Errors;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using NUnit.Framework.Legacy;
|
using NUnit.Framework.Legacy;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@@ -16,9 +18,9 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestBasicErrorCallResult()
|
public void TestBasicErrorCallResult()
|
||||||
{
|
{
|
||||||
var result = new CallResult(new ServerError("TestError"));
|
var result = new CallResult(new ServerError("TestError", ErrorInfo.Unknown));
|
||||||
|
|
||||||
ClassicAssert.AreSame(result.Error.Message, "TestError");
|
ClassicAssert.AreSame(result.Error.ErrorCode, "TestError");
|
||||||
ClassicAssert.IsFalse(result);
|
ClassicAssert.IsFalse(result);
|
||||||
ClassicAssert.IsFalse(result.Success);
|
ClassicAssert.IsFalse(result.Success);
|
||||||
}
|
}
|
||||||
@@ -36,9 +38,9 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestCallResultError()
|
public void TestCallResultError()
|
||||||
{
|
{
|
||||||
var result = new CallResult<object>(new ServerError("TestError"));
|
var result = new CallResult<object>(new ServerError("TestError", ErrorInfo.Unknown));
|
||||||
|
|
||||||
ClassicAssert.AreSame(result.Error.Message, "TestError");
|
ClassicAssert.AreSame(result.Error.ErrorCode, "TestError");
|
||||||
ClassicAssert.IsNull(result.Data);
|
ClassicAssert.IsNull(result.Data);
|
||||||
ClassicAssert.IsFalse(result);
|
ClassicAssert.IsFalse(result);
|
||||||
ClassicAssert.IsFalse(result.Success);
|
ClassicAssert.IsFalse(result.Success);
|
||||||
@@ -71,11 +73,11 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestCallResultErrorAs()
|
public void TestCallResultErrorAs()
|
||||||
{
|
{
|
||||||
var result = new CallResult<TestObjectResult>(new ServerError("TestError"));
|
var result = new CallResult<TestObjectResult>(new ServerError("TestError", ErrorInfo.Unknown));
|
||||||
var asResult = result.As<TestObject2>(default);
|
var asResult = result.As<TestObject2>(default);
|
||||||
|
|
||||||
ClassicAssert.IsNotNull(asResult.Error);
|
ClassicAssert.IsNotNull(asResult.Error);
|
||||||
ClassicAssert.AreSame(asResult.Error.Message, "TestError");
|
ClassicAssert.AreSame(asResult.Error.ErrorCode, "TestError");
|
||||||
ClassicAssert.IsNull(asResult.Data);
|
ClassicAssert.IsNull(asResult.Data);
|
||||||
ClassicAssert.IsFalse(asResult);
|
ClassicAssert.IsFalse(asResult);
|
||||||
ClassicAssert.IsFalse(asResult.Success);
|
ClassicAssert.IsFalse(asResult.Success);
|
||||||
@@ -84,11 +86,11 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestCallResultErrorAsError()
|
public void TestCallResultErrorAsError()
|
||||||
{
|
{
|
||||||
var result = new CallResult<TestObjectResult>(new ServerError("TestError"));
|
var result = new CallResult<TestObjectResult>(new ServerError("TestError", ErrorInfo.Unknown));
|
||||||
var asResult = result.AsError<TestObject2>(new ServerError("TestError2"));
|
var asResult = result.AsError<TestObject2>(new ServerError("TestError2", ErrorInfo.Unknown));
|
||||||
|
|
||||||
ClassicAssert.IsNotNull(asResult.Error);
|
ClassicAssert.IsNotNull(asResult.Error);
|
||||||
ClassicAssert.AreSame(asResult.Error.Message, "TestError2");
|
ClassicAssert.AreSame(asResult.Error.ErrorCode, "TestError2");
|
||||||
ClassicAssert.IsNull(asResult.Data);
|
ClassicAssert.IsNull(asResult.Data);
|
||||||
ClassicAssert.IsFalse(asResult);
|
ClassicAssert.IsFalse(asResult);
|
||||||
ClassicAssert.IsFalse(asResult.Success);
|
ClassicAssert.IsFalse(asResult.Success);
|
||||||
@@ -97,11 +99,11 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestWebCallResultErrorAsError()
|
public void TestWebCallResultErrorAsError()
|
||||||
{
|
{
|
||||||
var result = new WebCallResult<TestObjectResult>(new ServerError("TestError"));
|
var result = new WebCallResult<TestObjectResult>(new ServerError("TestError", ErrorInfo.Unknown));
|
||||||
var asResult = result.AsError<TestObject2>(new ServerError("TestError2"));
|
var asResult = result.AsError<TestObject2>(new ServerError("TestError2", ErrorInfo.Unknown));
|
||||||
|
|
||||||
ClassicAssert.IsNotNull(asResult.Error);
|
ClassicAssert.IsNotNull(asResult.Error);
|
||||||
ClassicAssert.AreSame(asResult.Error.Message, "TestError2");
|
ClassicAssert.AreSame(asResult.Error.ErrorCode, "TestError2");
|
||||||
ClassicAssert.IsNull(asResult.Data);
|
ClassicAssert.IsNull(asResult.Data);
|
||||||
ClassicAssert.IsFalse(asResult);
|
ClassicAssert.IsFalse(asResult);
|
||||||
ClassicAssert.IsFalse(asResult.Success);
|
ClassicAssert.IsFalse(asResult.Success);
|
||||||
@@ -112,6 +114,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
{
|
{
|
||||||
var result = new WebCallResult<TestObjectResult>(
|
var result = new WebCallResult<TestObjectResult>(
|
||||||
System.Net.HttpStatusCode.OK,
|
System.Net.HttpStatusCode.OK,
|
||||||
|
HttpVersion.Version11,
|
||||||
new KeyValuePair<string, string[]>[0],
|
new KeyValuePair<string, string[]>[0],
|
||||||
TimeSpan.FromSeconds(1),
|
TimeSpan.FromSeconds(1),
|
||||||
null,
|
null,
|
||||||
@@ -124,10 +127,10 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
ResultDataSource.Server,
|
ResultDataSource.Server,
|
||||||
new TestObjectResult(),
|
new TestObjectResult(),
|
||||||
null);
|
null);
|
||||||
var asResult = result.AsError<TestObject2>(new ServerError("TestError2"));
|
var asResult = result.AsError<TestObject2>(new ServerError("TestError2", ErrorInfo.Unknown));
|
||||||
|
|
||||||
ClassicAssert.IsNotNull(asResult.Error);
|
ClassicAssert.IsNotNull(asResult.Error);
|
||||||
Assert.That(asResult.Error.Message == "TestError2");
|
Assert.That(asResult.Error.ErrorCode == "TestError2");
|
||||||
Assert.That(asResult.ResponseStatusCode == System.Net.HttpStatusCode.OK);
|
Assert.That(asResult.ResponseStatusCode == System.Net.HttpStatusCode.OK);
|
||||||
Assert.That(asResult.ResponseTime == TimeSpan.FromSeconds(1));
|
Assert.That(asResult.ResponseTime == TimeSpan.FromSeconds(1));
|
||||||
Assert.That(asResult.RequestUrl == "https://test.com/api");
|
Assert.That(asResult.RequestUrl == "https://test.com/api");
|
||||||
@@ -142,6 +145,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
{
|
{
|
||||||
var result = new WebCallResult<TestObjectResult>(
|
var result = new WebCallResult<TestObjectResult>(
|
||||||
System.Net.HttpStatusCode.OK,
|
System.Net.HttpStatusCode.OK,
|
||||||
|
HttpVersion.Version11,
|
||||||
new KeyValuePair<string, string[]>[0],
|
new KeyValuePair<string, string[]>[0],
|
||||||
TimeSpan.FromSeconds(1),
|
TimeSpan.FromSeconds(1),
|
||||||
null,
|
null,
|
||||||
|
|||||||
@@ -6,10 +6,10 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0"></PackageReference>
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1"></PackageReference>
|
||||||
<PackageReference Include="Moq" Version="4.20.72" />
|
<PackageReference Include="Moq" Version="4.20.72" />
|
||||||
<PackageReference Include="NUnit" Version="4.2.2"></PackageReference>
|
<PackageReference Include="NUnit" Version="4.3.2"></PackageReference>
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0"></PackageReference>
|
<PackageReference Include="NUnit3TestAdapter" Version="5.0.0"></PackageReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
[TestCase(0.1, 1, 0.0001, RoundingType.Closest, 0.532, 0.532)]
|
[TestCase(0.1, 1, 0.0001, RoundingType.Closest, 0.532, 0.532)]
|
||||||
[TestCase(0.1, 1, 0.0001, RoundingType.Down, 0.5516592, 0.5516)]
|
[TestCase(0.1, 1, 0.0001, RoundingType.Down, 0.5516592, 0.5516)]
|
||||||
[TestCase(0.1, 1, 0.0001, RoundingType.Closest, 0.5516592, 0.5517)]
|
[TestCase(0.1, 1, 0.0001, RoundingType.Closest, 0.5516592, 0.5517)]
|
||||||
|
[TestCase(0, 1, 0.000000001, RoundingType.Closest, 0.0000097232, 0.000009723)]
|
||||||
public void AdjustValueStepTests(decimal min, decimal max, decimal? step, RoundingType roundingType, decimal input, decimal expected)
|
public void AdjustValueStepTests(decimal min, decimal max, decimal? step, RoundingType roundingType, decimal input, decimal expected)
|
||||||
{
|
{
|
||||||
var result = ExchangeHelpers.AdjustValueStep(min, max, step, roundingType, input);
|
var result = ExchangeHelpers.AdjustValueStep(min, max, step, roundingType, input);
|
||||||
|
|||||||
@@ -80,8 +80,6 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
ClassicAssert.IsFalse(result.Success);
|
ClassicAssert.IsFalse(result.Success);
|
||||||
Assert.That(result.Error != null);
|
Assert.That(result.Error != null);
|
||||||
Assert.That(result.Error is ServerError);
|
Assert.That(result.Error is ServerError);
|
||||||
Assert.That(result.Error.Message.Contains("Invalid request"));
|
|
||||||
Assert.That(result.Error.Message.Contains("123"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase]
|
[TestCase]
|
||||||
@@ -98,7 +96,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
ClassicAssert.IsFalse(result.Success);
|
ClassicAssert.IsFalse(result.Success);
|
||||||
Assert.That(result.Error != null);
|
Assert.That(result.Error != null);
|
||||||
Assert.That(result.Error is ServerError);
|
Assert.That(result.Error is ServerError);
|
||||||
Assert.That(result.Error.Code == 123);
|
Assert.That(result.Error.ErrorCode == "123");
|
||||||
Assert.That(result.Error.Message == "Invalid request");
|
Assert.That(result.Error.Message == "Invalid request");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,7 +182,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
[TestCase("/sapi/test1", true)]
|
[TestCase("/sapi/test1", true)]
|
||||||
[TestCase("/sapi/test2", true)]
|
[TestCase("/sapi/test2", true)]
|
||||||
[TestCase("/api/test1", false)]
|
[TestCase("/api/test1", false)]
|
||||||
[TestCase("sapi/test1", false)]
|
[TestCase("sapi/test1", true)]
|
||||||
[TestCase("/sapi/", true)]
|
[TestCase("/sapi/", true)]
|
||||||
public async Task PartialEndpointRateLimiterEndpoints(string endpoint, bool expectLimiting)
|
public async Task PartialEndpointRateLimiterEndpoints(string endpoint, bool expectLimiting)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -202,7 +202,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
await sub;
|
await sub;
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
ClassicAssert.IsFalse(client.SubClient.TestSubscription.Confirmed);
|
ClassicAssert.IsTrue(client.SubClient.TestSubscription.Status != SubscriptionStatus.Subscribed);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase()]
|
[TestCase()]
|
||||||
@@ -225,7 +225,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
await sub;
|
await sub;
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
Assert.That(client.SubClient.TestSubscription.Confirmed);
|
Assert.That(client.SubClient.TestSubscription.Status == SubscriptionStatus.Subscribed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using System.Text.Json.Serialization;
|
|||||||
using NUnit.Framework.Legacy;
|
using NUnit.Framework.Legacy;
|
||||||
using CryptoExchange.Net.Converters;
|
using CryptoExchange.Net.Converters;
|
||||||
using CryptoExchange.Net.Testing.Comparers;
|
using CryptoExchange.Net.Testing.Comparers;
|
||||||
|
using CryptoExchange.Net.SharedApis;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests
|
namespace CryptoExchange.Net.UnitTests
|
||||||
{
|
{
|
||||||
@@ -223,13 +224,17 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
[TestCase(null, null)]
|
[TestCase(null, null)]
|
||||||
[TestCase("", null)]
|
[TestCase("", null)]
|
||||||
[TestCase("null", null)]
|
[TestCase("null", null)]
|
||||||
|
[TestCase("nan", null)]
|
||||||
[TestCase("1E+2", 100)]
|
[TestCase("1E+2", 100)]
|
||||||
[TestCase("1E-2", 0.01)]
|
[TestCase("1E-2", 0.01)]
|
||||||
[TestCase("80228162514264337593543950335", -999)] // -999 is workaround for not being able to specify decimal.MaxValue
|
[TestCase("Infinity", 999)] // 999 is workaround for not being able to specify decimal.MinValue
|
||||||
|
[TestCase("-Infinity", -999)] // -999 is workaround for not being able to specify decimal.MaxValue
|
||||||
|
[TestCase("80228162514264337593543950335", 999)] // 999 is workaround for not being able to specify decimal.MaxValue
|
||||||
|
[TestCase("-80228162514264337593543950335", -999)] // -999 is workaround for not being able to specify decimal.MaxValue
|
||||||
public void TestDecimalConverterString(string value, decimal? expected)
|
public void TestDecimalConverterString(string value, decimal? expected)
|
||||||
{
|
{
|
||||||
var result = JsonSerializer.Deserialize<STJDecimalObject>("{ \"test\": \""+ value + "\"}");
|
var result = JsonSerializer.Deserialize<STJDecimalObject>("{ \"test\": \""+ value + "\"}");
|
||||||
Assert.That(result.Test, Is.EqualTo(expected == -999 ? decimal.MaxValue : expected));
|
Assert.That(result.Test, Is.EqualTo(expected == -999 ? decimal.MinValue : expected == 999 ? decimal.MaxValue: expected));
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("1", 1)]
|
[TestCase("1", 1)]
|
||||||
@@ -269,9 +274,18 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
TestInternal = new Test
|
TestInternal = new Test
|
||||||
{
|
{
|
||||||
Prop1 = 10
|
Prop1 = 10
|
||||||
}
|
},
|
||||||
|
Prop8 = new Test3
|
||||||
|
{
|
||||||
|
Prop31 = 5,
|
||||||
|
Prop32 = "101"
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var options = new JsonSerializerOptions()
|
||||||
|
{
|
||||||
|
TypeInfoResolver = new SerializationContext()
|
||||||
|
};
|
||||||
var serialized = JsonSerializer.Serialize(data);
|
var serialized = JsonSerializer.Serialize(data);
|
||||||
var deserialized = JsonSerializer.Deserialize<Test>(serialized);
|
var deserialized = JsonSerializer.Deserialize<Test>(serialized);
|
||||||
|
|
||||||
@@ -286,6 +300,42 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
Assert.That(deserialized.Prop6.Prop32, Is.EqualTo("789"));
|
Assert.That(deserialized.Prop6.Prop32, Is.EqualTo("789"));
|
||||||
Assert.That(deserialized.Prop7, Is.EqualTo(TestEnum.Two));
|
Assert.That(deserialized.Prop7, Is.EqualTo(TestEnum.Two));
|
||||||
Assert.That(deserialized.TestInternal.Prop1, Is.EqualTo(10));
|
Assert.That(deserialized.TestInternal.Prop1, Is.EqualTo(10));
|
||||||
|
Assert.That(deserialized.Prop8.Prop31, Is.EqualTo(5));
|
||||||
|
Assert.That(deserialized.Prop8.Prop32, Is.EqualTo("101"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(TradingMode.Spot, "ETH", "USDT", null)]
|
||||||
|
[TestCase(TradingMode.PerpetualLinear, "ETH", "USDT", null)]
|
||||||
|
[TestCase(TradingMode.DeliveryLinear, "ETH", "USDT", 1748432430)]
|
||||||
|
public void TestSharedSymbolConversion(TradingMode tradingMode, string baseAsset, string quoteAsset, int? deliverTime)
|
||||||
|
{
|
||||||
|
DateTime? time = deliverTime == null ? null : DateTimeConverter.ParseFromDouble(deliverTime.Value);
|
||||||
|
var symbol = new SharedSymbol(tradingMode, baseAsset, quoteAsset, time);
|
||||||
|
|
||||||
|
var serialized = JsonSerializer.Serialize(symbol);
|
||||||
|
var restored = JsonSerializer.Deserialize<SharedSymbol>(serialized);
|
||||||
|
|
||||||
|
Assert.That(restored.TradingMode, Is.EqualTo(symbol.TradingMode));
|
||||||
|
Assert.That(restored.BaseAsset, Is.EqualTo(symbol.BaseAsset));
|
||||||
|
Assert.That(restored.QuoteAsset, Is.EqualTo(symbol.QuoteAsset));
|
||||||
|
Assert.That(restored.DeliverTime, Is.EqualTo(symbol.DeliverTime));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(0.1, null, null)]
|
||||||
|
[TestCase(0.1, 0.1, null)]
|
||||||
|
[TestCase(0.1, 0.1, 0.1)]
|
||||||
|
[TestCase(null, 0.1, null)]
|
||||||
|
[TestCase(null, 0.1, 0.1)]
|
||||||
|
public void TestSharedQuantityConversion(double? baseQuantity, double? quoteQuantity, double? contractQuantity)
|
||||||
|
{
|
||||||
|
var symbol = new SharedOrderQuantity((decimal?)baseQuantity, (decimal?)quoteQuantity, (decimal?)contractQuantity);
|
||||||
|
|
||||||
|
var serialized = JsonSerializer.Serialize(symbol);
|
||||||
|
var restored = JsonSerializer.Deserialize<SharedOrderQuantity>(serialized);
|
||||||
|
|
||||||
|
Assert.That(restored.QuantityInBaseAsset, Is.EqualTo(symbol.QuantityInBaseAsset));
|
||||||
|
Assert.That(restored.QuantityInQuoteAsset, Is.EqualTo(symbol.QuantityInQuoteAsset));
|
||||||
|
Assert.That(restored.QuantityInContracts, Is.EqualTo(symbol.QuantityInContracts));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -323,7 +373,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
public bool Value { get; set; }
|
public bool Value { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
[JsonConverter(typeof(ArrayConverter<Test, SerializationContext>))]
|
[JsonConverter(typeof(ArrayConverter<Test>))]
|
||||||
record Test
|
record Test
|
||||||
{
|
{
|
||||||
[ArrayProperty(0)]
|
[ArrayProperty(0)]
|
||||||
@@ -344,9 +394,11 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
public TestEnum? Prop7 { get; set; }
|
public TestEnum? Prop7 { get; set; }
|
||||||
[ArrayProperty(7)]
|
[ArrayProperty(7)]
|
||||||
public Test TestInternal { get; set; }
|
public Test TestInternal { get; set; }
|
||||||
|
[ArrayProperty(8), JsonConversion]
|
||||||
|
public Test3 Prop8 { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
[JsonConverter(typeof(ArrayConverter<Test2, SerializationContext>))]
|
[JsonConverter(typeof(ArrayConverter<Test2>))]
|
||||||
record Test2
|
record Test2
|
||||||
{
|
{
|
||||||
[ArrayProperty(0)]
|
[ArrayProperty(0)]
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.Objects.Errors;
|
||||||
using CryptoExchange.Net.Objects.Sockets;
|
using CryptoExchange.Net.Objects.Sockets;
|
||||||
using CryptoExchange.Net.Sockets;
|
using CryptoExchange.Net.Sockets;
|
||||||
using System;
|
using System;
|
||||||
@@ -31,21 +32,19 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
|
|||||||
|
|
||||||
internal class TestChannelQuery : Query<SubResponse>
|
internal class TestChannelQuery : Query<SubResponse>
|
||||||
{
|
{
|
||||||
public override HashSet<string> ListenerIdentifiers { get; set; }
|
|
||||||
|
|
||||||
public TestChannelQuery(string channel, string request, bool authenticated, int weight = 1) : base(request, authenticated, weight)
|
public TestChannelQuery(string channel, string request, bool authenticated, int weight = 1) : base(request, authenticated, weight)
|
||||||
{
|
{
|
||||||
ListenerIdentifiers = new HashSet<string> { request + "-" + channel };
|
MessageMatcher = MessageMatcher.Create<SubResponse>(request + "-" + channel, HandleMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override CallResult<SubResponse> HandleMessage(SocketConnection connection, DataEvent<SubResponse> message)
|
public CallResult<SubResponse> HandleMessage(SocketConnection connection, DataEvent<SubResponse> message)
|
||||||
{
|
{
|
||||||
if (!message.Data.Status.Equals("confirmed", StringComparison.OrdinalIgnoreCase))
|
if (!message.Data.Status.Equals("confirmed", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return new CallResult<SubResponse>(new ServerError(message.Data.Status));
|
return new CallResult<SubResponse>(new ServerError(ErrorInfo.Unknown with { Message = message.Data.Status }));
|
||||||
}
|
}
|
||||||
|
|
||||||
return base.HandleMessage(connection, message);
|
return message.ToCallResult();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,11 +9,9 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
|
|||||||
{
|
{
|
||||||
internal class TestQuery : Query<object>
|
internal class TestQuery : Query<object>
|
||||||
{
|
{
|
||||||
public override HashSet<string> ListenerIdentifiers { get; set; }
|
|
||||||
|
|
||||||
public TestQuery(string identifier, object request, bool authenticated, int weight = 1) : base(request, authenticated, weight)
|
public TestQuery(string identifier, object request, bool authenticated, int weight = 1) : base(request, authenticated, weight)
|
||||||
{
|
{
|
||||||
ListenerIdentifiers = new HashSet<string> { identifier };
|
MessageMatcher = MessageMatcher.Create<object>(identifier);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,22 +15,20 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
|
|||||||
{
|
{
|
||||||
private readonly Action<DataEvent<T>> _handler;
|
private readonly Action<DataEvent<T>> _handler;
|
||||||
|
|
||||||
public override HashSet<string> ListenerIdentifiers { get; set; } = new HashSet<string> { "update-topic" };
|
|
||||||
|
|
||||||
public TestSubscription(ILogger logger, Action<DataEvent<T>> handler) : base(logger, false)
|
public TestSubscription(ILogger logger, Action<DataEvent<T>> handler) : base(logger, false)
|
||||||
{
|
{
|
||||||
_handler = handler;
|
_handler = handler;
|
||||||
|
|
||||||
|
MessageMatcher = MessageMatcher.Create<T>("update-topic", DoHandleMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override CallResult DoHandleMessage(SocketConnection connection, DataEvent<object> message)
|
public CallResult DoHandleMessage(SocketConnection connection, DataEvent<T> message)
|
||||||
{
|
{
|
||||||
var data = (T)message.Data;
|
_handler.Invoke(message);
|
||||||
_handler.Invoke(message.As(data));
|
|
||||||
return new CallResult(null);
|
return new CallResult(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Type GetMessageType(IMessageAccessor message) => typeof(T);
|
protected override Query GetSubQuery(SocketConnection connection) => new TestQuery("sub", new object(), false, 1);
|
||||||
public override Query GetSubQuery(SocketConnection connection) => new TestQuery("sub", new object(), false, 1);
|
protected override Query GetUnsubQuery(SocketConnection connection) => new TestQuery("unsub", new object(), false, 1);
|
||||||
public override Query GetUnsubQuery() => new TestQuery("unsub", new object(), false, 1);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-9
@@ -15,24 +15,20 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
|
|||||||
private readonly Action<DataEvent<T>> _handler;
|
private readonly Action<DataEvent<T>> _handler;
|
||||||
private readonly string _channel;
|
private readonly string _channel;
|
||||||
|
|
||||||
public override HashSet<string> ListenerIdentifiers { get; set; }
|
|
||||||
|
|
||||||
public TestSubscriptionWithResponseCheck(string channel, Action<DataEvent<T>> handler) : base(Mock.Of<ILogger>(), false)
|
public TestSubscriptionWithResponseCheck(string channel, Action<DataEvent<T>> handler) : base(Mock.Of<ILogger>(), false)
|
||||||
{
|
{
|
||||||
ListenerIdentifiers = new HashSet<string>() { channel };
|
MessageMatcher = MessageMatcher.Create<T>(channel, DoHandleMessage);
|
||||||
_handler = handler;
|
_handler = handler;
|
||||||
_channel = channel;
|
_channel = channel;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override CallResult DoHandleMessage(SocketConnection connection, DataEvent<object> message)
|
public CallResult DoHandleMessage(SocketConnection connection, DataEvent<T> message)
|
||||||
{
|
{
|
||||||
var data = (T)message.Data;
|
_handler.Invoke(message);
|
||||||
_handler.Invoke(message.As(data));
|
|
||||||
return new CallResult(null);
|
return new CallResult(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Type GetMessageType(IMessageAccessor message) => typeof(T);
|
protected override Query GetSubQuery(SocketConnection connection) => new TestChannelQuery(_channel, "subscribe", false, 1);
|
||||||
public override Query GetSubQuery(SocketConnection connection) => new TestChannelQuery(_channel, "subscribe", false, 1);
|
protected override Query GetUnsubQuery(SocketConnection connection) => new TestChannelQuery(_channel, "unsubscribe", false, 1);
|
||||||
public override Query GetUnsubQuery() => new TestChannelQuery(_channel, "unsubscribe", false, 1);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,10 +10,12 @@ using CryptoExchange.Net.Clients;
|
|||||||
using CryptoExchange.Net.Converters.SystemTextJson;
|
using CryptoExchange.Net.Converters.SystemTextJson;
|
||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces;
|
||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.Objects.Errors;
|
||||||
using CryptoExchange.Net.Objects.Options;
|
using CryptoExchange.Net.Objects.Options;
|
||||||
using CryptoExchange.Net.SharedApis;
|
using CryptoExchange.Net.SharedApis;
|
||||||
using CryptoExchange.Net.UnitTests.TestImplementations;
|
using CryptoExchange.Net.UnitTests.TestImplementations;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests
|
namespace CryptoExchange.Net.UnitTests
|
||||||
{
|
{
|
||||||
@@ -24,12 +26,14 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
public TestBaseClient(): base(null, "Test")
|
public TestBaseClient(): base(null, "Test")
|
||||||
{
|
{
|
||||||
var options = new TestClientOptions();
|
var options = new TestClientOptions();
|
||||||
|
_logger = NullLogger.Instance;
|
||||||
Initialize(options);
|
Initialize(options);
|
||||||
SubClient = AddApiClient(new TestSubClient(options, new RestApiOptions()));
|
SubClient = AddApiClient(new TestSubClient(options, new RestApiOptions()));
|
||||||
}
|
}
|
||||||
|
|
||||||
public TestBaseClient(TestClientOptions exchangeOptions) : base(null, "Test")
|
public TestBaseClient(TestClientOptions exchangeOptions) : base(null, "Test")
|
||||||
{
|
{
|
||||||
|
_logger = NullLogger.Instance;
|
||||||
Initialize(exchangeOptions);
|
Initialize(exchangeOptions);
|
||||||
SubClient = AddApiClient(new TestSubClient(exchangeOptions, new RestApiOptions()));
|
SubClient = AddApiClient(new TestSubClient(exchangeOptions, new RestApiOptions()));
|
||||||
}
|
}
|
||||||
@@ -52,7 +56,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
var accessor = CreateAccessor();
|
var accessor = CreateAccessor();
|
||||||
var valid = accessor.Read(stream, true).Result;
|
var valid = accessor.Read(stream, true).Result;
|
||||||
if (!valid)
|
if (!valid)
|
||||||
return new CallResult<T>(new ServerError(data));
|
return new CallResult<T>(new ServerError(ErrorInfo.Unknown with { Message = data }));
|
||||||
|
|
||||||
var deserializeResult = accessor.Deserialize<T>();
|
var deserializeResult = accessor.Deserialize<T>();
|
||||||
return deserializeResult;
|
return deserializeResult;
|
||||||
@@ -74,10 +78,10 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void AuthenticateRequest(RestApiClient apiClient, Uri uri, HttpMethod method, ref IDictionary<string, object> uriParams, ref IDictionary<string, object> bodyParams, ref Dictionary<string, string> headers, bool auth, ArrayParametersSerialization arraySerialization, HttpMethodParameterPosition parameterPosition, RequestBodyFormat bodyFormat)
|
public override void ProcessRequest(RestApiClient apiClient, RestRequestConfiguration requestConfig)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetKey() => _credentials.Key;
|
public string GetKey() => _credentials.Key;
|
||||||
public string GetSecret() => _credentials.Secret;
|
public string GetSecret() => _credentials.Secret;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ using Microsoft.Extensions.Options;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using CryptoExchange.Net.Converters.SystemTextJson;
|
using CryptoExchange.Net.Converters.SystemTextJson;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
using CryptoExchange.Net.Objects.Errors;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||||
{
|
{
|
||||||
@@ -59,8 +60,8 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
request.Setup(c => c.GetHeaders()).Returns(() => headers.ToArray());
|
request.Setup(c => c.GetHeaders()).Returns(() => headers.ToArray());
|
||||||
|
|
||||||
var factory = Mock.Get(Api1.RequestFactory);
|
var factory = Mock.Get(Api1.RequestFactory);
|
||||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||||
.Callback<HttpMethod, Uri, int>((method, uri, id) =>
|
.Callback<Version, HttpMethod, Uri, int>((version, method, uri, id) =>
|
||||||
{
|
{
|
||||||
request.Setup(a => a.Uri).Returns(uri);
|
request.Setup(a => a.Uri).Returns(uri);
|
||||||
request.Setup(a => a.Method).Returns(method);
|
request.Setup(a => a.Method).Returns(method);
|
||||||
@@ -68,8 +69,8 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
.Returns(request.Object);
|
.Returns(request.Object);
|
||||||
|
|
||||||
factory = Mock.Get(Api2.RequestFactory);
|
factory = Mock.Get(Api2.RequestFactory);
|
||||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||||
.Callback<HttpMethod, Uri, int>((method, uri, id) =>
|
.Callback<Version, HttpMethod, Uri, int>((version, method, uri, id) =>
|
||||||
{
|
{
|
||||||
request.Setup(a => a.Uri).Returns(uri);
|
request.Setup(a => a.Uri).Returns(uri);
|
||||||
request.Setup(a => a.Method).Returns(method);
|
request.Setup(a => a.Method).Returns(method);
|
||||||
@@ -89,12 +90,12 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Throws(we);
|
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Throws(we);
|
||||||
|
|
||||||
var factory = Mock.Get(Api1.RequestFactory);
|
var factory = Mock.Get(Api1.RequestFactory);
|
||||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||||
.Returns(request.Object);
|
.Returns(request.Object);
|
||||||
|
|
||||||
|
|
||||||
factory = Mock.Get(Api2.RequestFactory);
|
factory = Mock.Get(Api2.RequestFactory);
|
||||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||||
.Returns(request.Object);
|
.Returns(request.Object);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,13 +118,13 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
request.Setup(c => c.GetHeaders()).Returns(headers.ToArray());
|
request.Setup(c => c.GetHeaders()).Returns(headers.ToArray());
|
||||||
|
|
||||||
var factory = Mock.Get(Api1.RequestFactory);
|
var factory = Mock.Get(Api1.RequestFactory);
|
||||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||||
.Callback<HttpMethod, Uri, int>((method, uri, id) => request.Setup(a => a.Uri).Returns(uri))
|
.Callback<Version, HttpMethod, Uri, int>((version, method, uri, id) => request.Setup(a => a.Uri).Returns(uri))
|
||||||
.Returns(request.Object);
|
.Returns(request.Object);
|
||||||
|
|
||||||
factory = Mock.Get(Api2.RequestFactory);
|
factory = Mock.Get(Api2.RequestFactory);
|
||||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||||
.Callback<HttpMethod, Uri, int>((method, uri, id) => request.Setup(a => a.Uri).Returns(uri))
|
.Callback<Version, HttpMethod, Uri, int>((version, method, uri, id) => request.Setup(a => a.Uri).Returns(uri))
|
||||||
.Returns(request.Object);
|
.Returns(request.Object);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -193,11 +194,11 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
return await SendAsync<T>("http://www.test.com", new RequestDefinition("/", HttpMethod.Get) { Weight = 0 }, null, ct);
|
return await SendAsync<T>("http://www.test.com", new RequestDefinition("/", HttpMethod.Get) { Weight = 0 }, null, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Error ParseErrorResponse(int httpStatusCode, KeyValuePair<string, string[]>[] responseHeaders, IMessageAccessor accessor)
|
protected override Error ParseErrorResponse(int httpStatusCode, KeyValuePair<string, string[]>[] responseHeaders, IMessageAccessor accessor, Exception exception)
|
||||||
{
|
{
|
||||||
var errorData = accessor.Deserialize<TestError>();
|
var errorData = accessor.Deserialize<TestError>();
|
||||||
|
|
||||||
return new ServerError(errorData.Data.ErrorCode, errorData.Data.ErrorMessage);
|
return new ServerError(errorData.Data.ErrorCode, GetErrorInfo(errorData.Data.ErrorCode, errorData.Data.ErrorMessage));
|
||||||
}
|
}
|
||||||
|
|
||||||
public override TimeSpan? GetTimeOffset()
|
public override TimeSpan? GetTimeOffset()
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ using CryptoExchange.Net.Testing.Implementations;
|
|||||||
using CryptoExchange.Net.SharedApis;
|
using CryptoExchange.Net.SharedApis;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using CryptoExchange.Net.Converters.SystemTextJson;
|
using CryptoExchange.Net.Converters.SystemTextJson;
|
||||||
|
using System.Net.WebSockets;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||||
{
|
{
|
||||||
@@ -98,7 +99,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected internal override IByteMessageAccessor CreateAccessor() => new SystemTextJsonByteMessageAccessor(new System.Text.Json.JsonSerializerOptions());
|
protected internal override IByteMessageAccessor CreateAccessor(WebSocketMessageType type) => new SystemTextJsonByteMessageAccessor(new System.Text.Json.JsonSerializerOptions());
|
||||||
protected internal override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(new System.Text.Json.JsonSerializerOptions());
|
protected internal override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(new System.Text.Json.JsonSerializerOptions());
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -114,12 +115,12 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
|
|
||||||
public CallResult ConnectSocketSub(SocketConnection sub)
|
public CallResult ConnectSocketSub(SocketConnection sub)
|
||||||
{
|
{
|
||||||
return ConnectSocketAsync(sub).Result;
|
return ConnectSocketAsync(sub, default).Result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string GetListenerIdentifier(IMessageAccessor message)
|
public override string GetListenerIdentifier(IMessageAccessor message)
|
||||||
{
|
{
|
||||||
if (!message.IsJson)
|
if (!message.IsValid)
|
||||||
{
|
{
|
||||||
return "topic";
|
return "topic";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleClient", "Examples\C
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharedClients", "Examples\SharedClients\SharedClients.csproj", "{988A87EF-EAEA-4313-A6CF-FA869813D5AB}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharedClients", "Examples\SharedClients\SharedClients.csproj", "{988A87EF-EAEA-4313-A6CF-FA869813D5AB}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CryptoExchange.Net.Protobuf", "CryptoExchange.Net.Protobuf\CryptoExchange.Net.Protobuf.csproj", "{CC6A807A-9183-6F41-8EF1-8A70172B0E83}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -41,6 +43,10 @@ Global
|
|||||||
{988A87EF-EAEA-4313-A6CF-FA869813D5AB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{988A87EF-EAEA-4313-A6CF-FA869813D5AB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{988A87EF-EAEA-4313-A6CF-FA869813D5AB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{988A87EF-EAEA-4313-A6CF-FA869813D5AB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{988A87EF-EAEA-4313-A6CF-FA869813D5AB}.Release|Any CPU.Build.0 = Release|Any CPU
|
{988A87EF-EAEA-4313-A6CF-FA869813D5AB}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{CC6A807A-9183-6F41-8EF1-8A70172B0E83}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{CC6A807A-9183-6F41-8EF1-8A70172B0E83}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{CC6A807A-9183-6F41-8EF1-8A70172B0E83}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{CC6A807A-9183-6F41-8EF1-8A70172B0E83}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|||||||
@@ -51,30 +51,11 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Authenticate a request. Output parameters should include the providedParameters input
|
/// Authenticate a request
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="apiClient">The Api client sending the request</param>
|
/// <param name="apiClient">The Api client sending the request</param>
|
||||||
/// <param name="uri">The uri for the request</param>
|
/// <param name="requestConfig">The request configuration</param>
|
||||||
/// <param name="method">The method of the request</param>
|
public abstract void ProcessRequest(RestApiClient apiClient, RestRequestConfiguration requestConfig);
|
||||||
/// <param name="auth">If the requests should be authenticated</param>
|
|
||||||
/// <param name="arraySerialization">Array serialization type</param>
|
|
||||||
/// <param name="requestBodyFormat">The formatting of the request body</param>
|
|
||||||
/// <param name="uriParameters">Parameters that need to be in the Uri of the request. Should include the provided parameters if they should go in the uri</param>
|
|
||||||
/// <param name="bodyParameters">Parameters that need to be in the body of the request. Should include the provided parameters if they should go in the body</param>
|
|
||||||
/// <param name="headers">The headers that should be send with the request</param>
|
|
||||||
/// <param name="parameterPosition">The position where the providedParameters should go</param>
|
|
||||||
public abstract void AuthenticateRequest(
|
|
||||||
RestApiClient apiClient,
|
|
||||||
Uri uri,
|
|
||||||
HttpMethod method,
|
|
||||||
ref IDictionary<string, object>? uriParameters,
|
|
||||||
ref IDictionary<string, object>? bodyParameters,
|
|
||||||
ref Dictionary<string, string>? headers,
|
|
||||||
bool auth,
|
|
||||||
ArrayParametersSerialization arraySerialization,
|
|
||||||
HttpMethodParameterPosition parameterPosition,
|
|
||||||
RequestBodyFormat requestBodyFormat
|
|
||||||
);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// SHA256 sign the data and return the bytes
|
/// SHA256 sign the data and return the bytes
|
||||||
@@ -465,10 +446,13 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected static string GetSerializedBody(IMessageSerializer serializer, IDictionary<string, object> parameters)
|
protected static string GetSerializedBody(IMessageSerializer serializer, IDictionary<string, object> 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))
|
if (parameters.Count == 1 && parameters.TryGetValue(Constants.BodyPlaceHolderKey, out object? value))
|
||||||
return serializer.Serialize(value);
|
return stringSerializer.Serialize(value);
|
||||||
else
|
else
|
||||||
return serializer.Serialize(parameters);
|
return stringSerializer.Serialize(parameters);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Caching
|
namespace CryptoExchange.Net.Caching
|
||||||
{
|
{
|
||||||
internal class MemoryCache
|
internal class MemoryCache
|
||||||
{
|
{
|
||||||
private readonly ConcurrentDictionary<string, CacheItem> _cache = new ConcurrentDictionary<string, CacheItem>();
|
private readonly ConcurrentDictionary<string, CacheItem> _cache = new ConcurrentDictionary<string, CacheItem>();
|
||||||
|
private readonly object _lock = new object();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Add a new cache entry. Will override an existing entry if it already exists
|
/// Add a new cache entry. Will override an existing entry if it already exists
|
||||||
@@ -26,16 +28,13 @@ namespace CryptoExchange.Net.Caching
|
|||||||
/// <returns>Cached value if it was in cache</returns>
|
/// <returns>Cached value if it was in cache</returns>
|
||||||
public object? Get(string key, TimeSpan maxAge)
|
public object? Get(string key, TimeSpan maxAge)
|
||||||
{
|
{
|
||||||
|
foreach (var item in _cache.Where(x => DateTime.UtcNow - x.Value.CacheTime > maxAge).ToList())
|
||||||
|
_cache.TryRemove(item.Key, out _);
|
||||||
|
|
||||||
_cache.TryGetValue(key, out CacheItem? value);
|
_cache.TryGetValue(key, out CacheItem? value);
|
||||||
if (value == null)
|
if (value == null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
if (DateTime.UtcNow - value.CacheTime > maxAge)
|
|
||||||
{
|
|
||||||
_cache.TryRemove(key, out _);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return value.Value;
|
return value.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using CryptoExchange.Net.Authentication;
|
using CryptoExchange.Net.Authentication;
|
||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces;
|
||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.Objects.Errors;
|
||||||
using CryptoExchange.Net.Objects.Options;
|
using CryptoExchange.Net.Objects.Options;
|
||||||
using CryptoExchange.Net.SharedApis;
|
using CryptoExchange.Net.SharedApis;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -39,7 +41,10 @@ namespace CryptoExchange.Net.Clients
|
|||||||
public bool OutputOriginalData { get; }
|
public bool OutputOriginalData { get; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public bool Authenticated => ApiOptions.ApiCredentials != null || ClientOptions.ApiCredentials != null;
|
public bool Authenticated => ApiCredentials != null;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public ApiCredentials? ApiCredentials { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Api options
|
/// Api options
|
||||||
@@ -51,6 +56,11 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public ExchangeOptions ClientOptions { get; }
|
public ExchangeOptions ClientOptions { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Mapping of a response code to known error types
|
||||||
|
/// </summary>
|
||||||
|
protected internal virtual ErrorMapping ErrorMapping { get; } = new ErrorMapping([]);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -68,9 +78,10 @@ namespace CryptoExchange.Net.Clients
|
|||||||
ApiOptions = apiOptions;
|
ApiOptions = apiOptions;
|
||||||
OutputOriginalData = outputOriginalData;
|
OutputOriginalData = outputOriginalData;
|
||||||
BaseAddress = baseAddress;
|
BaseAddress = baseAddress;
|
||||||
|
ApiCredentials = apiCredentials?.Copy();
|
||||||
|
|
||||||
if (apiCredentials != null)
|
if (ApiCredentials != null)
|
||||||
AuthenticationProvider = CreateAuthenticationProvider(apiCredentials.Copy());
|
AuthenticationProvider = CreateAuthenticationProvider(ApiCredentials);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -83,12 +94,22 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public abstract string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverDate = null);
|
public abstract string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverDate = null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get error info for a response code
|
||||||
|
/// </summary>
|
||||||
|
public ErrorInfo GetErrorInfo(int code, string? message = null) => GetErrorInfo(code.ToString(), message);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get error info for a response code
|
||||||
|
/// </summary>
|
||||||
|
public ErrorInfo GetErrorInfo(string code, string? message = null) => ErrorMapping.GetErrorInfo(code.ToString(), message);
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void SetApiCredentials<T>(T credentials) where T : ApiCredentials
|
public void SetApiCredentials<T>(T credentials) where T : ApiCredentials
|
||||||
{
|
{
|
||||||
ApiOptions.ApiCredentials = credentials;
|
ApiCredentials = credentials?.Copy();
|
||||||
if (credentials != null)
|
if (ApiCredentials != null)
|
||||||
AuthenticationProvider = CreateAuthenticationProvider(credentials.Copy());
|
AuthenticationProvider = CreateAuthenticationProvider(ApiCredentials);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -97,9 +118,9 @@ namespace CryptoExchange.Net.Clients
|
|||||||
ClientOptions.Proxy = options.Proxy;
|
ClientOptions.Proxy = options.Proxy;
|
||||||
ClientOptions.RequestTimeout = options.RequestTimeout ?? ClientOptions.RequestTimeout;
|
ClientOptions.RequestTimeout = options.RequestTimeout ?? ClientOptions.RequestTimeout;
|
||||||
|
|
||||||
ApiOptions.ApiCredentials = options.ApiCredentials ?? ClientOptions.ApiCredentials;
|
ApiCredentials = options.ApiCredentials?.Copy() ?? ApiCredentials;
|
||||||
if (options.ApiCredentials != null)
|
if (ApiCredentials != null)
|
||||||
AuthenticationProvider = CreateAuthenticationProvider(options.ApiCredentials.Copy());
|
AuthenticationProvider = CreateAuthenticationProvider(ApiCredentials);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -66,8 +66,6 @@ namespace CryptoExchange.Net.Clients
|
|||||||
protected BaseClient(ILoggerFactory? logger, string exchange)
|
protected BaseClient(ILoggerFactory? logger, string exchange)
|
||||||
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||||
{
|
{
|
||||||
_logger = logger?.CreateLogger(exchange) ?? NullLoggerFactory.Instance.CreateLogger(exchange);
|
|
||||||
|
|
||||||
Exchange = exchange;
|
Exchange = exchange;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Clients
|
namespace CryptoExchange.Net.Clients
|
||||||
{
|
{
|
||||||
@@ -19,6 +20,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <param name="name">The name of the API this client is for</param>
|
/// <param name="name">The name of the API this client is for</param>
|
||||||
protected BaseRestClient(ILoggerFactory? loggerFactory, string name) : base(loggerFactory, name)
|
protected BaseRestClient(ILoggerFactory? loggerFactory, string name) : base(loggerFactory, name)
|
||||||
{
|
{
|
||||||
|
_logger = loggerFactory?.CreateLogger(name + ".RestClient") ?? NullLoggerFactory.Instance.CreateLogger(name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,12 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using System.Xml.Linq;
|
||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces;
|
||||||
using CryptoExchange.Net.Logging.Extensions;
|
using CryptoExchange.Net.Logging.Extensions;
|
||||||
using CryptoExchange.Net.Objects.Sockets;
|
using CryptoExchange.Net.Objects.Sockets;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Clients
|
namespace CryptoExchange.Net.Clients
|
||||||
{
|
{
|
||||||
@@ -33,10 +35,11 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="logger">Logger</param>
|
/// <param name="loggerFactory">Logger factory</param>
|
||||||
/// <param name="exchange">The name of the exchange this client is for</param>
|
/// <param name="name">The name of the exchange this client is for</param>
|
||||||
protected BaseSocketClient(ILoggerFactory? logger, string exchange) : base(logger, exchange)
|
protected BaseSocketClient(ILoggerFactory? loggerFactory, string name) : base(loggerFactory, name)
|
||||||
{
|
{
|
||||||
|
_logger = loggerFactory?.CreateLogger(name + ".SocketClient") ?? NullLoggerFactory.Instance.CreateLogger(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ using CryptoExchange.Net.Caching;
|
|||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces;
|
||||||
using CryptoExchange.Net.Logging.Extensions;
|
using CryptoExchange.Net.Logging.Extensions;
|
||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.Objects.Errors;
|
||||||
using CryptoExchange.Net.Objects.Options;
|
using CryptoExchange.Net.Objects.Options;
|
||||||
using CryptoExchange.Net.RateLimiting;
|
using CryptoExchange.Net.RateLimiting;
|
||||||
using CryptoExchange.Net.RateLimiting.Interfaces;
|
using CryptoExchange.Net.RateLimiting.Interfaces;
|
||||||
@@ -54,7 +55,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Request headers to be sent with each request
|
/// Request headers to be sent with each request
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected Dictionary<string, string>? StandardRequestHeaders { get; set; }
|
protected Dictionary<string, string> StandardRequestHeaders { get; set; } = [];
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether parameters need to be ordered
|
/// Whether parameters need to be ordered
|
||||||
@@ -105,7 +106,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
options,
|
options,
|
||||||
apiOptions)
|
apiOptions)
|
||||||
{
|
{
|
||||||
RequestFactory.Configure(options.Proxy, options.RequestTimeout, httpClient);
|
RequestFactory.Configure(options, httpClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -238,13 +239,14 @@ namespace CryptoExchange.Net.Clients
|
|||||||
additionalHeaders);
|
additionalHeaders);
|
||||||
_logger.RestApiSendRequest(request.RequestId, definition, request.Content, string.IsNullOrEmpty(request.Uri.Query) ? "-" : request.Uri.Query, string.Join(", ", request.GetHeaders().Select(h => h.Key + $"=[{string.Join(",", h.Value)}]")));
|
_logger.RestApiSendRequest(request.RequestId, definition, request.Content, string.IsNullOrEmpty(request.Uri.Query) ? "-" : request.Uri.Query, string.Join(", ", request.GetHeaders().Select(h => h.Key + $"=[{string.Join(",", h.Value)}]")));
|
||||||
TotalRequestsMade++;
|
TotalRequestsMade++;
|
||||||
var result = await GetResponseAsync<T>(request, definition.RateLimitGate, cancellationToken).ConfigureAwait(false);
|
var result = await GetResponseAsync<T>(definition, request, definition.RateLimitGate, cancellationToken).ConfigureAwait(false);
|
||||||
if (result.Error is not CancellationRequestedError)
|
if (result.Error is not CancellationRequestedError)
|
||||||
{
|
{
|
||||||
|
var originalData = OutputOriginalData ? result.OriginalData : "[Data only available when OutputOriginal = true]";
|
||||||
if (!result)
|
if (!result)
|
||||||
_logger.RestApiErrorReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), result.Error?.ToString());
|
_logger.RestApiErrorReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), result.Error?.ToString(), originalData, result.Error?.Exception);
|
||||||
else
|
else
|
||||||
_logger.RestApiResponseReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), OutputOriginalData ? result.OriginalData : "[Data only available when OutputOriginal = true]");
|
_logger.RestApiResponseReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), originalData);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -362,74 +364,58 @@ namespace CryptoExchange.Net.Clients
|
|||||||
ParameterCollection? bodyParameters,
|
ParameterCollection? bodyParameters,
|
||||||
Dictionary<string, string>? additionalHeaders)
|
Dictionary<string, string>? additionalHeaders)
|
||||||
{
|
{
|
||||||
var uriParams = uriParameters == null ? null : CreateParameterDictionary(uriParameters);
|
var requestConfiguration = new RestRequestConfiguration(
|
||||||
var bodyParams = bodyParameters == null ? null : CreateParameterDictionary(bodyParameters);
|
definition,
|
||||||
|
baseAddress,
|
||||||
|
uriParameters == null ? new Dictionary<string, object>() : CreateParameterDictionary(uriParameters),
|
||||||
|
bodyParameters == null ? new Dictionary<string, object>() : CreateParameterDictionary(bodyParameters),
|
||||||
|
new Dictionary<string, string>(additionalHeaders ?? []),
|
||||||
|
definition.ArraySerialization ?? ArraySerialization,
|
||||||
|
definition.ParameterPosition ?? ParameterPositions[definition.Method],
|
||||||
|
definition.RequestBodyFormat ?? RequestBodyFormat);
|
||||||
|
|
||||||
var uri = new Uri(baseAddress.AppendPath(definition.Path));
|
try
|
||||||
var arraySerialization = definition.ArraySerialization ?? ArraySerialization;
|
|
||||||
var bodyFormat = definition.RequestBodyFormat ?? RequestBodyFormat;
|
|
||||||
var parameterPosition = definition.ParameterPosition ?? ParameterPositions[definition.Method];
|
|
||||||
|
|
||||||
Dictionary<string, string>? headers = null;
|
|
||||||
if (AuthenticationProvider != null)
|
|
||||||
{
|
{
|
||||||
try
|
AuthenticationProvider?.ProcessRequest(this, requestConfiguration);
|
||||||
{
|
|
||||||
AuthenticationProvider.AuthenticateRequest(
|
|
||||||
this,
|
|
||||||
uri,
|
|
||||||
definition.Method,
|
|
||||||
ref uriParams,
|
|
||||||
ref bodyParams,
|
|
||||||
ref headers,
|
|
||||||
definition.Authenticated,
|
|
||||||
arraySerialization,
|
|
||||||
parameterPosition,
|
|
||||||
bodyFormat
|
|
||||||
);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
throw new Exception("Failed to authenticate request, make sure your API credentials are correct", ex);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw new Exception("Failed to authenticate request, make sure your API credentials are correct", ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
var queryString = requestConfiguration.GetQueryString(true);
|
||||||
|
if (!string.IsNullOrEmpty(queryString) && !queryString.StartsWith("?"))
|
||||||
|
queryString = $"?{queryString}";
|
||||||
|
|
||||||
// Add the auth parameters to the uri, start with a new URI to be able to sort the parameters including the auth parameters
|
var uri = new Uri(baseAddress.AppendPath(definition.Path) + queryString);
|
||||||
if (uriParams != null)
|
var request = RequestFactory.Create(ClientOptions.HttpVersion, definition.Method, uri, requestId);
|
||||||
uri = uri.SetParameters(uriParams, arraySerialization);
|
|
||||||
|
|
||||||
var request = RequestFactory.Create(definition.Method, uri, requestId);
|
|
||||||
request.Accept = Constants.JsonContentHeader;
|
request.Accept = Constants.JsonContentHeader;
|
||||||
|
|
||||||
if (headers != null)
|
foreach (var header in requestConfiguration.Headers)
|
||||||
{
|
request.AddHeader(header.Key, header.Value);
|
||||||
foreach (var header in headers)
|
|
||||||
request.AddHeader(header.Key, header.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (additionalHeaders != null)
|
foreach (var header in StandardRequestHeaders)
|
||||||
{
|
{
|
||||||
foreach (var header in additionalHeaders)
|
// Only add it if it isn't overwritten
|
||||||
|
if (!requestConfiguration.Headers.ContainsKey(header.Key))
|
||||||
request.AddHeader(header.Key, header.Value);
|
request.AddHeader(header.Key, header.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (StandardRequestHeaders != null)
|
if (requestConfiguration.ParameterPosition == HttpMethodParameterPosition.InBody)
|
||||||
{
|
{
|
||||||
foreach (var header in StandardRequestHeaders)
|
var contentType = requestConfiguration.BodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader;
|
||||||
|
var bodyContent = requestConfiguration.GetBodyContent();
|
||||||
|
if (bodyContent != null)
|
||||||
{
|
{
|
||||||
// Only add it if it isn't overwritten
|
request.SetContent(bodyContent, contentType);
|
||||||
if (additionalHeaders?.ContainsKey(header.Key) != true)
|
|
||||||
request.AddHeader(header.Key, header.Value);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (parameterPosition == HttpMethodParameterPosition.InBody)
|
|
||||||
{
|
|
||||||
var contentType = bodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader;
|
|
||||||
if (bodyParams != null && bodyParams.Count != 0)
|
|
||||||
WriteParamBody(request, bodyParams, contentType);
|
|
||||||
else
|
else
|
||||||
request.SetContent(RequestBodyEmptyContent, contentType);
|
{
|
||||||
|
if (requestConfiguration.BodyParameters != null && requestConfiguration.BodyParameters.Count != 0)
|
||||||
|
WriteParamBody(request, requestConfiguration.BodyParameters, contentType);
|
||||||
|
else
|
||||||
|
request.SetContent(RequestBodyEmptyContent, contentType);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return request;
|
return request;
|
||||||
@@ -438,11 +424,13 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Executes the request and returns the result deserialized into the type parameter class
|
/// Executes the request and returns the result deserialized into the type parameter class
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="requestDefinition">The request definition</param>
|
||||||
/// <param name="request">The request object to execute</param>
|
/// <param name="request">The request object to execute</param>
|
||||||
/// <param name="gate">The ratelimit gate used</param>
|
/// <param name="gate">The ratelimit gate used</param>
|
||||||
/// <param name="cancellationToken">Cancellation token</param>
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected virtual async Task<WebCallResult<T>> GetResponseAsync<T>(
|
protected virtual async Task<WebCallResult<T>> GetResponseAsync<T>(
|
||||||
|
RequestDefinition requestDefinition,
|
||||||
IRequest request,
|
IRequest request,
|
||||||
IRateLimitGate? gate,
|
IRateLimitGate? gate,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
@@ -455,17 +443,14 @@ namespace CryptoExchange.Net.Clients
|
|||||||
{
|
{
|
||||||
response = await request.GetResponseAsync(cancellationToken).ConfigureAwait(false);
|
response = await request.GetResponseAsync(cancellationToken).ConfigureAwait(false);
|
||||||
sw.Stop();
|
sw.Stop();
|
||||||
var statusCode = response.StatusCode;
|
|
||||||
var headers = response.ResponseHeaders;
|
|
||||||
var responseLength = response.ContentLength;
|
|
||||||
responseStream = await response.GetResponseStreamAsync().ConfigureAwait(false);
|
responseStream = await response.GetResponseStreamAsync().ConfigureAwait(false);
|
||||||
var outputOriginalData = ApiOptions.OutputOriginalData ?? ClientOptions.OutputOriginalData;
|
var outputOriginalData = ApiOptions.OutputOriginalData ?? ClientOptions.OutputOriginalData;
|
||||||
|
|
||||||
accessor = CreateAccessor();
|
accessor = CreateAccessor();
|
||||||
if (!response.IsSuccessStatusCode)
|
if (!response.IsSuccessStatusCode && !requestDefinition.TryParseOnNonSuccess)
|
||||||
{
|
{
|
||||||
// Error response
|
// Error response
|
||||||
await accessor.Read(responseStream, true).ConfigureAwait(false);
|
var readResult = await accessor.Read(responseStream, true).ConfigureAwait(false);
|
||||||
|
|
||||||
Error error;
|
Error error;
|
||||||
if (response.StatusCode == (HttpStatusCode)418 || response.StatusCode == (HttpStatusCode)429)
|
if (response.StatusCode == (HttpStatusCode)418 || response.StatusCode == (HttpStatusCode)429)
|
||||||
@@ -481,29 +466,28 @@ namespace CryptoExchange.Net.Clients
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
error = ParseErrorResponse((int)response.StatusCode, response.ResponseHeaders, accessor);
|
error = ParseErrorResponse((int)response.StatusCode, response.ResponseHeaders, accessor, readResult.Error?.Exception);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error.Code == null || error.Code == 0)
|
if (error.Code == null || error.Code == 0)
|
||||||
error.Code = (int)response.StatusCode;
|
error.Code = (int)response.StatusCode;
|
||||||
|
|
||||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error!);
|
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error!);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var valid = await accessor.Read(responseStream, outputOriginalData).ConfigureAwait(false);
|
||||||
if (typeof(T) == typeof(object))
|
if (typeof(T) == typeof(object))
|
||||||
// Success status code and expected empty response, assume it's correct
|
// Success status code and expected empty response, assume it's correct
|
||||||
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, 0, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, null);
|
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, 0, accessor.OriginalDataAvailable ? accessor.GetOriginalString() : "[Data only available when OutputOriginal = true in client options]", request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, null);
|
||||||
|
|
||||||
var valid = await accessor.Read(responseStream, outputOriginalData).ConfigureAwait(false);
|
|
||||||
if (!valid)
|
if (!valid)
|
||||||
{
|
{
|
||||||
// Invalid json
|
// Invalid json
|
||||||
var error = new ServerError("Failed to parse response: " + valid.Error!.Message, accessor.OriginalDataAvailable ? accessor.GetOriginalString() : "[Data only available when OutputOriginal = true in client options]");
|
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, valid.Error);
|
||||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Json response received
|
// Json response received
|
||||||
var parsedError = TryParseError(response.ResponseHeaders, accessor);
|
var parsedError = TryParseError(requestDefinition, response.ResponseHeaders, accessor);
|
||||||
if (parsedError != null)
|
if (parsedError != null)
|
||||||
{
|
{
|
||||||
if (parsedError is ServerRateLimitError rateError)
|
if (parsedError is ServerRateLimitError rateError)
|
||||||
@@ -516,31 +500,55 @@ namespace CryptoExchange.Net.Clients
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Success status code, but TryParseError determined it was an error response
|
// Success status code, but TryParseError determined it was an error response
|
||||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, parsedError);
|
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, parsedError);
|
||||||
}
|
}
|
||||||
|
|
||||||
var deserializeResult = accessor.Deserialize<T>();
|
var deserializeResult = accessor.Deserialize<T>();
|
||||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult.Data, deserializeResult.Error);
|
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult.Data, deserializeResult.Error);
|
||||||
}
|
}
|
||||||
catch (HttpRequestException requestException)
|
catch (HttpRequestException requestException)
|
||||||
{
|
{
|
||||||
// Request exception, can't reach server for instance
|
// Request exception, can't reach server for instance
|
||||||
var exceptionInfo = requestException.ToLogString();
|
var error = new WebError(requestException.Message, requestException);
|
||||||
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new WebError(exceptionInfo));
|
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);
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException canceledException)
|
catch (OperationCanceledException canceledException)
|
||||||
{
|
{
|
||||||
if (cancellationToken != default && canceledException.CancellationToken == cancellationToken)
|
if (cancellationToken != default && canceledException.CancellationToken == cancellationToken)
|
||||||
{
|
{
|
||||||
// Cancellation token canceled by caller
|
// Cancellation token canceled by caller
|
||||||
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new CancellationRequestedError());
|
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));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Request timed out
|
// Request timed out
|
||||||
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new WebError($"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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (ArgumentException argumentException)
|
||||||
|
{
|
||||||
|
if (argumentException.Message.StartsWith("Only HTTP/"))
|
||||||
|
{
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (NotSupportedException notSupportedException)
|
||||||
|
{
|
||||||
|
if (notSupportedException.Message.StartsWith("Request version value must be one of"))
|
||||||
|
{
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw;
|
||||||
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
accessor?.Clear();
|
accessor?.Clear();
|
||||||
@@ -554,10 +562,11 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// This method will be called for each response to be able to check if the response is an error or not.
|
/// This method will be called for each response to be able to check if the response is an error or not.
|
||||||
/// If the response is an error this method should return the parsed error, else it should return null
|
/// If the response is an error this method should return the parsed error, else it should return null
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="requestDefinition">Request definition</param>
|
||||||
/// <param name="accessor">Data accessor</param>
|
/// <param name="accessor">Data accessor</param>
|
||||||
/// <param name="responseHeaders">The response headers</param>
|
/// <param name="responseHeaders">The response headers</param>
|
||||||
/// <returns>Null if not an error, Error otherwise</returns>
|
/// <returns>Null if not an error, Error otherwise</returns>
|
||||||
protected virtual Error? TryParseError(KeyValuePair<string, string[]>[] responseHeaders, IMessageAccessor accessor) => null;
|
protected virtual Error? TryParseError(RequestDefinition requestDefinition, KeyValuePair<string, string[]>[] responseHeaders, IMessageAccessor accessor) => null;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Can be used to indicate that a request should be retried. Defaults to false. Make sure to retry a max number of times (based on the the tries parameter) or the request will retry forever.
|
/// Can be used to indicate that a request should be retried. Defaults to false. Make sure to retry a max number of times (based on the the tries parameter) or the request will retry forever.
|
||||||
@@ -603,12 +612,16 @@ namespace CryptoExchange.Net.Clients
|
|||||||
{
|
{
|
||||||
if (contentType == Constants.JsonContentHeader)
|
if (contentType == Constants.JsonContentHeader)
|
||||||
{
|
{
|
||||||
|
var serializer = CreateSerializer();
|
||||||
|
if (serializer is not IStringMessageSerializer stringSerializer)
|
||||||
|
throw new InvalidOperationException("Non-string message serializer can't get serialized request body");
|
||||||
|
|
||||||
// Write the parameters as json in the body
|
// Write the parameters as json in the body
|
||||||
string stringData;
|
string stringData;
|
||||||
if (parameters.Count == 1 && parameters.TryGetValue(Constants.BodyPlaceHolderKey, out object? value))
|
if (parameters.Count == 1 && parameters.TryGetValue(Constants.BodyPlaceHolderKey, out object? value))
|
||||||
stringData = CreateSerializer().Serialize(value);
|
stringData = stringSerializer.Serialize(value);
|
||||||
else
|
else
|
||||||
stringData = CreateSerializer().Serialize(parameters);
|
stringData = stringSerializer.Serialize(parameters);
|
||||||
request.SetContent(stringData, contentType);
|
request.SetContent(stringData, contentType);
|
||||||
}
|
}
|
||||||
else if (contentType == Constants.FormContentHeader)
|
else if (contentType == Constants.FormContentHeader)
|
||||||
@@ -625,11 +638,11 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <param name="httpStatusCode">The response status code</param>
|
/// <param name="httpStatusCode">The response status code</param>
|
||||||
/// <param name="responseHeaders">The response headers</param>
|
/// <param name="responseHeaders">The response headers</param>
|
||||||
/// <param name="accessor">Data accessor</param>
|
/// <param name="accessor">Data accessor</param>
|
||||||
|
/// <param name="exception">Exception</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected virtual Error ParseErrorResponse(int httpStatusCode, KeyValuePair<string, string[]>[] responseHeaders, IMessageAccessor accessor)
|
protected virtual Error ParseErrorResponse(int httpStatusCode, KeyValuePair<string, string[]>[] responseHeaders, IMessageAccessor accessor, Exception? exception)
|
||||||
{
|
{
|
||||||
var message = accessor.OriginalDataAvailable ? accessor.GetOriginalString() : "[Error response content only available when OutputOriginal = true in client options]";
|
return new ServerError(ErrorInfo.Unknown, exception);
|
||||||
return new ServerError(message);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -641,21 +654,19 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected virtual ServerRateLimitError ParseRateLimitResponse(int httpStatusCode, KeyValuePair<string, string[]>[] responseHeaders, IMessageAccessor accessor)
|
protected virtual ServerRateLimitError ParseRateLimitResponse(int httpStatusCode, KeyValuePair<string, string[]>[] responseHeaders, IMessageAccessor accessor)
|
||||||
{
|
{
|
||||||
var message = accessor.OriginalDataAvailable ? accessor.GetOriginalString() : "[Error response content only available when OutputOriginal = true in client options]";
|
|
||||||
|
|
||||||
// Handle retry after header
|
// Handle retry after header
|
||||||
var retryAfterHeader = responseHeaders.SingleOrDefault(r => r.Key.Equals("Retry-After", StringComparison.InvariantCultureIgnoreCase));
|
var retryAfterHeader = responseHeaders.SingleOrDefault(r => r.Key.Equals("Retry-After", StringComparison.InvariantCultureIgnoreCase));
|
||||||
if (retryAfterHeader.Value?.Any() != true)
|
if (retryAfterHeader.Value?.Any() != true)
|
||||||
return new ServerRateLimitError(message);
|
return new ServerRateLimitError();
|
||||||
|
|
||||||
var value = retryAfterHeader.Value.First();
|
var value = retryAfterHeader.Value.First();
|
||||||
if (int.TryParse(value, out var seconds))
|
if (int.TryParse(value, out var seconds))
|
||||||
return new ServerRateLimitError(message) { RetryAfter = DateTime.UtcNow.AddSeconds(seconds) };
|
return new ServerRateLimitError() { RetryAfter = DateTime.UtcNow.AddSeconds(seconds) };
|
||||||
|
|
||||||
if (DateTime.TryParse(value, out var datetime))
|
if (DateTime.TryParse(value, out var datetime))
|
||||||
return new ServerRateLimitError(message) { RetryAfter = datetime };
|
return new ServerRateLimitError() { RetryAfter = datetime };
|
||||||
|
|
||||||
return new ServerRateLimitError(message);
|
return new ServerRateLimitError();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -682,21 +693,21 @@ namespace CryptoExchange.Net.Clients
|
|||||||
{
|
{
|
||||||
base.SetOptions(options);
|
base.SetOptions(options);
|
||||||
|
|
||||||
RequestFactory.UpdateSettings(options.Proxy, options.RequestTimeout ?? ClientOptions.RequestTimeout);
|
RequestFactory.UpdateSettings(options.Proxy, options.RequestTimeout ?? ClientOptions.RequestTimeout, ClientOptions.HttpKeepAliveInterval);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal async Task<WebCallResult<bool>> SyncTimeAsync()
|
internal async Task<WebCallResult<bool>> SyncTimeAsync()
|
||||||
{
|
{
|
||||||
var timeSyncParams = GetTimeSyncInfo();
|
var timeSyncParams = GetTimeSyncInfo();
|
||||||
if (timeSyncParams == null)
|
if (timeSyncParams == null)
|
||||||
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null);
|
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null);
|
||||||
|
|
||||||
if (await timeSyncParams.TimeSyncState.Semaphore.WaitAsync(0).ConfigureAwait(false))
|
if (await timeSyncParams.TimeSyncState.Semaphore.WaitAsync(0).ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
if (!timeSyncParams.SyncTime || DateTime.UtcNow - timeSyncParams.TimeSyncState.LastSyncTime < timeSyncParams.RecalculationInterval)
|
if (!timeSyncParams.SyncTime || DateTime.UtcNow - timeSyncParams.TimeSyncState.LastSyncTime < timeSyncParams.RecalculationInterval)
|
||||||
{
|
{
|
||||||
timeSyncParams.TimeSyncState.Semaphore.Release();
|
timeSyncParams.TimeSyncState.Semaphore.Release();
|
||||||
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null);
|
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
var localTime = DateTime.UtcNow;
|
var localTime = DateTime.UtcNow;
|
||||||
@@ -725,7 +736,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
timeSyncParams.TimeSyncState.Semaphore.Release();
|
timeSyncParams.TimeSyncState.Semaphore.Release();
|
||||||
}
|
}
|
||||||
|
|
||||||
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null);
|
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool ShouldCache(RequestDefinition definition)
|
private bool ShouldCache(RequestDefinition definition)
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces;
|
||||||
using CryptoExchange.Net.Logging.Extensions;
|
using CryptoExchange.Net.Logging.Extensions;
|
||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.Objects.Errors;
|
||||||
using CryptoExchange.Net.Objects.Options;
|
using CryptoExchange.Net.Objects.Options;
|
||||||
using CryptoExchange.Net.Objects.Sockets;
|
using CryptoExchange.Net.Objects.Sockets;
|
||||||
|
using CryptoExchange.Net.RateLimiting;
|
||||||
using CryptoExchange.Net.RateLimiting.Interfaces;
|
using CryptoExchange.Net.RateLimiting.Interfaces;
|
||||||
using CryptoExchange.Net.Sockets;
|
using CryptoExchange.Net.Sockets;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -41,6 +43,11 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
protected TimeSpan KeepAliveInterval { get; set; } = TimeSpan.FromSeconds(10);
|
protected TimeSpan KeepAliveInterval { get; set; } = TimeSpan.FromSeconds(10);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Keep alive timeout for websocket connection
|
||||||
|
/// </summary>
|
||||||
|
protected TimeSpan KeepAliveTimeout { get; set; } = TimeSpan.FromSeconds(10);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handlers for data from the socket which doesn't need to be forwarded to the caller. Ping or welcome messages for example.
|
/// Handlers for data from the socket which doesn't need to be forwarded to the caller. Ping or welcome messages for example.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -76,6 +83,11 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
protected bool AllowTopicsOnTheSameConnection { get; set; } = true;
|
protected bool AllowTopicsOnTheSameConnection { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether to continue processing and forward unparsable messages to handlers
|
||||||
|
/// </summary>
|
||||||
|
protected internal bool ProcessUnparsableMessages { get; set; } = false;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public double IncomingKbps
|
public double IncomingKbps
|
||||||
{
|
{
|
||||||
@@ -132,7 +144,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// Create a message accessor instance
|
/// Create a message accessor instance
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected internal abstract IByteMessageAccessor CreateAccessor();
|
protected internal abstract IByteMessageAccessor CreateAccessor(WebSocketMessageType messageType);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a serializer instance
|
/// Create a serializer instance
|
||||||
@@ -205,9 +217,9 @@ namespace CryptoExchange.Net.Clients
|
|||||||
{
|
{
|
||||||
await semaphoreSlim.WaitAsync(ct).ConfigureAwait(false);
|
await semaphoreSlim.WaitAsync(ct).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException tce)
|
||||||
{
|
{
|
||||||
return new CallResult<UpdateSubscription>(new CancellationRequestedError());
|
return new CallResult<UpdateSubscription>(new CancellationRequestedError(tce));
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -215,7 +227,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
// Get a new or existing socket connection
|
// Get a new or existing socket connection
|
||||||
var socketResult = await GetSocketConnection(url, subscription.Authenticated, false, subscription.Topic).ConfigureAwait(false);
|
var socketResult = await GetSocketConnection(url, subscription.Authenticated, false, ct, subscription.Topic).ConfigureAwait(false);
|
||||||
if (!socketResult)
|
if (!socketResult)
|
||||||
return socketResult.As<UpdateSubscription>(null);
|
return socketResult.As<UpdateSubscription>(null);
|
||||||
|
|
||||||
@@ -238,7 +250,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
|
|
||||||
var needsConnecting = !socketConnection.Connected;
|
var needsConnecting = !socketConnection.Connected;
|
||||||
|
|
||||||
var connectResult = await ConnectIfNeededAsync(socketConnection, subscription.Authenticated).ConfigureAwait(false);
|
var connectResult = await ConnectIfNeededAsync(socketConnection, subscription.Authenticated, ct).ConfigureAwait(false);
|
||||||
if (!connectResult)
|
if (!connectResult)
|
||||||
return new CallResult<UpdateSubscription>(connectResult.Error!);
|
return new CallResult<UpdateSubscription>(connectResult.Error!);
|
||||||
|
|
||||||
@@ -254,20 +266,21 @@ namespace CryptoExchange.Net.Clients
|
|||||||
if (socketConnection.PausedActivity)
|
if (socketConnection.PausedActivity)
|
||||||
{
|
{
|
||||||
_logger.HasBeenPausedCantSubscribeAtThisMoment(socketConnection.SocketId);
|
_logger.HasBeenPausedCantSubscribeAtThisMoment(socketConnection.SocketId);
|
||||||
return new CallResult<UpdateSubscription>(new ServerError("Socket is paused"));
|
return new CallResult<UpdateSubscription>(new ServerError(new ErrorInfo(ErrorType.WebsocketPaused, "Socket is paused")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
subscription.Status = SubscriptionStatus.Subscribing;
|
||||||
var waitEvent = new AsyncResetEvent(false);
|
var waitEvent = new AsyncResetEvent(false);
|
||||||
var subQuery = subscription.GetSubQuery(socketConnection);
|
var subQuery = subscription.CreateSubscriptionQuery(socketConnection);
|
||||||
if (subQuery != null)
|
if (subQuery != null)
|
||||||
{
|
{
|
||||||
// Send the request and wait for answer
|
// Send the request and wait for answer
|
||||||
var subResult = await socketConnection.SendAndWaitQueryAsync(subQuery, waitEvent).ConfigureAwait(false);
|
var subResult = await socketConnection.SendAndWaitQueryAsync(subQuery, waitEvent, ct).ConfigureAwait(false);
|
||||||
if (!subResult)
|
if (!subResult)
|
||||||
{
|
{
|
||||||
waitEvent?.Set();
|
waitEvent?.Set();
|
||||||
var isTimeout = subResult.Error is CancellationRequestedError;
|
var isTimeout = subResult.Error is CancellationRequestedError;
|
||||||
if (isTimeout && subscription.Confirmed)
|
if (isTimeout && subscription.Status == SubscriptionStatus.Subscribed)
|
||||||
{
|
{
|
||||||
// No response received, but the subscription did receive updates. We'll assume success
|
// No response received, but the subscription did receive updates. We'll assume success
|
||||||
}
|
}
|
||||||
@@ -275,6 +288,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
{
|
{
|
||||||
_logger.FailedToSubscribe(socketConnection.SocketId, subResult.Error?.ToString());
|
_logger.FailedToSubscribe(socketConnection.SocketId, subResult.Error?.ToString());
|
||||||
// If this was a timeout we still need to send an unsubscribe to prevent messages coming in later
|
// If this was a timeout we still need to send an unsubscribe to prevent messages coming in later
|
||||||
|
subscription.Status = SubscriptionStatus.Pending;
|
||||||
await socketConnection.CloseAsync(subscription).ConfigureAwait(false);
|
await socketConnection.CloseAsync(subscription).ConfigureAwait(false);
|
||||||
return new CallResult<UpdateSubscription>(subResult.Error!);
|
return new CallResult<UpdateSubscription>(subResult.Error!);
|
||||||
}
|
}
|
||||||
@@ -283,7 +297,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
subscription.HandleSubQueryResponse(subQuery.Response!);
|
subscription.HandleSubQueryResponse(subQuery.Response!);
|
||||||
}
|
}
|
||||||
|
|
||||||
subscription.Confirmed = true;
|
subscription.Status = SubscriptionStatus.Subscribed;
|
||||||
if (ct != default)
|
if (ct != default)
|
||||||
{
|
{
|
||||||
subscription.CancellationTokenRegistration = ct.Register(async () =>
|
subscription.CancellationTokenRegistration = ct.Register(async () =>
|
||||||
@@ -302,11 +316,10 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// Send a query on a socket connection to the BaseAddress and wait for the response
|
/// Send a query on a socket connection to the BaseAddress and wait for the response
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="THandlerResponse">Expected result type</typeparam>
|
/// <typeparam name="THandlerResponse">Expected result type</typeparam>
|
||||||
/// <typeparam name="TServerResponse">The type returned to the caller</typeparam>
|
|
||||||
/// <param name="query">The query</param>
|
/// <param name="query">The query</param>
|
||||||
/// <param name="ct">Cancellation token</param>
|
/// <param name="ct">Cancellation token</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected virtual Task<CallResult<THandlerResponse>> QueryAsync<TServerResponse, THandlerResponse>(Query<TServerResponse, THandlerResponse> query, CancellationToken ct = default)
|
protected virtual Task<CallResult<THandlerResponse>> QueryAsync<THandlerResponse>(Query<THandlerResponse> query, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
return QueryAsync(BaseAddress, query, ct);
|
return QueryAsync(BaseAddress, query, ct);
|
||||||
}
|
}
|
||||||
@@ -315,12 +328,11 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// Send a query on a socket connection and wait for the response
|
/// Send a query on a socket connection and wait for the response
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="THandlerResponse">Expected result type</typeparam>
|
/// <typeparam name="THandlerResponse">Expected result type</typeparam>
|
||||||
/// <typeparam name="TServerResponse">The type returned to the caller</typeparam>
|
|
||||||
/// <param name="url">The url for the request</param>
|
/// <param name="url">The url for the request</param>
|
||||||
/// <param name="query">The query</param>
|
/// <param name="query">The query</param>
|
||||||
/// <param name="ct">Cancellation token</param>
|
/// <param name="ct">Cancellation token</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected virtual async Task<CallResult<THandlerResponse>> QueryAsync<TServerResponse, THandlerResponse>(string url, Query<TServerResponse, THandlerResponse> query, CancellationToken ct = default)
|
protected virtual async Task<CallResult<THandlerResponse>> QueryAsync<THandlerResponse>(string url, Query<THandlerResponse> query, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
if (_disposing)
|
if (_disposing)
|
||||||
return new CallResult<THandlerResponse>(new InvalidOperationError("Client disposed, can't query"));
|
return new CallResult<THandlerResponse>(new InvalidOperationError("Client disposed, can't query"));
|
||||||
@@ -333,7 +345,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
await semaphoreSlim.WaitAsync().ConfigureAwait(false);
|
await semaphoreSlim.WaitAsync().ConfigureAwait(false);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var socketResult = await GetSocketConnection(url, query.Authenticated, true).ConfigureAwait(false);
|
var socketResult = await GetSocketConnection(url, query.Authenticated, true, ct).ConfigureAwait(false);
|
||||||
if (!socketResult)
|
if (!socketResult)
|
||||||
return socketResult.As<THandlerResponse>(default);
|
return socketResult.As<THandlerResponse>(default);
|
||||||
|
|
||||||
@@ -346,7 +358,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
released = true;
|
released = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
var connectResult = await ConnectIfNeededAsync(socketConnection, query.Authenticated).ConfigureAwait(false);
|
var connectResult = await ConnectIfNeededAsync(socketConnection, query.Authenticated, ct).ConfigureAwait(false);
|
||||||
if (!connectResult)
|
if (!connectResult)
|
||||||
return new CallResult<THandlerResponse>(connectResult.Error!);
|
return new CallResult<THandlerResponse>(connectResult.Error!);
|
||||||
}
|
}
|
||||||
@@ -359,7 +371,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
if (socketConnection.PausedActivity)
|
if (socketConnection.PausedActivity)
|
||||||
{
|
{
|
||||||
_logger.HasBeenPausedCantSendQueryAtThisMoment(socketConnection.SocketId);
|
_logger.HasBeenPausedCantSendQueryAtThisMoment(socketConnection.SocketId);
|
||||||
return new CallResult<THandlerResponse>(new ServerError("Socket is paused"));
|
return new CallResult<THandlerResponse>(new ServerError(new ErrorInfo(ErrorType.WebsocketPaused, "Socket is paused")));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ct.IsCancellationRequested)
|
if (ct.IsCancellationRequested)
|
||||||
@@ -373,13 +385,14 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="socket">The connection to check</param>
|
/// <param name="socket">The connection to check</param>
|
||||||
/// <param name="authenticated">Whether the socket should authenticated</param>
|
/// <param name="authenticated">Whether the socket should authenticated</param>
|
||||||
|
/// <param name="ct">Cancellation token</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected virtual async Task<CallResult> ConnectIfNeededAsync(SocketConnection socket, bool authenticated)
|
protected virtual async Task<CallResult> ConnectIfNeededAsync(SocketConnection socket, bool authenticated, CancellationToken ct)
|
||||||
{
|
{
|
||||||
if (socket.Connected)
|
if (socket.Connected)
|
||||||
return CallResult.SuccessResult;
|
return CallResult.SuccessResult;
|
||||||
|
|
||||||
var connectResult = await ConnectSocketAsync(socket).ConfigureAwait(false);
|
var connectResult = await ConnectSocketAsync(socket, ct).ConfigureAwait(false);
|
||||||
if (!connectResult)
|
if (!connectResult)
|
||||||
return connectResult;
|
return connectResult;
|
||||||
|
|
||||||
@@ -483,25 +496,56 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <param name="address">The address the socket is for</param>
|
/// <param name="address">The address the socket is for</param>
|
||||||
/// <param name="authenticated">Whether the socket should be authenticated</param>
|
/// <param name="authenticated">Whether the socket should be authenticated</param>
|
||||||
/// <param name="dedicatedRequestConnection">Whether a dedicated request connection should be returned</param>
|
/// <param name="dedicatedRequestConnection">Whether a dedicated request connection should be returned</param>
|
||||||
|
/// <param name="ct">Cancellation token</param>
|
||||||
/// <param name="topic">The subscription topic, can be provided when multiple of the same topics are not allowed on a connection</param>
|
/// <param name="topic">The subscription topic, can be provided when multiple of the same topics are not allowed on a connection</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected virtual async Task<CallResult<SocketConnection>> GetSocketConnection(string address, bool authenticated, bool dedicatedRequestConnection, string? topic = null)
|
protected virtual async Task<CallResult<SocketConnection>> GetSocketConnection(string address, bool authenticated, bool dedicatedRequestConnection, CancellationToken ct, string? topic = null)
|
||||||
{
|
{
|
||||||
var socketQuery = socketConnections.Where(s => (s.Value.Status == SocketConnection.SocketStatus.None || s.Value.Status == SocketConnection.SocketStatus.Connected)
|
var socketQuery = socketConnections.Where(s => s.Value.Tag.TrimEnd('/') == address.TrimEnd('/')
|
||||||
&& s.Value.Tag.TrimEnd('/') == address.TrimEnd('/')
|
|
||||||
&& s.Value.ApiClient.GetType() == GetType()
|
&& s.Value.ApiClient.GetType() == GetType()
|
||||||
&& (s.Value.Authenticated == authenticated || !authenticated)
|
&& (AllowTopicsOnTheSameConnection || !s.Value.Topics.Contains(topic)))
|
||||||
&& (AllowTopicsOnTheSameConnection || !s.Value.Topics.Contains(topic))
|
.Select(x => x.Value)
|
||||||
&& s.Value.Connected);
|
.ToList();
|
||||||
|
|
||||||
SocketConnection connection;
|
// If all current socket connections are reconnecting or resubscribing wait for that to finish as we can probably use the existing connection
|
||||||
|
var delayStart = DateTime.UtcNow;
|
||||||
|
var delayed = false;
|
||||||
|
while (socketQuery.Count >= 1 && socketQuery.All(x => x.Status == SocketConnection.SocketStatus.Reconnecting || x.Status == SocketConnection.SocketStatus.Resubscribing))
|
||||||
|
{
|
||||||
|
if (DateTime.UtcNow - delayStart > TimeSpan.FromSeconds(10))
|
||||||
|
{
|
||||||
|
if (socketQuery.Count >= 1 && socketQuery.All(x => x.Status == SocketConnection.SocketStatus.Reconnecting || x.Status == SocketConnection.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());
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
delayed = true;
|
||||||
|
try { await Task.Delay(50, ct).ConfigureAwait(false); } catch (Exception) { }
|
||||||
|
|
||||||
|
if (ct.IsCancellationRequested)
|
||||||
|
return new CallResult<SocketConnection>(new CancellationRequestedError());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (delayed)
|
||||||
|
_logger.WaitedForReconnectingSocket((long)(DateTime.UtcNow - delayStart).TotalMilliseconds);
|
||||||
|
|
||||||
|
socketQuery = socketQuery.Where(s => (s.Status == SocketConnection.SocketStatus.None || s.Status == SocketConnection.SocketStatus.Connected)
|
||||||
|
&& (s.Authenticated == authenticated || !authenticated)
|
||||||
|
&& s.Connected).ToList();
|
||||||
|
|
||||||
|
SocketConnection? connection;
|
||||||
if (!dedicatedRequestConnection)
|
if (!dedicatedRequestConnection)
|
||||||
{
|
{
|
||||||
connection = socketQuery.Where(s => !s.Value.DedicatedRequestConnection.IsDedicatedRequestConnection).OrderBy(s => s.Value.UserSubscriptionCount).FirstOrDefault().Value;
|
connection = socketQuery.Where(s => !s.DedicatedRequestConnection.IsDedicatedRequestConnection).OrderBy(s => s.UserSubscriptionCount).FirstOrDefault();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
connection = socketQuery.Where(s => s.Value.DedicatedRequestConnection.IsDedicatedRequestConnection).FirstOrDefault().Value;
|
connection = socketQuery.Where(s => s.DedicatedRequestConnection.IsDedicatedRequestConnection).FirstOrDefault();
|
||||||
if (connection != null && !connection.DedicatedRequestConnection.Authenticated)
|
if (connection != null && !connection.DedicatedRequestConnection.Authenticated)
|
||||||
// Mark dedicated request connection as authenticated if the request is authenticated
|
// Mark dedicated request connection as authenticated if the request is authenticated
|
||||||
connection.DedicatedRequestConnection.Authenticated = authenticated;
|
connection.DedicatedRequestConnection.Authenticated = authenticated;
|
||||||
@@ -509,9 +553,12 @@ namespace CryptoExchange.Net.Clients
|
|||||||
|
|
||||||
if (connection != null)
|
if (connection != null)
|
||||||
{
|
{
|
||||||
if (connection.UserSubscriptionCount < ClientOptions.SocketSubscriptionsCombineTarget || (socketConnections.Count >= (ApiOptions.MaxSocketConnections ?? ClientOptions.MaxSocketConnections) && socketConnections.All(s => s.Value.UserSubscriptionCount >= ClientOptions.SocketSubscriptionsCombineTarget)))
|
if (connection.UserSubscriptionCount < ClientOptions.SocketSubscriptionsCombineTarget
|
||||||
|
|| (socketConnections.Count >= (ApiOptions.MaxSocketConnections ?? ClientOptions.MaxSocketConnections) && socketConnections.All(s => s.Value.UserSubscriptionCount >= ClientOptions.SocketSubscriptionsCombineTarget)))
|
||||||
|
{
|
||||||
// Use existing socket if it has less than target connections OR it has the least connections and we can't make new
|
// Use existing socket if it has less than target connections OR it has the least connections and we can't make new
|
||||||
return new CallResult<SocketConnection>(connection);
|
return new CallResult<SocketConnection>(connection);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var connectionAddress = await GetConnectionUrlAsync(address, authenticated).ConfigureAwait(false);
|
var connectionAddress = await GetConnectionUrlAsync(address, authenticated).ConfigureAwait(false);
|
||||||
@@ -560,11 +607,12 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
protected async virtual Task HandleConnectRateLimitedAsync()
|
protected async virtual Task HandleConnectRateLimitedAsync()
|
||||||
{
|
{
|
||||||
if (ClientOptions.RateLimiterEnabled && RateLimiter is not null && ClientOptions.ConnectDelayAfterRateLimited is not null)
|
if (ClientOptions.RateLimiterEnabled && ClientOptions.ConnectDelayAfterRateLimited.HasValue)
|
||||||
{
|
{
|
||||||
var retryAfter = DateTime.UtcNow.Add(ClientOptions.ConnectDelayAfterRateLimited.Value);
|
var retryAfter = DateTime.UtcNow.Add(ClientOptions.ConnectDelayAfterRateLimited.Value);
|
||||||
_logger.AddingRetryAfterGuard(retryAfter);
|
_logger.AddingRetryAfterGuard(retryAfter);
|
||||||
await RateLimiter.SetRetryAfterGuardAsync(retryAfter, RateLimiting.RateLimitItemType.Connection).ConfigureAwait(false);
|
RateLimiter ??= new RateLimitGate("Connection");
|
||||||
|
await RateLimiter.SetRetryAfterGuardAsync(retryAfter, RateLimitItemType.Connection).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -572,10 +620,11 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// Connect a socket
|
/// Connect a socket
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="socketConnection">The socket to connect</param>
|
/// <param name="socketConnection">The socket to connect</param>
|
||||||
|
/// <param name="ct">Cancellation token</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected virtual async Task<CallResult> ConnectSocketAsync(SocketConnection socketConnection)
|
protected virtual async Task<CallResult> ConnectSocketAsync(SocketConnection socketConnection, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var connectResult = await socketConnection.ConnectAsync().ConfigureAwait(false);
|
var connectResult = await socketConnection.ConnectAsync(ct).ConfigureAwait(false);
|
||||||
if (connectResult)
|
if (connectResult)
|
||||||
{
|
{
|
||||||
socketConnections.TryAdd(socketConnection.SocketId, socketConnection);
|
socketConnections.TryAdd(socketConnection.SocketId, socketConnection);
|
||||||
@@ -595,6 +644,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
=> new(new Uri(address), ClientOptions.ReconnectPolicy)
|
=> new(new Uri(address), ClientOptions.ReconnectPolicy)
|
||||||
{
|
{
|
||||||
KeepAliveInterval = KeepAliveInterval,
|
KeepAliveInterval = KeepAliveInterval,
|
||||||
|
KeepAliveTimeout = KeepAliveTimeout,
|
||||||
ReconnectInterval = ClientOptions.ReconnectInterval,
|
ReconnectInterval = ClientOptions.ReconnectInterval,
|
||||||
RateLimiter = ClientOptions.RateLimiterEnabled ? RateLimiter : null,
|
RateLimiter = ClientOptions.RateLimiterEnabled ? RateLimiter : null,
|
||||||
RateLimitingBehavior = ClientOptions.RateLimitingBehaviour,
|
RateLimitingBehavior = ClientOptions.RateLimitingBehaviour,
|
||||||
@@ -702,11 +752,11 @@ namespace CryptoExchange.Net.Clients
|
|||||||
{
|
{
|
||||||
foreach (var item in DedicatedConnectionConfigs)
|
foreach (var item in DedicatedConnectionConfigs)
|
||||||
{
|
{
|
||||||
var socketResult = await GetSocketConnection(item.SocketAddress, item.Authenticated, true).ConfigureAwait(false);
|
var socketResult = await GetSocketConnection(item.SocketAddress, item.Authenticated, true, CancellationToken.None).ConfigureAwait(false);
|
||||||
if (!socketResult)
|
if (!socketResult)
|
||||||
return socketResult.AsDataless();
|
return socketResult.AsDataless();
|
||||||
|
|
||||||
var connectResult = await ConnectIfNeededAsync(socketResult.Data, item.Authenticated).ConfigureAwait(false);
|
var connectResult = await ConnectIfNeededAsync(socketResult.Data, item.Authenticated, default).ConfigureAwait(false);
|
||||||
if (!connectResult)
|
if (!connectResult)
|
||||||
return new CallResult(connectResult.Error!);
|
return new CallResult(connectResult.Error!);
|
||||||
}
|
}
|
||||||
@@ -799,9 +849,9 @@ namespace CryptoExchange.Net.Clients
|
|||||||
cs.SubscriptionStates.ForEach(subState =>
|
cs.SubscriptionStates.ForEach(subState =>
|
||||||
{
|
{
|
||||||
sb.AppendLine($"\t\t\tId: {subState.Id}");
|
sb.AppendLine($"\t\t\tId: {subState.Id}");
|
||||||
sb.AppendLine($"\t\t\tConfirmed: {subState.Confirmed}");
|
sb.AppendLine($"\t\t\tStatus: {subState.Status}");
|
||||||
sb.AppendLine($"\t\t\tInvocations: {subState.Invocations}");
|
sb.AppendLine($"\t\t\tInvocations: {subState.Invocations}");
|
||||||
sb.AppendLine($"\t\t\tIdentifiers: [{string.Join(",", subState.Identifiers)}]");
|
sb.AppendLine($"\t\t\tIdentifiers: [{subState.ListenMatcher.ToString()}]");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Converters
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Caching for JsonSerializerContext instances
|
||||||
|
/// </summary>
|
||||||
|
public static class JsonSerializerContextCache
|
||||||
|
{
|
||||||
|
private static ConcurrentDictionary<Type, JsonSerializerContext> _cache = new ConcurrentDictionary<Type, JsonSerializerContext>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the instance of the provided type T. It will be created if it doesn't exist yet.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">Implementation type of the JsonSerializerContext</typeparam>
|
||||||
|
public static JsonSerializerContext GetOrCreate<T>() where T: JsonSerializerContext, new()
|
||||||
|
{
|
||||||
|
var contextType = typeof(T);
|
||||||
|
if (_cache.TryGetValue(contextType, out var context))
|
||||||
|
return context;
|
||||||
|
|
||||||
|
var instance = new T();
|
||||||
|
_cache[contextType] = instance;
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,8 @@ using System.Text.Json;
|
|||||||
using CryptoExchange.Net.Attributes;
|
using CryptoExchange.Net.Attributes;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||||
{
|
{
|
||||||
@@ -16,14 +18,13 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
/// with [ArrayProperty(x)] where x is the index of the property in the array
|
/// with [ArrayProperty(x)] where x is the index of the property in the array
|
||||||
/// </summary>
|
/// </summary>
|
||||||
#if NET5_0_OR_GREATER
|
#if NET5_0_OR_GREATER
|
||||||
public class ArrayConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TContext> : JsonConverter<T> where T : new() where TContext: JsonSerializerContext
|
public class ArrayConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T> : JsonConverter<T> where T : new()
|
||||||
# else
|
#else
|
||||||
public class ArrayConverter<T, TContext> : JsonConverter<T> where T : new() where TContext: JsonSerializerContext
|
public class ArrayConverter<T> : JsonConverter<T> where T : new()
|
||||||
#endif
|
#endif
|
||||||
{
|
{
|
||||||
private static readonly ConcurrentDictionary<Type, List<ArrayPropertyInfo>> _typeAttributesCache = new ConcurrentDictionary<Type, List<ArrayPropertyInfo>>();
|
private static readonly Lazy<List<ArrayPropertyInfo>> _typePropertyInfo = new Lazy<List<ArrayPropertyInfo>>(CacheTypeAttributes, LazyThreadSafetyMode.PublicationOnly);
|
||||||
private static readonly ConcurrentDictionary<JsonConverter, JsonSerializerOptions> _converterOptionsCache = new ConcurrentDictionary<JsonConverter, JsonSerializerOptions>();
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
#if NET5_0_OR_GREATER
|
#if NET5_0_OR_GREATER
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||||
@@ -39,11 +40,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
|
|
||||||
writer.WriteStartArray();
|
writer.WriteStartArray();
|
||||||
|
|
||||||
var valueType = typeof(T);
|
var ordered = _typePropertyInfo.Value.Where(x => x.ArrayProperty != null).OrderBy(p => p.ArrayProperty.Index);
|
||||||
if (!_typeAttributesCache.TryGetValue(valueType, out var typeAttributes))
|
|
||||||
typeAttributes = CacheTypeAttributes(valueType);
|
|
||||||
|
|
||||||
var ordered = typeAttributes.Where(x => x.ArrayProperty != null).OrderBy(p => p.ArrayProperty.Index);
|
|
||||||
var last = -1;
|
var last = -1;
|
||||||
foreach (var prop in ordered)
|
foreach (var prop in ordered)
|
||||||
{
|
{
|
||||||
@@ -72,7 +69,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
{
|
{
|
||||||
NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals,
|
NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals,
|
||||||
PropertyNameCaseInsensitive = false,
|
PropertyNameCaseInsensitive = false,
|
||||||
TypeInfoResolver = (TContext)Activator.CreateInstance(typeof(TContext))!,
|
TypeInfoResolver = options.TypeInfoResolver,
|
||||||
};
|
};
|
||||||
typeOptions.Converters.Add(prop.JsonConverter);
|
typeOptions.Converters.Add(prop.JsonConverter);
|
||||||
}
|
}
|
||||||
@@ -101,77 +98,29 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
if (reader.TokenType == JsonTokenType.Null)
|
if (reader.TokenType == JsonTokenType.Null)
|
||||||
return default;
|
return default;
|
||||||
|
|
||||||
var result = Activator.CreateInstance(typeof(T))!;
|
var result = new T();
|
||||||
return (T)ParseObject(ref reader, result, typeof(T), options);
|
return ParseObject(ref reader, result, options);
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
#if NET5_0_OR_GREATER
|
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
|
||||||
private static List<ArrayPropertyInfo> CacheTypeAttributes([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type type)
|
|
||||||
#else
|
|
||||||
private static List<ArrayPropertyInfo> CacheTypeAttributes(Type type)
|
|
||||||
#endif
|
|
||||||
{
|
|
||||||
var attributes = new List<ArrayPropertyInfo>();
|
|
||||||
var properties = type.GetProperties();
|
|
||||||
foreach (var property in properties)
|
|
||||||
{
|
|
||||||
var att = property.GetCustomAttribute<ArrayPropertyAttribute>();
|
|
||||||
if (att == null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var targetType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType;
|
|
||||||
var converterType = property.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType ?? targetType.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType;
|
|
||||||
attributes.Add(new ArrayPropertyInfo
|
|
||||||
{
|
|
||||||
ArrayProperty = att,
|
|
||||||
PropertyInfo = property,
|
|
||||||
DefaultDeserialization = property.GetCustomAttribute<CryptoExchange.Net.Attributes.JsonConversionAttribute>() != null,
|
|
||||||
JsonConverter = converterType == null ? null : (JsonConverter)Activator.CreateInstance(converterType)!,
|
|
||||||
TargetType = targetType
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_typeAttributesCache.TryAdd(type, attributes);
|
|
||||||
return attributes;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#if NET5_0_OR_GREATER
|
#if NET5_0_OR_GREATER
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||||
private static object ParseObject(ref Utf8JsonReader reader, object result, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type objectType, JsonSerializerOptions options)
|
private static T ParseObject(ref Utf8JsonReader reader, T result, JsonSerializerOptions options)
|
||||||
#else
|
#else
|
||||||
private static object ParseObject(ref Utf8JsonReader reader, object result, Type objectType, JsonSerializerOptions options)
|
private static T ParseObject(ref Utf8JsonReader reader, T result, JsonSerializerOptions options)
|
||||||
#endif
|
#endif
|
||||||
{
|
{
|
||||||
if (reader.TokenType != JsonTokenType.StartArray)
|
if (reader.TokenType != JsonTokenType.StartArray)
|
||||||
throw new Exception("Not an array");
|
throw new Exception("Not an array");
|
||||||
|
|
||||||
if (!_typeAttributesCache.TryGetValue(objectType, out var attributes))
|
|
||||||
attributes = CacheTypeAttributes(objectType);
|
|
||||||
|
|
||||||
int index = 0;
|
int index = 0;
|
||||||
while (reader.Read())
|
while (reader.Read())
|
||||||
{
|
{
|
||||||
if (reader.TokenType == JsonTokenType.EndArray)
|
if (reader.TokenType == JsonTokenType.EndArray)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
var indexAttributes = attributes.Where(a => a.ArrayProperty.Index == index);
|
var indexAttributes = _typePropertyInfo.Value.Where(a => a.ArrayProperty.Index == index);
|
||||||
if (!indexAttributes.Any())
|
if (!indexAttributes.Any())
|
||||||
{
|
{
|
||||||
index++;
|
index++;
|
||||||
@@ -184,25 +133,23 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
object? value = null;
|
object? value = null;
|
||||||
if (attribute.JsonConverter != null)
|
if (attribute.JsonConverter != null)
|
||||||
{
|
{
|
||||||
if (!_converterOptionsCache.TryGetValue(attribute.JsonConverter, out var newOptions))
|
if (attribute.JsonSerializerOptions == null)
|
||||||
{
|
{
|
||||||
newOptions = new JsonSerializerOptions
|
attribute.JsonSerializerOptions = new JsonSerializerOptions
|
||||||
{
|
{
|
||||||
NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals,
|
NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals,
|
||||||
PropertyNameCaseInsensitive = false,
|
PropertyNameCaseInsensitive = false,
|
||||||
Converters = { attribute.JsonConverter },
|
Converters = { attribute.JsonConverter },
|
||||||
TypeInfoResolver = options.TypeInfoResolver,
|
TypeInfoResolver = options.TypeInfoResolver,
|
||||||
};
|
};
|
||||||
_converterOptionsCache.TryAdd(attribute.JsonConverter, newOptions);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var doc = JsonDocument.ParseValue(ref reader);
|
var doc = JsonDocument.ParseValue(ref reader);
|
||||||
value = doc.Deserialize(attribute.PropertyInfo.PropertyType, newOptions);
|
value = doc.Deserialize(attribute.PropertyInfo.PropertyType, attribute.JsonSerializerOptions);
|
||||||
}
|
}
|
||||||
else if (attribute.DefaultDeserialization)
|
else if (attribute.DefaultDeserialization)
|
||||||
{
|
{
|
||||||
// Use default deserialization
|
value = JsonDocument.ParseValue(ref reader).Deserialize(options.GetTypeInfo(attribute.PropertyInfo.PropertyType));
|
||||||
value = JsonDocument.ParseValue(ref reader).Deserialize(attribute.PropertyInfo.PropertyType, SerializerOptions.WithConverters((TContext)Activator.CreateInstance(typeof(TContext))!));
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -230,6 +177,50 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
#if NET5_0_OR_GREATER
|
||||||
|
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||||
|
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||||
|
private static List<ArrayPropertyInfo> CacheTypeAttributes()
|
||||||
|
#else
|
||||||
|
private static List<ArrayPropertyInfo> CacheTypeAttributes()
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
var attributes = new List<ArrayPropertyInfo>();
|
||||||
|
var properties = typeof(T).GetProperties();
|
||||||
|
foreach (var property in properties)
|
||||||
|
{
|
||||||
|
var att = property.GetCustomAttribute<ArrayPropertyAttribute>();
|
||||||
|
if (att == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var targetType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType;
|
||||||
|
var converterType = property.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType ?? targetType.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType;
|
||||||
|
attributes.Add(new ArrayPropertyInfo
|
||||||
|
{
|
||||||
|
ArrayProperty = att,
|
||||||
|
PropertyInfo = property,
|
||||||
|
DefaultDeserialization = property.GetCustomAttribute<CryptoExchange.Net.Attributes.JsonConversionAttribute>() != null,
|
||||||
|
JsonConverter = converterType == null ? null : (JsonConverter)Activator.CreateInstance(converterType)!,
|
||||||
|
TargetType = targetType
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return attributes;
|
||||||
|
}
|
||||||
|
|
||||||
private class ArrayPropertyInfo
|
private class ArrayPropertyInfo
|
||||||
{
|
{
|
||||||
public PropertyInfo PropertyInfo { get; set; } = null!;
|
public PropertyInfo PropertyInfo { get; set; } = null!;
|
||||||
@@ -237,6 +228,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
public JsonConverter? JsonConverter { get; set; }
|
public JsonConverter? JsonConverter { get; set; }
|
||||||
public bool DefaultDeserialization { get; set; }
|
public bool DefaultDeserialization { get; set; }
|
||||||
public Type TargetType { get; set; } = null!;
|
public Type TargetType { get; set; } = null!;
|
||||||
|
public JsonSerializerOptions? JsonSerializerOptions { get; set; } = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,11 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override T[]? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
public override T[]? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
{
|
{
|
||||||
return (reader.GetString()?.Split(',').Select(x => (T)EnumConverter.ParseString<T>(x)!).ToArray() ?? []);
|
var str = reader.GetString();
|
||||||
|
if (string.IsNullOrEmpty(str))
|
||||||
|
return [];
|
||||||
|
|
||||||
|
return str!.Split(',').Select(x => (T)EnumConverter.ParseString<T>(x)!).ToArray() ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
{
|
{
|
||||||
private static readonly DateTime _epoch = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
private static readonly DateTime _epoch = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||||
private const long _ticksPerSecond = TimeSpan.TicksPerMillisecond * 1000;
|
private const long _ticksPerSecond = TimeSpan.TicksPerMillisecond * 1000;
|
||||||
private const double _ticksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000d;
|
private const decimal _ticksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000m;
|
||||||
private const double _ticksPerNanosecond = TimeSpan.TicksPerMillisecond / 1000d / 1000;
|
private const decimal _ticksPerNanosecond = TimeSpan.TicksPerMillisecond / 1000m / 1000;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override bool CanConvert(Type typeToConvert)
|
public override bool CanConvert(Type typeToConvert)
|
||||||
@@ -45,11 +45,11 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
|
|
||||||
if (reader.TokenType is JsonTokenType.Number)
|
if (reader.TokenType is JsonTokenType.Number)
|
||||||
{
|
{
|
||||||
var longValue = reader.GetDouble();
|
var decValue = reader.GetDecimal();
|
||||||
if (longValue == 0 || longValue < 0)
|
if (decValue == 0 || decValue < 0)
|
||||||
return default;
|
return default;
|
||||||
|
|
||||||
return ParseFromDouble(longValue);
|
return ParseFromDecimal(decValue);
|
||||||
}
|
}
|
||||||
else if (reader.TokenType is JsonTokenType.String)
|
else if (reader.TokenType is JsonTokenType.String)
|
||||||
{
|
{
|
||||||
@@ -57,7 +57,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
if (string.IsNullOrWhiteSpace(stringValue)
|
if (string.IsNullOrWhiteSpace(stringValue)
|
||||||
|| stringValue == "-1"
|
|| stringValue == "-1"
|
||||||
|| stringValue == "0001-01-01T00:00:00Z"
|
|| stringValue == "0001-01-01T00:00:00Z"
|
||||||
|| double.TryParse(stringValue, out var doubleVal) && doubleVal == 0)
|
|| decimal.TryParse(stringValue, out var decVal) && decVal == 0)
|
||||||
{
|
{
|
||||||
return default;
|
return default;
|
||||||
}
|
}
|
||||||
@@ -88,20 +88,24 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Parse a long value to datetime
|
/// Parse a double value to datetime
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="longValue"></param>
|
public static DateTime ParseFromDouble(double value)
|
||||||
/// <returns></returns>
|
=> ParseFromDecimal((decimal)value);
|
||||||
public static DateTime ParseFromDouble(double longValue)
|
|
||||||
{
|
|
||||||
if (longValue < 19999999999)
|
|
||||||
return ConvertFromSeconds(longValue);
|
|
||||||
if (longValue < 19999999999999)
|
|
||||||
return ConvertFromMilliseconds(longValue);
|
|
||||||
if (longValue < 19999999999999999)
|
|
||||||
return ConvertFromMicroseconds(longValue);
|
|
||||||
|
|
||||||
return ConvertFromNanoseconds(longValue);
|
/// <summary>
|
||||||
|
/// Parse a decimal value to datetime
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime ParseFromDecimal(decimal value)
|
||||||
|
{
|
||||||
|
if (value < 19999999999)
|
||||||
|
return ConvertFromSeconds(value);
|
||||||
|
if (value < 19999999999999)
|
||||||
|
return ConvertFromMilliseconds(value);
|
||||||
|
if (value < 19999999999999999)
|
||||||
|
return ConvertFromMicroseconds(value);
|
||||||
|
|
||||||
|
return ConvertFromNanoseconds(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -152,19 +156,19 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
return new DateTime(year + 2000, month, day, 0, 0, 0, DateTimeKind.Utc);
|
return new DateTime(year + 2000, month, day, 0, 0, 0, DateTimeKind.Utc);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (double.TryParse(stringValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var doubleValue))
|
if (decimal.TryParse(stringValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var decimalValue))
|
||||||
{
|
{
|
||||||
// Parse 1637745563.000 format
|
// Parse 1637745563.000 format
|
||||||
if (doubleValue <= 0)
|
if (decimalValue <= 0)
|
||||||
return default;
|
return default;
|
||||||
if (doubleValue < 19999999999)
|
if (decimalValue < 19999999999)
|
||||||
return ConvertFromSeconds(doubleValue);
|
return ConvertFromSeconds(decimalValue);
|
||||||
if (doubleValue < 19999999999999)
|
if (decimalValue < 19999999999999)
|
||||||
return ConvertFromMilliseconds((long)doubleValue);
|
return ConvertFromMilliseconds(decimalValue);
|
||||||
if (doubleValue < 19999999999999999)
|
if (decimalValue < 19999999999999999)
|
||||||
return ConvertFromMicroseconds((long)doubleValue);
|
return ConvertFromMicroseconds(decimalValue);
|
||||||
|
|
||||||
return ConvertFromNanoseconds((long)doubleValue);
|
return ConvertFromNanoseconds(decimalValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stringValue.Length == 10)
|
if (stringValue.Length == 10)
|
||||||
@@ -188,54 +192,70 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Convert a seconds since epoch (01-01-1970) value to DateTime
|
/// Convert a seconds since epoch (01-01-1970) value to DateTime
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="seconds"></param>
|
public static DateTime ConvertFromSeconds(decimal seconds) => _epoch.AddTicks((long)Math.Round(seconds * _ticksPerSecond));
|
||||||
/// <returns></returns>
|
|
||||||
public static DateTime ConvertFromSeconds(double seconds) => _epoch.AddTicks((long)Math.Round(seconds * _ticksPerSecond));
|
|
||||||
/// <summary>
|
|
||||||
/// Convert a milliseconds since epoch (01-01-1970) value to DateTime
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="milliseconds"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static DateTime ConvertFromMilliseconds(double milliseconds) => _epoch.AddTicks((long)Math.Round(milliseconds * TimeSpan.TicksPerMillisecond));
|
|
||||||
/// <summary>
|
|
||||||
/// Convert a microseconds since epoch (01-01-1970) value to DateTime
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="microseconds"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static DateTime ConvertFromMicroseconds(double microseconds) => _epoch.AddTicks((long)Math.Round(microseconds * _ticksPerMicrosecond));
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="nanoseconds"></param>
|
public static DateTime ConvertFromSeconds(double seconds) => ConvertFromSeconds((decimal)seconds);
|
||||||
/// <returns></returns>
|
/// <summary>
|
||||||
public static DateTime ConvertFromNanoseconds(double nanoseconds) => _epoch.AddTicks((long)Math.Round(nanoseconds * _ticksPerNanosecond));
|
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime ConvertFromSeconds(long seconds) => ConvertFromSeconds((decimal)seconds);
|
||||||
|
/// <summary>
|
||||||
|
/// Convert a milliseconds since epoch (01-01-1970) value to DateTime
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime ConvertFromMilliseconds(decimal milliseconds) => _epoch.AddTicks((long)Math.Round(milliseconds * TimeSpan.TicksPerMillisecond));
|
||||||
|
/// <summary>
|
||||||
|
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime ConvertFromMilliseconds(double milliseconds) => ConvertFromMilliseconds((decimal)milliseconds);
|
||||||
|
/// <summary>
|
||||||
|
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime ConvertFromMilliseconds(long milliseconds) => ConvertFromMilliseconds((decimal)milliseconds);
|
||||||
|
/// <summary>
|
||||||
|
/// Convert a microseconds since epoch (01-01-1970) value to DateTime
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime ConvertFromMicroseconds(decimal microseconds) => _epoch.AddTicks((long)Math.Round(microseconds * _ticksPerMicrosecond));
|
||||||
|
/// <summary>
|
||||||
|
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime ConvertFromMicroseconds(double microseconds) => ConvertFromMicroseconds((decimal)microseconds);
|
||||||
|
/// <summary>
|
||||||
|
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime ConvertFromMicroseconds(long microseconds) => ConvertFromMicroseconds((decimal)microseconds);
|
||||||
|
/// <summary>
|
||||||
|
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime ConvertFromNanoseconds(decimal nanoseconds) => _epoch.AddTicks((long)Math.Round(nanoseconds * _ticksPerNanosecond));
|
||||||
|
/// <summary>
|
||||||
|
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime ConvertFromNanoseconds(double nanoseconds) => ConvertFromNanoseconds((decimal)nanoseconds);
|
||||||
|
/// <summary>
|
||||||
|
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime ConvertFromNanoseconds(long nanoseconds) => ConvertFromNanoseconds((decimal)nanoseconds);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Convert a DateTime value to seconds since epoch (01-01-1970) value
|
/// Convert a DateTime value to seconds since epoch (01-01-1970) value
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="time"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[return: NotNullIfNotNull("time")]
|
[return: NotNullIfNotNull("time")]
|
||||||
public static long? ConvertToSeconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).TotalSeconds);
|
public static long? ConvertToSeconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).TotalSeconds);
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Convert a DateTime value to milliseconds since epoch (01-01-1970) value
|
/// Convert a DateTime value to milliseconds since epoch (01-01-1970) value
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="time"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[return: NotNullIfNotNull("time")]
|
[return: NotNullIfNotNull("time")]
|
||||||
public static long? ConvertToMilliseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).TotalMilliseconds);
|
public static long? ConvertToMilliseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).TotalMilliseconds);
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Convert a DateTime value to microseconds since epoch (01-01-1970) value
|
/// Convert a DateTime value to microseconds since epoch (01-01-1970) value
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="time"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[return: NotNullIfNotNull("time")]
|
[return: NotNullIfNotNull("time")]
|
||||||
public static long? ConvertToMicroseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / _ticksPerMicrosecond);
|
public static long? ConvertToMicroseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / _ticksPerMicrosecond);
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Convert a DateTime value to nanoseconds since epoch (01-01-1970) value
|
/// Convert a DateTime value to nanoseconds since epoch (01-01-1970) value
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="time"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[return: NotNullIfNotNull("time")]
|
[return: NotNullIfNotNull("time")]
|
||||||
public static long? ConvertToNanoseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / _ticksPerNanosecond);
|
public static long? ConvertToNanoseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / _ticksPerNanosecond);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,22 +19,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
if (reader.TokenType == JsonTokenType.String)
|
if (reader.TokenType == JsonTokenType.String)
|
||||||
{
|
{
|
||||||
var value = reader.GetString();
|
var value = reader.GetString();
|
||||||
if (string.IsNullOrEmpty(value) || string.Equals("null", value, StringComparison.OrdinalIgnoreCase))
|
return ExchangeHelpers.ParseDecimal(value);
|
||||||
return null;
|
|
||||||
|
|
||||||
if (string.Equals("Infinity", value, StringComparison.Ordinal))
|
|
||||||
// Infinity returned by the server, default to max value
|
|
||||||
return decimal.MaxValue;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return decimal.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture);
|
|
||||||
}
|
|
||||||
catch(OverflowException)
|
|
||||||
{
|
|
||||||
// Value doesn't fit decimal, default to max value
|
|
||||||
return decimal.MaxValue;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -67,6 +67,8 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
private static List<KeyValuePair<T, string>>? _mapping = null;
|
private static List<KeyValuePair<T, string>>? _mapping = null;
|
||||||
private NullableEnumConverter? _nullableEnumConverter = null;
|
private NullableEnumConverter? _nullableEnumConverter = null;
|
||||||
|
|
||||||
|
private static ConcurrentBag<string> _unknownValuesWarned = new ConcurrentBag<string>();
|
||||||
|
|
||||||
internal class NullableEnumConverter : JsonConverter<T?>
|
internal class NullableEnumConverter : JsonConverter<T?>
|
||||||
{
|
{
|
||||||
private readonly EnumConverter<T> _enumConverter;
|
private readonly EnumConverter<T> _enumConverter;
|
||||||
@@ -77,7 +79,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
}
|
}
|
||||||
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
{
|
{
|
||||||
return _enumConverter.ReadNullable(ref reader, typeToConvert, options, out var isEmptyString);
|
return _enumConverter.ReadNullable(ref reader, typeToConvert, options, out var isEmptyString, out var warn);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options)
|
public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options)
|
||||||
@@ -96,18 +98,22 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
{
|
{
|
||||||
var t = ReadNullable(ref reader, typeToConvert, options, out var isEmptyString);
|
var t = ReadNullable(ref reader, typeToConvert, options, out var isEmptyString, out var warn);
|
||||||
if (t == null)
|
if (t == null)
|
||||||
{
|
{
|
||||||
if (isEmptyString)
|
if (warn)
|
||||||
{
|
{
|
||||||
// We received an empty string and have no mapping for it, and the property isn't nullable
|
if (isEmptyString)
|
||||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Received empty string as enum value, but property type is not a nullable enum. EnumType: {typeof(T).Name}. If you think {typeof(T).Name} should be nullable please open an issue on the Github repo");
|
{
|
||||||
}
|
// We received an empty string and have no mapping for it, and the property isn't nullable
|
||||||
else
|
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Received empty string as enum value, but property type is not a nullable enum. EnumType: {typeof(T).Name}. If you think {typeof(T).Name} should be nullable please open an issue on the Github repo");
|
||||||
{
|
}
|
||||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Received null enum value, but property type is not a nullable enum. EnumType: {typeof(T).Name}. If you think {typeof(T).Name} should be nullable please open an issue on the Github repo");
|
else
|
||||||
|
{
|
||||||
|
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Received null enum value, but property type is not a nullable enum. EnumType: {typeof(T).Name}. If you think {typeof(T).Name} should be nullable please open an issue on the Github repo");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new T(); // return default value
|
return new T(); // return default value
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -116,9 +122,10 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private T? ReadNullable(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, out bool isEmptyString)
|
private T? ReadNullable(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, out bool isEmptyString, out bool warn)
|
||||||
{
|
{
|
||||||
isEmptyString = false;
|
isEmptyString = false;
|
||||||
|
warn = false;
|
||||||
var enumType = typeof(T);
|
var enumType = typeof(T);
|
||||||
if (_mapping == null)
|
if (_mapping == null)
|
||||||
_mapping = AddMapping();
|
_mapping = AddMapping();
|
||||||
@@ -126,17 +133,17 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
var stringValue = reader.TokenType switch
|
var stringValue = reader.TokenType switch
|
||||||
{
|
{
|
||||||
JsonTokenType.String => reader.GetString(),
|
JsonTokenType.String => reader.GetString(),
|
||||||
JsonTokenType.Number => reader.GetInt16().ToString(),
|
JsonTokenType.Number => reader.GetInt32().ToString(),
|
||||||
JsonTokenType.True => reader.GetBoolean().ToString(),
|
JsonTokenType.True => reader.GetBoolean().ToString(),
|
||||||
JsonTokenType.False => reader.GetBoolean().ToString(),
|
JsonTokenType.False => reader.GetBoolean().ToString(),
|
||||||
JsonTokenType.Null => null,
|
JsonTokenType.Null => null,
|
||||||
_ => throw new Exception("Invalid token type for enum deserialization: " + reader.TokenType)
|
_ => throw new Exception("Invalid token type for enum deserialization: " + reader.TokenType)
|
||||||
};
|
};
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(stringValue))
|
if (stringValue is null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
if (!GetValue(enumType, stringValue!, out var result))
|
if (!GetValue(enumType, stringValue, out var result))
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(stringValue))
|
if (string.IsNullOrWhiteSpace(stringValue))
|
||||||
{
|
{
|
||||||
@@ -145,7 +152,12 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
// We received an enum value but weren't able to parse it.
|
// We received an enum value but weren't able to parse it.
|
||||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Cannot map enum value. EnumType: {enumType.Name}, Value: {stringValue}, Known values: {string.Join(", ", _mapping.Select(m => m.Value))}. If you think {stringValue} should added please open an issue on the Github repo");
|
if (!_unknownValuesWarned.Contains(stringValue))
|
||||||
|
{
|
||||||
|
warn = true;
|
||||||
|
_unknownValuesWarned.Add(stringValue!);
|
||||||
|
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Cannot map enum value. EnumType: {enumType.Name}, Value: {stringValue}, Known values: {string.Join(", ", _mapping.Select(m => m.Value))}. If you think {stringValue} should added please open an issue on the Github repo");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@@ -184,6 +196,21 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_unknownValuesWarned.Contains(value))
|
||||||
|
{
|
||||||
|
// Check if it is an known unknown value
|
||||||
|
// Done here to prevent lookup overhead for normal conversions, but prevent expensive exception throwing
|
||||||
|
result = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (String.IsNullOrEmpty(value))
|
||||||
|
{
|
||||||
|
// An empty/null value will always fail when parsing, so just return here
|
||||||
|
result = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// If no explicit mapping is found try to parse string
|
// If no explicit mapping is found try to parse string
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get Json serializer settings which includes standard converters for DateTime, bool, enum and number types
|
/// Get Json serializer settings which includes standard converters for DateTime, bool, enum and number types
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static JsonSerializerOptions WithConverters(JsonSerializerContext typeResolver)
|
public static JsonSerializerOptions WithConverters(JsonSerializerContext typeResolver, params JsonConverter[] additionalConverters)
|
||||||
{
|
{
|
||||||
if (!_cache.TryGetValue(typeResolver, out var options))
|
if (!_cache.TryGetValue(typeResolver, out var options))
|
||||||
{
|
{
|
||||||
@@ -33,6 +33,11 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
},
|
},
|
||||||
TypeInfoResolver = typeResolver,
|
TypeInfoResolver = typeResolver,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
foreach (var converter in additionalConverters)
|
||||||
|
options.Converters.Add(converter);
|
||||||
|
|
||||||
|
options.TypeInfoResolver = typeResolver;
|
||||||
_cache.TryAdd(typeResolver, options);
|
_cache.TryAdd(typeResolver, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
using CryptoExchange.Net.SharedApis;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||||
|
{
|
||||||
|
internal class SharedQuantityConverter : SharedQuantityReferenceConverter<SharedQuantity> { }
|
||||||
|
internal class SharedOrderQuantityConverter : SharedQuantityReferenceConverter<SharedOrderQuantity> { }
|
||||||
|
|
||||||
|
internal class SharedQuantityReferenceConverter<T> : JsonConverter<T> where T: SharedQuantityReference, new()
|
||||||
|
{
|
||||||
|
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
if (reader.TokenType != JsonTokenType.StartArray)
|
||||||
|
throw new Exception("");
|
||||||
|
|
||||||
|
reader.Read(); // Start array
|
||||||
|
var baseQuantity = reader.TokenType == JsonTokenType.Null ? (decimal?)null : reader.GetDecimal();
|
||||||
|
reader.Read();
|
||||||
|
var quoteQuantity = reader.TokenType == JsonTokenType.Null ? (decimal?)null : reader.GetDecimal();
|
||||||
|
reader.Read();
|
||||||
|
var contractQuantity = reader.TokenType == JsonTokenType.Null ? (decimal?)null : reader.GetDecimal();
|
||||||
|
reader.Read();
|
||||||
|
|
||||||
|
if (reader.TokenType != JsonTokenType.EndArray)
|
||||||
|
throw new Exception("");
|
||||||
|
|
||||||
|
reader.Read(); // End array
|
||||||
|
|
||||||
|
var result = new T();
|
||||||
|
result.QuantityInBaseAsset = baseQuantity;
|
||||||
|
result.QuantityInQuoteAsset = quoteQuantity;
|
||||||
|
result.QuantityInContracts = contractQuantity;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
writer.WriteStartArray();
|
||||||
|
if (value.QuantityInBaseAsset == null)
|
||||||
|
writer.WriteNullValue();
|
||||||
|
else
|
||||||
|
writer.WriteNumberValue(value.QuantityInBaseAsset.Value);
|
||||||
|
|
||||||
|
if (value.QuantityInQuoteAsset == null)
|
||||||
|
writer.WriteNullValue();
|
||||||
|
else
|
||||||
|
writer.WriteNumberValue(value.QuantityInQuoteAsset.Value);
|
||||||
|
|
||||||
|
if (value.QuantityInContracts == null)
|
||||||
|
writer.WriteNullValue();
|
||||||
|
else
|
||||||
|
writer.WriteNumberValue(value.QuantityInContracts.Value);
|
||||||
|
writer.WriteEndArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
using CryptoExchange.Net.SharedApis;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||||
|
{
|
||||||
|
internal class SharedSymbolConverter : JsonConverter<SharedSymbol>
|
||||||
|
{
|
||||||
|
public override SharedSymbol? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
if (reader.TokenType != JsonTokenType.StartArray)
|
||||||
|
throw new Exception("");
|
||||||
|
|
||||||
|
reader.Read(); // Start array
|
||||||
|
var tradingMode = (TradingMode)Enum.Parse(typeof(TradingMode), reader.GetString()!);
|
||||||
|
reader.Read();
|
||||||
|
var baseAsset = reader.GetString()!;
|
||||||
|
reader.Read();
|
||||||
|
var quoteAsset = reader.GetString()!;
|
||||||
|
reader.Read();
|
||||||
|
var timeStr = reader.GetString()!;
|
||||||
|
var deliverTime = string.IsNullOrEmpty(timeStr) ? (DateTime?)null : DateTime.Parse(timeStr);
|
||||||
|
reader.Read();
|
||||||
|
|
||||||
|
if (reader.TokenType != JsonTokenType.EndArray)
|
||||||
|
throw new Exception("");
|
||||||
|
|
||||||
|
reader.Read(); // End array
|
||||||
|
|
||||||
|
return new SharedSymbol(tradingMode, baseAsset, quoteAsset, deliverTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Write(Utf8JsonWriter writer, SharedSymbol value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
writer.WriteStartArray();
|
||||||
|
writer.WriteStringValue(value.TradingMode.ToString());
|
||||||
|
writer.WriteStringValue(value.BaseAsset);
|
||||||
|
writer.WriteStringValue(value.QuoteAsset);
|
||||||
|
writer.WriteStringValue(value.DeliverTime?.ToString());
|
||||||
|
writer.WriteEndArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,7 +24,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
private readonly JsonSerializerOptions? _customSerializerOptions;
|
private readonly JsonSerializerOptions? _customSerializerOptions;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public bool IsJson { get; set; }
|
public bool IsValid { get; set; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public abstract bool OriginalDataAvailable { get; }
|
public abstract bool OriginalDataAvailable { get; }
|
||||||
@@ -47,7 +47,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
#endif
|
#endif
|
||||||
public CallResult<object> Deserialize(Type type, MessagePath? path = null)
|
public CallResult<object> Deserialize(Type type, MessagePath? path = null)
|
||||||
{
|
{
|
||||||
if (!IsJson)
|
if (!IsValid)
|
||||||
return new CallResult<object>(GetOriginalString());
|
return new CallResult<object>(GetOriginalString());
|
||||||
|
|
||||||
if (_document == null)
|
if (_document == null)
|
||||||
@@ -60,13 +60,12 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
}
|
}
|
||||||
catch (JsonException ex)
|
catch (JsonException ex)
|
||||||
{
|
{
|
||||||
var info = $"Deserialize JsonException: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}";
|
var info = $"Json deserialization failed: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}";
|
||||||
return new CallResult<object>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
|
return new CallResult<object>(new DeserializeError(info, ex));
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
var info = $"Deserialize unknown Exception: {ex.Message}";
|
return new CallResult<object>(new DeserializeError($"Json deserialization failed: {ex.Message}", ex));
|
||||||
return new CallResult<object>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,20 +86,19 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
}
|
}
|
||||||
catch (JsonException ex)
|
catch (JsonException ex)
|
||||||
{
|
{
|
||||||
var info = $"Deserialize JsonException: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}";
|
var info = $"Json deserialization failed: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}";
|
||||||
return new CallResult<T>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
|
return new CallResult<T>(new DeserializeError(info, ex));
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
var info = $"Unknown exception: {ex.Message}";
|
return new CallResult<T>(new DeserializeError($"Json deserialization failed: {ex.Message}", ex));
|
||||||
return new CallResult<T>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public NodeType? GetNodeType()
|
public NodeType? GetNodeType()
|
||||||
{
|
{
|
||||||
if (!IsJson)
|
if (!IsValid)
|
||||||
throw new InvalidOperationException("Can't access json data on non-json message");
|
throw new InvalidOperationException("Can't access json data on non-json message");
|
||||||
|
|
||||||
if (_document == null)
|
if (_document == null)
|
||||||
@@ -117,7 +115,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public NodeType? GetNodeType(MessagePath path)
|
public NodeType? GetNodeType(MessagePath path)
|
||||||
{
|
{
|
||||||
if (!IsJson)
|
if (!IsValid)
|
||||||
throw new InvalidOperationException("Can't access json data on non-json message");
|
throw new InvalidOperationException("Can't access json data on non-json message");
|
||||||
|
|
||||||
var node = GetPathNode(path);
|
var node = GetPathNode(path);
|
||||||
@@ -139,7 +137,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
#endif
|
#endif
|
||||||
public T? GetValue<T>(MessagePath path)
|
public T? GetValue<T>(MessagePath path)
|
||||||
{
|
{
|
||||||
if (!IsJson)
|
if (!IsValid)
|
||||||
throw new InvalidOperationException("Can't access json data on non-json message");
|
throw new InvalidOperationException("Can't access json data on non-json message");
|
||||||
|
|
||||||
var value = GetPathNode(path);
|
var value = GetPathNode(path);
|
||||||
@@ -171,9 +169,9 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||||
#endif
|
#endif
|
||||||
public List<T?>? GetValues<T>(MessagePath path)
|
public T?[]? GetValues<T>(MessagePath path)
|
||||||
{
|
{
|
||||||
if (!IsJson)
|
if (!IsValid)
|
||||||
throw new InvalidOperationException("Can't access json data on non-json message");
|
throw new InvalidOperationException("Can't access json data on non-json message");
|
||||||
|
|
||||||
var value = GetPathNode(path);
|
var value = GetPathNode(path);
|
||||||
@@ -183,12 +181,12 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
if (value.Value.ValueKind != JsonValueKind.Array)
|
if (value.Value.ValueKind != JsonValueKind.Array)
|
||||||
return default;
|
return default;
|
||||||
|
|
||||||
return value.Value.Deserialize<List<T>>(_customSerializerOptions)!;
|
return value.Value.Deserialize<T[]>(_customSerializerOptions)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
private JsonElement? GetPathNode(MessagePath path)
|
private JsonElement? GetPathNode(MessagePath path)
|
||||||
{
|
{
|
||||||
if (!IsJson)
|
if (!IsValid)
|
||||||
throw new InvalidOperationException("Can't access json data on non-json message");
|
throw new InvalidOperationException("Can't access json data on non-json message");
|
||||||
|
|
||||||
if (_document == null)
|
if (_document == null)
|
||||||
@@ -279,14 +277,14 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
_document = await JsonDocument.ParseAsync(_stream ?? stream).ConfigureAwait(false);
|
_document = await JsonDocument.ParseAsync(_stream ?? stream).ConfigureAwait(false);
|
||||||
IsJson = true;
|
IsValid = true;
|
||||||
return CallResult.SuccessResult;
|
return CallResult.SuccessResult;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// Not a json message
|
// Not a json message
|
||||||
IsJson = false;
|
IsValid = false;
|
||||||
return new CallResult(new ServerError("JsonError: " + ex.Message));
|
return new CallResult(new DeserializeError($"Json deserialization failed: {ex.Message}", ex));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -337,19 +335,19 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
if (firstByte != 0x7b && firstByte != 0x5b)
|
if (firstByte != 0x7b && firstByte != 0x5b)
|
||||||
{
|
{
|
||||||
// Value doesn't start with `{` or `[`, prevent deserialization attempt as it's slow
|
// Value doesn't start with `{` or `[`, prevent deserialization attempt as it's slow
|
||||||
IsJson = false;
|
IsValid = false;
|
||||||
return new CallResult(new ServerError("Not a json value"));
|
return new CallResult(new DeserializeError("Not a json value"));
|
||||||
}
|
}
|
||||||
|
|
||||||
_document = JsonDocument.Parse(data);
|
_document = JsonDocument.Parse(data);
|
||||||
IsJson = true;
|
IsValid = true;
|
||||||
return CallResult.SuccessResult;
|
return CallResult.SuccessResult;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// Not a json message
|
// Not a json message
|
||||||
IsJson = false;
|
IsValid = false;
|
||||||
return new CallResult(new ServerError("JsonError: " + ex.Message));
|
return new CallResult(new DeserializeError($"Json deserialization failed: {ex.Message}", ex));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ using System.Text.Json.Serialization.Metadata;
|
|||||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public class SystemTextJsonMessageSerializer : IMessageSerializer
|
public class SystemTextJsonMessageSerializer : IStringMessageSerializer
|
||||||
{
|
{
|
||||||
private readonly JsonSerializerOptions _options;
|
private readonly JsonSerializerOptions _options;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFrameworks>netstandard2.0;netstandard2.1;net8.0;net9.0</TargetFrameworks>
|
<TargetFrameworks>netstandard2.0;netstandard2.1;net8.0;net9.0</TargetFrameworks>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
@@ -6,11 +6,11 @@
|
|||||||
<PackageId>CryptoExchange.Net</PackageId>
|
<PackageId>CryptoExchange.Net</PackageId>
|
||||||
<Authors>JKorf</Authors>
|
<Authors>JKorf</Authors>
|
||||||
<Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description>
|
<Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description>
|
||||||
<PackageVersion>8.8.0</PackageVersion>
|
<PackageVersion>9.10.0</PackageVersion>
|
||||||
<AssemblyVersion>8.8.0</AssemblyVersion>
|
<AssemblyVersion>9.10.0</AssemblyVersion>
|
||||||
<FileVersion>8.8.0</FileVersion>
|
<FileVersion>9.10.0</FileVersion>
|
||||||
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
||||||
<PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange</PackageTags>
|
<PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange;CryptoExchange.Net</PackageTags>
|
||||||
<RepositoryType>git</RepositoryType>
|
<RepositoryType>git</RepositoryType>
|
||||||
<RepositoryUrl>https://github.com/JKorf/CryptoExchange.Net.git</RepositoryUrl>
|
<RepositoryUrl>https://github.com/JKorf/CryptoExchange.Net.git</RepositoryUrl>
|
||||||
<PackageProjectUrl>https://github.com/JKorf/CryptoExchange.Net</PackageProjectUrl>
|
<PackageProjectUrl>https://github.com/JKorf/CryptoExchange.Net</PackageProjectUrl>
|
||||||
@@ -27,8 +27,8 @@
|
|||||||
<None Include="Icon\icon.png" Pack="true" PackagePath="\" />
|
<None Include="Icon\icon.png" Pack="true" PackagePath="\" />
|
||||||
<None Include="..\README.md" Pack="true" PackagePath="\" />
|
<None Include="..\README.md" Pack="true" PackagePath="\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<PropertyGroup Label="AOT" Condition=" '$(TargetFramework)' == 'NET8_0' Or '$(TargetFramework)' == 'NET9_0' ">
|
<PropertyGroup Label="AOT" Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net7.0'))">
|
||||||
<IsAotCompatible>true</IsAotCompatible>
|
<IsAotCompatible>true</IsAotCompatible>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<PropertyGroup Label="Deterministic Build" Condition="'$(Configuration)' == 'Release'">
|
<PropertyGroup Label="Deterministic Build" Condition="'$(Configuration)' == 'Release'">
|
||||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||||
@@ -37,12 +37,6 @@
|
|||||||
<EmbedUntrackedSources>true</EmbedUntrackedSources>
|
<EmbedUntrackedSources>true</EmbedUntrackedSources>
|
||||||
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
|
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup Label="Deterministic Build" Condition="'$(Configuration)' == 'Release'">
|
|
||||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0">
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
|
||||||
</PackageReference>
|
|
||||||
</ItemGroup>
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<DocumentationFile>CryptoExchange.Net.xml</DocumentationFile>
|
<DocumentationFile>CryptoExchange.Net.xml</DocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
@@ -57,10 +51,11 @@
|
|||||||
</PackageReference>
|
</PackageReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.6" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
|
<PackageReference Include="System.Text.Json" Version="9.0.6" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
|
</ItemGroup>
|
||||||
<PackageReference Include="System.Text.Json" Version="9.0.0" />
|
<ItemGroup Label="Transitive Client Packages">
|
||||||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.6" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.6" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
using CryptoExchange.Net.SharedApis;
|
using CryptoExchange.Net.SharedApis;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
@@ -87,8 +88,6 @@ namespace CryptoExchange.Net
|
|||||||
value -= offset;
|
value -= offset;
|
||||||
else value += (step.Value - offset);
|
else value += (step.Value - offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
value = RoundDown(value, 8);
|
|
||||||
|
|
||||||
return value.Normalize();
|
return value.Normalize();
|
||||||
}
|
}
|
||||||
@@ -112,6 +111,34 @@ namespace CryptoExchange.Net
|
|||||||
return RoundToSignificantDigits(value, precision.Value, roundingType);
|
return RoundToSignificantDigits(value, precision.Value, roundingType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Apply the provided rules to the value
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">Value to be adjusted</param>
|
||||||
|
/// <param name="decimals">Max decimal places</param>
|
||||||
|
/// <param name="valueStep">The value step for increase/decrease value</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static decimal ApplyRules(
|
||||||
|
decimal value,
|
||||||
|
int? decimals = null,
|
||||||
|
decimal? valueStep = null)
|
||||||
|
{
|
||||||
|
if (valueStep.HasValue)
|
||||||
|
{
|
||||||
|
var offset = value % valueStep.Value;
|
||||||
|
if (offset != 0)
|
||||||
|
{
|
||||||
|
if (offset < valueStep.Value / 2)
|
||||||
|
value -= offset;
|
||||||
|
else value += (valueStep.Value - offset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (decimals.HasValue)
|
||||||
|
value = Math.Round(value, decimals.Value);
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Round a value to have the provided total number of digits. For example, value 253.12332 with 5 digits would be 253.12
|
/// Round a value to have the provided total number of digits. For example, value 253.12332 with 5 digits would be 253.12
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -313,5 +340,48 @@ namespace CryptoExchange.Net
|
|||||||
adjustedQuantity = symbol.QuantityDecimals.HasValue ? (minNotionalAdjust ? RoundUp(adjustedQuantity, symbol.QuantityDecimals.Value) : RoundDown(adjustedQuantity, symbol.QuantityDecimals.Value)) : adjustedQuantity;
|
adjustedQuantity = symbol.QuantityDecimals.HasValue ? (minNotionalAdjust ? RoundUp(adjustedQuantity, symbol.QuantityDecimals.Value) : RoundDown(adjustedQuantity, symbol.QuantityDecimals.Value)) : adjustedQuantity;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parse a decimal value from a string
|
||||||
|
/// </summary>
|
||||||
|
public static decimal? ParseDecimal(string? value)
|
||||||
|
{
|
||||||
|
// Value is null or empty is the most common case to return null so check before trying to parse
|
||||||
|
if (string.IsNullOrEmpty(value))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
// Try parse, only fails for these reasons:
|
||||||
|
// 1. string is null or empty
|
||||||
|
// 2. value is larger or smaller than decimal max/min
|
||||||
|
// 3. unparsable format
|
||||||
|
if (decimal.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var decValue))
|
||||||
|
return decValue;
|
||||||
|
|
||||||
|
// Check for values which should be parsed to null
|
||||||
|
if (string.Equals("null", value, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals("NaN", value, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Infinity value should be parsed to min/max value
|
||||||
|
if (string.Equals("Infinity", value, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return decimal.MaxValue;
|
||||||
|
else if(string.Equals("-Infinity", value, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return decimal.MinValue;
|
||||||
|
|
||||||
|
if (value!.Length > 27 && decimal.TryParse(value.Substring(0, 27), out var overflowValue))
|
||||||
|
{
|
||||||
|
// Not a valid decimal value and more than 27 chars, from which the first part can be parsed correctly.
|
||||||
|
// assume overflow
|
||||||
|
if (overflowValue < 0)
|
||||||
|
return decimal.MinValue;
|
||||||
|
else
|
||||||
|
return decimal.MaxValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unknown decimal format, return null
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,14 +23,14 @@ namespace CryptoExchange.Net
|
|||||||
{
|
{
|
||||||
if(!_symbolInfos.TryGetValue(topicId, out var exchangeInfo))
|
if(!_symbolInfos.TryGetValue(topicId, out var exchangeInfo))
|
||||||
{
|
{
|
||||||
exchangeInfo = new ExchangeInfo(DateTime.UtcNow, updateData.ToDictionary(x => x.Name, x => new SharedSymbol(x.TradingMode, x.BaseAsset, x.QuoteAsset, (x as SharedFuturesSymbol)?.DeliveryTime) { SymbolName = x.Name }));
|
exchangeInfo = new ExchangeInfo(DateTime.UtcNow, updateData.ToDictionary(x => x.Name, x => x.SharedSymbol));
|
||||||
_symbolInfos.TryAdd(topicId, exchangeInfo);
|
_symbolInfos.TryAdd(topicId, exchangeInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (DateTime.UtcNow - exchangeInfo.UpdateTime < TimeSpan.FromMinutes(60))
|
if (DateTime.UtcNow - exchangeInfo.UpdateTime < TimeSpan.FromMinutes(60))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
_symbolInfos[topicId] = new ExchangeInfo(DateTime.UtcNow, updateData.ToDictionary(x => x.Name, x => new SharedSymbol(x.TradingMode, x.BaseAsset, x.QuoteAsset, (x as SharedFuturesSymbol)?.DeliveryTime) { SymbolName = x.Name }));
|
_symbolInfos[topicId] = new ExchangeInfo(DateTime.UtcNow, updateData.ToDictionary(x => x.Name, x => x.SharedSymbol));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -399,7 +399,7 @@ namespace CryptoExchange.Net
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether the trading mode is linear
|
/// Whether the trading mode is linear
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static bool IsLinear(this TradingMode type) => type == TradingMode.PerpetualLinear || type == TradingMode.DeliveryLinear;
|
public static bool IsLinear(this TradingMode type) => type == TradingMode.PerpetualLinear || type == TradingMode.DeliveryLinear;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether the trading mode is inverse
|
/// Whether the trading mode is inverse
|
||||||
@@ -416,6 +416,36 @@ namespace CryptoExchange.Net
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static bool IsDelivery(this TradingMode type) => type == TradingMode.DeliveryInverse || type == TradingMode.DeliveryLinear;
|
public static bool IsDelivery(this TradingMode type) => type == TradingMode.DeliveryInverse || type == TradingMode.DeliveryLinear;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the account type is a futures account
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsFuturesAccount(this SharedAccountType type) =>
|
||||||
|
type == SharedAccountType.PerpetualLinearFutures
|
||||||
|
|| type == SharedAccountType.DeliveryLinearFutures
|
||||||
|
|| type == SharedAccountType.PerpetualInverseFutures
|
||||||
|
|| type == SharedAccountType.DeliveryInverseFutures;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the account type is a margin account
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsMarginAccount(this SharedAccountType type) =>
|
||||||
|
type == SharedAccountType.CrossMargin
|
||||||
|
|| type == SharedAccountType.IsolatedMargin;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Map a TradingMode value to a SharedAccountType enum value
|
||||||
|
/// </summary>
|
||||||
|
public static SharedAccountType ToAccountType(this TradingMode mode)
|
||||||
|
{
|
||||||
|
if (mode == TradingMode.Spot) return SharedAccountType.Spot;
|
||||||
|
if (mode == TradingMode.PerpetualLinear) return SharedAccountType.PerpetualLinearFutures;
|
||||||
|
if (mode == TradingMode.PerpetualInverse) return SharedAccountType.PerpetualInverseFutures;
|
||||||
|
if (mode == TradingMode.DeliveryInverse) return SharedAccountType.DeliveryInverseFutures;
|
||||||
|
if (mode == TradingMode.DeliveryLinear) return SharedAccountType.DeliveryLinearFutures;
|
||||||
|
|
||||||
|
throw new ArgumentException(nameof(mode), "Unmapped trading mode");
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Register rest client interfaces
|
/// Register rest client interfaces
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -443,6 +473,10 @@ namespace CryptoExchange.Net
|
|||||||
services.AddTransient(x => (IWithdrawRestClient)client(x)!);
|
services.AddTransient(x => (IWithdrawRestClient)client(x)!);
|
||||||
if (typeof(IFeeRestClient).IsAssignableFrom(typeof(T)))
|
if (typeof(IFeeRestClient).IsAssignableFrom(typeof(T)))
|
||||||
services.AddTransient(x => (IFeeRestClient)client(x)!);
|
services.AddTransient(x => (IFeeRestClient)client(x)!);
|
||||||
|
if (typeof(IBookTickerRestClient).IsAssignableFrom(typeof(T)))
|
||||||
|
services.AddTransient(x => (IBookTickerRestClient)client(x)!);
|
||||||
|
if (typeof(ITransferRestClient).IsAssignableFrom(typeof(T)))
|
||||||
|
services.AddTransient(x => (ITransferRestClient)client(x)!);
|
||||||
|
|
||||||
if (typeof(ISpotOrderRestClient).IsAssignableFrom(typeof(T)))
|
if (typeof(ISpotOrderRestClient).IsAssignableFrom(typeof(T)))
|
||||||
services.AddTransient(x => (ISpotOrderRestClient)client(x)!);
|
services.AddTransient(x => (ISpotOrderRestClient)client(x)!);
|
||||||
@@ -450,6 +484,10 @@ namespace CryptoExchange.Net
|
|||||||
services.AddTransient(x => (ISpotSymbolRestClient)client(x)!);
|
services.AddTransient(x => (ISpotSymbolRestClient)client(x)!);
|
||||||
if (typeof(ISpotTickerRestClient).IsAssignableFrom(typeof(T)))
|
if (typeof(ISpotTickerRestClient).IsAssignableFrom(typeof(T)))
|
||||||
services.AddTransient(x => (ISpotTickerRestClient)client(x)!);
|
services.AddTransient(x => (ISpotTickerRestClient)client(x)!);
|
||||||
|
if (typeof(ISpotTriggerOrderRestClient).IsAssignableFrom(typeof(T)))
|
||||||
|
services.AddTransient(x => (ISpotTriggerOrderRestClient)client(x)!);
|
||||||
|
if (typeof(ISpotOrderClientIdRestClient).IsAssignableFrom(typeof(T)))
|
||||||
|
services.AddTransient(x => (ISpotOrderClientIdRestClient)client(x)!);
|
||||||
|
|
||||||
if (typeof(IFundingRateRestClient).IsAssignableFrom(typeof(T)))
|
if (typeof(IFundingRateRestClient).IsAssignableFrom(typeof(T)))
|
||||||
services.AddTransient(x => (IFundingRateRestClient)client(x)!);
|
services.AddTransient(x => (IFundingRateRestClient)client(x)!);
|
||||||
@@ -471,6 +509,12 @@ namespace CryptoExchange.Net
|
|||||||
services.AddTransient(x => (IPositionHistoryRestClient)client(x)!);
|
services.AddTransient(x => (IPositionHistoryRestClient)client(x)!);
|
||||||
if (typeof(IPositionModeRestClient).IsAssignableFrom(typeof(T)))
|
if (typeof(IPositionModeRestClient).IsAssignableFrom(typeof(T)))
|
||||||
services.AddTransient(x => (IPositionModeRestClient)client(x)!);
|
services.AddTransient(x => (IPositionModeRestClient)client(x)!);
|
||||||
|
if (typeof(IFuturesTpSlRestClient).IsAssignableFrom(typeof(T)))
|
||||||
|
services.AddTransient(x => (IFuturesTpSlRestClient)client(x)!);
|
||||||
|
if (typeof(IFuturesTriggerOrderRestClient).IsAssignableFrom(typeof(T)))
|
||||||
|
services.AddTransient(x => (IFuturesTriggerOrderRestClient)client(x)!);
|
||||||
|
if (typeof(IFuturesOrderClientIdRestClient).IsAssignableFrom(typeof(T)))
|
||||||
|
services.AddTransient(x => (IFuturesOrderClientIdRestClient)client(x)!);
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
@@ -486,8 +530,8 @@ namespace CryptoExchange.Net
|
|||||||
services.AddTransient(x => (IBookTickerSocketClient)client(x)!);
|
services.AddTransient(x => (IBookTickerSocketClient)client(x)!);
|
||||||
if (typeof(IKlineSocketClient).IsAssignableFrom(typeof(T)))
|
if (typeof(IKlineSocketClient).IsAssignableFrom(typeof(T)))
|
||||||
services.AddTransient(x => (IKlineSocketClient)client(x)!);
|
services.AddTransient(x => (IKlineSocketClient)client(x)!);
|
||||||
if (typeof(IOrderBookRestClient).IsAssignableFrom(typeof(T)))
|
if (typeof(IOrderBookSocketClient).IsAssignableFrom(typeof(T)))
|
||||||
services.AddTransient(x => (IOrderBookRestClient)client(x)!);
|
services.AddTransient(x => (IOrderBookSocketClient)client(x)!);
|
||||||
if (typeof(ITickerSocketClient).IsAssignableFrom(typeof(T)))
|
if (typeof(ITickerSocketClient).IsAssignableFrom(typeof(T)))
|
||||||
services.AddTransient(x => (ITickerSocketClient)client(x)!);
|
services.AddTransient(x => (ITickerSocketClient)client(x)!);
|
||||||
if (typeof(ITickersSocketClient).IsAssignableFrom(typeof(T)))
|
if (typeof(ITickersSocketClient).IsAssignableFrom(typeof(T)))
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
@@ -13,9 +14,9 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
public interface IMessageAccessor
|
public interface IMessageAccessor
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Is this a json message
|
/// Is this a valid message
|
||||||
/// </summary>
|
/// </summary>
|
||||||
bool IsJson { get; }
|
bool IsValid { get; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Is the original data available for retrieval
|
/// Is the original data available for retrieval
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -52,19 +53,27 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// <typeparam name="T"></typeparam>
|
/// <typeparam name="T"></typeparam>
|
||||||
/// <param name="path"></param>
|
/// <param name="path"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
List<T?>? GetValues<T>(MessagePath path);
|
T?[]? GetValues<T>(MessagePath path);
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Deserialize the message into this type
|
/// Deserialize the message into this type
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="type"></param>
|
/// <param name="type"></param>
|
||||||
/// <param name="path"></param>
|
/// <param name="path"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
|
#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
|
||||||
CallResult<object> Deserialize(Type type, MessagePath? path = null);
|
CallResult<object> Deserialize(Type type, MessagePath? path = null);
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Deserialize the message into this type
|
/// Deserialize the message into this type
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="path"></param>
|
/// <param name="path"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
|
#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
|
||||||
CallResult<T> Deserialize<T>(MessagePath? path = null);
|
CallResult<T> Deserialize<T>(MessagePath? path = null);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -17,22 +17,13 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int Id { get; }
|
public int Id { get; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The identifiers for this processor
|
/// The matcher for this listener
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public HashSet<string> ListenerIdentifiers { get; }
|
public MessageMatcher MessageMatcher { get; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handle a message
|
/// Handle a message
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="connection"></param>
|
Task<CallResult> Handle(SocketConnection connection, DataEvent<object> message, MessageHandlerLink matchedHandler);
|
||||||
/// <param name="message"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
Task<CallResult> Handle(SocketConnection connection, DataEvent<object> message);
|
|
||||||
/// <summary>
|
|
||||||
/// Get the type the message should be deserialized to
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="messageAccessor"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
Type? GetMessageType(IMessageAccessor messageAccessor);
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Deserialize a message into object of type
|
/// Deserialize a message into object of type
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -1,9 +1,31 @@
|
|||||||
namespace CryptoExchange.Net.Interfaces
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Interfaces
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Serializer interface
|
/// Serializer interface
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IMessageSerializer
|
public interface IMessageSerializer
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Serialize to byte array
|
||||||
|
/// </summary>
|
||||||
|
public interface IByteMessageSerializer: IMessageSerializer
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Serialize an object to a string
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
byte[] Serialize<T>(T message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Serialize to string
|
||||||
|
/// </summary>
|
||||||
|
public interface IStringMessageSerializer: IMessageSerializer
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Serialize an object to a string
|
/// Serialize an object to a string
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
Uri Uri { get; }
|
Uri Uri { get; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
/// HTTP protocol version
|
||||||
|
/// </summary>
|
||||||
|
Version HttpVersion { get; }
|
||||||
|
/// <summary>
|
||||||
/// internal request id for tracing
|
/// internal request id for tracing
|
||||||
/// </summary>
|
/// </summary>
|
||||||
int RequestId { get; }
|
int RequestId { get; }
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.Objects.Options;
|
||||||
using System;
|
using System;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
|
|
||||||
@@ -12,25 +13,21 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a request for an uri
|
/// Create a request for an uri
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="method"></param>
|
IRequest Create(Version httpRequestVersion, HttpMethod method, Uri uri, int requestId);
|
||||||
/// <param name="uri"></param>
|
|
||||||
/// <param name="requestId"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
IRequest Create(HttpMethod method, Uri uri, int requestId);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Configure the requests created by this factory
|
/// Configure the requests created by this factory
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="requestTimeout">Request timeout to use</param>
|
/// <param name="options">Rest client options</param>
|
||||||
/// <param name="httpClient">Optional shared http client instance</param>
|
/// <param name="httpClient">Optional shared http client instance</param>
|
||||||
/// <param name="proxy">Optional proxy to use when no http client is provided</param>
|
void Configure(RestExchangeOptions options, HttpClient? httpClient = null);
|
||||||
void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? httpClient = null);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Update settings
|
/// Update settings
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="proxy">Proxy to use</param>
|
/// <param name="proxy">Proxy to use</param>
|
||||||
/// <param name="requestTimeout">Request timeout to use</param>
|
/// <param name="requestTimeout">Request timeout to use</param>
|
||||||
void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout);
|
/// <param name="httpKeepAliveInterval">Http client keep alive interval</param>
|
||||||
|
void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout, TimeSpan? httpKeepAliveInterval);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@@ -15,6 +16,11 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
HttpStatusCode StatusCode { get; }
|
HttpStatusCode StatusCode { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Http protocol version
|
||||||
|
/// </summary>
|
||||||
|
Version HttpVersion { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether the status code indicates a success status
|
/// Whether the status code indicates a success status
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
using CryptoExchange.Net.SharedApis;
|
||||||
|
using CryptoExchange.Net.Trackers.Klines;
|
||||||
|
using CryptoExchange.Net.Trackers.Trades;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Interfaces
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Tracker factory
|
||||||
|
/// </summary>
|
||||||
|
public interface ITrackerFactory
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the factory supports creating a KlineTracker instance for this symbol and interval
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="symbol">The symbol</param>
|
||||||
|
/// <param name="interval">The kline interval</param>
|
||||||
|
bool CanCreateKlineTracker(SharedSymbol symbol, SharedKlineInterval interval);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new kline tracker
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="symbol">The symbol</param>
|
||||||
|
/// <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>
|
||||||
|
/// <returns></returns>
|
||||||
|
IKlineTracker CreateKlineTracker(SharedSymbol symbol, SharedKlineInterval interval, int? limit = null, TimeSpan? period = null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the factory supports creating a TradeTracker instance for this symbol
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="symbol">The symbol</param>
|
||||||
|
bool CanCreateTradeTracker(SharedSymbol symbol);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new trade tracker for a symbol
|
||||||
|
/// </summary>
|
||||||
|
/// <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>
|
||||||
|
/// <returns></returns>
|
||||||
|
ITradeTracker CreateTradeTracker(SharedSymbol symbol, int? limit = null, TimeSpan? period = null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
using System;
|
using System;
|
||||||
using System.Net.WebSockets;
|
using System.Net.WebSockets;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Interfaces
|
namespace CryptoExchange.Net.Interfaces
|
||||||
@@ -75,15 +76,22 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// Connect the socket
|
/// Connect the socket
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Task<CallResult> ConnectAsync();
|
Task<CallResult> ConnectAsync(CancellationToken ct);
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Send data
|
/// Send string data
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="id"></param>
|
/// <param name="id"></param>
|
||||||
/// <param name="data"></param>
|
/// <param name="data"></param>
|
||||||
/// <param name="weight"></param>
|
/// <param name="weight"></param>
|
||||||
bool Send(int id, string data, int weight);
|
bool Send(int id, string data, int weight);
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
/// Send byte data
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id"></param>
|
||||||
|
/// <param name="data"></param>
|
||||||
|
/// <param name="weight"></param>
|
||||||
|
bool Send(int id, byte[] data, int weight);
|
||||||
|
/// <summary>
|
||||||
/// Reconnect the socket
|
/// Reconnect the socket
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
using System;
|
using CryptoExchange.Net.Objects;
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace CryptoExchange.Net
|
namespace CryptoExchange.Net
|
||||||
@@ -43,5 +46,58 @@ namespace CryptoExchange.Net
|
|||||||
|
|
||||||
return clientOrderId;
|
return clientOrderId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new HttpMessageHandler instance
|
||||||
|
/// </summary>
|
||||||
|
public static HttpMessageHandler CreateHttpClientMessageHandler(ApiProxy? proxy, TimeSpan? keepAliveInterval)
|
||||||
|
{
|
||||||
|
#if NET5_0_OR_GREATER
|
||||||
|
var socketHandler = new SocketsHttpHandler();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (keepAliveInterval != null && keepAliveInterval != TimeSpan.Zero)
|
||||||
|
{
|
||||||
|
socketHandler.KeepAlivePingPolicy = HttpKeepAlivePingPolicy.Always;
|
||||||
|
socketHandler.KeepAlivePingDelay = keepAliveInterval.Value;
|
||||||
|
socketHandler.KeepAlivePingTimeout = TimeSpan.FromSeconds(10);
|
||||||
|
}
|
||||||
|
|
||||||
|
socketHandler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
|
||||||
|
socketHandler.DefaultProxyCredentials = CredentialCache.DefaultCredentials;
|
||||||
|
}
|
||||||
|
catch (PlatformNotSupportedException) { }
|
||||||
|
catch (NotImplementedException) { } // Mono runtime throws NotImplementedException
|
||||||
|
|
||||||
|
if (proxy != null)
|
||||||
|
{
|
||||||
|
socketHandler.Proxy = new WebProxy
|
||||||
|
{
|
||||||
|
Address = new Uri($"{proxy.Host}:{proxy.Port}"),
|
||||||
|
Credentials = proxy.Password == null ? null : new NetworkCredential(proxy.Login, proxy.Password)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return socketHandler;
|
||||||
|
#else
|
||||||
|
var httpHandler = new HttpClientHandler();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
httpHandler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
|
||||||
|
httpHandler.DefaultProxyCredentials = CredentialCache.DefaultCredentials;
|
||||||
|
}
|
||||||
|
catch (PlatformNotSupportedException) { }
|
||||||
|
catch (NotImplementedException) { } // Mono runtime throws NotImplementedException
|
||||||
|
|
||||||
|
if (proxy != null)
|
||||||
|
{
|
||||||
|
httpHandler.Proxy = new WebProxy
|
||||||
|
{
|
||||||
|
Address = new Uri($"{proxy.Host}:{proxy.Port}"),
|
||||||
|
Credentials = proxy.Password == null ? null : new NetworkCredential(proxy.Login, proxy.Password)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return httpHandler;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+13
@@ -8,6 +8,7 @@ namespace CryptoExchange.Net.Logging.Extensions
|
|||||||
{
|
{
|
||||||
private static readonly Action<ILogger, int, Exception?> _connecting;
|
private static readonly Action<ILogger, int, Exception?> _connecting;
|
||||||
private static readonly Action<ILogger, int, string, Exception?> _connectionFailed;
|
private static readonly Action<ILogger, int, string, Exception?> _connectionFailed;
|
||||||
|
private static readonly Action<ILogger, int, Exception?> _connectingCanceled;
|
||||||
private static readonly Action<ILogger, int, Uri, Exception?> _connected;
|
private static readonly Action<ILogger, int, Uri, Exception?> _connected;
|
||||||
private static readonly Action<ILogger, int, Exception?> _startingProcessing;
|
private static readonly Action<ILogger, int, Exception?> _startingProcessing;
|
||||||
private static readonly Action<ILogger, int, Exception?> _finishedProcessing;
|
private static readonly Action<ILogger, int, Exception?> _finishedProcessing;
|
||||||
@@ -189,6 +190,12 @@ namespace CryptoExchange.Net.Logging.Extensions
|
|||||||
new EventId(1030, "SocketPingTimeout"),
|
new EventId(1030, "SocketPingTimeout"),
|
||||||
"[Sckt {Id}] ping frame timeout; reconnecting socket");
|
"[Sckt {Id}] ping frame timeout; reconnecting socket");
|
||||||
|
|
||||||
|
_connectingCanceled = LoggerMessage.Define<int>(
|
||||||
|
LogLevel.Debug,
|
||||||
|
new EventId(1031, "ConnectingCanceled"),
|
||||||
|
"[Sckt {SocketId}] connecting canceled");
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void SocketConnecting(
|
public static void SocketConnecting(
|
||||||
@@ -370,5 +377,11 @@ namespace CryptoExchange.Net.Logging.Extensions
|
|||||||
{
|
{
|
||||||
_socketPingTimeout(logger, socketId, null);
|
_socketPingTimeout(logger, socketId, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void SocketConnectingCanceled(
|
||||||
|
this ILogger logger, int socketId)
|
||||||
|
{
|
||||||
|
_connectingCanceled(logger, socketId, null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ namespace CryptoExchange.Net.Logging.Extensions
|
|||||||
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
||||||
public static class RestApiClientLoggingExtensions
|
public static class RestApiClientLoggingExtensions
|
||||||
{
|
{
|
||||||
private static readonly Action<ILogger, int?, int?, long, string?, Exception?> _restApiErrorReceived;
|
private static readonly Action<ILogger, int?, int?, long, string?, string?, Exception?> _restApiErrorReceived;
|
||||||
private static readonly Action<ILogger, int?, int?, long, string?, Exception?> _restApiResponseReceived;
|
private static readonly Action<ILogger, int?, int?, long, string?, Exception?> _restApiResponseReceived;
|
||||||
private static readonly Action<ILogger, int, string, Exception?> _restApiFailedToSyncTime;
|
private static readonly Action<ILogger, int, string, Exception?> _restApiFailedToSyncTime;
|
||||||
private static readonly Action<ILogger, int, string, Exception?> _restApiNoApiCredentials;
|
private static readonly Action<ILogger, int, string, Exception?> _restApiNoApiCredentials;
|
||||||
@@ -25,10 +25,10 @@ namespace CryptoExchange.Net.Logging.Extensions
|
|||||||
|
|
||||||
static RestApiClientLoggingExtensions()
|
static RestApiClientLoggingExtensions()
|
||||||
{
|
{
|
||||||
_restApiErrorReceived = LoggerMessage.Define<int?, int?, long, string?>(
|
_restApiErrorReceived = LoggerMessage.Define<int?, int?, long, string?, string?>(
|
||||||
LogLevel.Warning,
|
LogLevel.Warning,
|
||||||
new EventId(4000, "RestApiErrorReceived"),
|
new EventId(4000, "RestApiErrorReceived"),
|
||||||
"[Req {RequestId}] {ResponseStatusCode} - Error received in {ResponseTime}ms: {ErrorMessage}");
|
"[Req {RequestId}] {ResponseStatusCode} - Error received in {ResponseTime}ms: {ErrorMessage}, Data: {OriginalData}");
|
||||||
|
|
||||||
_restApiResponseReceived = LoggerMessage.Define<int?, int?, long, string?>(
|
_restApiResponseReceived = LoggerMessage.Define<int?, int?, long, string?>(
|
||||||
LogLevel.Debug,
|
LogLevel.Debug,
|
||||||
@@ -92,9 +92,9 @@ namespace CryptoExchange.Net.Logging.Extensions
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void RestApiErrorReceived(this ILogger logger, int? requestId, HttpStatusCode? responseStatusCode, long responseTime, string? error)
|
public static void RestApiErrorReceived(this ILogger logger, int? requestId, HttpStatusCode? responseStatusCode, long responseTime, string? error, string? originalData, Exception? exception)
|
||||||
{
|
{
|
||||||
_restApiErrorReceived(logger, requestId, (int?)responseStatusCode, responseTime, error, null);
|
_restApiErrorReceived(logger, requestId, (int?)responseStatusCode, responseTime, error, originalData, exception);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void RestApiResponseReceived(this ILogger logger, int? requestId, HttpStatusCode? responseStatusCode, long responseTime, string? originalData)
|
public static void RestApiResponseReceived(this ILogger logger, int? requestId, HttpStatusCode? responseStatusCode, long responseTime, string? originalData)
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ namespace CryptoExchange.Net.Logging.Extensions
|
|||||||
private static readonly Action<ILogger, int, int, Exception?> _unsubscribingSubscription;
|
private static readonly Action<ILogger, int, int, Exception?> _unsubscribingSubscription;
|
||||||
private static readonly Action<ILogger, int, Exception?> _reconnectingAllConnections;
|
private static readonly Action<ILogger, int, Exception?> _reconnectingAllConnections;
|
||||||
private static readonly Action<ILogger, DateTime, Exception?> _addingRetryAfterGuard;
|
private static readonly Action<ILogger, DateTime, Exception?> _addingRetryAfterGuard;
|
||||||
|
private static readonly Action<ILogger, Exception?> _timeoutWaitingForReconnectingSocket;
|
||||||
|
private static readonly Action<ILogger, long, Exception?> _waitedForReconnectingSocket;
|
||||||
|
|
||||||
static SocketApiClientLoggingExtension()
|
static SocketApiClientLoggingExtension()
|
||||||
{
|
{
|
||||||
@@ -110,6 +112,16 @@ namespace CryptoExchange.Net.Logging.Extensions
|
|||||||
LogLevel.Warning,
|
LogLevel.Warning,
|
||||||
new EventId(3018, "AddRetryAfterGuard"),
|
new EventId(3018, "AddRetryAfterGuard"),
|
||||||
"Adding RetryAfterGuard ({RetryAfter}) because the connection attempt was rate limited");
|
"Adding RetryAfterGuard ({RetryAfter}) because the connection attempt was rate limited");
|
||||||
|
|
||||||
|
_timeoutWaitingForReconnectingSocket = LoggerMessage.Define(
|
||||||
|
LogLevel.Debug,
|
||||||
|
new EventId(3019, "TimeoutWaitingForReconnectingSocket"),
|
||||||
|
"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");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void FailedToAddSubscriptionRetryOnDifferentConnection(this ILogger logger, int socketId)
|
public static void FailedToAddSubscriptionRetryOnDifferentConnection(this ILogger logger, int socketId)
|
||||||
@@ -196,5 +208,14 @@ namespace CryptoExchange.Net.Logging.Extensions
|
|||||||
{
|
{
|
||||||
_addingRetryAfterGuard(logger, retryAfter, null);
|
_addingRetryAfterGuard(logger, retryAfter, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void TimeoutWaitingForReconnectingSocket(this ILogger logger)
|
||||||
|
{
|
||||||
|
_timeoutWaitingForReconnectingSocket(logger, null);
|
||||||
|
}
|
||||||
|
public static void WaitedForReconnectingSocket(this ILogger logger, long milliseconds)
|
||||||
|
{
|
||||||
|
_waitedForReconnectingSocket(logger, milliseconds, null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,9 +15,10 @@ namespace CryptoExchange.Net.Logging.Extensions
|
|||||||
private static readonly Action<ILogger, int, string?, Exception?> _webSocketError;
|
private static readonly Action<ILogger, int, string?, Exception?> _webSocketError;
|
||||||
private static readonly Action<ILogger, int, int, Exception?> _messageSentNotPending;
|
private static readonly Action<ILogger, int, int, Exception?> _messageSentNotPending;
|
||||||
private static readonly Action<ILogger, int, string, Exception?> _receivedData;
|
private static readonly Action<ILogger, int, string, Exception?> _receivedData;
|
||||||
|
private static readonly Action<ILogger, int, string, Exception?> _failedToParse;
|
||||||
private static readonly Action<ILogger, int, string, Exception?> _failedToEvaluateMessage;
|
private static readonly Action<ILogger, int, string, Exception?> _failedToEvaluateMessage;
|
||||||
private static readonly Action<ILogger, int, Exception?> _errorProcessingMessage;
|
private static readonly Action<ILogger, int, Exception?> _errorProcessingMessage;
|
||||||
private static readonly Action<ILogger, int, int, string, Exception?> _processorMatched;
|
private static readonly Action<ILogger, int, string, string, Exception?> _processorMatched;
|
||||||
private static readonly Action<ILogger, int, int, Exception?> _receivedMessageNotRecognized;
|
private static readonly Action<ILogger, int, int, Exception?> _receivedMessageNotRecognized;
|
||||||
private static readonly Action<ILogger, int, string?, Exception?> _failedToDeserializeMessage;
|
private static readonly Action<ILogger, int, string?, Exception?> _failedToDeserializeMessage;
|
||||||
private static readonly Action<ILogger, int, string, Exception?> _userMessageProcessingFailed;
|
private static readonly Action<ILogger, int, string, Exception?> _userMessageProcessingFailed;
|
||||||
@@ -37,6 +38,7 @@ namespace CryptoExchange.Net.Logging.Extensions
|
|||||||
private static readonly Action<ILogger, int, string, string, Exception?> _periodicSendFailed;
|
private static readonly Action<ILogger, int, string, string, Exception?> _periodicSendFailed;
|
||||||
private static readonly Action<ILogger, int, int, string, Exception?> _sendingData;
|
private static readonly Action<ILogger, int, int, string, Exception?> _sendingData;
|
||||||
private static readonly Action<ILogger, int, string, string, Exception?> _receivedMessageNotMatchedToAnyListener;
|
private static readonly Action<ILogger, int, string, string, Exception?> _receivedMessageNotMatchedToAnyListener;
|
||||||
|
private static readonly Action<ILogger, int, int, int, Exception?> _sendingByteData;
|
||||||
|
|
||||||
static SocketConnectionLoggingExtension()
|
static SocketConnectionLoggingExtension()
|
||||||
{
|
{
|
||||||
@@ -90,11 +92,6 @@ namespace CryptoExchange.Net.Logging.Extensions
|
|||||||
new EventId(2009, "ErrorProcessingMessage"),
|
new EventId(2009, "ErrorProcessingMessage"),
|
||||||
"[Sckt {SocketId}] error processing message");
|
"[Sckt {SocketId}] error processing message");
|
||||||
|
|
||||||
_processorMatched = LoggerMessage.Define<int, int, string>(
|
|
||||||
LogLevel.Trace,
|
|
||||||
new EventId(2010, "ProcessorMatched"),
|
|
||||||
"[Sckt {SocketId}] {Count} processor(s) matched to message with listener identifier {ListenerId}");
|
|
||||||
|
|
||||||
_receivedMessageNotRecognized = LoggerMessage.Define<int, int>(
|
_receivedMessageNotRecognized = LoggerMessage.Define<int, int>(
|
||||||
LogLevel.Warning,
|
LogLevel.Warning,
|
||||||
new EventId(2011, "ReceivedMessageNotRecognized"),
|
new EventId(2011, "ReceivedMessageNotRecognized"),
|
||||||
@@ -188,7 +185,23 @@ namespace CryptoExchange.Net.Logging.Extensions
|
|||||||
_receivedMessageNotMatchedToAnyListener = LoggerMessage.Define<int, string, string>(
|
_receivedMessageNotMatchedToAnyListener = LoggerMessage.Define<int, string, string>(
|
||||||
LogLevel.Warning,
|
LogLevel.Warning,
|
||||||
new EventId(2029, "ReceivedMessageNotMatchedToAnyListener"),
|
new EventId(2029, "ReceivedMessageNotMatchedToAnyListener"),
|
||||||
"[Sckt {SocketId}] received message not matched to any listener. ListenId: {ListenId}, current listeners: {ListenIds}");
|
"[Sckt {SocketId}] received message not matched to any listener. ListenId: {ListenId}, current listeners: [{ListenIds}]");
|
||||||
|
|
||||||
|
_failedToParse = LoggerMessage.Define<int, string>(
|
||||||
|
LogLevel.Warning,
|
||||||
|
new EventId(2030, "FailedToParse"),
|
||||||
|
"[Sckt {SocketId}] failed to parse data: {Error}");
|
||||||
|
|
||||||
|
_sendingByteData = LoggerMessage.Define<int, int, int>(
|
||||||
|
LogLevel.Trace,
|
||||||
|
new EventId(2031, "SendingByteData"),
|
||||||
|
"[Sckt {SocketId}] [Req {RequestId}] sending byte message of length: {Length}");
|
||||||
|
|
||||||
|
_processorMatched = LoggerMessage.Define<int, string, string>(
|
||||||
|
LogLevel.Trace,
|
||||||
|
new EventId(2032, "ProcessorMatched"),
|
||||||
|
"[Sckt {SocketId}] listener '{ListenId}' matched to message with listener identifier {ListenerId}");
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void ActivityPaused(this ILogger logger, int socketId, bool paused)
|
public static void ActivityPaused(this ILogger logger, int socketId, bool paused)
|
||||||
@@ -230,6 +243,12 @@ namespace CryptoExchange.Net.Logging.Extensions
|
|||||||
{
|
{
|
||||||
_receivedData(logger, socketId, originalData, null);
|
_receivedData(logger, socketId, originalData, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void FailedToParse(this ILogger logger, int socketId, string error)
|
||||||
|
{
|
||||||
|
_failedToParse(logger, socketId, error, null);
|
||||||
|
}
|
||||||
|
|
||||||
public static void FailedToEvaluateMessage(this ILogger logger, int socketId, string originalData)
|
public static void FailedToEvaluateMessage(this ILogger logger, int socketId, string originalData)
|
||||||
{
|
{
|
||||||
_failedToEvaluateMessage(logger, socketId, originalData, null);
|
_failedToEvaluateMessage(logger, socketId, originalData, null);
|
||||||
@@ -238,17 +257,17 @@ namespace CryptoExchange.Net.Logging.Extensions
|
|||||||
{
|
{
|
||||||
_errorProcessingMessage(logger, socketId, e);
|
_errorProcessingMessage(logger, socketId, e);
|
||||||
}
|
}
|
||||||
public static void ProcessorMatched(this ILogger logger, int socketId, int count, string listenerId)
|
public static void ProcessorMatched(this ILogger logger, int socketId, string listener, string listenerId)
|
||||||
{
|
{
|
||||||
_processorMatched(logger, socketId, count, listenerId, null);
|
_processorMatched(logger, socketId, listener, listenerId, null);
|
||||||
}
|
}
|
||||||
public static void ReceivedMessageNotRecognized(this ILogger logger, int socketId, int id)
|
public static void ReceivedMessageNotRecognized(this ILogger logger, int socketId, int id)
|
||||||
{
|
{
|
||||||
_receivedMessageNotRecognized(logger, socketId, id, null);
|
_receivedMessageNotRecognized(logger, socketId, id, null);
|
||||||
}
|
}
|
||||||
public static void FailedToDeserializeMessage(this ILogger logger, int socketId, string? errorMessage)
|
public static void FailedToDeserializeMessage(this ILogger logger, int socketId, string? errorMessage, Exception? ex)
|
||||||
{
|
{
|
||||||
_failedToDeserializeMessage(logger, socketId, errorMessage, null);
|
_failedToDeserializeMessage(logger, socketId, errorMessage, ex);
|
||||||
}
|
}
|
||||||
public static void UserMessageProcessingFailed(this ILogger logger, int socketId, string errorMessage, Exception e)
|
public static void UserMessageProcessingFailed(this ILogger logger, int socketId, string errorMessage, Exception e)
|
||||||
{
|
{
|
||||||
@@ -321,5 +340,10 @@ namespace CryptoExchange.Net.Logging.Extensions
|
|||||||
{
|
{
|
||||||
_receivedMessageNotMatchedToAnyListener(logger, socketId, listenId, listenIds, null);
|
_receivedMessageNotMatchedToAnyListener(logger, socketId, listenId, listenIds, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void SendingByteData(this ILogger logger, int socketId, int requestId, int length)
|
||||||
|
{
|
||||||
|
_sendingByteData(logger, socketId, requestId, length, null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -53,7 +53,7 @@ namespace CryptoExchange.Net.Logging.Extensions
|
|||||||
"{Api} order book {Symbol} connection lost");
|
"{Api} order book {Symbol} connection lost");
|
||||||
|
|
||||||
_orderBookDisconnected = LoggerMessage.Define<string, string>(
|
_orderBookDisconnected = LoggerMessage.Define<string, string>(
|
||||||
LogLevel.Warning,
|
LogLevel.Debug,
|
||||||
new EventId(5004, "OrderBookDisconnected"),
|
new EventId(5004, "OrderBookDisconnected"),
|
||||||
"{Api} order book {Symbol} disconnected");
|
"{Api} order book {Symbol} disconnected");
|
||||||
|
|
||||||
|
|||||||
@@ -173,9 +173,9 @@ namespace CryptoExchange.Net.Logging.Extensions
|
|||||||
_klineTrackerStarting(logger, symbol, null);
|
_klineTrackerStarting(logger, symbol, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void KlineTrackerStartFailed(this ILogger logger, string symbol, string error)
|
public static void KlineTrackerStartFailed(this ILogger logger, string symbol, string error, Exception? exception)
|
||||||
{
|
{
|
||||||
_klineTrackerStartFailed(logger, symbol, error, null);
|
_klineTrackerStartFailed(logger, symbol, error, exception);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void KlineTrackerStarted(this ILogger logger, string symbol)
|
public static void KlineTrackerStarted(this ILogger logger, string symbol)
|
||||||
@@ -233,9 +233,9 @@ namespace CryptoExchange.Net.Logging.Extensions
|
|||||||
_tradeTrackerStarting(logger, symbol, null);
|
_tradeTrackerStarting(logger, symbol, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void TradeTrackerStartFailed(this ILogger logger, string symbol, string error)
|
public static void TradeTrackerStartFailed(this ILogger logger, string symbol, string error, Exception? ex)
|
||||||
{
|
{
|
||||||
_tradeTrackerStartFailed(logger, symbol, error, null);
|
_tradeTrackerStartFailed(logger, symbol, error, ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void TradeTrackerStarted(this ILogger logger, string symbol)
|
public static void TradeTrackerStarted(this ILogger logger, string symbol)
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Objects
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// An alias used by the exchange for an asset commonly known by another name
|
||||||
|
/// </summary>
|
||||||
|
public class AssetAlias
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The name of the asset on the exchange
|
||||||
|
/// </summary>
|
||||||
|
public string ExchangeAssetName { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// The name of the asset as it's commonly known
|
||||||
|
/// </summary>
|
||||||
|
public string CommonAssetName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
public AssetAlias(string exchangeName, string commonName)
|
||||||
|
{
|
||||||
|
ExchangeAssetName = exchangeName;
|
||||||
|
CommonAssetName = commonName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Objects
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Exchange configuration for asset aliases
|
||||||
|
/// </summary>
|
||||||
|
public class AssetAliasConfiguration
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Defined aliases
|
||||||
|
/// </summary>
|
||||||
|
public AssetAlias[] Aliases { get; set; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Auto convert asset names when using the Shared interfaces. Defaults to true
|
||||||
|
/// </summary>
|
||||||
|
public bool AutoConvertEnabled { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Map the common name to an exchange name for an asset. If there is no alias the input name is returned
|
||||||
|
/// </summary>
|
||||||
|
public string CommonToExchangeName(string commonName) => !AutoConvertEnabled ? commonName : Aliases.SingleOrDefault(x => x.CommonAssetName == commonName)?.ExchangeAssetName ?? commonName;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Map the exchange name to a common name for an asset. If there is no alias the input name is returned
|
||||||
|
/// </summary>
|
||||||
|
public string ExchangeToCommonName(string exchangeName) => !AutoConvertEnabled ? exchangeName : Aliases.SingleOrDefault(x => x.ExchangeAssetName == exchangeName)?.CommonAssetName ?? exchangeName;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -166,6 +166,18 @@ namespace CryptoExchange.Net.Objects
|
|||||||
return new CallResult(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>
|
/// <summary>
|
||||||
/// Copy the WebCallResult to a new data type
|
/// Copy the WebCallResult to a new data type
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -193,6 +205,11 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// The request http method
|
/// The request http method
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public HttpMethod? RequestMethod { get; set; }
|
public HttpMethod? RequestMethod { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// HTTP protocol version
|
||||||
|
/// </summary>
|
||||||
|
public Version? HttpVersion { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The headers sent with the request
|
/// The headers sent with the request
|
||||||
@@ -214,6 +231,11 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string? RequestBody { get; set; }
|
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>
|
/// <summary>
|
||||||
/// The status code of the response. Note that a OK status does not always indicate success, check the Success parameter for this.
|
/// The status code of the response. Note that a OK status does not always indicate success, check the Success parameter for this.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -232,19 +254,12 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="code"></param>
|
|
||||||
/// <param name="responseHeaders"></param>
|
|
||||||
/// <param name="responseTime"></param>
|
|
||||||
/// <param name="requestId"></param>
|
|
||||||
/// <param name="requestUrl"></param>
|
|
||||||
/// <param name="requestBody"></param>
|
|
||||||
/// <param name="requestMethod"></param>
|
|
||||||
/// <param name="requestHeaders"></param>
|
|
||||||
/// <param name="error"></param>
|
|
||||||
public WebCallResult(
|
public WebCallResult(
|
||||||
HttpStatusCode? code,
|
HttpStatusCode? code,
|
||||||
|
Version? httpVersion,
|
||||||
KeyValuePair<string, string[]>[]? responseHeaders,
|
KeyValuePair<string, string[]>[]? responseHeaders,
|
||||||
TimeSpan? responseTime,
|
TimeSpan? responseTime,
|
||||||
|
string? originalData,
|
||||||
int? requestId,
|
int? requestId,
|
||||||
string? requestUrl,
|
string? requestUrl,
|
||||||
string? requestBody,
|
string? requestBody,
|
||||||
@@ -253,9 +268,11 @@ namespace CryptoExchange.Net.Objects
|
|||||||
Error? error) : base(error)
|
Error? error) : base(error)
|
||||||
{
|
{
|
||||||
ResponseStatusCode = code;
|
ResponseStatusCode = code;
|
||||||
|
HttpVersion = httpVersion;
|
||||||
ResponseHeaders = responseHeaders;
|
ResponseHeaders = responseHeaders;
|
||||||
ResponseTime = responseTime;
|
ResponseTime = responseTime;
|
||||||
RequestId = requestId;
|
RequestId = requestId;
|
||||||
|
OriginalData = originalData;
|
||||||
|
|
||||||
RequestUrl = requestUrl;
|
RequestUrl = requestUrl;
|
||||||
RequestBody = requestBody;
|
RequestBody = requestBody;
|
||||||
@@ -276,7 +293,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public WebCallResult AsError(Error error)
|
public WebCallResult AsError(Error error)
|
||||||
{
|
{
|
||||||
return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error);
|
return new WebCallResult(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -287,7 +304,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public WebCallResult<K> As<K>([AllowNull] K data)
|
public WebCallResult<K> As<K>([AllowNull] K data)
|
||||||
{
|
{
|
||||||
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, 0, null, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, ResultDataSource.Server, data, Error);
|
return new WebCallResult<K>(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, 0, null, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, ResultDataSource.Server, data, Error);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -324,7 +341,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public WebCallResult<K> AsError<K>(Error error)
|
public WebCallResult<K> AsError<K>(Error error)
|
||||||
{
|
{
|
||||||
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, 0, null, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, ResultDataSource.Server, default, error);
|
return new WebCallResult<K>(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, 0, null, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, ResultDataSource.Server, default, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -345,6 +362,11 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public HttpMethod? RequestMethod { get; set; }
|
public HttpMethod? RequestMethod { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// HTTP protocol version
|
||||||
|
/// </summary>
|
||||||
|
public Version? HttpVersion { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The headers sent with the request
|
/// The headers sent with the request
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -393,21 +415,9 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a new result
|
/// Create a new result
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="code"></param>
|
|
||||||
/// <param name="responseHeaders"></param>
|
|
||||||
/// <param name="responseTime"></param>
|
|
||||||
/// <param name="responseLength"></param>
|
|
||||||
/// <param name="originalData"></param>
|
|
||||||
/// <param name="requestId"></param>
|
|
||||||
/// <param name="requestUrl"></param>
|
|
||||||
/// <param name="requestBody"></param>
|
|
||||||
/// <param name="requestMethod"></param>
|
|
||||||
/// <param name="requestHeaders"></param>
|
|
||||||
/// <param name="dataSource"></param>
|
|
||||||
/// <param name="data"></param>
|
|
||||||
/// <param name="error"></param>
|
|
||||||
public WebCallResult(
|
public WebCallResult(
|
||||||
HttpStatusCode? code,
|
HttpStatusCode? code,
|
||||||
|
Version? httpVersion,
|
||||||
KeyValuePair<string, string[]>[]? responseHeaders,
|
KeyValuePair<string, string[]>[]? responseHeaders,
|
||||||
TimeSpan? responseTime,
|
TimeSpan? responseTime,
|
||||||
long? responseLength,
|
long? responseLength,
|
||||||
@@ -421,6 +431,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
[AllowNull] T data,
|
[AllowNull] T data,
|
||||||
Error? error) : base(data, originalData, error)
|
Error? error) : base(data, originalData, error)
|
||||||
{
|
{
|
||||||
|
HttpVersion = httpVersion;
|
||||||
ResponseStatusCode = code;
|
ResponseStatusCode = code;
|
||||||
ResponseHeaders = responseHeaders;
|
ResponseHeaders = responseHeaders;
|
||||||
ResponseTime = responseTime;
|
ResponseTime = responseTime;
|
||||||
@@ -440,7 +451,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public new WebCallResult AsDataless()
|
public new WebCallResult AsDataless()
|
||||||
{
|
{
|
||||||
return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, Error);
|
return new WebCallResult(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, Error);
|
||||||
}
|
}
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Copy as a dataless result
|
/// Copy as a dataless result
|
||||||
@@ -448,14 +459,14 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public new WebCallResult AsDatalessError(Error error)
|
public new WebCallResult AsDatalessError(Error error)
|
||||||
{
|
{
|
||||||
return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error);
|
return new WebCallResult(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a new error result
|
/// Create a new error result
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="error">The error</param>
|
/// <param name="error">The error</param>
|
||||||
public WebCallResult(Error? error) : this(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, default, error) { }
|
public WebCallResult(Error? error) : this(null, null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, default, error) { }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Copy the WebCallResult to a new data type
|
/// Copy the WebCallResult to a new data type
|
||||||
@@ -465,7 +476,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public new WebCallResult<K> As<K>([AllowNull] K data)
|
public new WebCallResult<K> As<K>([AllowNull] K data)
|
||||||
{
|
{
|
||||||
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, data, Error);
|
return new WebCallResult<K>(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, data, Error);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -476,7 +487,19 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public new WebCallResult<K> AsError<K>(Error error)
|
public new WebCallResult<K> AsError<K>(Error error)
|
||||||
{
|
{
|
||||||
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, default, 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>
|
/// <summary>
|
||||||
@@ -547,7 +570,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
internal WebCallResult<T> Cached()
|
internal WebCallResult<T> Cached()
|
||||||
{
|
{
|
||||||
return new WebCallResult<T>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, ResultDataSource.Cache, Data, Error);
|
return new WebCallResult<T>(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, ResultDataSource.Cache, Data, Error);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -558,7 +581,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
if (ResponseLength != null)
|
if (ResponseLength != null)
|
||||||
sb.Append($", {ResponseLength} bytes");
|
sb.Append($", {ResponseLength} bytes");
|
||||||
if (ResponseTime != null)
|
if (ResponseTime != null)
|
||||||
sb.Append($" received in {Math.Round(ResponseTime?.TotalMilliseconds ?? 0)}ms");
|
sb.Append($", received in {Math.Round(ResponseTime?.TotalMilliseconds ?? 0)}ms");
|
||||||
|
|
||||||
return sb.ToString();
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -251,4 +251,47 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
DEX
|
DEX
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Timeout behavior for queries
|
||||||
|
/// </summary>
|
||||||
|
public enum TimeoutBehavior
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Fail the request
|
||||||
|
/// </summary>
|
||||||
|
Fail,
|
||||||
|
/// <summary>
|
||||||
|
/// Mark the query as successful
|
||||||
|
/// </summary>
|
||||||
|
Succeed
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Subscription status
|
||||||
|
/// </summary>
|
||||||
|
public enum SubscriptionStatus
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Pending, waiting before (re)subscription can be started
|
||||||
|
/// </summary>
|
||||||
|
Pending,
|
||||||
|
/// <summary>
|
||||||
|
/// Currently (re)subscribing, will start producing updates soon if subscription is successful
|
||||||
|
/// </summary>
|
||||||
|
Subscribing,
|
||||||
|
/// <summary>
|
||||||
|
/// Subscribed and listening to updates
|
||||||
|
/// </summary>
|
||||||
|
Subscribed,
|
||||||
|
/// <summary>
|
||||||
|
/// Subscription is being closed and will stop producing updates
|
||||||
|
/// </summary>
|
||||||
|
Closing,
|
||||||
|
/// <summary>
|
||||||
|
/// Subscription is closed and will no long produce updates
|
||||||
|
/// </summary>
|
||||||
|
Closed
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+147
-117
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using CryptoExchange.Net.Objects.Errors;
|
||||||
|
using System;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Objects
|
namespace CryptoExchange.Net.Objects
|
||||||
{
|
{
|
||||||
@@ -7,32 +8,69 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class Error
|
public abstract class Error
|
||||||
{
|
{
|
||||||
|
|
||||||
|
private int? _code;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The error code from the server
|
/// The int error code the server returned; or the http status code int value if there was no error code.<br />
|
||||||
|
/// <br />
|
||||||
|
/// <i>Note:</i><br />
|
||||||
|
/// The <see cref="ErrorCode"/> property should be used for more generic error checking; it might contain a string error code if the server does not return an int code.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? Code { get; set; }
|
public int? Code
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_code.HasValue)
|
||||||
|
return _code;
|
||||||
|
|
||||||
|
return int.TryParse(ErrorCode, out var r) ? r : null;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_code = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The message for the error that occurred
|
/// The error code returned by the server
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Message { get; set; }
|
public string? ErrorCode { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// The error description
|
||||||
|
/// </summary>
|
||||||
|
public string? ErrorDescription { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The data which caused the error
|
/// Error type
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public object? Data { get; set; }
|
public ErrorType ErrorType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the error is transient and can be retried
|
||||||
|
/// </summary>
|
||||||
|
public bool IsTransient { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The server message for the error that occurred
|
||||||
|
/// </summary>
|
||||||
|
public string? Message { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Underlying exception
|
||||||
|
/// </summary>
|
||||||
|
public Exception? Exception { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="code"></param>
|
protected Error(string? errorCode, ErrorInfo errorInfo, Exception? exception)
|
||||||
/// <param name="message"></param>
|
|
||||||
/// <param name="data"></param>
|
|
||||||
protected Error(int? code, string message, object? data)
|
|
||||||
{
|
{
|
||||||
Code = code;
|
ErrorCode = errorCode;
|
||||||
Message = message;
|
ErrorType = errorInfo.ErrorType;
|
||||||
Data = data;
|
Message = errorInfo.Message;
|
||||||
|
ErrorDescription = errorInfo.ErrorDescription;
|
||||||
|
IsTransient = errorInfo.IsTransient;
|
||||||
|
Exception = exception;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -41,7 +79,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return Code != null ? $"[{GetType().Name}] {Code}: {Message} {Data}" : $"[{GetType().Name}] {Message} {Data}";
|
return ErrorCode != null ? $"[{GetType().Name}.{ErrorType}] {ErrorCode}: {Message ?? ErrorDescription}" : $"[{GetType().Name}.{ErrorType}] {Message ?? ErrorDescription}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,17 +89,24 @@ namespace CryptoExchange.Net.Objects
|
|||||||
public class CantConnectError : Error
|
public class CantConnectError : Error
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// Default error info
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public CantConnectError() : base(null, "Can't connect to the server", null) { }
|
protected static readonly ErrorInfo _errorInfo = new ErrorInfo(ErrorType.UnableToConnect, false, "Can't connect to the server");
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="code"></param>
|
public CantConnectError() : base(null, _errorInfo, null) { }
|
||||||
/// <param name="message"></param>
|
|
||||||
/// <param name="data"></param>
|
/// <summary>
|
||||||
protected CantConnectError(int? code, string message, object? data) : base(code, message, data) { }
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
public CantConnectError(Exception? exception) : base(null, _errorInfo, exception) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
protected CantConnectError(ErrorInfo info, Exception? exception) : base(null, info, exception) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -70,17 +115,19 @@ namespace CryptoExchange.Net.Objects
|
|||||||
public class NoApiCredentialsError : Error
|
public class NoApiCredentialsError : Error
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// Default error info
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public NoApiCredentialsError() : base(null, "No credentials provided for private endpoint", null) { }
|
protected static readonly ErrorInfo _errorInfo = new ErrorInfo(ErrorType.MissingCredentials, false, "No credentials provided for private endpoint");
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="code"></param>
|
public NoApiCredentialsError() : base(null, _errorInfo, null) { }
|
||||||
/// <param name="message"></param>
|
|
||||||
/// <param name="data"></param>
|
/// <summary>
|
||||||
protected NoApiCredentialsError(int? code, string message, object? data) : base(code, message, data) { }
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
protected NoApiCredentialsError(ErrorInfo info, Exception? exception) : base(null, info, exception) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -91,25 +138,19 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="message"></param>
|
public ServerError(ErrorInfo errorInfo, Exception? exception = null)
|
||||||
/// <param name="data"></param>
|
: base(null, errorInfo, exception) { }
|
||||||
public ServerError(string message, object? data = null) : base(null, message, data) { }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="code"></param>
|
public ServerError(int errorCode, ErrorInfo errorInfo, Exception? exception = null)
|
||||||
/// <param name="message"></param>
|
: this(errorCode.ToString(), errorInfo, exception) { }
|
||||||
/// <param name="data"></param>
|
|
||||||
public ServerError(int code, string message, object? data = null) : base(code, message, data) { }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="code"></param>
|
public ServerError(string errorCode, ErrorInfo errorInfo, Exception? exception = null) : base(errorCode, errorInfo, exception) { }
|
||||||
/// <param name="message"></param>
|
|
||||||
/// <param name="data"></param>
|
|
||||||
protected ServerError(int? code, string message, object? data) : base(code, message, data) { }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -118,27 +159,30 @@ namespace CryptoExchange.Net.Objects
|
|||||||
public class WebError : Error
|
public class WebError : Error
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// Default error info
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="message"></param>
|
protected static readonly ErrorInfo _errorInfo = new ErrorInfo(ErrorType.NetworkError, true, "Failed to complete the request to the server due to a network error");
|
||||||
/// <param name="data"></param>
|
|
||||||
public WebError(string message, object? data = null) : base(null, message, data) { }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="code"></param>
|
public WebError(string? message = null, Exception? exception = null) : base(null, _errorInfo with { Message = (message?.Length > 0 ? _errorInfo.Message + ": " + message : _errorInfo.Message) }, exception) { }
|
||||||
/// <param name="message"></param>
|
}
|
||||||
/// <param name="data"></param>
|
|
||||||
public WebError(int code, string message, object? data = null) : base(code, message, data) { }
|
/// <summary>
|
||||||
|
/// Timeout error waiting for a response from the server
|
||||||
|
/// </summary>
|
||||||
|
public class TimeoutError : Error
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Default error info
|
||||||
|
/// </summary>
|
||||||
|
protected static readonly ErrorInfo _errorInfo = new ErrorInfo(ErrorType.Timeout, false, "Failed to receive a response from the server in time");
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="code"></param>
|
public TimeoutError(string? message = null, Exception? exception = null) : base(null, _errorInfo with { Message = (message?.Length > 0 ? _errorInfo.Message + ": " + message : _errorInfo.Message) }, exception) { }
|
||||||
/// <param name="message"></param>
|
|
||||||
/// <param name="data"></param>
|
|
||||||
protected WebError(int? code, string message, object? data): base(code, message, data) { }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -147,40 +191,14 @@ namespace CryptoExchange.Net.Objects
|
|||||||
public class DeserializeError : Error
|
public class DeserializeError : Error
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// Default error info
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="message">The error message</param>
|
protected static readonly ErrorInfo _errorInfo = new ErrorInfo(ErrorType.DeserializationFailed, false, "Failed to deserialize data");
|
||||||
/// <param name="data">The data which caused the error</param>
|
|
||||||
public DeserializeError(string message, object? data) : base(null, message, data) { }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="code"></param>
|
public DeserializeError(string? message = null, Exception? exception = null) : base(null, _errorInfo with { Message = (message?.Length > 0 ? _errorInfo.Message + ": " + message : _errorInfo.Message) }, exception) { }
|
||||||
/// <param name="message"></param>
|
|
||||||
/// <param name="data"></param>
|
|
||||||
protected DeserializeError(int? code, string message, object? data): base(code, message, data) { }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Unknown error
|
|
||||||
/// </summary>
|
|
||||||
public class UnknownError : Error
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="message">Error message</param>
|
|
||||||
/// <param name="data">Error data</param>
|
|
||||||
public UnknownError(string message, object? data = null) : base(null, message, data) { }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="code"></param>
|
|
||||||
/// <param name="message"></param>
|
|
||||||
/// <param name="data"></param>
|
|
||||||
protected UnknownError(int? code, string message, object? data): base(code, message, data) { }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -189,18 +207,28 @@ namespace CryptoExchange.Net.Objects
|
|||||||
public class ArgumentError : Error
|
public class ArgumentError : Error
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// Default error info for missing parameter
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="message"></param>
|
protected static readonly ErrorInfo _missingInfo = new ErrorInfo(ErrorType.MissingParameter, false, "Missing parameter");
|
||||||
public ArgumentError(string message) : base(null, "Invalid parameter: " + message, null) { }
|
/// <summary>
|
||||||
|
/// Default error info for invalid parameter
|
||||||
|
/// </summary>
|
||||||
|
protected static readonly ErrorInfo _invalidInfo = new ErrorInfo(ErrorType.InvalidParameter, false, "Invalid parameter");
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="code"></param>
|
public static ArgumentError Missing(string parameterName, string? message = null) => new ArgumentError(_missingInfo with { Message = message == null ? $"{_missingInfo.Message} '{parameterName}'" : $"{_missingInfo.Message} '{parameterName}': {message}" }, null);
|
||||||
/// <param name="message"></param>
|
|
||||||
/// <param name="data"></param>
|
/// <summary>
|
||||||
protected ArgumentError(int? code, string message, object? data): base(code, message, data) { }
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
public static ArgumentError Invalid(string parameterName, string message) => new ArgumentError(_invalidInfo with { Message = $"{_invalidInfo.Message} '{parameterName}': {message}" }, null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
protected ArgumentError(ErrorInfo info, Exception? exception) : base(null, info, exception) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -216,10 +244,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="code"></param>
|
protected BaseRateLimitError(ErrorInfo errorInfo, Exception? exception) : base(null, errorInfo, exception) { }
|
||||||
/// <param name="message"></param>
|
|
||||||
/// <param name="data"></param>
|
|
||||||
protected BaseRateLimitError(int? code, string message, object? data) : base(code, message, data) { }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -228,18 +253,19 @@ namespace CryptoExchange.Net.Objects
|
|||||||
public class ClientRateLimitError : BaseRateLimitError
|
public class ClientRateLimitError : BaseRateLimitError
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// Default error info
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="message"></param>
|
protected static readonly ErrorInfo _errorInfo = new ErrorInfo(ErrorType.RateLimitRequest, false, "Client rate limit exceeded");
|
||||||
public ClientRateLimitError(string message) : base(null, "Client rate limit exceeded: " + message, null) { }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="code"></param>
|
public ClientRateLimitError(string? message = null, Exception? exception = null) : base(_errorInfo with { Message = (message?.Length > 0 ? _errorInfo.Message + ": " + message : _errorInfo.Message) }, exception) { }
|
||||||
/// <param name="message"></param>
|
|
||||||
/// <param name="data"></param>
|
/// <summary>
|
||||||
protected ClientRateLimitError(int? code, string message, object? data): base(code, message, data) { }
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
protected ClientRateLimitError(ErrorInfo info, Exception? exception) : base(info, exception) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -248,18 +274,19 @@ namespace CryptoExchange.Net.Objects
|
|||||||
public class ServerRateLimitError : BaseRateLimitError
|
public class ServerRateLimitError : BaseRateLimitError
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// Default error info
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="message"></param>
|
protected static readonly ErrorInfo _errorInfo = new ErrorInfo(ErrorType.RateLimitRequest, false, "Server rate limit exceeded");
|
||||||
public ServerRateLimitError(string message) : base(null, "Server rate limit exceeded: " + message, null) { }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="code"></param>
|
public ServerRateLimitError(string? message = null, Exception? exception = null) : base(_errorInfo with { Message = (message?.Length > 0 ? _errorInfo.Message + ": " + message : _errorInfo.Message) }, exception) { }
|
||||||
/// <param name="message"></param>
|
|
||||||
/// <param name="data"></param>
|
/// <summary>
|
||||||
protected ServerRateLimitError(int? code, string message, object? data) : base(code, message, data) { }
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
protected ServerRateLimitError(ErrorInfo info, Exception? exception) : base(info, exception) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -268,17 +295,19 @@ namespace CryptoExchange.Net.Objects
|
|||||||
public class CancellationRequestedError : Error
|
public class CancellationRequestedError : Error
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// Default error info
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public CancellationRequestedError() : base(null, "Cancellation requested", null) { }
|
protected static readonly ErrorInfo _errorInfo = new ErrorInfo(ErrorType.CancellationRequested, false, "Cancellation requested");
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="code"></param>
|
public CancellationRequestedError(Exception? exception = null) : base(null, _errorInfo, null) { }
|
||||||
/// <param name="message"></param>
|
|
||||||
/// <param name="data"></param>
|
/// <summary>
|
||||||
public CancellationRequestedError(int? code, string message, object? data): base(code, message, data) { }
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
protected CancellationRequestedError(ErrorInfo info, Exception? exception) : base(null, info, exception) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -287,17 +316,18 @@ namespace CryptoExchange.Net.Objects
|
|||||||
public class InvalidOperationError : Error
|
public class InvalidOperationError : Error
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// Default error info
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="message"></param>
|
protected static readonly ErrorInfo _errorInfo = new ErrorInfo(ErrorType.InvalidOperation, false, "Operation invalid");
|
||||||
public InvalidOperationError(string message) : base(null, message, null) { }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="code"></param>
|
public InvalidOperationError(string message) : base(null, _errorInfo with { Message = message }, null) { }
|
||||||
/// <param name="message"></param>
|
|
||||||
/// <param name="data"></param>
|
/// <summary>
|
||||||
protected InvalidOperationError(int? code, string message, object? data): base(code, message, data) { }
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
protected InvalidOperationError(ErrorInfo info, Exception? exception) : base(null, info, exception) { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Objects.Errors
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Error evaluator
|
||||||
|
/// </summary>
|
||||||
|
public class ErrorEvaluator
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Error code
|
||||||
|
/// </summary>
|
||||||
|
public string[] ErrorCodes { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Evaluation callback for determining the error type
|
||||||
|
/// </summary>
|
||||||
|
public Func<string, string?, ErrorInfo> ErrorTypeEvaluator { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
public ErrorEvaluator(string errorCode, Func<string, string?, ErrorInfo> errorTypeEvaluator)
|
||||||
|
{
|
||||||
|
ErrorCodes = [errorCode];
|
||||||
|
ErrorTypeEvaluator = errorTypeEvaluator;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
public ErrorEvaluator(string[] errorCodes, Func<string, string?, ErrorInfo> errorTypeEvaluator)
|
||||||
|
{
|
||||||
|
ErrorCodes = errorCodes;
|
||||||
|
ErrorTypeEvaluator = errorTypeEvaluator;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Objects.Errors
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Error info
|
||||||
|
/// </summary>
|
||||||
|
public record ErrorInfo
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Unknown error info
|
||||||
|
/// </summary>
|
||||||
|
public static ErrorInfo Unknown { get; } = new ErrorInfo(ErrorType.Unknown, false, "Unknown error", []);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The server error code
|
||||||
|
/// </summary>
|
||||||
|
public string[] ErrorCodes { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Error description
|
||||||
|
/// </summary>
|
||||||
|
public string? ErrorDescription { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// The error type
|
||||||
|
/// </summary>
|
||||||
|
public ErrorType ErrorType { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the error is transient and can be retried
|
||||||
|
/// </summary>
|
||||||
|
public bool IsTransient { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Server response message
|
||||||
|
/// </summary>
|
||||||
|
public string? Message { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
public ErrorInfo(ErrorType errorType, string description)
|
||||||
|
{
|
||||||
|
ErrorCodes = [];
|
||||||
|
ErrorType = errorType;
|
||||||
|
IsTransient = false;
|
||||||
|
ErrorDescription = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
public ErrorInfo(ErrorType errorType, bool isTransient, string description, params string[] errorCodes)
|
||||||
|
{
|
||||||
|
ErrorCodes = errorCodes;
|
||||||
|
ErrorType = errorType;
|
||||||
|
IsTransient = isTransient;
|
||||||
|
ErrorDescription = description;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Objects.Errors
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Error mapping collection
|
||||||
|
/// </summary>
|
||||||
|
public class ErrorMapping
|
||||||
|
{
|
||||||
|
private Dictionary<string, ErrorEvaluator> _evaluators = new Dictionary<string, ErrorEvaluator>();
|
||||||
|
private Dictionary<string, ErrorInfo> _directMapping = new Dictionary<string, ErrorInfo>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
public ErrorMapping(ErrorInfo[] errorMappings, ErrorEvaluator[]? errorTypeEvaluators = null)
|
||||||
|
{
|
||||||
|
foreach (var item in errorMappings)
|
||||||
|
{
|
||||||
|
if (!item.ErrorCodes.Any())
|
||||||
|
throw new Exception("Error codes can't be null in error mapping");
|
||||||
|
|
||||||
|
foreach(var code in item.ErrorCodes!)
|
||||||
|
_directMapping.Add(code, item);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errorTypeEvaluators == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
foreach (var item in errorTypeEvaluators)
|
||||||
|
{
|
||||||
|
foreach(var code in item.ErrorCodes)
|
||||||
|
_evaluators.Add(code, item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get error info for an error code
|
||||||
|
/// </summary>
|
||||||
|
public ErrorInfo GetErrorInfo(string code, string? message)
|
||||||
|
{
|
||||||
|
if (_directMapping.TryGetValue(code!, out var info))
|
||||||
|
return info with { Message = message };
|
||||||
|
|
||||||
|
if (_evaluators.TryGetValue(code!, out var eva))
|
||||||
|
return eva.ErrorTypeEvaluator.Invoke(code!, message) with { Message = message };
|
||||||
|
|
||||||
|
return ErrorInfo.Unknown with { Message = message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Objects.Errors
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Type of error
|
||||||
|
/// </summary>
|
||||||
|
public enum ErrorType
|
||||||
|
{
|
||||||
|
#region Library errors
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Failed to connect to server
|
||||||
|
/// </summary>
|
||||||
|
UnableToConnect,
|
||||||
|
/// <summary>
|
||||||
|
/// Failed to complete the request to the server
|
||||||
|
/// </summary>
|
||||||
|
NetworkError,
|
||||||
|
/// <summary>
|
||||||
|
/// No API credentials have been specified
|
||||||
|
/// </summary>
|
||||||
|
MissingCredentials,
|
||||||
|
/// <summary>
|
||||||
|
/// Invalid parameter value
|
||||||
|
/// </summary>
|
||||||
|
InvalidParameter,
|
||||||
|
/// <summary>
|
||||||
|
/// Missing parameter value
|
||||||
|
/// </summary>
|
||||||
|
MissingParameter,
|
||||||
|
/// <summary>
|
||||||
|
/// Cancellation requested by user
|
||||||
|
/// </summary>
|
||||||
|
CancellationRequested,
|
||||||
|
/// <summary>
|
||||||
|
/// Invalid operation requested
|
||||||
|
/// </summary>
|
||||||
|
InvalidOperation,
|
||||||
|
/// <summary>
|
||||||
|
/// Failed to deserialize data
|
||||||
|
/// </summary>
|
||||||
|
DeserializationFailed,
|
||||||
|
/// <summary>
|
||||||
|
/// Websocket is temporarily paused
|
||||||
|
/// </summary>
|
||||||
|
WebsocketPaused,
|
||||||
|
/// <summary>
|
||||||
|
/// Timeout while waiting for data from the order book subscription
|
||||||
|
/// </summary>
|
||||||
|
OrderBookTimeout,
|
||||||
|
/// <summary>
|
||||||
|
/// All orders failed for a multi-order operation
|
||||||
|
/// </summary>
|
||||||
|
AllOrdersFailed,
|
||||||
|
/// <summary>
|
||||||
|
/// Request timeout
|
||||||
|
/// </summary>
|
||||||
|
Timeout,
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Server errors
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unknown error
|
||||||
|
/// </summary>
|
||||||
|
Unknown,
|
||||||
|
/// <summary>
|
||||||
|
/// Not authorized or insufficient permissions
|
||||||
|
/// </summary>
|
||||||
|
Unauthorized,
|
||||||
|
/// <summary>
|
||||||
|
/// Request rate limit error, too many requests
|
||||||
|
/// </summary>
|
||||||
|
RateLimitRequest,
|
||||||
|
/// <summary>
|
||||||
|
/// Connection rate limit error, too many connections
|
||||||
|
/// </summary>
|
||||||
|
RateLimitConnection,
|
||||||
|
/// <summary>
|
||||||
|
/// Subscription rate limit error, too many subscriptions
|
||||||
|
/// </summary>
|
||||||
|
RateLimitSubscription,
|
||||||
|
/// <summary>
|
||||||
|
/// Order rate limit error, too many orders
|
||||||
|
/// </summary>
|
||||||
|
RateLimitOrder,
|
||||||
|
/// <summary>
|
||||||
|
/// Request timestamp invalid
|
||||||
|
/// </summary>
|
||||||
|
InvalidTimestamp,
|
||||||
|
/// <summary>
|
||||||
|
/// Unknown symbol
|
||||||
|
/// </summary>
|
||||||
|
UnknownSymbol,
|
||||||
|
/// <summary>
|
||||||
|
/// Unknown asset
|
||||||
|
/// </summary>
|
||||||
|
UnknownAsset,
|
||||||
|
/// <summary>
|
||||||
|
/// Unknown order
|
||||||
|
/// </summary>
|
||||||
|
UnknownOrder,
|
||||||
|
/// <summary>
|
||||||
|
/// Duplicate subscription
|
||||||
|
/// </summary>
|
||||||
|
DuplicateSubscription,
|
||||||
|
/// <summary>
|
||||||
|
/// Invalid quantity
|
||||||
|
/// </summary>
|
||||||
|
InvalidQuantity,
|
||||||
|
/// <summary>
|
||||||
|
/// Invalid price
|
||||||
|
/// </summary>
|
||||||
|
InvalidPrice,
|
||||||
|
/// <summary>
|
||||||
|
/// Parameter(s) for stop or tp/sl order invalid
|
||||||
|
/// </summary>
|
||||||
|
InvalidStopParameters,
|
||||||
|
/// <summary>
|
||||||
|
/// Not enough balance to execute request
|
||||||
|
/// </summary>
|
||||||
|
InsufficientBalance,
|
||||||
|
/// <summary>
|
||||||
|
/// Client order id already in use
|
||||||
|
/// </summary>
|
||||||
|
DuplicateClientOrderId,
|
||||||
|
/// <summary>
|
||||||
|
/// Symbol is not currently trading
|
||||||
|
/// </summary>
|
||||||
|
UnavailableSymbol,
|
||||||
|
/// <summary>
|
||||||
|
/// Order rejected due to order configuration such as order type or time in force restrictions
|
||||||
|
/// </summary>
|
||||||
|
RejectedOrderConfiguration,
|
||||||
|
/// <summary>
|
||||||
|
/// There is no open position
|
||||||
|
/// </summary>
|
||||||
|
NoPosition,
|
||||||
|
/// <summary>
|
||||||
|
/// Max position reached
|
||||||
|
/// </summary>
|
||||||
|
MaxPosition,
|
||||||
|
/// <summary>
|
||||||
|
/// Error in the internal system
|
||||||
|
/// </summary>
|
||||||
|
SystemError,
|
||||||
|
/// <summary>
|
||||||
|
/// The target object is not in the correct state for an operation
|
||||||
|
/// </summary>
|
||||||
|
IncorrectState,
|
||||||
|
/// <summary>
|
||||||
|
/// Risk management error
|
||||||
|
/// </summary>
|
||||||
|
RiskError
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -46,7 +46,7 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public T Set<T>(T targetOptions) where T: LibraryOptions<TRestOptions, TSocketOptions, TApiCredentials, TEnvironment>
|
public T Set<T>(T targetOptions) where T: LibraryOptions<TRestOptions, TSocketOptions, TApiCredentials, TEnvironment>
|
||||||
{
|
{
|
||||||
targetOptions.ApiCredentials = ApiCredentials;
|
targetOptions.ApiCredentials = (TApiCredentials?)ApiCredentials?.Copy();
|
||||||
targetOptions.Environment = Environment;
|
targetOptions.Environment = Environment;
|
||||||
targetOptions.SocketClientLifeTime = SocketClientLifeTime;
|
targetOptions.SocketClientLifeTime = SocketClientLifeTime;
|
||||||
targetOptions.Rest = Rest.Set(targetOptions.Rest);
|
targetOptions.Rest = Rest.Set(targetOptions.Rest);
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
using CryptoExchange.Net.Authentication;
|
using CryptoExchange.Net.Authentication;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Objects.Options
|
namespace CryptoExchange.Net.Objects.Options
|
||||||
{
|
{
|
||||||
@@ -28,6 +30,20 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public TimeSpan CachingMaxAge { get; set; } = TimeSpan.FromSeconds(5);
|
public TimeSpan CachingMaxAge { get; set; } = TimeSpan.FromSeconds(5);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The HTTP protocol version to use, typically 2.0 or 1.1
|
||||||
|
/// </summary>
|
||||||
|
public Version HttpVersion { get; set; }
|
||||||
|
#if NET5_0_OR_GREATER
|
||||||
|
= new Version(2, 0);
|
||||||
|
#else
|
||||||
|
= new Version(1, 1);
|
||||||
|
#endif
|
||||||
|
/// <summary>
|
||||||
|
/// Http client keep alive interval for keeping connections open
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan? HttpKeepAliveInterval { get; set; } = TimeSpan.FromSeconds(15);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Set the values of this options on the target options
|
/// Set the values of this options on the target options
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -43,6 +59,8 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
item.RateLimitingBehaviour = RateLimitingBehaviour;
|
item.RateLimitingBehaviour = RateLimitingBehaviour;
|
||||||
item.CachingEnabled = CachingEnabled;
|
item.CachingEnabled = CachingEnabled;
|
||||||
item.CachingMaxAge = CachingMaxAge;
|
item.CachingMaxAge = CachingMaxAge;
|
||||||
|
item.HttpVersion = HttpVersion;
|
||||||
|
item.HttpKeepAliveInterval = HttpKeepAliveInterval;
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Trade environment. Contains info about URL's to use to connect to the API. To swap environment select another environment for
|
/// Trade environment. Contains info about URL's to use to connect to the API. To swap environment select another environment for
|
||||||
/// the exhange's environment list or create a custom environment using either `[Exchange]Environment.CreateCustom()` or `[Exchange]Environment.[Environment]`, for example `KucoinEnvironment.TestNet` or `BinanceEnvironment.Live`
|
/// the exchange's environment list or create a custom environment using either `[Exchange]Environment.CreateCustom()` or `[Exchange]Environment.[Environment]`, for example `KucoinEnvironment.TestNet` or `BinanceEnvironment.Live`
|
||||||
/// </summary>
|
/// </summary>
|
||||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||||
public TEnvironment Environment { get; set; }
|
public TEnvironment Environment { get; set; }
|
||||||
|
|||||||
@@ -62,6 +62,11 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool PreventCaching { get; set; }
|
public bool PreventCaching { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the response to this requests should attempted to be parsed even when the status indicates failure
|
||||||
|
/// </summary>
|
||||||
|
public bool TryParseOnNonSuccess { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Connection id
|
/// Connection id
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -76,6 +81,9 @@ namespace CryptoExchange.Net.Objects
|
|||||||
{
|
{
|
||||||
Path = path;
|
Path = path;
|
||||||
Method = method;
|
Method = method;
|
||||||
|
|
||||||
|
if (!Path.StartsWith("/"))
|
||||||
|
Path = $"/{Path}";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <param name="parameterPosition">Parameter position</param>
|
/// <param name="parameterPosition">Parameter position</param>
|
||||||
/// <param name="arraySerialization">Array serialization type</param>
|
/// <param name="arraySerialization">Array serialization type</param>
|
||||||
/// <param name="preventCaching">Prevent request caching</param>
|
/// <param name="preventCaching">Prevent request caching</param>
|
||||||
|
/// <param name="tryParseOnNonSuccess">Try parse the response even when status is not success</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public RequestDefinition GetOrCreate(
|
public RequestDefinition GetOrCreate(
|
||||||
HttpMethod method,
|
HttpMethod method,
|
||||||
@@ -57,8 +58,9 @@ namespace CryptoExchange.Net.Objects
|
|||||||
RequestBodyFormat? requestBodyFormat = null,
|
RequestBodyFormat? requestBodyFormat = null,
|
||||||
HttpMethodParameterPosition? parameterPosition = null,
|
HttpMethodParameterPosition? parameterPosition = null,
|
||||||
ArrayParametersSerialization? arraySerialization = null,
|
ArrayParametersSerialization? arraySerialization = null,
|
||||||
bool? preventCaching = null)
|
bool? preventCaching = null,
|
||||||
=> GetOrCreate(method + path, method, path, rateLimitGate, weight, authenticated, limitGuard, requestBodyFormat, parameterPosition, arraySerialization, preventCaching);
|
bool? tryParseOnNonSuccess = null)
|
||||||
|
=> GetOrCreate(method + path, method, path, rateLimitGate, weight, authenticated, limitGuard, requestBodyFormat, parameterPosition, arraySerialization, preventCaching, tryParseOnNonSuccess);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get a definition if it is already in the cache or create a new definition and add it to the cache
|
/// Get a definition if it is already in the cache or create a new definition and add it to the cache
|
||||||
@@ -74,6 +76,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <param name="parameterPosition">Parameter position</param>
|
/// <param name="parameterPosition">Parameter position</param>
|
||||||
/// <param name="arraySerialization">Array serialization type</param>
|
/// <param name="arraySerialization">Array serialization type</param>
|
||||||
/// <param name="preventCaching">Prevent request caching</param>
|
/// <param name="preventCaching">Prevent request caching</param>
|
||||||
|
/// <param name="tryParseOnNonSuccess">Try parse the response even when status is not success</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public RequestDefinition GetOrCreate(
|
public RequestDefinition GetOrCreate(
|
||||||
string identifier,
|
string identifier,
|
||||||
@@ -86,7 +89,8 @@ namespace CryptoExchange.Net.Objects
|
|||||||
RequestBodyFormat? requestBodyFormat = null,
|
RequestBodyFormat? requestBodyFormat = null,
|
||||||
HttpMethodParameterPosition? parameterPosition = null,
|
HttpMethodParameterPosition? parameterPosition = null,
|
||||||
ArrayParametersSerialization? arraySerialization = null,
|
ArrayParametersSerialization? arraySerialization = null,
|
||||||
bool? preventCaching = null)
|
bool? preventCaching = null,
|
||||||
|
bool? tryParseOnNonSuccess = null)
|
||||||
{
|
{
|
||||||
|
|
||||||
if (!_definitions.TryGetValue(identifier, out var def))
|
if (!_definitions.TryGetValue(identifier, out var def))
|
||||||
@@ -100,7 +104,8 @@ namespace CryptoExchange.Net.Objects
|
|||||||
ArraySerialization = arraySerialization,
|
ArraySerialization = arraySerialization,
|
||||||
RequestBodyFormat = requestBodyFormat,
|
RequestBodyFormat = requestBodyFormat,
|
||||||
ParameterPosition = parameterPosition,
|
ParameterPosition = parameterPosition,
|
||||||
PreventCaching = preventCaching ?? false
|
PreventCaching = preventCaching ?? false,
|
||||||
|
TryParseOnNonSuccess = tryParseOnNonSuccess ?? false
|
||||||
};
|
};
|
||||||
_definitions.TryAdd(identifier, def);
|
_definitions.TryAdd(identifier, def);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Net.Http;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Objects
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Rest request configuration
|
||||||
|
/// </summary>
|
||||||
|
public class RestRequestConfiguration
|
||||||
|
{
|
||||||
|
private string? _bodyContent;
|
||||||
|
private string? _queryString;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Http method
|
||||||
|
/// </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; }
|
||||||
|
/// <summary>
|
||||||
|
/// Query parameters
|
||||||
|
/// </summary>
|
||||||
|
public IDictionary<string, object> QueryParameters { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Body parameters
|
||||||
|
/// </summary>
|
||||||
|
public IDictionary<string, object> BodyParameters { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Request headers
|
||||||
|
/// </summary>
|
||||||
|
public IDictionary<string, string> Headers { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Array serialization type
|
||||||
|
/// </summary>
|
||||||
|
public ArrayParametersSerialization ArraySerialization { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Position of the parameters
|
||||||
|
/// </summary>
|
||||||
|
public HttpMethodParameterPosition ParameterPosition { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Body format
|
||||||
|
/// </summary>
|
||||||
|
public RequestBodyFormat BodyFormat { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
public RestRequestConfiguration(
|
||||||
|
RequestDefinition requestDefinition,
|
||||||
|
string baseAddress,
|
||||||
|
IDictionary<string, object> queryParams,
|
||||||
|
IDictionary<string, object> bodyParams,
|
||||||
|
IDictionary<string, string> headers,
|
||||||
|
ArrayParametersSerialization arraySerialization,
|
||||||
|
HttpMethodParameterPosition parametersPosition,
|
||||||
|
RequestBodyFormat bodyFormat)
|
||||||
|
{
|
||||||
|
Method = requestDefinition.Method;
|
||||||
|
Authenticated = requestDefinition.Authenticated;
|
||||||
|
Path = requestDefinition.Path;
|
||||||
|
BaseAddress = baseAddress;
|
||||||
|
QueryParameters = queryParams;
|
||||||
|
BodyParameters = bodyParams;
|
||||||
|
Headers = headers;
|
||||||
|
ArraySerialization = arraySerialization;
|
||||||
|
ParameterPosition = parametersPosition;
|
||||||
|
BodyFormat = bodyFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the parameter collection based on the ParameterPosition
|
||||||
|
/// </summary>
|
||||||
|
public IDictionary<string, object> GetPositionParameters()
|
||||||
|
{
|
||||||
|
if (ParameterPosition == HttpMethodParameterPosition.InBody)
|
||||||
|
return BodyParameters;
|
||||||
|
|
||||||
|
return QueryParameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the query string. If it's not previously set it will return a newly formatted query string. If previously set return that.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="urlEncode">Whether to URL encode the parameter string if creating new</param>
|
||||||
|
public string GetQueryString(bool urlEncode = true)
|
||||||
|
{
|
||||||
|
return _queryString ?? QueryParameters.CreateParamString(urlEncode, ArraySerialization);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set the query string of the request. Will be returned by subsequent <see cref="GetQueryString" /> calls
|
||||||
|
/// </summary>
|
||||||
|
public void SetQueryString(string value)
|
||||||
|
{
|
||||||
|
_queryString = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the body content if it's previously set
|
||||||
|
/// </summary>
|
||||||
|
public string? GetBodyContent()
|
||||||
|
{
|
||||||
|
return _bodyContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set the body content for the request
|
||||||
|
/// </summary>
|
||||||
|
public void SetBodyContent(string content)
|
||||||
|
{
|
||||||
|
_bodyContent = content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
using CryptoExchange.Net.Sockets;
|
using CryptoExchange.Net.Sockets;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Objects.Sockets
|
namespace CryptoExchange.Net.Objects.Sockets
|
||||||
@@ -12,13 +14,27 @@ namespace CryptoExchange.Net.Objects.Sockets
|
|||||||
private readonly SocketConnection _connection;
|
private readonly SocketConnection _connection;
|
||||||
private readonly Subscription _listener;
|
private readonly Subscription _listener;
|
||||||
|
|
||||||
|
private object _eventLock = new object();
|
||||||
|
private bool _connectionEventsSubscribed = true;
|
||||||
|
private List<Action> _connectionClosedEventHandlers = new List<Action>();
|
||||||
|
private List<Action> _connectionLostEventHandlers = new List<Action>();
|
||||||
|
private List<Action<Error>> _resubscribeFailedEventHandlers = new List<Action<Error>>();
|
||||||
|
private List<Action<TimeSpan>> _connectionRestoredEventHandlers = new List<Action<TimeSpan>>();
|
||||||
|
private List<Action> _activityPausedEventHandlers = new List<Action>();
|
||||||
|
private List<Action> _activityUnpausedEventHandlers = new List<Action>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Event when the status of the subscription changes
|
||||||
|
/// </summary>
|
||||||
|
public event Action<SubscriptionStatus>? SubscriptionStatusChanged;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Event when the connection is lost. The socket will automatically reconnect when possible.
|
/// Event when the connection is lost. The socket will automatically reconnect when possible.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public event Action ConnectionLost
|
public event Action ConnectionLost
|
||||||
{
|
{
|
||||||
add => _connection.ConnectionLost += value;
|
add { lock (_eventLock) _connectionLostEventHandlers.Add(value); }
|
||||||
remove => _connection.ConnectionLost -= value;
|
remove { lock (_eventLock) _connectionLostEventHandlers.Remove(value); }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -26,8 +42,8 @@ namespace CryptoExchange.Net.Objects.Sockets
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public event Action ConnectionClosed
|
public event Action ConnectionClosed
|
||||||
{
|
{
|
||||||
add => _connection.ConnectionClosed += value;
|
add { lock (_eventLock) _connectionClosedEventHandlers.Add(value); }
|
||||||
remove => _connection.ConnectionClosed -= value;
|
remove { lock (_eventLock) _connectionClosedEventHandlers.Remove(value); }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -35,8 +51,8 @@ namespace CryptoExchange.Net.Objects.Sockets
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public event Action<Error> ResubscribingFailed
|
public event Action<Error> ResubscribingFailed
|
||||||
{
|
{
|
||||||
add => _connection.ResubscribingFailed += value;
|
add { lock (_eventLock) _resubscribeFailedEventHandlers.Add(value); }
|
||||||
remove => _connection.ResubscribingFailed -= value;
|
remove { lock (_eventLock) _resubscribeFailedEventHandlers.Remove(value); }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -46,8 +62,8 @@ namespace CryptoExchange.Net.Objects.Sockets
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public event Action<TimeSpan> ConnectionRestored
|
public event Action<TimeSpan> ConnectionRestored
|
||||||
{
|
{
|
||||||
add => _connection.ConnectionRestored += value;
|
add { lock (_eventLock) _connectionRestoredEventHandlers.Add(value); }
|
||||||
remove => _connection.ConnectionRestored -= value;
|
remove { lock (_eventLock) _connectionRestoredEventHandlers.Remove(value); }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -55,8 +71,8 @@ namespace CryptoExchange.Net.Objects.Sockets
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public event Action ActivityPaused
|
public event Action ActivityPaused
|
||||||
{
|
{
|
||||||
add => _connection.ActivityPaused += value;
|
add { lock (_eventLock) _activityPausedEventHandlers.Add(value); }
|
||||||
remove => _connection.ActivityPaused -= value;
|
remove { lock (_eventLock) _activityPausedEventHandlers.Remove(value); }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -64,8 +80,8 @@ namespace CryptoExchange.Net.Objects.Sockets
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public event Action ActivityUnpaused
|
public event Action ActivityUnpaused
|
||||||
{
|
{
|
||||||
add => _connection.ActivityUnpaused += value;
|
add { lock (_eventLock) _activityUnpausedEventHandlers.Add(value); }
|
||||||
remove => _connection.ActivityUnpaused -= value;
|
remove { lock (_eventLock) _activityUnpausedEventHandlers.Remove(value); }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -95,7 +111,128 @@ namespace CryptoExchange.Net.Objects.Sockets
|
|||||||
public UpdateSubscription(SocketConnection connection, Subscription subscription)
|
public UpdateSubscription(SocketConnection connection, Subscription subscription)
|
||||||
{
|
{
|
||||||
_connection = connection;
|
_connection = connection;
|
||||||
|
_connection.ConnectionClosed += HandleConnectionClosedEvent;
|
||||||
|
_connection.ConnectionLost += HandleConnectionLostEvent;
|
||||||
|
_connection.ConnectionRestored += HandleConnectionRestoredEvent;
|
||||||
|
_connection.ResubscribingFailed += HandleResubscribeFailedEvent;
|
||||||
|
_connection.ActivityPaused += HandlePausedEvent;
|
||||||
|
_connection.ActivityUnpaused += HandleUnpausedEvent;
|
||||||
|
|
||||||
_listener = subscription;
|
_listener = subscription;
|
||||||
|
_listener.StatusChanged += (x) => SubscriptionStatusChanged?.Invoke(x);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UnsubscribeConnectionEvents()
|
||||||
|
{
|
||||||
|
lock (_eventLock)
|
||||||
|
{
|
||||||
|
if (!_connectionEventsSubscribed)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_connection.ConnectionClosed -= HandleConnectionClosedEvent;
|
||||||
|
_connection.ConnectionLost -= HandleConnectionLostEvent;
|
||||||
|
_connection.ConnectionRestored -= HandleConnectionRestoredEvent;
|
||||||
|
_connection.ResubscribingFailed -= HandleResubscribeFailedEvent;
|
||||||
|
_connection.ActivityPaused -= HandlePausedEvent;
|
||||||
|
_connection.ActivityUnpaused -= HandleUnpausedEvent;
|
||||||
|
_connectionEventsSubscribed = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleConnectionClosedEvent()
|
||||||
|
{
|
||||||
|
UnsubscribeConnectionEvents();
|
||||||
|
|
||||||
|
// If we're not the subscription closing this connection don't bother emitting
|
||||||
|
if (!_listener.IsClosingConnection)
|
||||||
|
return;
|
||||||
|
|
||||||
|
List<Action> handlers;
|
||||||
|
lock (_eventLock)
|
||||||
|
handlers = _connectionClosedEventHandlers.ToList();
|
||||||
|
|
||||||
|
foreach(var callback in handlers)
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleConnectionLostEvent()
|
||||||
|
{
|
||||||
|
if (!_listener.Active)
|
||||||
|
{
|
||||||
|
UnsubscribeConnectionEvents();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Action> handlers;
|
||||||
|
lock (_eventLock)
|
||||||
|
handlers = _connectionLostEventHandlers.ToList();
|
||||||
|
|
||||||
|
foreach (var callback in handlers)
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleConnectionRestoredEvent(TimeSpan period)
|
||||||
|
{
|
||||||
|
if (!_listener.Active)
|
||||||
|
{
|
||||||
|
UnsubscribeConnectionEvents();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Action<TimeSpan>> handlers;
|
||||||
|
lock (_eventLock)
|
||||||
|
handlers = _connectionRestoredEventHandlers.ToList();
|
||||||
|
|
||||||
|
foreach (var callback in handlers)
|
||||||
|
callback(period);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleResubscribeFailedEvent(Error error)
|
||||||
|
{
|
||||||
|
if (!_listener.Active)
|
||||||
|
{
|
||||||
|
UnsubscribeConnectionEvents();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Action<Error>> handlers;
|
||||||
|
lock (_eventLock)
|
||||||
|
handlers = _resubscribeFailedEventHandlers.ToList();
|
||||||
|
|
||||||
|
foreach (var callback in handlers)
|
||||||
|
callback(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandlePausedEvent()
|
||||||
|
{
|
||||||
|
if (!_listener.Active)
|
||||||
|
{
|
||||||
|
UnsubscribeConnectionEvents();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Action> handlers;
|
||||||
|
lock (_eventLock)
|
||||||
|
handlers = _activityPausedEventHandlers.ToList();
|
||||||
|
|
||||||
|
foreach (var callback in handlers)
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleUnpausedEvent()
|
||||||
|
{
|
||||||
|
if (!_listener.Active)
|
||||||
|
{
|
||||||
|
UnsubscribeConnectionEvents();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Action> handlers;
|
||||||
|
lock (_eventLock)
|
||||||
|
handlers = _activityUnpausedEventHandlers.ToList();
|
||||||
|
|
||||||
|
foreach (var callback in handlers)
|
||||||
|
callback();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -50,6 +50,11 @@ namespace CryptoExchange.Net.Objects.Sockets
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public TimeSpan? KeepAliveInterval { get; set; }
|
public TimeSpan? KeepAliveInterval { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Timeout for keep alive response messages
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan? KeepAliveTimeout { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The rate limiter for the socket connection
|
/// The rate limiter for the socket connection
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
if (!IsEnabled(logLevel))
|
if (!IsEnabled(logLevel))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var logMessage = $"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | {logLevel} | {(_categoryName == null ? "" : $"{_categoryName} | ")}{formatter(state, exception)}";
|
var logMessage = $"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | {logLevel} | {(_categoryName == null ? "" : $"{_categoryName} | ")}{formatter(state, exception)}{(exception == null ? string.Empty : (", " + exception.ToLogString()))}";
|
||||||
Trace.WriteLine(logMessage);
|
Trace.WriteLine(logMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ using System.Threading.Tasks;
|
|||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces;
|
||||||
using CryptoExchange.Net.Logging.Extensions;
|
using CryptoExchange.Net.Logging.Extensions;
|
||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.Objects.Errors;
|
||||||
using CryptoExchange.Net.Objects.Options;
|
using CryptoExchange.Net.Objects.Options;
|
||||||
using CryptoExchange.Net.Objects.Sockets;
|
using CryptoExchange.Net.Objects.Sockets;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -549,7 +550,7 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
return new CallResult<bool>(new CancellationRequestedError());
|
return new CallResult<bool>(new CancellationRequestedError());
|
||||||
|
|
||||||
if (DateTime.UtcNow - startWait > timeout)
|
if (DateTime.UtcNow - startWait > timeout)
|
||||||
return new CallResult<bool>(new ServerError("Timeout while waiting for data"));
|
return new CallResult<bool>(new ServerError(new ErrorInfo(ErrorType.OrderBookTimeout, "Timeout while waiting for data")));
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -46,11 +46,11 @@ namespace CryptoExchange.Net.RateLimiting
|
|||||||
{
|
{
|
||||||
return await CheckGuardsAsync(_guards, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, keySuffix, ct).ConfigureAwait(false);
|
return await CheckGuardsAsync(_guards, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, keySuffix, ct).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (TaskCanceledException)
|
catch (TaskCanceledException tce)
|
||||||
{
|
{
|
||||||
// The semaphore has already been released if the task was cancelled
|
// The semaphore has already been released if the task was cancelled
|
||||||
release = false;
|
release = false;
|
||||||
return new CallResult(new CancellationRequestedError());
|
return new CallResult(new CancellationRequestedError(tce));
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -81,11 +81,11 @@ namespace CryptoExchange.Net.RateLimiting
|
|||||||
{
|
{
|
||||||
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, host, apiKey, requestWeight, rateLimitingBehaviour, keySuffix, ct).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (TaskCanceledException)
|
catch (TaskCanceledException tce)
|
||||||
{
|
{
|
||||||
// The semaphore has already been released if the task was cancelled
|
// The semaphore has already been released if the task was cancelled
|
||||||
release = false;
|
release = false;
|
||||||
return new CallResult(new CancellationRequestedError());
|
return new CallResult(new CancellationRequestedError(tce));
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -50,6 +50,9 @@ namespace CryptoExchange.Net.Requests
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Uri Uri => _request.RequestUri!;
|
public Uri Uri => _request.RequestUri!;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Version HttpVersion => _request.Version!;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public int RequestId { get; }
|
public int RequestId { get; }
|
||||||
|
|
||||||
@@ -81,7 +84,9 @@ namespace CryptoExchange.Net.Requests
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<IResponse> GetResponseAsync(CancellationToken cancellationToken)
|
public async Task<IResponse> GetResponseAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
return new Response(await _httpClient.SendAsync(_request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false));
|
var response = await _httpClient.SendAsync(_request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
return new Response(response);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using System.Net;
|
|||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces;
|
||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.Objects.Options;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Requests
|
namespace CryptoExchange.Net.Requests
|
||||||
{
|
{
|
||||||
@@ -14,54 +15,43 @@ namespace CryptoExchange.Net.Requests
|
|||||||
private HttpClient? _httpClient;
|
private HttpClient? _httpClient;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? client = null)
|
public void Configure(RestExchangeOptions options, HttpClient? client = null)
|
||||||
{
|
{
|
||||||
if (client == null)
|
if (client == null)
|
||||||
client = CreateClient(proxy, requestTimeout);
|
client = CreateClient(options.Proxy, options.RequestTimeout, options.HttpKeepAliveInterval);
|
||||||
|
|
||||||
_httpClient = client;
|
_httpClient = client;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public IRequest Create(HttpMethod method, Uri uri, int requestId)
|
public IRequest Create(Version httpRequestVersion, HttpMethod method, Uri uri, int requestId)
|
||||||
{
|
{
|
||||||
if (_httpClient == null)
|
if (_httpClient == null)
|
||||||
throw new InvalidOperationException("Cant create request before configuring http client");
|
throw new InvalidOperationException("Cant create request before configuring http client");
|
||||||
|
|
||||||
return new Request(new HttpRequestMessage(method, uri), _httpClient, requestId);
|
var requestMessage = new HttpRequestMessage(method, uri);
|
||||||
|
requestMessage.Version = httpRequestVersion;
|
||||||
|
#if NET5_0_OR_GREATER
|
||||||
|
requestMessage.VersionPolicy = HttpVersionPolicy.RequestVersionOrLower;
|
||||||
|
#endif
|
||||||
|
return new Request(requestMessage, _httpClient, requestId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout)
|
public void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout, TimeSpan? httpKeepAliveInterval)
|
||||||
{
|
{
|
||||||
_httpClient = CreateClient(proxy, requestTimeout);
|
_httpClient = CreateClient(proxy, requestTimeout, httpKeepAliveInterval);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static HttpClient CreateClient(ApiProxy? proxy, TimeSpan requestTimeout)
|
private static HttpClient CreateClient(ApiProxy? proxy, TimeSpan requestTimeout, TimeSpan? httpKeepAliveInterval)
|
||||||
{
|
{
|
||||||
var handler = new HttpClientHandler();
|
var handler = LibraryHelpers.CreateHttpClientMessageHandler(proxy, httpKeepAliveInterval);
|
||||||
try
|
|
||||||
{
|
|
||||||
handler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
|
|
||||||
handler.DefaultProxyCredentials = CredentialCache.DefaultCredentials;
|
|
||||||
}
|
|
||||||
catch (PlatformNotSupportedException) { }
|
|
||||||
catch (NotImplementedException) { } // Mono runtime throws NotImplementedException
|
|
||||||
|
|
||||||
if (proxy != null)
|
|
||||||
{
|
|
||||||
handler.Proxy = new WebProxy
|
|
||||||
{
|
|
||||||
Address = new Uri($"{proxy.Host}:{proxy.Port}"),
|
|
||||||
Credentials = proxy.Password == null ? null : new NetworkCredential(proxy.Login, proxy.Password)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
var client = new HttpClient(handler)
|
var client = new HttpClient(handler)
|
||||||
{
|
{
|
||||||
Timeout = requestTimeout
|
Timeout = requestTimeout
|
||||||
};
|
};
|
||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
@@ -18,6 +19,9 @@ namespace CryptoExchange.Net.Requests
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public HttpStatusCode StatusCode => _response.StatusCode;
|
public HttpStatusCode StatusCode => _response.StatusCode;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Version HttpVersion => _response.Version;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public bool IsSuccessStatusCode => _response.IsSuccessStatusCode;
|
public bool IsSuccessStatusCode => _response.IsSuccessStatusCode;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.SharedApis
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Account type
|
||||||
|
/// </summary>
|
||||||
|
public enum SharedAccountType
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Unified account, combined account for multiple different types of trading
|
||||||
|
/// </summary>
|
||||||
|
Unified,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Funding account, where withdrawals and deposits are made from and to
|
||||||
|
/// </summary>
|
||||||
|
Funding,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Spot trading account
|
||||||
|
/// </summary>
|
||||||
|
Spot,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cross margin account
|
||||||
|
/// </summary>
|
||||||
|
CrossMargin,
|
||||||
|
/// <summary>
|
||||||
|
/// Isolated margin account
|
||||||
|
/// </summary>
|
||||||
|
IsolatedMargin,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Perpetual linear futures account
|
||||||
|
/// </summary>
|
||||||
|
PerpetualLinearFutures,
|
||||||
|
/// <summary>
|
||||||
|
/// Delivery linear futures account
|
||||||
|
/// </summary>
|
||||||
|
DeliveryLinearFutures,
|
||||||
|
/// <summary>
|
||||||
|
/// Perpetual inverse futures account
|
||||||
|
/// </summary>
|
||||||
|
PerpetualInverseFutures,
|
||||||
|
/// <summary>
|
||||||
|
/// Delivery inverse futures account
|
||||||
|
/// </summary>
|
||||||
|
DeliveryInverseFutures,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Option account
|
||||||
|
/// </summary>
|
||||||
|
Option,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Other
|
||||||
|
/// </summary>
|
||||||
|
Other
|
||||||
|
}
|
||||||
|
}
|
||||||
+5
-1
@@ -20,6 +20,10 @@
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Quantity can be either base or quote quantity
|
/// Quantity can be either base or quote quantity
|
||||||
/// </summary>
|
/// </summary>
|
||||||
BaseAndQuoteAsset
|
BaseAndQuoteAsset,
|
||||||
|
/// <summary>
|
||||||
|
/// Quantity can be either base or quote quantity, or in contracts
|
||||||
|
/// </summary>
|
||||||
|
BaseAndQuoteAssetAndContracts
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.SharedApis
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Take Profit / Stop Loss side
|
||||||
|
/// </summary>
|
||||||
|
public enum SharedTpSlSide
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Take profit
|
||||||
|
/// </summary>
|
||||||
|
TakeProfit,
|
||||||
|
/// <summary>
|
||||||
|
/// Stop loss
|
||||||
|
/// </summary>
|
||||||
|
StopLoss
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.SharedApis
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The order direction when order trigger parameters are reached
|
||||||
|
/// </summary>
|
||||||
|
public enum SharedTriggerOrderDirection
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Enter, Buy for Spot and long futures positions, Sell for short futures positions
|
||||||
|
/// </summary>
|
||||||
|
Enter,
|
||||||
|
/// <summary>
|
||||||
|
/// Exit, Sell for Spot and long futures positions, Buy for short futures positions
|
||||||
|
/// </summary>
|
||||||
|
Exit
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.SharedApis
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Trigger order status
|
||||||
|
/// </summary>
|
||||||
|
public enum SharedTriggerOrderStatus
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Order is active
|
||||||
|
/// </summary>
|
||||||
|
Active,
|
||||||
|
/// <summary>
|
||||||
|
/// Order has been filled
|
||||||
|
/// </summary>
|
||||||
|
Filled,
|
||||||
|
/// <summary>
|
||||||
|
/// Trigger canceled, can be user cancelation or system cancelation due to an error
|
||||||
|
/// </summary>
|
||||||
|
CanceledOrRejected,
|
||||||
|
/// <summary>
|
||||||
|
/// Trigger order has been triggered. Resulting order might be filled or not.
|
||||||
|
/// </summary>
|
||||||
|
Triggered
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.SharedApis
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Price direction for trigger order
|
||||||
|
/// </summary>
|
||||||
|
public enum SharedTriggerPriceDirection
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Trigger when the price goes below the specified trigger price
|
||||||
|
/// </summary>
|
||||||
|
PriceBelow,
|
||||||
|
/// <summary>
|
||||||
|
/// Trigger when the price goes above the specified trigger price
|
||||||
|
/// </summary>
|
||||||
|
PriceAbove
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.SharedApis
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Price direction for trigger order
|
||||||
|
/// </summary>
|
||||||
|
public enum SharedTriggerPriceType
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Last traded price
|
||||||
|
/// </summary>
|
||||||
|
LastPrice,
|
||||||
|
/// <summary>
|
||||||
|
/// Mark price
|
||||||
|
/// </summary>
|
||||||
|
MarkPrice,
|
||||||
|
/// <summary>
|
||||||
|
/// Index price
|
||||||
|
/// </summary>
|
||||||
|
IndexPrice
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -9,7 +9,7 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Client for managing futures orders using a client order id
|
/// Client for managing futures orders using a client order id
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IFuturesOrderClientIdClient : ISharedClient
|
public interface IFuturesOrderClientIdRestClient : ISharedClient
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Futures get order by client order id request options
|
/// Futures get order by client order id request options
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.SharedApis
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Take profit / Stop loss client
|
||||||
|
/// </summary>
|
||||||
|
public interface IFuturesTpSlRestClient : ISharedClient
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Set take profit and/or stop loss options
|
||||||
|
/// </summary>
|
||||||
|
EndpointOptions<SetTpSlRequest> SetFuturesTpSlOptions { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Set a take profit and/or stop loss for an open position
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">Request info</param>
|
||||||
|
/// <param name="ct">Cancellation token</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<ExchangeWebResult<SharedId>> SetFuturesTpSlAsync(SetTpSlRequest request, CancellationToken ct = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cancel a take profit and/or stop loss options
|
||||||
|
/// </summary>
|
||||||
|
EndpointOptions<CancelTpSlRequest> CancelFuturesTpSlOptions { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Cancel an active take profit and/or stop loss for an open position
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">Request info</param>
|
||||||
|
/// <param name="ct">Cancellation token</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<ExchangeWebResult<bool>> CancelFuturesTpSlAsync(CancelTpSlRequest request, CancellationToken ct = default);
|
||||||
|
}
|
||||||
|
}
|
||||||
+50
@@ -0,0 +1,50 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.SharedApis
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Client for placing trigger orders
|
||||||
|
/// </summary>
|
||||||
|
public interface IFuturesTriggerOrderRestClient : ISharedClient
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Place spot trigger order options
|
||||||
|
/// </summary>
|
||||||
|
PlaceFuturesTriggerOrderOptions PlaceFuturesTriggerOrderOptions { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Place a new trigger order
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">Request info</param>
|
||||||
|
/// <param name="ct">Cancellation token</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<ExchangeWebResult<SharedId>> PlaceFuturesTriggerOrderAsync(PlaceFuturesTriggerOrderRequest request, CancellationToken ct = default);
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get trigger order request options
|
||||||
|
/// </summary>
|
||||||
|
EndpointOptions<GetOrderRequest> GetFuturesTriggerOrderOptions { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Get info on a specific trigger order
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">Request info</param>
|
||||||
|
/// <param name="ct">Cancellation token</param>
|
||||||
|
Task<ExchangeWebResult<SharedFuturesTriggerOrder>> GetFuturesTriggerOrderAsync(GetOrderRequest request, CancellationToken ct = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cancel trigger order request options
|
||||||
|
/// </summary>
|
||||||
|
EndpointOptions<CancelOrderRequest> CancelFuturesTriggerOrderOptions { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Cancel a trigger order
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">Request info</param>
|
||||||
|
/// <param name="ct">Cancellation token</param>
|
||||||
|
Task<ExchangeWebResult<SharedId>> CancelFuturesTriggerOrderAsync(CancelOrderRequest request, CancellationToken ct = default);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Balances request options
|
/// Balances request options
|
||||||
/// </summary>
|
/// </summary>
|
||||||
EndpointOptions<GetBalancesRequest> GetBalancesOptions { get; }
|
GetBalancesOptions GetBalancesOptions { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get balances for the user
|
/// Get balances for the user
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.SharedApis
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Client for retrieving the current best bid/ask price
|
||||||
|
/// </summary>
|
||||||
|
public interface IBookTickerRestClient : ISharedClient
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Book ticker request options
|
||||||
|
/// </summary>
|
||||||
|
EndpointOptions<GetBookTickerRequest> GetBookTickerOptions { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the best ask/bid info for a symbol
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">Request info</param>
|
||||||
|
/// <param name="ct">Cancellation token</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<ExchangeWebResult<SharedBookTicker>> GetBookTickerAsync(GetBookTickerRequest request, CancellationToken ct = default);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.SharedApis
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Client for transferring funds between account types
|
||||||
|
/// </summary>
|
||||||
|
public interface ITransferRestClient : ISharedClient
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Transfer request options
|
||||||
|
/// </summary>
|
||||||
|
TransferOptions TransferOptions { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Transfer funds between account types
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">Request info</param>
|
||||||
|
/// <param name="ct">Cancellation token</param>
|
||||||
|
Task<ExchangeWebResult<SharedId>> TransferAsync(TransferRequest request, CancellationToken ct = default);
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -9,7 +9,7 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Client for managing spot orders using a client order id
|
/// Client for managing spot orders using a client order id
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface ISpotOrderClientIdClient : ISharedClient
|
public interface ISpotOrderClientIdRestClient : ISharedClient
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Spot get order by client order id request options
|
/// Spot get order by client order id request options
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.SharedApis
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Client for placing trigger orders
|
||||||
|
/// </summary>
|
||||||
|
public interface ISpotTriggerOrderRestClient : ISharedClient
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Place spot trigger order options
|
||||||
|
/// </summary>
|
||||||
|
PlaceSpotTriggerOrderOptions PlaceSpotTriggerOrderOptions { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Place a new trigger order
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">Request info</param>
|
||||||
|
/// <param name="ct">Cancellation token</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<ExchangeWebResult<SharedId>> PlaceSpotTriggerOrderAsync(PlaceSpotTriggerOrderRequest request, CancellationToken ct = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get trigger order request options
|
||||||
|
/// </summary>
|
||||||
|
EndpointOptions<GetOrderRequest> GetSpotTriggerOrderOptions { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Get info on a specific trigger order
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">Request info</param>
|
||||||
|
/// <param name="ct">Cancellation token</param>
|
||||||
|
Task<ExchangeWebResult<SharedSpotTriggerOrder>> GetSpotTriggerOrderAsync(GetOrderRequest request, CancellationToken ct = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cancel trigger order request options
|
||||||
|
/// </summary>
|
||||||
|
EndpointOptions<CancelOrderRequest> CancelSpotTriggerOrderOptions { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Cancel a trigger order
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">Request info</param>
|
||||||
|
/// <param name="ct">Cancellation token</param>
|
||||||
|
Task<ExchangeWebResult<SharedId>> CancelSpotTriggerOrderAsync(CancelOrderRequest request, CancellationToken ct = default);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -48,6 +48,7 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
WebCallResult<T> result,
|
WebCallResult<T> result,
|
||||||
INextPageToken? nextPageToken = null) :
|
INextPageToken? nextPageToken = null) :
|
||||||
base(result.ResponseStatusCode,
|
base(result.ResponseStatusCode,
|
||||||
|
result.HttpVersion,
|
||||||
result.ResponseHeaders,
|
result.ResponseHeaders,
|
||||||
result.ResponseTime,
|
result.ResponseTime,
|
||||||
result.ResponseLength,
|
result.ResponseLength,
|
||||||
@@ -75,6 +76,7 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
WebCallResult<T> result,
|
WebCallResult<T> result,
|
||||||
INextPageToken? nextPageToken = null) :
|
INextPageToken? nextPageToken = null) :
|
||||||
base(result.ResponseStatusCode,
|
base(result.ResponseStatusCode,
|
||||||
|
result.HttpVersion,
|
||||||
result.ResponseHeaders,
|
result.ResponseHeaders,
|
||||||
result.ResponseTime,
|
result.ResponseTime,
|
||||||
result.ResponseLength,
|
result.ResponseLength,
|
||||||
@@ -100,6 +102,7 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
string exchange,
|
string exchange,
|
||||||
TradingMode[]? dataTradeModes,
|
TradingMode[]? dataTradeModes,
|
||||||
HttpStatusCode? code,
|
HttpStatusCode? code,
|
||||||
|
Version? httpVersion,
|
||||||
KeyValuePair<string, string[]>[]? responseHeaders,
|
KeyValuePair<string, string[]>[]? responseHeaders,
|
||||||
TimeSpan? responseTime,
|
TimeSpan? responseTime,
|
||||||
long? responseLength,
|
long? responseLength,
|
||||||
@@ -114,6 +117,7 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
Error? error,
|
Error? error,
|
||||||
INextPageToken? nextPageToken = null) : base(
|
INextPageToken? nextPageToken = null) : base(
|
||||||
code,
|
code,
|
||||||
|
httpVersion,
|
||||||
responseHeaders,
|
responseHeaders,
|
||||||
responseTime,
|
responseTime,
|
||||||
responseLength,
|
responseLength,
|
||||||
@@ -140,7 +144,7 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public new ExchangeWebResult<K> As<K>([AllowNull] K data)
|
public new ExchangeWebResult<K> As<K>([AllowNull] K data)
|
||||||
{
|
{
|
||||||
return new ExchangeWebResult<K>(Exchange, DataTradeMode, ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, data, Error, NextPageToken);
|
return new ExchangeWebResult<K>(Exchange, DataTradeMode, ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, data, Error, NextPageToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|||||||
@@ -58,19 +58,19 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
public virtual Error? ValidateRequest(string exchange, ExchangeParameters? exchangeParameters, TradingMode? tradingMode, TradingMode[] supportedTradingModes)
|
public virtual Error? ValidateRequest(string exchange, ExchangeParameters? exchangeParameters, TradingMode? tradingMode, TradingMode[] supportedTradingModes)
|
||||||
{
|
{
|
||||||
if (tradingMode != null && !supportedTradingModes.Contains(tradingMode.Value))
|
if (tradingMode != null && !supportedTradingModes.Contains(tradingMode.Value))
|
||||||
return new ArgumentError($"ApiType.{tradingMode} is not supported, supported types: {string.Join(", ", supportedTradingModes)}");
|
return ArgumentError.Invalid("TradingMode", $"TradingMode.{tradingMode} is not supported, supported types: {string.Join(", ", supportedTradingModes)}");
|
||||||
|
|
||||||
foreach (var param in RequiredExchangeParameters)
|
foreach (var param in RequiredExchangeParameters)
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(param.Name))
|
if (!string.IsNullOrEmpty(param.Name))
|
||||||
{
|
{
|
||||||
if (ExchangeParameters.HasValue(exchangeParameters, exchange, param.Name!, param.ValueType) != true)
|
if (ExchangeParameters.HasValue(exchangeParameters, exchange, param.Name!, param.ValueType) != true)
|
||||||
return new ArgumentError($"Required exchange parameter `{param.Name}` for exchange `{exchange}` is missing or has incorrect type. Expected type is {param.ValueType.Name}. Example: {param.ExampleValue}");
|
return ArgumentError.Invalid(param.Name!, $"Required exchange parameter `{param.Name}` for exchange `{exchange}` is missing or has incorrect type. Expected type is {param.ValueType.Name}. Example: {param.ExampleValue}");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (param.Names!.All(x => ExchangeParameters.HasValue(exchangeParameters, exchange, x, param.ValueType) != true))
|
if (param.Names!.All(x => ExchangeParameters.HasValue(exchangeParameters, exchange, x, param.ValueType) != true))
|
||||||
return new ArgumentError($"One of exchange parameters `{string.Join(", ", param.Names!)}` for exchange `{exchange}` should be provided. Example: {param.ExampleValue}");
|
return ArgumentError.Invalid(string.Join("/", param.Names!), $"One of exchange parameters `{string.Join(", ", param.Names!)}` for exchange `{exchange}` should be provided. Example: {param.ExampleValue}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,6 +109,15 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public List<ParameterDescription> RequiredOptionalParameters { get; set; } = new List<ParameterDescription>();
|
public List<ParameterDescription> RequiredOptionalParameters { get; set; } = new List<ParameterDescription>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether this accepts multiple symbols (Only applicable to request requiring symbol parameters)
|
||||||
|
/// </summary>
|
||||||
|
public bool SupportsMultipleSymbols { get; set; } = false;
|
||||||
|
/// <summary>
|
||||||
|
/// The max number of symbols which can be passed in a call (Only applicable to request requiring symbol parameters)
|
||||||
|
/// </summary>
|
||||||
|
public int? MaxSymbolCount { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -131,12 +140,25 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
if (!string.IsNullOrEmpty(param.Name))
|
if (!string.IsNullOrEmpty(param.Name))
|
||||||
{
|
{
|
||||||
if (typeof(T).GetProperty(param.Name)!.GetValue(request, null) == null)
|
if (typeof(T).GetProperty(param.Name)!.GetValue(request, null) == null)
|
||||||
return new ArgumentError($"Required optional parameter `{param.Name}` for exchange `{exchange}` is missing. Example: {param.ExampleValue}");
|
return ArgumentError.Invalid(param.Name!, $"Required optional parameter `{param.Name}` for exchange `{exchange}` is missing. Example: {param.ExampleValue}");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (param.Names!.All(x => typeof(T).GetProperty(param.Name!)!.GetValue(request, null) == null))
|
if (param.Names!.All(x => typeof(T).GetProperty(param.Name!)!.GetValue(request, null) == null))
|
||||||
return new ArgumentError($"One of optional parameters `{string.Join(", ", param.Names!)}` for exchange `{exchange}` should be provided. Example: {param.ExampleValue}");
|
return ArgumentError.Invalid(string.Join("/", param.Names!), $"One of optional parameters `{string.Join(", ", param.Names!)}` for exchange `{exchange}` should be provided. Example: {param.ExampleValue}");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request is SharedSymbolRequest symbolsRequest)
|
||||||
|
{
|
||||||
|
if (symbolsRequest.Symbols != null)
|
||||||
|
{
|
||||||
|
if (!SupportsMultipleSymbols)
|
||||||
|
return ArgumentError.Invalid(nameof(SharedSymbolRequest.Symbols), $"Only a single symbol parameter is allowed, multiple symbols are not supported");
|
||||||
|
|
||||||
|
if (symbolsRequest.Symbols.Length > MaxSymbolCount)
|
||||||
|
return ArgumentError.Invalid(nameof(SharedSymbolRequest.Symbols), $"Max number of symbols is {MaxSymbolCount} but {symbolsRequest.Symbols.Length} were passed");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.SharedApis
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Options for requesting a transfer
|
||||||
|
/// </summary>
|
||||||
|
public class GetBalancesOptions : EndpointOptions<GetBalancesRequest>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Supported account types
|
||||||
|
/// </summary>
|
||||||
|
public AccountTypeFilter[] SupportedAccountTypes { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
public GetBalancesOptions(params AccountTypeFilter[] accountTypes) : base(true)
|
||||||
|
{
|
||||||
|
SupportedAccountTypes = accountTypes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validate a request
|
||||||
|
/// </summary>
|
||||||
|
public Error? ValidateRequest(
|
||||||
|
string exchange,
|
||||||
|
GetBalancesRequest request,
|
||||||
|
TradingMode[] supportedApiTypes)
|
||||||
|
{
|
||||||
|
if (request.AccountType != null && !IsValid(request.AccountType.Value))
|
||||||
|
return ArgumentError.Invalid(nameof(request.AccountType), "Invalid AccountType");
|
||||||
|
|
||||||
|
return base.ValidateRequest(exchange, request, null, supportedApiTypes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Is the account type valid for this client
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="accountType"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public bool IsValid(SharedAccountType accountType)
|
||||||
|
{
|
||||||
|
if (accountType == SharedAccountType.Funding)
|
||||||
|
return SupportedAccountTypes.Contains(AccountTypeFilter.Funding);
|
||||||
|
|
||||||
|
if (accountType == SharedAccountType.Spot)
|
||||||
|
return SupportedAccountTypes.Contains(AccountTypeFilter.Spot);
|
||||||
|
|
||||||
|
if (accountType == SharedAccountType.PerpetualLinearFutures
|
||||||
|
|| accountType == SharedAccountType.PerpetualInverseFutures
|
||||||
|
|| accountType == SharedAccountType.DeliveryLinearFutures
|
||||||
|
|| accountType == SharedAccountType.DeliveryInverseFutures)
|
||||||
|
{
|
||||||
|
return SupportedAccountTypes.Contains(AccountTypeFilter.Futures);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (accountType == SharedAccountType.CrossMargin
|
||||||
|
|| accountType == SharedAccountType.IsolatedMargin)
|
||||||
|
{
|
||||||
|
return SupportedAccountTypes.Contains(AccountTypeFilter.Margin);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SupportedAccountTypes.Contains(AccountTypeFilter.Option);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Account type filter
|
||||||
|
/// </summary>
|
||||||
|
public enum AccountTypeFilter
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Funding account
|
||||||
|
/// </summary>
|
||||||
|
Funding,
|
||||||
|
/// <summary>
|
||||||
|
/// Spot account
|
||||||
|
/// </summary>
|
||||||
|
Spot,
|
||||||
|
/// <summary>
|
||||||
|
/// Futures account
|
||||||
|
/// </summary>
|
||||||
|
Futures,
|
||||||
|
/// <summary>
|
||||||
|
/// Margin account
|
||||||
|
/// </summary>
|
||||||
|
Margin,
|
||||||
|
/// <summary>
|
||||||
|
/// Option account
|
||||||
|
/// </summary>
|
||||||
|
Option
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,7 +25,7 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
public override Error? ValidateRequest(string exchange, GetClosedOrdersRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes)
|
public override Error? ValidateRequest(string exchange, GetClosedOrdersRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes)
|
||||||
{
|
{
|
||||||
if (TimeFilterSupported && request.StartTime != null)
|
if (TimeFilterSupported && request.StartTime != null)
|
||||||
return new ArgumentError($"Time filter is not supported");
|
return ArgumentError.Invalid(nameof(GetClosedOrdersRequest.StartTime), $"Time filter is not supported");
|
||||||
|
|
||||||
return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes);
|
return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
public override Error? ValidateRequest(string exchange, GetDepositsRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes)
|
public override Error? ValidateRequest(string exchange, GetDepositsRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes)
|
||||||
{
|
{
|
||||||
if (TimeFilterSupported && request.StartTime != null)
|
if (TimeFilterSupported && request.StartTime != null)
|
||||||
return new ArgumentError($"Time filter is not supported");
|
return ArgumentError.Invalid(nameof(GetDepositsRequest.StartTime), $"Time filter is not supported");
|
||||||
|
|
||||||
return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes);
|
return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes);
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user