mirror of
https://github.com/JKorf/CryptoExchange.Net.git
synced 2026-08-12 00:43:03 +00:00
Compare commits
59 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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.6.0</PackageVersion>
|
||||||
|
<AssemblyVersion>9.6.0</AssemblyVersion>
|
||||||
|
<FileVersion>9.6.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.6.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,22 @@
|
|||||||
|
#  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.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,4 +1,5 @@
|
|||||||
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;
|
||||||
@@ -16,9 +17,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 +37,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 +72,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 +85,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 +98,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,7 +113,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
{
|
{
|
||||||
var result = new WebCallResult<TestObjectResult>(
|
var result = new WebCallResult<TestObjectResult>(
|
||||||
System.Net.HttpStatusCode.OK,
|
System.Net.HttpStatusCode.OK,
|
||||||
new List<KeyValuePair<string, IEnumerable<string>>>(),
|
new KeyValuePair<string, string[]>[0],
|
||||||
TimeSpan.FromSeconds(1),
|
TimeSpan.FromSeconds(1),
|
||||||
null,
|
null,
|
||||||
"{}",
|
"{}",
|
||||||
@@ -120,14 +121,14 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
"https://test.com/api",
|
"https://test.com/api",
|
||||||
null,
|
null,
|
||||||
HttpMethod.Get,
|
HttpMethod.Get,
|
||||||
new List<KeyValuePair<string, IEnumerable<string>>>(),
|
new KeyValuePair<string, string[]>[0],
|
||||||
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,7 +143,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
{
|
{
|
||||||
var result = new WebCallResult<TestObjectResult>(
|
var result = new WebCallResult<TestObjectResult>(
|
||||||
System.Net.HttpStatusCode.OK,
|
System.Net.HttpStatusCode.OK,
|
||||||
new List<KeyValuePair<string, IEnumerable<string>>>(),
|
new KeyValuePair<string, string[]>[0],
|
||||||
TimeSpan.FromSeconds(1),
|
TimeSpan.FromSeconds(1),
|
||||||
null,
|
null,
|
||||||
"{}",
|
"{}",
|
||||||
@@ -150,7 +151,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
"https://test.com/api",
|
"https://test.com/api",
|
||||||
null,
|
null,
|
||||||
HttpMethod.Get,
|
HttpMethod.Get,
|
||||||
new List<KeyValuePair<string, IEnumerable<string>>>(),
|
new KeyValuePair<string, string[]>[0],
|
||||||
ResultDataSource.Server,
|
ResultDataSource.Server,
|
||||||
new TestObjectResult(),
|
new TestObjectResult(),
|
||||||
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>
|
||||||
|
|||||||
@@ -1,248 +0,0 @@
|
|||||||
using CryptoExchange.Net.Attributes;
|
|
||||||
using CryptoExchange.Net.Converters;
|
|
||||||
using CryptoExchange.Net.Converters.JsonNet;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using NUnit.Framework;
|
|
||||||
using NUnit.Framework.Legacy;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests
|
|
||||||
{
|
|
||||||
[TestFixture()]
|
|
||||||
public class JsonNetConverterTests
|
|
||||||
{
|
|
||||||
[TestCase("2021-05-12")]
|
|
||||||
[TestCase("20210512")]
|
|
||||||
[TestCase("210512")]
|
|
||||||
[TestCase("1620777600.000")]
|
|
||||||
[TestCase("1620777600000")]
|
|
||||||
[TestCase("2021-05-12T00:00:00.000Z")]
|
|
||||||
[TestCase("2021-05-12T00:00:00.000000000Z")]
|
|
||||||
[TestCase("0.000000", true)]
|
|
||||||
[TestCase("0", true)]
|
|
||||||
[TestCase("", true)]
|
|
||||||
[TestCase(" ", true)]
|
|
||||||
public void TestDateTimeConverterString(string input, bool expectNull = false)
|
|
||||||
{
|
|
||||||
var output = JsonConvert.DeserializeObject<TimeObject>($"{{ \"time\": \"{input}\" }}");
|
|
||||||
Assert.That(output.Time == (expectNull ? null: new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc)));
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase(1620777600.000)]
|
|
||||||
[TestCase(1620777600000d)]
|
|
||||||
public void TestDateTimeConverterDouble(double input)
|
|
||||||
{
|
|
||||||
var output = JsonConvert.DeserializeObject<TimeObject>($"{{ \"time\": {input} }}");
|
|
||||||
Assert.That(output.Time == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase(1620777600)]
|
|
||||||
[TestCase(1620777600000)]
|
|
||||||
[TestCase(1620777600000000)]
|
|
||||||
[TestCase(1620777600000000000)]
|
|
||||||
[TestCase(0, true)]
|
|
||||||
public void TestDateTimeConverterLong(long input, bool expectNull = false)
|
|
||||||
{
|
|
||||||
var output = JsonConvert.DeserializeObject<TimeObject>($"{{ \"time\": {input} }}");
|
|
||||||
Assert.That(output.Time == (expectNull ? null : new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc)));
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase(1620777600)]
|
|
||||||
[TestCase(1620777600.000)]
|
|
||||||
public void TestDateTimeConverterFromSeconds(double input)
|
|
||||||
{
|
|
||||||
var output = DateTimeConverter.ConvertFromSeconds(input);
|
|
||||||
Assert.That(output == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void TestDateTimeConverterToSeconds()
|
|
||||||
{
|
|
||||||
var output = DateTimeConverter.ConvertToSeconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
|
||||||
Assert.That(output == 1620777600);
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase(1620777600000)]
|
|
||||||
[TestCase(1620777600000.000)]
|
|
||||||
public void TestDateTimeConverterFromMilliseconds(double input)
|
|
||||||
{
|
|
||||||
var output = DateTimeConverter.ConvertFromMilliseconds(input);
|
|
||||||
Assert.That(output == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void TestDateTimeConverterToMilliseconds()
|
|
||||||
{
|
|
||||||
var output = DateTimeConverter.ConvertToMilliseconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
|
||||||
Assert.That(output == 1620777600000);
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase(1620777600000000)]
|
|
||||||
public void TestDateTimeConverterFromMicroseconds(long input)
|
|
||||||
{
|
|
||||||
var output = DateTimeConverter.ConvertFromMicroseconds(input);
|
|
||||||
Assert.That(output == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void TestDateTimeConverterToMicroseconds()
|
|
||||||
{
|
|
||||||
var output = DateTimeConverter.ConvertToMicroseconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
|
||||||
Assert.That(output == 1620777600000000);
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase(1620777600000000000)]
|
|
||||||
public void TestDateTimeConverterFromNanoseconds(long input)
|
|
||||||
{
|
|
||||||
var output = DateTimeConverter.ConvertFromNanoseconds(input);
|
|
||||||
Assert.That(output == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void TestDateTimeConverterToNanoseconds()
|
|
||||||
{
|
|
||||||
var output = DateTimeConverter.ConvertToNanoseconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
|
||||||
Assert.That(output == 1620777600000000000);
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase()]
|
|
||||||
public void TestDateTimeConverterNull()
|
|
||||||
{
|
|
||||||
var output = JsonConvert.DeserializeObject<TimeObject>($"{{ \"time\": null }}");
|
|
||||||
Assert.That(output.Time == null);
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase(TestEnum.One, "1")]
|
|
||||||
[TestCase(TestEnum.Two, "2")]
|
|
||||||
[TestCase(TestEnum.Three, "three")]
|
|
||||||
[TestCase(TestEnum.Four, "Four")]
|
|
||||||
[TestCase(null, null)]
|
|
||||||
public void TestEnumConverterNullableGetStringTests(TestEnum? value, string expected)
|
|
||||||
{
|
|
||||||
var output = EnumConverter.GetString(value);
|
|
||||||
Assert.That(output == expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase(TestEnum.One, "1")]
|
|
||||||
[TestCase(TestEnum.Two, "2")]
|
|
||||||
[TestCase(TestEnum.Three, "three")]
|
|
||||||
[TestCase(TestEnum.Four, "Four")]
|
|
||||||
public void TestEnumConverterGetStringTests(TestEnum value, string expected)
|
|
||||||
{
|
|
||||||
var output = EnumConverter.GetString(value);
|
|
||||||
Assert.That(output == expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase("1", TestEnum.One)]
|
|
||||||
[TestCase("2", TestEnum.Two)]
|
|
||||||
[TestCase("3", TestEnum.Three)]
|
|
||||||
[TestCase("three", TestEnum.Three)]
|
|
||||||
[TestCase("Four", TestEnum.Four)]
|
|
||||||
[TestCase("four", TestEnum.Four)]
|
|
||||||
[TestCase("Four1", null)]
|
|
||||||
[TestCase(null, null)]
|
|
||||||
public void TestEnumConverterNullableDeserializeTests(string value, TestEnum? expected)
|
|
||||||
{
|
|
||||||
var val = value == null ? "null" : $"\"{value}\"";
|
|
||||||
var output = JsonConvert.DeserializeObject<EnumObject>($"{{ \"Value\": {val} }}");
|
|
||||||
Assert.That(output.Value == expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase("1", TestEnum.One)]
|
|
||||||
[TestCase("2", TestEnum.Two)]
|
|
||||||
[TestCase("3", TestEnum.Three)]
|
|
||||||
[TestCase("three", TestEnum.Three)]
|
|
||||||
[TestCase("Four", TestEnum.Four)]
|
|
||||||
[TestCase("four", TestEnum.Four)]
|
|
||||||
[TestCase("Four1", TestEnum.One)]
|
|
||||||
[TestCase(null, TestEnum.One)]
|
|
||||||
public void TestEnumConverterNotNullableDeserializeTests(string value, TestEnum? expected)
|
|
||||||
{
|
|
||||||
var val = value == null ? "null" : $"\"{value}\"";
|
|
||||||
var output = JsonConvert.DeserializeObject<NotNullableEnumObject>($"{{ \"Value\": {val} }}");
|
|
||||||
Assert.That(output.Value == expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase("1", true)]
|
|
||||||
[TestCase("true", true)]
|
|
||||||
[TestCase("yes", true)]
|
|
||||||
[TestCase("y", true)]
|
|
||||||
[TestCase("on", true)]
|
|
||||||
[TestCase("-1", false)]
|
|
||||||
[TestCase("0", false)]
|
|
||||||
[TestCase("n", false)]
|
|
||||||
[TestCase("no", false)]
|
|
||||||
[TestCase("false", false)]
|
|
||||||
[TestCase("off", false)]
|
|
||||||
[TestCase("", null)]
|
|
||||||
public void TestBoolConverter(string value, bool? expected)
|
|
||||||
{
|
|
||||||
var val = value == null ? "null" : $"\"{value}\"";
|
|
||||||
var output = JsonConvert.DeserializeObject<BoolObject>($"{{ \"Value\": {val} }}");
|
|
||||||
Assert.That(output.Value == expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase("1", true)]
|
|
||||||
[TestCase("true", true)]
|
|
||||||
[TestCase("yes", true)]
|
|
||||||
[TestCase("y", true)]
|
|
||||||
[TestCase("on", true)]
|
|
||||||
[TestCase("-1", false)]
|
|
||||||
[TestCase("0", false)]
|
|
||||||
[TestCase("n", false)]
|
|
||||||
[TestCase("no", false)]
|
|
||||||
[TestCase("false", false)]
|
|
||||||
[TestCase("off", false)]
|
|
||||||
[TestCase("", false)]
|
|
||||||
public void TestBoolConverterNotNullable(string value, bool expected)
|
|
||||||
{
|
|
||||||
var val = value == null ? "null" : $"\"{value}\"";
|
|
||||||
var output = JsonConvert.DeserializeObject<NotNullableBoolObject>($"{{ \"Value\": {val} }}");
|
|
||||||
Assert.That(output.Value == expected);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class TimeObject
|
|
||||||
{
|
|
||||||
[JsonConverter(typeof(DateTimeConverter))]
|
|
||||||
public DateTime? Time { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class EnumObject
|
|
||||||
{
|
|
||||||
public TestEnum? Value { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class NotNullableEnumObject
|
|
||||||
{
|
|
||||||
public TestEnum Value { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class BoolObject
|
|
||||||
{
|
|
||||||
[JsonConverter(typeof(BoolConverter))]
|
|
||||||
public bool? Value { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class NotNullableBoolObject
|
|
||||||
{
|
|
||||||
[JsonConverter(typeof(BoolConverter))]
|
|
||||||
public bool Value { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
[JsonConverter(typeof(EnumConverter))]
|
|
||||||
public enum TestEnum
|
|
||||||
{
|
|
||||||
[Map("1")]
|
|
||||||
One,
|
|
||||||
[Map("2")]
|
|
||||||
Two,
|
|
||||||
[Map("three", "3")]
|
|
||||||
Three,
|
|
||||||
Four
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +1,9 @@
|
|||||||
using CryptoExchange.Net.Authentication;
|
using CryptoExchange.Net.Objects;
|
||||||
using CryptoExchange.Net.Objects;
|
|
||||||
using CryptoExchange.Net.UnitTests.TestImplementations;
|
using CryptoExchange.Net.UnitTests.TestImplementations;
|
||||||
using Newtonsoft.Json;
|
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using CryptoExchange.Net.Interfaces;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
@@ -18,6 +13,7 @@ using System.Net;
|
|||||||
using CryptoExchange.Net.RateLimiting.Guards;
|
using CryptoExchange.Net.RateLimiting.Guards;
|
||||||
using CryptoExchange.Net.RateLimiting.Filters;
|
using CryptoExchange.Net.RateLimiting.Filters;
|
||||||
using CryptoExchange.Net.RateLimiting.Interfaces;
|
using CryptoExchange.Net.RateLimiting.Interfaces;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests
|
namespace CryptoExchange.Net.UnitTests
|
||||||
{
|
{
|
||||||
@@ -30,7 +26,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
// arrange
|
// arrange
|
||||||
var client = new TestRestClient();
|
var client = new TestRestClient();
|
||||||
var expected = new TestObject() { DecimalData = 1.23M, IntData = 10, StringData = "Some data" };
|
var expected = new TestObject() { DecimalData = 1.23M, IntData = 10, StringData = "Some data" };
|
||||||
client.SetResponse(JsonConvert.SerializeObject(expected), out _);
|
client.SetResponse(JsonSerializer.Serialize(expected, new JsonSerializerOptions { TypeInfoResolver = new TestSerializerContext() }), out _);
|
||||||
|
|
||||||
// act
|
// act
|
||||||
var result = client.Api1.Request<TestObject>().Result;
|
var result = client.Api1.Request<TestObject>().Result;
|
||||||
@@ -84,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]
|
||||||
@@ -102,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");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,7 +134,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
|
|
||||||
client.SetResponse("{}", out var request);
|
client.SetResponse("{}", out var request);
|
||||||
|
|
||||||
await client.Api1.RequestWithParams<TestObject>(new HttpMethod(method), new Dictionary<string, object>
|
await client.Api1.RequestWithParams<TestObject>(new HttpMethod(method), new ParameterCollection
|
||||||
{
|
{
|
||||||
{ "TestParam1", "Value1" },
|
{ "TestParam1", "Value1" },
|
||||||
{ "TestParam2", 2 },
|
{ "TestParam2", 2 },
|
||||||
@@ -188,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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Text.Json;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
@@ -9,7 +10,6 @@ using CryptoExchange.Net.UnitTests.TestImplementations;
|
|||||||
using CryptoExchange.Net.UnitTests.TestImplementations.Sockets;
|
using CryptoExchange.Net.UnitTests.TestImplementations.Sockets;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Moq;
|
using Moq;
|
||||||
using Newtonsoft.Json;
|
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using NUnit.Framework.Legacy;
|
using NUnit.Framework.Legacy;
|
||||||
|
|
||||||
@@ -103,7 +103,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
rstEvent.Set();
|
rstEvent.Set();
|
||||||
});
|
});
|
||||||
sub.AddSubscription(subObj);
|
sub.AddSubscription(subObj);
|
||||||
var msgToSend = JsonConvert.SerializeObject(new { topic = "topic", action = "update", property = 123 });
|
var msgToSend = JsonSerializer.Serialize(new { topic = "topic", action = "update", property = "123" });
|
||||||
|
|
||||||
// act
|
// act
|
||||||
socket.InvokeMessage(msgToSend);
|
socket.InvokeMessage(msgToSend);
|
||||||
@@ -198,7 +198,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
|
|
||||||
// act
|
// act
|
||||||
var sub = client.SubClient.SubscribeToSomethingAsync(channel, onUpdate => {}, ct: default);
|
var sub = client.SubClient.SubscribeToSomethingAsync(channel, onUpdate => {}, ct: default);
|
||||||
socket.InvokeMessage(JsonConvert.SerializeObject(new { channel, action = "subscribe", status = "error" }));
|
socket.InvokeMessage(JsonSerializer.Serialize(new { channel, action = "subscribe", status = "error" }));
|
||||||
await sub;
|
await sub;
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
@@ -221,7 +221,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
|
|
||||||
// act
|
// act
|
||||||
var sub = client.SubClient.SubscribeToSomethingAsync(channel, onUpdate => {}, ct: default);
|
var sub = client.SubClient.SubscribeToSomethingAsync(channel, onUpdate => {}, ct: default);
|
||||||
socket.InvokeMessage(JsonConvert.SerializeObject(new { channel, action = "subscribe", status = "confirmed" }));
|
socket.InvokeMessage(JsonSerializer.Serialize(new { channel, action = "subscribe", status = "confirmed" }));
|
||||||
await sub;
|
await sub;
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
@@ -146,7 +147,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
public void TestEnumConverterNullableDeserializeTests(string value, TestEnum? expected)
|
public void TestEnumConverterNullableDeserializeTests(string value, TestEnum? expected)
|
||||||
{
|
{
|
||||||
var val = value == null ? "null" : $"\"{value}\"";
|
var val = value == null ? "null" : $"\"{value}\"";
|
||||||
var output = JsonSerializer.Deserialize<STJEnumObject>($"{{ \"Value\": {val} }}");
|
var output = JsonSerializer.Deserialize<STJEnumObject>($"{{ \"Value\": {val} }}", SerializerOptions.WithConverters(new SerializationContext()));
|
||||||
Assert.That(output.Value == expected);
|
Assert.That(output.Value == expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,8 +172,8 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
[TestCase("three", TestEnum.Three)]
|
[TestCase("three", TestEnum.Three)]
|
||||||
[TestCase("Four", TestEnum.Four)]
|
[TestCase("Four", TestEnum.Four)]
|
||||||
[TestCase("four", TestEnum.Four)]
|
[TestCase("four", TestEnum.Four)]
|
||||||
[TestCase("Four1", TestEnum.One)]
|
[TestCase("Four1", null)]
|
||||||
[TestCase(null, TestEnum.One)]
|
[TestCase(null, null)]
|
||||||
public void TestEnumConverterParseStringTests(string value, TestEnum? expected)
|
public void TestEnumConverterParseStringTests(string value, TestEnum? expected)
|
||||||
{
|
{
|
||||||
var result = EnumConverter.ParseString<TestEnum>(value);
|
var result = EnumConverter.ParseString<TestEnum>(value);
|
||||||
@@ -194,7 +195,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
public void TestBoolConverter(string value, bool? expected)
|
public void TestBoolConverter(string value, bool? expected)
|
||||||
{
|
{
|
||||||
var val = value == null ? "null" : $"\"{value}\"";
|
var val = value == null ? "null" : $"\"{value}\"";
|
||||||
var output = JsonSerializer.Deserialize<STJBoolObject>($"{{ \"Value\": {val} }}");
|
var output = JsonSerializer.Deserialize<STJBoolObject>($"{{ \"Value\": {val} }}", SerializerOptions.WithConverters(new SerializationContext()));
|
||||||
Assert.That(output.Value == expected);
|
Assert.That(output.Value == expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,7 +214,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
public void TestBoolConverterNotNullable(string value, bool expected)
|
public void TestBoolConverterNotNullable(string value, bool expected)
|
||||||
{
|
{
|
||||||
var val = value == null ? "null" : $"\"{value}\"";
|
var val = value == null ? "null" : $"\"{value}\"";
|
||||||
var output = JsonSerializer.Deserialize<NotNullableSTJBoolObject>($"{{ \"Value\": {val} }}");
|
var output = JsonSerializer.Deserialize<NotNullableSTJBoolObject>($"{{ \"Value\": {val} }}", SerializerOptions.WithConverters(new SerializationContext()));
|
||||||
Assert.That(output.Value == expected);
|
Assert.That(output.Value == expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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)]
|
||||||
@@ -265,9 +270,22 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
Prop31 = 4,
|
Prop31 = 4,
|
||||||
Prop32 = "789"
|
Prop32 = "789"
|
||||||
},
|
},
|
||||||
Prop7 = TestEnum.Two
|
Prop7 = TestEnum.Two,
|
||||||
|
TestInternal = new Test
|
||||||
|
{
|
||||||
|
Prop1 = 10
|
||||||
|
},
|
||||||
|
Prop8 = new Test3
|
||||||
|
{
|
||||||
|
Prop31 = 5,
|
||||||
|
Prop32 = "101"
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var options = new JsonSerializerOptions()
|
||||||
|
{
|
||||||
|
TypeInfoResolver = new SerializationContext()
|
||||||
|
};
|
||||||
var serialized = JsonSerializer.Serialize(data);
|
var serialized = JsonSerializer.Serialize(data);
|
||||||
var deserialized = JsonSerializer.Deserialize<Test>(serialized);
|
var deserialized = JsonSerializer.Deserialize<Test>(serialized);
|
||||||
|
|
||||||
@@ -281,6 +299,43 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
Assert.That(deserialized.Prop6.Prop31, Is.EqualTo(4));
|
Assert.That(deserialized.Prop6.Prop31, Is.EqualTo(4));
|
||||||
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.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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -300,29 +355,25 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
|
|
||||||
public class STJEnumObject
|
public class STJEnumObject
|
||||||
{
|
{
|
||||||
[JsonConverter(typeof(EnumConverter))]
|
|
||||||
public TestEnum? Value { get; set; }
|
public TestEnum? Value { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class NotNullableSTJEnumObject
|
public class NotNullableSTJEnumObject
|
||||||
{
|
{
|
||||||
[JsonConverter(typeof(EnumConverter))]
|
|
||||||
public TestEnum Value { get; set; }
|
public TestEnum Value { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class STJBoolObject
|
public class STJBoolObject
|
||||||
{
|
{
|
||||||
[JsonConverter(typeof(BoolConverter))]
|
|
||||||
public bool? Value { get; set; }
|
public bool? Value { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class NotNullableSTJBoolObject
|
public class NotNullableSTJBoolObject
|
||||||
{
|
{
|
||||||
[JsonConverter(typeof(BoolConverter))]
|
|
||||||
public bool Value { get; set; }
|
public bool Value { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
[JsonConverter(typeof(ArrayConverter))]
|
[JsonConverter(typeof(ArrayConverter<Test>))]
|
||||||
record Test
|
record Test
|
||||||
{
|
{
|
||||||
[ArrayProperty(0)]
|
[ArrayProperty(0)]
|
||||||
@@ -339,11 +390,15 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
public Test2 Prop5 { get; set; }
|
public Test2 Prop5 { get; set; }
|
||||||
[ArrayProperty(5)]
|
[ArrayProperty(5)]
|
||||||
public Test3 Prop6 { get; set; }
|
public Test3 Prop6 { get; set; }
|
||||||
[ArrayProperty(6), JsonConverter(typeof(EnumConverter))]
|
[ArrayProperty(6), JsonConverter(typeof(EnumConverter<TestEnum>))]
|
||||||
public TestEnum? Prop7 { get; set; }
|
public TestEnum? Prop7 { get; set; }
|
||||||
|
[ArrayProperty(7)]
|
||||||
|
public Test TestInternal { get; set; }
|
||||||
|
[ArrayProperty(8), JsonConversion]
|
||||||
|
public Test3 Prop8 { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
[JsonConverter(typeof(ArrayConverter))]
|
[JsonConverter(typeof(ArrayConverter<Test2>))]
|
||||||
record Test2
|
record Test2
|
||||||
{
|
{
|
||||||
[ArrayProperty(0)]
|
[ArrayProperty(0)]
|
||||||
@@ -359,4 +414,29 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
[JsonPropertyName("prop32")]
|
[JsonPropertyName("prop32")]
|
||||||
public string Prop32 { get; set; }
|
public string Prop32 { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[JsonConverter(typeof(EnumConverter<TestEnum>))]
|
||||||
|
public enum TestEnum
|
||||||
|
{
|
||||||
|
[Map("1")]
|
||||||
|
One,
|
||||||
|
[Map("2")]
|
||||||
|
Two,
|
||||||
|
[Map("three", "3")]
|
||||||
|
Three,
|
||||||
|
Four
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonSerializable(typeof(Test))]
|
||||||
|
[JsonSerializable(typeof(Test2))]
|
||||||
|
[JsonSerializable(typeof(Test3))]
|
||||||
|
[JsonSerializable(typeof(NotNullableSTJBoolObject))]
|
||||||
|
[JsonSerializable(typeof(STJBoolObject))]
|
||||||
|
[JsonSerializable(typeof(NotNullableSTJEnumObject))]
|
||||||
|
[JsonSerializable(typeof(STJEnumObject))]
|
||||||
|
[JsonSerializable(typeof(STJDecimalObject))]
|
||||||
|
[JsonSerializable(typeof(STJTimeObject))]
|
||||||
|
internal partial class SerializationContext : JsonSerializerContext
|
||||||
|
{
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,52 +1,50 @@
|
|||||||
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 Newtonsoft.Json;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
|
namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
|
||||||
{
|
{
|
||||||
internal class SubResponse
|
internal class SubResponse
|
||||||
{
|
{
|
||||||
|
|
||||||
[JsonProperty("action")]
|
[JsonPropertyName("action")]
|
||||||
public string Action { get; set; } = null!;
|
public string Action { get; set; } = null!;
|
||||||
|
|
||||||
[JsonProperty("channel")]
|
[JsonPropertyName("channel")]
|
||||||
public string Channel { get; set; } = null!;
|
public string Channel { get; set; } = null!;
|
||||||
|
|
||||||
[JsonProperty("status")]
|
[JsonPropertyName("status")]
|
||||||
public string Status { get; set; } = null!;
|
public string Status { get; set; } = null!;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class UnsubResponse
|
internal class UnsubResponse
|
||||||
{
|
{
|
||||||
[JsonProperty("action")]
|
[JsonPropertyName("action")]
|
||||||
public string Action { get; set; } = null!;
|
public string Action { get; set; } = null!;
|
||||||
|
|
||||||
[JsonProperty("status")]
|
[JsonPropertyName("status")]
|
||||||
public string Status { get; set; } = null!;
|
public string Status { get; set; } = null!;
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,14 +3,19 @@ using System.Collections.Generic;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CryptoExchange.Net.Authentication;
|
using CryptoExchange.Net.Authentication;
|
||||||
using CryptoExchange.Net.Clients;
|
using CryptoExchange.Net.Clients;
|
||||||
|
using CryptoExchange.Net.Converters.SystemTextJson;
|
||||||
|
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
|
||||||
{
|
{
|
||||||
@@ -21,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()));
|
||||||
}
|
}
|
||||||
@@ -49,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;
|
||||||
@@ -59,6 +66,8 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
|
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
|
||||||
public override TimeSpan? GetTimeOffset() => null;
|
public override TimeSpan? GetTimeOffset() => null;
|
||||||
public override TimeSyncInfo GetTimeSyncInfo() => null;
|
public override TimeSyncInfo GetTimeSyncInfo() => null;
|
||||||
|
protected override IStreamMessageAccessor CreateAccessor() => new SystemTextJsonStreamMessageAccessor(new System.Text.Json.JsonSerializerOptions());
|
||||||
|
protected override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(new System.Text.Json.JsonSerializerOptions());
|
||||||
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials) => throw new NotImplementedException();
|
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials) => throw new NotImplementedException();
|
||||||
protected override Task<WebCallResult<DateTime>> GetServerTimestampAsync() => throw new NotImplementedException();
|
protected override Task<WebCallResult<DateTime>> GetServerTimestampAsync() => throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
@@ -69,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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
using Newtonsoft.Json;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||||
{
|
{
|
||||||
public class TestObject
|
public class TestObject
|
||||||
{
|
{
|
||||||
[JsonProperty("other")]
|
[JsonPropertyName("other")]
|
||||||
public string StringData { get; set; }
|
public string StringData { get; set; }
|
||||||
|
[JsonPropertyName("intData")]
|
||||||
public int IntData { get; set; }
|
public int IntData { get; set; }
|
||||||
|
[JsonPropertyName("decimalData")]
|
||||||
public decimal DecimalData { get; set; }
|
public decimal DecimalData { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces;
|
||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
using Moq;
|
using Moq;
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
@@ -12,11 +11,14 @@ using System.Threading;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CryptoExchange.Net.Authentication;
|
using CryptoExchange.Net.Authentication;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using CryptoExchange.Net.Objects.Options;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using CryptoExchange.Net.Clients;
|
using CryptoExchange.Net.Clients;
|
||||||
using CryptoExchange.Net.SharedApis;
|
using CryptoExchange.Net.SharedApis;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
using System.Linq;
|
||||||
|
using CryptoExchange.Net.Converters.SystemTextJson;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using CryptoExchange.Net.Objects.Errors;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||||
{
|
{
|
||||||
@@ -49,13 +51,13 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
response.Setup(c => c.IsSuccessStatusCode).Returns(true);
|
response.Setup(c => c.IsSuccessStatusCode).Returns(true);
|
||||||
response.Setup(c => c.GetResponseStreamAsync()).Returns(Task.FromResult((Stream)responseStream));
|
response.Setup(c => c.GetResponseStreamAsync()).Returns(Task.FromResult((Stream)responseStream));
|
||||||
|
|
||||||
var headers = new Dictionary<string, IEnumerable<string>>();
|
var headers = new Dictionary<string, string[]>();
|
||||||
var request = new Mock<IRequest>();
|
var request = new Mock<IRequest>();
|
||||||
request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com"));
|
request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com"));
|
||||||
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Returns(Task.FromResult(response.Object));
|
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Returns(Task.FromResult(response.Object));
|
||||||
request.Setup(c => c.SetContent(It.IsAny<string>(), It.IsAny<string>())).Callback(new Action<string, string>((content, type) => { request.Setup(r => r.Content).Returns(content); }));
|
request.Setup(c => c.SetContent(It.IsAny<string>(), It.IsAny<string>())).Callback(new Action<string, string>((content, type) => { request.Setup(r => r.Content).Returns(content); }));
|
||||||
request.Setup(c => c.AddHeader(It.IsAny<string>(), It.IsAny<string>())).Callback<string, string>((key, val) => headers.Add(key, new List<string> { val }));
|
request.Setup(c => c.AddHeader(It.IsAny<string>(), It.IsAny<string>())).Callback<string, string>((key, val) => headers.Add(key, new string[] { val }));
|
||||||
request.Setup(c => c.GetHeaders()).Returns(() => headers);
|
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<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||||
@@ -84,7 +86,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
|
|
||||||
var request = new Mock<IRequest>();
|
var request = new Mock<IRequest>();
|
||||||
request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com"));
|
request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com"));
|
||||||
request.Setup(c => c.GetHeaders()).Returns(new Dictionary<string, IEnumerable<string>>());
|
request.Setup(c => c.GetHeaders()).Returns(new KeyValuePair<string, string[]>[0]);
|
||||||
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);
|
||||||
@@ -108,12 +110,12 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
response.Setup(c => c.IsSuccessStatusCode).Returns(false);
|
response.Setup(c => c.IsSuccessStatusCode).Returns(false);
|
||||||
response.Setup(c => c.GetResponseStreamAsync()).Returns(Task.FromResult((Stream)responseStream));
|
response.Setup(c => c.GetResponseStreamAsync()).Returns(Task.FromResult((Stream)responseStream));
|
||||||
|
|
||||||
var headers = new Dictionary<string, IEnumerable<string>>();
|
var headers = new List<KeyValuePair<string, string[]>>();
|
||||||
var request = new Mock<IRequest>();
|
var request = new Mock<IRequest>();
|
||||||
request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com"));
|
request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com"));
|
||||||
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Returns(Task.FromResult(response.Object));
|
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Returns(Task.FromResult(response.Object));
|
||||||
request.Setup(c => c.AddHeader(It.IsAny<string>(), It.IsAny<string>())).Callback<string, string>((key, val) => headers.Add(key, new List<string> { val }));
|
request.Setup(c => c.AddHeader(It.IsAny<string>(), It.IsAny<string>())).Callback<string, string>((key, val) => headers.Add(new KeyValuePair<string, string[]>(key, new string[] { val })));
|
||||||
request.Setup(c => c.GetHeaders()).Returns(headers);
|
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<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||||
@@ -137,14 +139,17 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
|
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
|
||||||
|
|
||||||
|
protected override IStreamMessageAccessor CreateAccessor() => new SystemTextJsonStreamMessageAccessor(new System.Text.Json.JsonSerializerOptions() { TypeInfoResolver = new TestSerializerContext() });
|
||||||
|
protected override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(new System.Text.Json.JsonSerializerOptions());
|
||||||
|
|
||||||
public async Task<CallResult<T>> Request<T>(CancellationToken ct = default) where T : class
|
public async Task<CallResult<T>> Request<T>(CancellationToken ct = default) where T : class
|
||||||
{
|
{
|
||||||
return await SendRequestAsync<T>(new Uri("http://www.test.com"), HttpMethod.Get, ct, requestWeight: 0);
|
return await SendAsync<T>("http://www.test.com", new RequestDefinition("/", HttpMethod.Get) { Weight = 0 }, null, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<CallResult<T>> RequestWithParams<T>(HttpMethod method, Dictionary<string, object> parameters, Dictionary<string, string> headers) where T : class
|
public async Task<CallResult<T>> RequestWithParams<T>(HttpMethod method, ParameterCollection parameters, Dictionary<string, string> headers) where T : class
|
||||||
{
|
{
|
||||||
return await SendRequestAsync<T>(new Uri("http://www.test.com"), method, default, parameters, requestWeight: 0, additionalHeaders: headers);
|
return await SendAsync<T>("http://www.test.com", new RequestDefinition("/", method) { Weight = 0 }, parameters, default, additionalHeaders: headers);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetParameterPosition(HttpMethod method, HttpMethodParameterPosition position)
|
public void SetParameterPosition(HttpMethod method, HttpMethodParameterPosition position)
|
||||||
@@ -178,19 +183,22 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
RequestFactory = new Mock<IRequestFactory>().Object;
|
RequestFactory = new Mock<IRequestFactory>().Object;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override IStreamMessageAccessor CreateAccessor() => new SystemTextJsonStreamMessageAccessor(new System.Text.Json.JsonSerializerOptions());
|
||||||
|
protected override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(new System.Text.Json.JsonSerializerOptions());
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
|
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
|
||||||
|
|
||||||
public async Task<CallResult<T>> Request<T>(CancellationToken ct = default) where T : class
|
public async Task<CallResult<T>> Request<T>(CancellationToken ct = default) where T : class
|
||||||
{
|
{
|
||||||
return await SendRequestAsync<T>(new Uri("http://www.test.com"), HttpMethod.Get, ct, requestWeight: 0);
|
return await SendAsync<T>("http://www.test.com", new RequestDefinition("/", HttpMethod.Get) { Weight = 0 }, null, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Error ParseErrorResponse(int httpStatusCode, IEnumerable<KeyValuePair<string, IEnumerable<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()
|
||||||
@@ -214,7 +222,9 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
|
|
||||||
public class TestError
|
public class TestError
|
||||||
{
|
{
|
||||||
|
[JsonPropertyName("errorCode")]
|
||||||
public int ErrorCode { get; set; }
|
public int ErrorCode { get; set; }
|
||||||
|
[JsonPropertyName("errorMessage")]
|
||||||
public string ErrorMessage { get; set; }
|
public string ErrorMessage { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ using Moq;
|
|||||||
using CryptoExchange.Net.Testing.Implementations;
|
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 System.Net.WebSockets;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||||
{
|
{
|
||||||
@@ -97,6 +99,9 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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());
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
|
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
|
||||||
|
|
||||||
@@ -110,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";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using CryptoExchange.Net.UnitTests.TestImplementations;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.UnitTests
|
||||||
|
{
|
||||||
|
[JsonSerializable(typeof(string))]
|
||||||
|
[JsonSerializable(typeof(int))]
|
||||||
|
[JsonSerializable(typeof(Dictionary<string, string>))]
|
||||||
|
[JsonSerializable(typeof(IDictionary<string, string>))]
|
||||||
|
[JsonSerializable(typeof(Dictionary<string, object>))]
|
||||||
|
[JsonSerializable(typeof(IDictionary<string, object>))]
|
||||||
|
[JsonSerializable(typeof(TestObject))]
|
||||||
|
internal partial class TestSerializerContext : JsonSerializerContext
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -20,6 +20,11 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string Secret { get; set; }
|
public string Secret { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The api passphrase. Not needed on all exchanges
|
||||||
|
/// </summary>
|
||||||
|
public string? Pass { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Type of the credentials
|
/// Type of the credentials
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -30,8 +35,9 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="key">The api key / label used for identification</param>
|
/// <param name="key">The api key / label used for identification</param>
|
||||||
/// <param name="secret">The api secret or private key used for signing</param>
|
/// <param name="secret">The api secret or private key used for signing</param>
|
||||||
|
/// <param name="pass">The api pass for the key. Not always needed</param>
|
||||||
/// <param name="credentialType">The type of credentials</param>
|
/// <param name="credentialType">The type of credentials</param>
|
||||||
public ApiCredentials(string key, string secret, ApiCredentialsType credentialType = ApiCredentialsType.Hmac)
|
public ApiCredentials(string key, string secret, string? pass = null, ApiCredentialsType credentialType = ApiCredentialsType.Hmac)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(secret))
|
if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(secret))
|
||||||
throw new ArgumentException("Key and secret can't be null/empty");
|
throw new ArgumentException("Key and secret can't be null/empty");
|
||||||
@@ -39,6 +45,7 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
CredentialType = credentialType;
|
CredentialType = credentialType;
|
||||||
Key = key;
|
Key = key;
|
||||||
Secret = secret;
|
Secret = secret;
|
||||||
|
Pass = pass;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -47,28 +54,7 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public virtual ApiCredentials Copy()
|
public virtual ApiCredentials Copy()
|
||||||
{
|
{
|
||||||
return new ApiCredentials(Key, Secret, CredentialType);
|
return new ApiCredentials(Key, Secret, Pass, CredentialType);
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create Api credentials providing a stream containing json data. The json data should include two values: apiKey and apiSecret
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="inputStream">The stream containing the json data</param>
|
|
||||||
/// <param name="identifierKey">A key to identify the credentials for the API. For example, when set to `binanceKey` the json data should contain a value for the property `binanceKey`. Defaults to 'apiKey'.</param>
|
|
||||||
/// <param name="identifierSecret">A key to identify the credentials for the API. For example, when set to `binanceSecret` the json data should contain a value for the property `binanceSecret`. Defaults to 'apiSecret'.</param>
|
|
||||||
public static ApiCredentials FromStream(Stream inputStream, string? identifierKey = null, string? identifierSecret = null)
|
|
||||||
{
|
|
||||||
var accessor = new SystemTextJsonStreamMessageAccessor();
|
|
||||||
if (!accessor.Read(inputStream, false).Result)
|
|
||||||
throw new ArgumentException("Input stream not valid json data");
|
|
||||||
|
|
||||||
var key = accessor.GetValue<string>(MessagePath.Get().Property(identifierKey ?? "apiKey"));
|
|
||||||
var secret = accessor.GetValue<string>(MessagePath.Get().Property(identifierSecret ?? "apiSecret"));
|
|
||||||
if (key == null || secret == null)
|
|
||||||
throw new ArgumentException("apiKey or apiSecret value not found in Json credential file");
|
|
||||||
|
|
||||||
inputStream.Seek(0, SeekOrigin.Begin);
|
|
||||||
return new ApiCredentials(key, secret);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,10 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
/// Get the API key of the current credentials
|
/// Get the API key of the current credentials
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string ApiKey => _credentials.Key!;
|
public string ApiKey => _credentials.Key!;
|
||||||
|
/// <summary>
|
||||||
|
/// Get the Passphrase of the current credentials
|
||||||
|
/// </summary>
|
||||||
|
public string? Pass => _credentials.Pass;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
@@ -47,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
|
||||||
@@ -461,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>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces;
|
||||||
using CryptoExchange.Net.Interfaces.CommonClients;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@@ -24,24 +23,5 @@ namespace CryptoExchange.Net.Clients
|
|||||||
public CryptoRestClient(IServiceProvider serviceProvider) : base(serviceProvider)
|
public CryptoRestClient(IServiceProvider serviceProvider) : base(serviceProvider)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get a list of the registered ISpotClient implementations
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
public IEnumerable<ISpotClient> GetSpotClients()
|
|
||||||
{
|
|
||||||
if (_serviceProvider == null)
|
|
||||||
return new List<ISpotClient>();
|
|
||||||
|
|
||||||
return _serviceProvider.GetServices<ISpotClient>().ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get an ISpotClient implementation by exchange name
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="exchangeName"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public ISpotClient? SpotClient(string exchangeName) => _serviceProvider?.GetServices<ISpotClient>()?.SingleOrDefault(s => s.ExchangeName.Equals(exchangeName, StringComparison.InvariantCultureIgnoreCase));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
@@ -9,10 +8,10 @@ using System.Net.Http;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CryptoExchange.Net.Caching;
|
using CryptoExchange.Net.Caching;
|
||||||
using CryptoExchange.Net.Converters.JsonNet;
|
|
||||||
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;
|
||||||
@@ -56,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
|
||||||
@@ -114,13 +113,13 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// Create a message accessor instance
|
/// Create a message accessor instance
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected virtual IStreamMessageAccessor CreateAccessor() => new JsonNetStreamMessageAccessor();
|
protected abstract IStreamMessageAccessor CreateAccessor();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a serializer instance
|
/// Create a serializer instance
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected virtual IMessageSerializer CreateSerializer() => new JsonNetMessageSerializer();
|
protected abstract IMessageSerializer CreateSerializer();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Send a request to the base address based on the request definition
|
/// Send a request to the base address based on the request definition
|
||||||
@@ -240,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
|
||||||
{
|
{
|
||||||
@@ -343,7 +343,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new CallResult(null);
|
return CallResult.SuccessResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -364,296 +364,73 @@ 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)
|
|
||||||
uri = uri.SetParameters(uriParams, arraySerialization);
|
|
||||||
|
|
||||||
var request = RequestFactory.Create(definition.Method, uri, requestId);
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Execute a request to the uri and returns if it was successful
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="uri">The uri to send the request to</param>
|
|
||||||
/// <param name="method">The method of the request</param>
|
|
||||||
/// <param name="cancellationToken">Cancellation token</param>
|
|
||||||
/// <param name="parameters">The parameters of the request</param>
|
|
||||||
/// <param name="signed">Whether or not the request should be authenticated</param>
|
|
||||||
/// <param name="requestBodyFormat">The format of the body content</param>
|
|
||||||
/// <param name="parameterPosition">Where the parameters should be placed, overwrites the value set in the client</param>
|
|
||||||
/// <param name="arraySerialization">How array parameters should be serialized, overwrites the value set in the client</param>
|
|
||||||
/// <param name="requestWeight">Credits used for the request</param>
|
|
||||||
/// <param name="additionalHeaders">Additional headers to send with the request</param>
|
|
||||||
/// <param name="gate">The ratelimit gate to use</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[return: NotNull]
|
|
||||||
protected virtual async Task<WebCallResult> SendRequestAsync(
|
|
||||||
Uri uri,
|
|
||||||
HttpMethod method,
|
|
||||||
CancellationToken cancellationToken,
|
|
||||||
Dictionary<string, object>? parameters = null,
|
|
||||||
bool signed = false,
|
|
||||||
RequestBodyFormat? requestBodyFormat = null,
|
|
||||||
HttpMethodParameterPosition? parameterPosition = null,
|
|
||||||
ArrayParametersSerialization? arraySerialization = null,
|
|
||||||
int requestWeight = 1,
|
|
||||||
Dictionary<string, string>? additionalHeaders = null,
|
|
||||||
IRateLimitGate? gate = null)
|
|
||||||
{
|
|
||||||
int currentTry = 0;
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
currentTry++;
|
|
||||||
var request = await PrepareRequestAsync(uri, method, cancellationToken, parameters, signed, requestBodyFormat, parameterPosition, arraySerialization, requestWeight, additionalHeaders, gate).ConfigureAwait(false);
|
|
||||||
if (!request)
|
|
||||||
return new WebCallResult(request.Error!);
|
|
||||||
|
|
||||||
var result = await GetResponseAsync<object>(request.Data, gate, cancellationToken).ConfigureAwait(false);
|
|
||||||
if (!result)
|
|
||||||
_logger.RestApiErrorReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), result.Error?.ToString());
|
|
||||||
else
|
|
||||||
_logger.RestApiResponseReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), OutputOriginalData ? result.OriginalData : "[Data only available when OutputOriginal = true]");
|
|
||||||
|
|
||||||
if (await ShouldRetryRequestAsync(gate, result, currentTry).ConfigureAwait(false))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
return result.AsDataless();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Execute a request to the uri and deserialize the response into the provided type parameter
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T">The type to deserialize into</typeparam>
|
|
||||||
/// <param name="uri">The uri to send the request to</param>
|
|
||||||
/// <param name="method">The method of the request</param>
|
|
||||||
/// <param name="cancellationToken">Cancellation token</param>
|
|
||||||
/// <param name="parameters">The parameters of the request</param>
|
|
||||||
/// <param name="signed">Whether or not the request should be authenticated</param>
|
|
||||||
/// <param name="requestBodyFormat">The format of the body content</param>
|
|
||||||
/// <param name="parameterPosition">Where the parameters should be placed, overwrites the value set in the client</param>
|
|
||||||
/// <param name="arraySerialization">How array parameters should be serialized, overwrites the value set in the client</param>
|
|
||||||
/// <param name="requestWeight">Credits used for the request</param>
|
|
||||||
/// <param name="additionalHeaders">Additional headers to send with the request</param>
|
|
||||||
/// <param name="gate">The ratelimit gate to use</param>
|
|
||||||
/// <param name="preventCaching">Whether caching should be prevented for this request</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[return: NotNull]
|
|
||||||
protected virtual async Task<WebCallResult<T>> SendRequestAsync<T>(
|
|
||||||
Uri uri,
|
|
||||||
HttpMethod method,
|
|
||||||
CancellationToken cancellationToken,
|
|
||||||
Dictionary<string, object>? parameters = null,
|
|
||||||
bool signed = false,
|
|
||||||
RequestBodyFormat? requestBodyFormat = null,
|
|
||||||
HttpMethodParameterPosition? parameterPosition = null,
|
|
||||||
ArrayParametersSerialization? arraySerialization = null,
|
|
||||||
int requestWeight = 1,
|
|
||||||
Dictionary<string, string>? additionalHeaders = null,
|
|
||||||
IRateLimitGate? gate = null,
|
|
||||||
bool preventCaching = false
|
|
||||||
) where T : class
|
|
||||||
{
|
|
||||||
var key = uri.ToString() + method + signed + parameters?.ToFormData();
|
|
||||||
if (ShouldCache(method) && !preventCaching)
|
|
||||||
{
|
|
||||||
_logger.CheckingCache(key);
|
|
||||||
var cachedValue = _cache.Get(key, ClientOptions.CachingMaxAge);
|
|
||||||
if (cachedValue != null)
|
|
||||||
{
|
|
||||||
_logger.CacheHit(key);
|
|
||||||
var original = (WebCallResult<T>)cachedValue;
|
|
||||||
return original.Cached();
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.CacheNotHit(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
int currentTry = 0;
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
currentTry++;
|
|
||||||
var request = await PrepareRequestAsync(uri, method, cancellationToken, parameters, signed, requestBodyFormat, parameterPosition, arraySerialization, requestWeight, additionalHeaders, gate).ConfigureAwait(false);
|
|
||||||
if (!request)
|
|
||||||
return new WebCallResult<T>(request.Error!);
|
|
||||||
|
|
||||||
var result = await GetResponseAsync<T>(request.Data, gate, cancellationToken).ConfigureAwait(false);
|
|
||||||
if (!result)
|
|
||||||
_logger.RestApiErrorReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), result.Error?.ToString());
|
|
||||||
else
|
|
||||||
_logger.RestApiResponseReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), OutputOriginalData ? result.OriginalData : "[Data only available when OutputOriginal = true]");
|
|
||||||
|
|
||||||
if (await ShouldRetryRequestAsync(gate, result, currentTry).ConfigureAwait(false))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (result.Success &&
|
|
||||||
ShouldCache(method) &&
|
|
||||||
!preventCaching)
|
|
||||||
{
|
|
||||||
_cache.Add(key, result);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Prepares a request to be sent to the server
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="uri">The uri to send the request to</param>
|
|
||||||
/// <param name="method">The method of the request</param>
|
|
||||||
/// <param name="cancellationToken">Cancellation token</param>
|
|
||||||
/// <param name="parameters">The parameters of the request</param>
|
|
||||||
/// <param name="signed">Whether or not the request should be authenticated</param>
|
|
||||||
/// <param name="requestBodyFormat">The format of the body content</param>
|
|
||||||
/// <param name="parameterPosition">Where the parameters should be placed, overwrites the value set in the client</param>
|
|
||||||
/// <param name="arraySerialization">How array parameters should be serialized, overwrites the value set in the client</param>
|
|
||||||
/// <param name="requestWeight">Credits used for the request</param>
|
|
||||||
/// <param name="additionalHeaders">Additional headers to send with the request</param>
|
|
||||||
/// <param name="gate">The rate limit gate to use</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected virtual async Task<CallResult<IRequest>> PrepareRequestAsync(
|
|
||||||
Uri uri,
|
|
||||||
HttpMethod method,
|
|
||||||
CancellationToken cancellationToken,
|
|
||||||
Dictionary<string, object>? parameters = null,
|
|
||||||
bool signed = false,
|
|
||||||
RequestBodyFormat? requestBodyFormat = null,
|
|
||||||
HttpMethodParameterPosition? parameterPosition = null,
|
|
||||||
ArrayParametersSerialization? arraySerialization = null,
|
|
||||||
int requestWeight = 1,
|
|
||||||
Dictionary<string, string>? additionalHeaders = null,
|
|
||||||
IRateLimitGate? gate = null)
|
|
||||||
{
|
|
||||||
var requestId = ExchangeHelpers.NextId();
|
|
||||||
|
|
||||||
if (signed)
|
|
||||||
{
|
|
||||||
if (AuthenticationProvider == null)
|
|
||||||
{
|
|
||||||
_logger.RestApiNoApiCredentials(requestId, uri.AbsolutePath);
|
|
||||||
return new CallResult<IRequest>(new NoApiCredentialsError());
|
|
||||||
}
|
|
||||||
|
|
||||||
var syncTask = SyncTimeAsync();
|
|
||||||
var timeSyncInfo = GetTimeSyncInfo();
|
|
||||||
|
|
||||||
if (timeSyncInfo != null && timeSyncInfo.TimeSyncState.LastSyncTime == default)
|
|
||||||
{
|
|
||||||
// Initially with first request we'll need to wait for the time syncing, if it's not the first request we can just continue
|
|
||||||
var syncTimeResult = await syncTask.ConfigureAwait(false);
|
|
||||||
if (!syncTimeResult)
|
|
||||||
{
|
|
||||||
_logger.RestApiFailedToSyncTime(requestId, syncTimeResult.Error!.ToString());
|
|
||||||
return syncTimeResult.As<IRequest>(default);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (requestWeight != 0)
|
|
||||||
{
|
|
||||||
if (gate == null)
|
|
||||||
throw new Exception("Ratelimit gate not set when request weight is not 0");
|
|
||||||
|
|
||||||
if (ClientOptions.RateLimiterEnabled)
|
|
||||||
{
|
|
||||||
var limitResult = await gate.ProcessAsync(_logger, requestId, RateLimitItemType.Request, new RequestDefinition(uri.AbsolutePath.TrimStart('/'), method) { Authenticated = signed }, uri.Host, AuthenticationProvider?._credentials.Key, requestWeight, ClientOptions.RateLimitingBehaviour, null, cancellationToken).ConfigureAwait(false);
|
|
||||||
if (!limitResult)
|
|
||||||
return new CallResult<IRequest>(limitResult.Error!);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.RestApiCreatingRequest(requestId, uri);
|
|
||||||
var paramsPosition = parameterPosition ?? ParameterPositions[method];
|
|
||||||
var request = ConstructRequest(uri, method, parameters?.OrderBy(p => p.Key).ToDictionary(p => p.Key, p => p.Value), signed, paramsPosition, arraySerialization ?? ArraySerialization, requestBodyFormat ?? RequestBodyFormat, requestId, additionalHeaders);
|
|
||||||
|
|
||||||
string? paramString = "";
|
|
||||||
if (paramsPosition == HttpMethodParameterPosition.InBody)
|
|
||||||
paramString = $" with request body '{request.Content}'";
|
|
||||||
|
|
||||||
var headers = request.GetHeaders();
|
|
||||||
if (headers.Count != 0)
|
|
||||||
paramString += " with headers " + string.Join(", ", headers.Select(h => h.Key + $"=[{string.Join(",", h.Value)}]"));
|
|
||||||
|
|
||||||
TotalRequestsMade++;
|
|
||||||
_logger.RestApiSendingRequest(requestId, method, signed ? "signed": "", request.Uri, paramString);
|
|
||||||
return new CallResult<IRequest>(request);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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)
|
||||||
@@ -673,10 +450,10 @@ namespace CryptoExchange.Net.Clients
|
|||||||
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)
|
||||||
@@ -692,7 +469,7 @@ 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)
|
||||||
@@ -701,20 +478,19 @@ namespace CryptoExchange.Net.Clients
|
|||||||
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.ResponseHeaders, sw.Elapsed, responseLength, 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>(statusCode, headers, 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.ResponseHeaders, sw.Elapsed, responseLength, 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)
|
||||||
@@ -736,20 +512,22 @@ namespace CryptoExchange.Net.Clients
|
|||||||
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, 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, 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, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@@ -765,10 +543,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(IEnumerable<KeyValuePair<string, IEnumerable<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.
|
||||||
@@ -804,112 +583,6 @@ namespace CryptoExchange.Net.Clients
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a request object
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="uri">The uri to send the request to</param>
|
|
||||||
/// <param name="method">The method of the request</param>
|
|
||||||
/// <param name="parameters">The parameters of the request</param>
|
|
||||||
/// <param name="signed">Whether or not the request should be authenticated</param>
|
|
||||||
/// <param name="parameterPosition">Where the parameters should be placed</param>
|
|
||||||
/// <param name="arraySerialization">How array parameters should be serialized</param>
|
|
||||||
/// <param name="bodyFormat">Format of the body content</param>
|
|
||||||
/// <param name="requestId">Unique id of a request</param>
|
|
||||||
/// <param name="additionalHeaders">Additional headers to send with the request</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected virtual IRequest ConstructRequest(
|
|
||||||
Uri uri,
|
|
||||||
HttpMethod method,
|
|
||||||
Dictionary<string, object>? parameters,
|
|
||||||
bool signed,
|
|
||||||
HttpMethodParameterPosition parameterPosition,
|
|
||||||
ArrayParametersSerialization arraySerialization,
|
|
||||||
RequestBodyFormat bodyFormat,
|
|
||||||
int requestId,
|
|
||||||
Dictionary<string, string>? additionalHeaders)
|
|
||||||
{
|
|
||||||
parameters ??= new Dictionary<string, object>();
|
|
||||||
|
|
||||||
for (var i = 0; i < parameters.Count; i++)
|
|
||||||
{
|
|
||||||
var kvp = parameters.ElementAt(i);
|
|
||||||
if (kvp.Value is Func<object> delegateValue)
|
|
||||||
parameters[kvp.Key] = delegateValue();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parameterPosition == HttpMethodParameterPosition.InUri)
|
|
||||||
{
|
|
||||||
foreach (var parameter in parameters)
|
|
||||||
uri = uri.AddQueryParameter(parameter.Key, parameter.Value.ToString()!);
|
|
||||||
}
|
|
||||||
|
|
||||||
var headers = new Dictionary<string, string>();
|
|
||||||
var uriParameters = parameterPosition == HttpMethodParameterPosition.InUri ? CreateParameterDictionary(parameters) : null;
|
|
||||||
var bodyParameters = parameterPosition == HttpMethodParameterPosition.InBody ? CreateParameterDictionary(parameters) : null;
|
|
||||||
if (AuthenticationProvider != null)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
AuthenticationProvider.AuthenticateRequest(
|
|
||||||
this,
|
|
||||||
uri,
|
|
||||||
method,
|
|
||||||
ref uriParameters,
|
|
||||||
ref bodyParameters,
|
|
||||||
ref headers,
|
|
||||||
signed,
|
|
||||||
arraySerialization,
|
|
||||||
parameterPosition,
|
|
||||||
bodyFormat
|
|
||||||
);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
throw new Exception("Failed to authenticate request, make sure your API credentials are correct", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the auth parameters to the uri, start with a new URI to be able to sort the parameters including the auth parameters
|
|
||||||
if (uriParameters != null)
|
|
||||||
uri = uri.SetParameters(uriParameters, arraySerialization);
|
|
||||||
|
|
||||||
var request = RequestFactory.Create(method, uri, requestId);
|
|
||||||
request.Accept = Constants.JsonContentHeader;
|
|
||||||
|
|
||||||
if (headers != null)
|
|
||||||
{
|
|
||||||
foreach (var header in headers)
|
|
||||||
request.AddHeader(header.Key, header.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (additionalHeaders != null)
|
|
||||||
{
|
|
||||||
foreach (var header in additionalHeaders)
|
|
||||||
request.AddHeader(header.Key, header.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (StandardRequestHeaders != null)
|
|
||||||
{
|
|
||||||
foreach (var header in StandardRequestHeaders)
|
|
||||||
{
|
|
||||||
// Only add it if it isn't overwritten
|
|
||||||
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 (bodyParameters?.Any() == true)
|
|
||||||
WriteParamBody(request, bodyParameters, contentType);
|
|
||||||
else
|
|
||||||
request.SetContent(RequestBodyEmptyContent, contentType);
|
|
||||||
}
|
|
||||||
|
|
||||||
return request;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Writes the parameters of the request to the request object body
|
/// Writes the parameters of the request to the request object body
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -920,12 +593,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)
|
||||||
@@ -942,11 +619,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, IEnumerable<KeyValuePair<string, IEnumerable<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>
|
||||||
@@ -956,23 +633,21 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <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>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected virtual ServerRateLimitError ParseRateLimitResponse(int httpStatusCode, IEnumerable<KeyValuePair<string, IEnumerable<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>
|
||||||
@@ -1049,9 +724,5 @@ namespace CryptoExchange.Net.Clients
|
|||||||
=> ClientOptions.CachingEnabled
|
=> ClientOptions.CachingEnabled
|
||||||
&& definition.Method == HttpMethod.Get
|
&& definition.Method == HttpMethod.Get
|
||||||
&& !definition.PreventCaching;
|
&& !definition.PreventCaching;
|
||||||
|
|
||||||
private bool ShouldCache(HttpMethod method)
|
|
||||||
=> ClientOptions.CachingEnabled
|
|
||||||
&& method == HttpMethod.Get;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
using CryptoExchange.Net.Converters.JsonNet;
|
|
||||||
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;
|
||||||
@@ -42,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>
|
||||||
@@ -77,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
|
||||||
{
|
{
|
||||||
@@ -133,13 +144,13 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// Create a message accessor instance
|
/// Create a message accessor instance
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected internal virtual IByteMessageAccessor CreateAccessor() => new JsonNetByteMessageAccessor();
|
protected internal abstract IByteMessageAccessor CreateAccessor(WebSocketMessageType messageType);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a serializer instance
|
/// Create a serializer instance
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected internal virtual IMessageSerializer CreateSerializer() => new JsonNetMessageSerializer();
|
protected internal abstract IMessageSerializer CreateSerializer();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Keep an open connection to this url
|
/// Keep an open connection to this url
|
||||||
@@ -206,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
|
||||||
@@ -239,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!);
|
||||||
|
|
||||||
@@ -255,15 +266,15 @@ 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")));
|
||||||
}
|
}
|
||||||
|
|
||||||
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();
|
||||||
@@ -303,11 +314,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);
|
||||||
}
|
}
|
||||||
@@ -316,12 +326,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"));
|
||||||
@@ -347,7 +356,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!);
|
||||||
}
|
}
|
||||||
@@ -360,7 +369,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)
|
||||||
@@ -374,13 +383,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 new CallResult(null);
|
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;
|
||||||
|
|
||||||
@@ -388,7 +398,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
await Task.Delay(ClientOptions.DelayAfterConnect).ConfigureAwait(false);
|
await Task.Delay(ClientOptions.DelayAfterConnect).ConfigureAwait(false);
|
||||||
|
|
||||||
if (!authenticated || socket.Authenticated)
|
if (!authenticated || socket.Authenticated)
|
||||||
return new CallResult(null);
|
return CallResult.SuccessResult;
|
||||||
|
|
||||||
var result = await AuthenticateSocketAsync(socket).ConfigureAwait(false);
|
var result = await AuthenticateSocketAsync(socket).ConfigureAwait(false);
|
||||||
if (!result)
|
if (!result)
|
||||||
@@ -427,7 +437,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
}
|
}
|
||||||
|
|
||||||
socket.Authenticated = true;
|
socket.Authenticated = true;
|
||||||
return new CallResult(null);
|
return CallResult.SuccessResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -475,7 +485,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected internal virtual Task<CallResult> RevitalizeRequestAsync(Subscription subscription)
|
protected internal virtual Task<CallResult> RevitalizeRequestAsync(Subscription subscription)
|
||||||
{
|
{
|
||||||
return Task.FromResult(new CallResult(null));
|
return Task.FromResult(CallResult.SuccessResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -561,11 +571,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -573,10 +584,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);
|
||||||
@@ -596,6 +608,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,
|
||||||
@@ -707,12 +720,12 @@ namespace CryptoExchange.Net.Clients
|
|||||||
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!);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new CallResult(null);
|
return CallResult.SuccessResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -802,7 +815,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
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\tConfirmed: {subState.Confirmed}");
|
||||||
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()}]");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
namespace CryptoExchange.Net.CommonObjects
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Balance data
|
|
||||||
/// </summary>
|
|
||||||
public class Balance: BaseCommonObject
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The asset name
|
|
||||||
/// </summary>
|
|
||||||
public string Asset { get; set; } = string.Empty;
|
|
||||||
/// <summary>
|
|
||||||
/// Quantity available
|
|
||||||
/// </summary>
|
|
||||||
public decimal? Available { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Total quantity
|
|
||||||
/// </summary>
|
|
||||||
public decimal? Total { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
namespace CryptoExchange.Net.CommonObjects
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Base class for common objects
|
|
||||||
/// </summary>
|
|
||||||
public class BaseCommonObject
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The source object the data is derived from
|
|
||||||
/// </summary>
|
|
||||||
public object SourceObject { get; set; } = null!;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
namespace CryptoExchange.Net.CommonObjects
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Order type
|
|
||||||
/// </summary>
|
|
||||||
public enum CommonOrderType
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Limit type
|
|
||||||
/// </summary>
|
|
||||||
Limit,
|
|
||||||
/// <summary>
|
|
||||||
/// Market type
|
|
||||||
/// </summary>
|
|
||||||
Market,
|
|
||||||
/// <summary>
|
|
||||||
/// Other order type
|
|
||||||
/// </summary>
|
|
||||||
Other
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Order side
|
|
||||||
/// </summary>
|
|
||||||
public enum CommonOrderSide
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Buy order
|
|
||||||
/// </summary>
|
|
||||||
Buy,
|
|
||||||
/// <summary>
|
|
||||||
/// Sell order
|
|
||||||
/// </summary>
|
|
||||||
Sell
|
|
||||||
}
|
|
||||||
/// <summary>
|
|
||||||
/// Order status
|
|
||||||
/// </summary>
|
|
||||||
public enum CommonOrderStatus
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// placed and not fully filled order
|
|
||||||
/// </summary>
|
|
||||||
Active,
|
|
||||||
/// <summary>
|
|
||||||
/// canceled order
|
|
||||||
/// </summary>
|
|
||||||
Canceled,
|
|
||||||
/// <summary>
|
|
||||||
/// filled order
|
|
||||||
/// </summary>
|
|
||||||
Filled
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Position side
|
|
||||||
/// </summary>
|
|
||||||
public enum CommonPositionSide
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Long position
|
|
||||||
/// </summary>
|
|
||||||
Long,
|
|
||||||
/// <summary>
|
|
||||||
/// Short position
|
|
||||||
/// </summary>
|
|
||||||
Short,
|
|
||||||
/// <summary>
|
|
||||||
/// Both
|
|
||||||
/// </summary>
|
|
||||||
Both
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.CommonObjects
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Kline data
|
|
||||||
/// </summary>
|
|
||||||
public class Kline: BaseCommonObject
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Opening time of the kline
|
|
||||||
/// </summary>
|
|
||||||
public DateTime OpenTime { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Price at the open time
|
|
||||||
/// </summary>
|
|
||||||
public decimal? OpenPrice { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Highest price of the kline
|
|
||||||
/// </summary>
|
|
||||||
public decimal? HighPrice { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Lowest price of the kline
|
|
||||||
/// </summary>
|
|
||||||
public decimal? LowPrice { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Close price of the kline
|
|
||||||
/// </summary>
|
|
||||||
public decimal? ClosePrice { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Volume of the kline
|
|
||||||
/// </summary>
|
|
||||||
public decimal? Volume { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.CommonObjects
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Order data
|
|
||||||
/// </summary>
|
|
||||||
public class Order: BaseCommonObject
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Id of the order
|
|
||||||
/// </summary>
|
|
||||||
public string Id { get; set; } = string.Empty;
|
|
||||||
/// <summary>
|
|
||||||
/// Symbol of the order
|
|
||||||
/// </summary>
|
|
||||||
public string Symbol { get; set; } = string.Empty;
|
|
||||||
/// <summary>
|
|
||||||
/// Price of the order
|
|
||||||
/// </summary>
|
|
||||||
public decimal? Price { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Quantity of the order
|
|
||||||
/// </summary>
|
|
||||||
public decimal? Quantity { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// The quantity of the order which has been filled
|
|
||||||
/// </summary>
|
|
||||||
public decimal? QuantityFilled { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Status of the order
|
|
||||||
/// </summary>
|
|
||||||
public CommonOrderStatus Status { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Side of the order
|
|
||||||
/// </summary>
|
|
||||||
public CommonOrderSide Side { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Type of the order
|
|
||||||
/// </summary>
|
|
||||||
public CommonOrderType Type { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Order time
|
|
||||||
/// </summary>
|
|
||||||
public DateTime Timestamp { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.CommonObjects
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Order book data
|
|
||||||
/// </summary>
|
|
||||||
public class OrderBook: BaseCommonObject
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// List of bids
|
|
||||||
/// </summary>
|
|
||||||
public IEnumerable<OrderBookEntry> Bids { get; set; } = Array.Empty<OrderBookEntry>();
|
|
||||||
/// <summary>
|
|
||||||
/// List of asks
|
|
||||||
/// </summary>
|
|
||||||
public IEnumerable<OrderBookEntry> Asks { get; set; } = Array.Empty<OrderBookEntry>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
namespace CryptoExchange.Net.CommonObjects
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Order book entry
|
|
||||||
/// </summary>
|
|
||||||
public class OrderBookEntry
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Quantity of the entry
|
|
||||||
/// </summary>
|
|
||||||
public decimal Quantity { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Price of the entry
|
|
||||||
/// </summary>
|
|
||||||
public decimal Price { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
namespace CryptoExchange.Net.CommonObjects
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Id of an order
|
|
||||||
/// </summary>
|
|
||||||
public class OrderId: BaseCommonObject
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Id of an order
|
|
||||||
/// </summary>
|
|
||||||
public string Id { get; set; } = string.Empty;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
namespace CryptoExchange.Net.CommonObjects
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Position data
|
|
||||||
/// </summary>
|
|
||||||
public class Position: BaseCommonObject
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Id of the position
|
|
||||||
/// </summary>
|
|
||||||
public string? Id { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Symbol of the position
|
|
||||||
/// </summary>
|
|
||||||
public string Symbol { get; set; } = string.Empty;
|
|
||||||
/// <summary>
|
|
||||||
/// Leverage
|
|
||||||
/// </summary>
|
|
||||||
public decimal Leverage { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Position quantity
|
|
||||||
/// </summary>
|
|
||||||
public decimal Quantity { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Entry price
|
|
||||||
/// </summary>
|
|
||||||
public decimal? EntryPrice { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Liquidation price
|
|
||||||
/// </summary>
|
|
||||||
public decimal? LiquidationPrice { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Unrealized profit and loss
|
|
||||||
/// </summary>
|
|
||||||
public decimal? UnrealizedPnl { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Realized profit and loss
|
|
||||||
/// </summary>
|
|
||||||
public decimal? RealizedPnl { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Mark price
|
|
||||||
/// </summary>
|
|
||||||
public decimal? MarkPrice { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Auto adding margin
|
|
||||||
/// </summary>
|
|
||||||
public bool? AutoMargin { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Position margin
|
|
||||||
/// </summary>
|
|
||||||
public decimal? PositionMargin { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Position side
|
|
||||||
/// </summary>
|
|
||||||
public CommonPositionSide? Side { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Is isolated
|
|
||||||
/// </summary>
|
|
||||||
public bool? Isolated { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Maintenance margin
|
|
||||||
/// </summary>
|
|
||||||
public decimal? MaintananceMargin { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
namespace CryptoExchange.Net.CommonObjects
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Symbol data
|
|
||||||
/// </summary>
|
|
||||||
public class Symbol: BaseCommonObject
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Name of the symbol
|
|
||||||
/// </summary>
|
|
||||||
public string Name { get; set; } = string.Empty;
|
|
||||||
/// <summary>
|
|
||||||
/// Minimal quantity of an order
|
|
||||||
/// </summary>
|
|
||||||
public decimal? MinTradeQuantity { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Step with which the quantity should increase
|
|
||||||
/// </summary>
|
|
||||||
public decimal? QuantityStep { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// step with which the price should increase
|
|
||||||
/// </summary>
|
|
||||||
public decimal? PriceStep { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// The max amount of decimals for quantity
|
|
||||||
/// </summary>
|
|
||||||
public int? QuantityDecimals { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// The max amount of decimal for price
|
|
||||||
/// </summary>
|
|
||||||
public int? PriceDecimals { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
namespace CryptoExchange.Net.CommonObjects
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Ticker data
|
|
||||||
/// </summary>
|
|
||||||
public class Ticker: BaseCommonObject
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Symbol
|
|
||||||
/// </summary>
|
|
||||||
public string Symbol { get; set; } = string.Empty;
|
|
||||||
/// <summary>
|
|
||||||
/// Price 24 hours ago
|
|
||||||
/// </summary>
|
|
||||||
public decimal? Price24H { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Last trade price
|
|
||||||
/// </summary>
|
|
||||||
public decimal? LastPrice { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// 24 hour low price
|
|
||||||
/// </summary>
|
|
||||||
public decimal? LowPrice { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// 24 hour high price
|
|
||||||
/// </summary>
|
|
||||||
public decimal? HighPrice { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// 24 hour volume
|
|
||||||
/// </summary>
|
|
||||||
public decimal? Volume { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.CommonObjects
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Trade data
|
|
||||||
/// </summary>
|
|
||||||
public class Trade: BaseCommonObject
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Symbol of the trade
|
|
||||||
/// </summary>
|
|
||||||
public string Symbol { get; set; } = string.Empty;
|
|
||||||
/// <summary>
|
|
||||||
/// Price of the trade
|
|
||||||
/// </summary>
|
|
||||||
public decimal Price { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Quantity of the trade
|
|
||||||
/// </summary>
|
|
||||||
public decimal Quantity { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Timestamp of the trade
|
|
||||||
/// </summary>
|
|
||||||
public DateTime Timestamp { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// User trade info
|
|
||||||
/// </summary>
|
|
||||||
public class UserTrade: Trade
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Id of the trade
|
|
||||||
/// </summary>
|
|
||||||
public string Id { get; set; } = string.Empty;
|
|
||||||
/// <summary>
|
|
||||||
/// Order id of the trade
|
|
||||||
/// </summary>
|
|
||||||
public string? OrderId { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Fee of the trade
|
|
||||||
/// </summary>
|
|
||||||
public decimal? Fee { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// The asset the fee is paid in
|
|
||||||
/// </summary>
|
|
||||||
public string? FeeAsset { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,195 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections;
|
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Reflection;
|
|
||||||
using CryptoExchange.Net.Attributes;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.JsonNet
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Converter for arrays to objects. Can deserialize data like [0.1, 0.2, "test"] to an object. Mapping is done by marking the class with [JsonConverter(typeof(ArrayConverter))] and the properties
|
|
||||||
/// with [ArrayProperty(x)] where x is the index of the property in the array
|
|
||||||
/// </summary>
|
|
||||||
public class ArrayConverter : JsonConverter
|
|
||||||
{
|
|
||||||
private static readonly ConcurrentDictionary<(MemberInfo, Type), Attribute> _attributeByMemberInfoAndTypeCache = new ConcurrentDictionary<(MemberInfo, Type), Attribute>();
|
|
||||||
private static readonly ConcurrentDictionary<(Type, Type), Attribute> _attributeByTypeAndTypeCache = new ConcurrentDictionary<(Type, Type), Attribute>();
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override bool CanConvert(Type objectType)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
|
||||||
{
|
|
||||||
if (reader.TokenType == JsonToken.Null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
if (objectType == typeof(JToken))
|
|
||||||
return JToken.Load(reader);
|
|
||||||
|
|
||||||
var result = Activator.CreateInstance(objectType);
|
|
||||||
var arr = JArray.Load(reader);
|
|
||||||
return ParseObject(arr, result!, objectType);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static object ParseObject(JArray arr, object result, Type objectType)
|
|
||||||
{
|
|
||||||
foreach (var property in objectType.GetProperties())
|
|
||||||
{
|
|
||||||
var attribute = GetCustomAttribute<ArrayPropertyAttribute>(property);
|
|
||||||
|
|
||||||
if (attribute == null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (attribute.Index >= arr.Count)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (property.PropertyType.BaseType == typeof(Array))
|
|
||||||
{
|
|
||||||
var objType = property.PropertyType.GetElementType();
|
|
||||||
var innerArray = (JArray)arr[attribute.Index];
|
|
||||||
var count = 0;
|
|
||||||
if (innerArray.Count == 0)
|
|
||||||
{
|
|
||||||
var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { 0 })!;
|
|
||||||
property.SetValue(result, arrayResult);
|
|
||||||
}
|
|
||||||
else if (innerArray[0].Type == JTokenType.Array)
|
|
||||||
{
|
|
||||||
var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { innerArray.Count })!;
|
|
||||||
foreach (var obj in innerArray)
|
|
||||||
{
|
|
||||||
var innerObj = Activator.CreateInstance(objType!);
|
|
||||||
arrayResult[count] = ParseObject((JArray)obj, innerObj!, objType!);
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
property.SetValue(result, arrayResult);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { 1 })!;
|
|
||||||
var innerObj = Activator.CreateInstance(objType!);
|
|
||||||
arrayResult[0] = ParseObject(innerArray, innerObj!, objType!);
|
|
||||||
property.SetValue(result, arrayResult);
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var converterAttribute = GetCustomAttribute<JsonConverterAttribute>(property) ?? GetCustomAttribute<JsonConverterAttribute>(property.PropertyType);
|
|
||||||
var conversionAttribute = GetCustomAttribute<JsonConversionAttribute>(property) ?? GetCustomAttribute<JsonConversionAttribute>(property.PropertyType);
|
|
||||||
|
|
||||||
object? value;
|
|
||||||
if (converterAttribute != null)
|
|
||||||
{
|
|
||||||
value = arr[attribute.Index].ToObject(property.PropertyType, new JsonSerializer {Converters = {(JsonConverter) Activator.CreateInstance(converterAttribute.ConverterType)!}});
|
|
||||||
}
|
|
||||||
else if (conversionAttribute != null)
|
|
||||||
{
|
|
||||||
value = arr[attribute.Index].ToObject(property.PropertyType);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
value = arr[attribute.Index];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value != null && property.PropertyType.IsInstanceOfType(value))
|
|
||||||
{
|
|
||||||
property.SetValue(result, value);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (value is JToken token)
|
|
||||||
{
|
|
||||||
if (token.Type == JTokenType.Null)
|
|
||||||
value = null;
|
|
||||||
|
|
||||||
if (token.Type == JTokenType.Float)
|
|
||||||
value = token.Value<decimal>();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value is decimal)
|
|
||||||
{
|
|
||||||
property.SetValue(result, value);
|
|
||||||
}
|
|
||||||
else if ((property.PropertyType == typeof(decimal)
|
|
||||||
|| property.PropertyType == typeof(decimal?))
|
|
||||||
&& (value != null && value.ToString()!.IndexOf("e", StringComparison.OrdinalIgnoreCase) >= 0))
|
|
||||||
{
|
|
||||||
var v = value.ToString();
|
|
||||||
if (decimal.TryParse(v, NumberStyles.Float, CultureInfo.InvariantCulture, out var dec))
|
|
||||||
property.SetValue(result, dec);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
property.SetValue(result, value == null ? null : Convert.ChangeType(value, property.PropertyType));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
|
||||||
{
|
|
||||||
if (value == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
writer.WriteStartArray();
|
|
||||||
var props = value.GetType().GetProperties();
|
|
||||||
var ordered = props.OrderBy(p => GetCustomAttribute<ArrayPropertyAttribute>(p)?.Index);
|
|
||||||
|
|
||||||
var last = -1;
|
|
||||||
foreach (var prop in ordered)
|
|
||||||
{
|
|
||||||
var arrayProp = GetCustomAttribute<ArrayPropertyAttribute>(prop);
|
|
||||||
if (arrayProp == null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (arrayProp.Index == last)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
while (arrayProp.Index != last + 1)
|
|
||||||
{
|
|
||||||
writer.WriteValue((string?)null);
|
|
||||||
last += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
last = arrayProp.Index;
|
|
||||||
var converterAttribute = GetCustomAttribute<JsonConverterAttribute>(prop);
|
|
||||||
if (converterAttribute != null)
|
|
||||||
writer.WriteRawValue(JsonConvert.SerializeObject(prop.GetValue(value), (JsonConverter)Activator.CreateInstance(converterAttribute.ConverterType)!));
|
|
||||||
else if (!IsSimple(prop.PropertyType))
|
|
||||||
serializer.Serialize(writer, prop.GetValue(value));
|
|
||||||
else
|
|
||||||
writer.WriteValue(prop.GetValue(value));
|
|
||||||
}
|
|
||||||
writer.WriteEndArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static T? GetCustomAttribute<T>(MemberInfo memberInfo) where T : Attribute =>
|
|
||||||
(T?)_attributeByMemberInfoAndTypeCache.GetOrAdd((memberInfo, typeof(T)), tuple => memberInfo.GetCustomAttribute(typeof(T))!);
|
|
||||||
|
|
||||||
private static T? GetCustomAttribute<T>(Type type) where T : Attribute =>
|
|
||||||
(T?)_attributeByTypeAndTypeCache.GetOrAdd((type, typeof(T)), tuple => type.GetCustomAttribute(typeof(T))!);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Linq;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.JsonNet
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Base class for enum converters
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T">Type of enum to convert</typeparam>
|
|
||||||
public abstract class BaseConverter<T>: JsonConverter where T: struct
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The enum->string mapping
|
|
||||||
/// </summary>
|
|
||||||
protected abstract List<KeyValuePair<T, string>> Mapping { get; }
|
|
||||||
private readonly bool _quotes;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="useQuotes"></param>
|
|
||||||
protected BaseConverter(bool useQuotes)
|
|
||||||
{
|
|
||||||
_quotes = useQuotes;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
|
||||||
{
|
|
||||||
var stringValue = value == null? null: GetValue((T) value);
|
|
||||||
if (_quotes)
|
|
||||||
writer.WriteValue(stringValue);
|
|
||||||
else
|
|
||||||
writer.WriteRawValue(stringValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
|
||||||
{
|
|
||||||
if (reader.Value == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var stringValue = reader.Value.ToString();
|
|
||||||
if (string.IsNullOrWhiteSpace(stringValue))
|
|
||||||
return null;
|
|
||||||
|
|
||||||
if (!GetValue(stringValue, out var result))
|
|
||||||
{
|
|
||||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Cannot map enum value. EnumType: {typeof(T)}, Value: {reader.Value}, Known values: {string.Join(", ", Mapping.Select(m => m.Value))}. If you think {reader.Value} should added please open an issue on the Github repo");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Convert a string value
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="data"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public T ReadString(string data)
|
|
||||||
{
|
|
||||||
return Mapping.FirstOrDefault(v => v.Value == data).Key;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override bool CanConvert(Type objectType)
|
|
||||||
{
|
|
||||||
// Check if it is type, or nullable of type
|
|
||||||
return objectType == typeof(T) || Nullable.GetUnderlyingType(objectType) == typeof(T);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool GetValue(string value, out T result)
|
|
||||||
{
|
|
||||||
// Check for exact match first, then if not found fallback to a case insensitive match
|
|
||||||
var mapping = Mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCulture));
|
|
||||||
if(mapping.Equals(default(KeyValuePair<T, string>)))
|
|
||||||
mapping = Mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase));
|
|
||||||
|
|
||||||
if (!mapping.Equals(default(KeyValuePair<T, string>)))
|
|
||||||
{
|
|
||||||
result = mapping.Key;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
result = default;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private string GetValue(T value)
|
|
||||||
{
|
|
||||||
return Mapping.FirstOrDefault(v => v.Key.Equals(value)).Value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Globalization;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.JsonNet
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Decimal converter that handles overflowing decimal values (by setting it to decimal.MaxValue)
|
|
||||||
/// </summary>
|
|
||||||
public class BigDecimalConverter : JsonConverter
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override bool CanConvert(Type objectType)
|
|
||||||
{
|
|
||||||
if (Nullable.GetUnderlyingType(objectType) != null)
|
|
||||||
return Nullable.GetUnderlyingType(objectType) == typeof(decimal);
|
|
||||||
return objectType == typeof(decimal);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
|
||||||
{
|
|
||||||
if (reader.TokenType == JsonToken.Null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
if (reader.TokenType == JsonToken.Float || reader.TokenType == JsonToken.Integer)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return decimal.Parse(reader.Value!.ToString()!, NumberStyles.Float, CultureInfo.InvariantCulture);
|
|
||||||
}
|
|
||||||
catch (OverflowException)
|
|
||||||
{
|
|
||||||
// Value doesn't fit decimal; set it to max value
|
|
||||||
return decimal.MaxValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reader.TokenType == JsonToken.String)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var value = reader.Value!.ToString()!;
|
|
||||||
return decimal.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture);
|
|
||||||
}
|
|
||||||
catch (OverflowException)
|
|
||||||
{
|
|
||||||
// Value doesn't fit decimal; set it to max value
|
|
||||||
return decimal.MaxValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
|
||||||
{
|
|
||||||
writer.WriteValue(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
using System;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.JsonNet
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Boolean converter with support for "0"/"1" (strings)
|
|
||||||
/// </summary>
|
|
||||||
public class BoolConverter : JsonConverter
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Determines whether this instance can convert the specified object type.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="objectType">Type of the object.</param>
|
|
||||||
/// <returns>
|
|
||||||
/// <c>true</c> if this instance can convert the specified object type; otherwise, <c>false</c>.
|
|
||||||
/// </returns>
|
|
||||||
public override bool CanConvert(Type objectType)
|
|
||||||
{
|
|
||||||
if (Nullable.GetUnderlyingType(objectType) != null)
|
|
||||||
return Nullable.GetUnderlyingType(objectType) == typeof(bool);
|
|
||||||
return objectType == typeof(bool);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Reads the JSON representation of the object.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="reader">The <see cref="T:Newtonsoft.Json.JsonReader"/> to read from.</param>
|
|
||||||
/// <param name="objectType">Type of the object.</param>
|
|
||||||
/// <param name="existingValue">The existing value of object being read.</param>
|
|
||||||
/// <param name="serializer">The calling serializer.</param>
|
|
||||||
/// <returns>
|
|
||||||
/// The object value.
|
|
||||||
/// </returns>
|
|
||||||
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
|
||||||
{
|
|
||||||
var value = reader.Value?.ToString()!.ToLower().Trim();
|
|
||||||
if (value == null || value == "")
|
|
||||||
{
|
|
||||||
if (Nullable.GetUnderlyingType(objectType) != null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (value)
|
|
||||||
{
|
|
||||||
case "true":
|
|
||||||
case "yes":
|
|
||||||
case "y":
|
|
||||||
case "1":
|
|
||||||
case "on":
|
|
||||||
return true;
|
|
||||||
case "false":
|
|
||||||
case "no":
|
|
||||||
case "n":
|
|
||||||
case "0":
|
|
||||||
case "off":
|
|
||||||
case "-1":
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we reach here, we're pretty much going to throw an error so let's let Json.NET throw it's pretty-fied error message.
|
|
||||||
return new JsonSerializer().Deserialize(reader, objectType);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Specifies that this converter will not participate in writing results.
|
|
||||||
/// </summary>
|
|
||||||
public override bool CanWrite { get { return false; } }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Writes the JSON representation of the object.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="writer">The <see cref="T:Newtonsoft.Json.JsonWriter"/> to write to.</param><param name="value">The value.</param><param name="serializer">The calling serializer.</param>
|
|
||||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,240 +0,0 @@
|
|||||||
using Newtonsoft.Json;
|
|
||||||
using System;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using System.Globalization;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.JsonNet
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Datetime converter. Supports converting from string/long/double to DateTime and back. Numbers are assumed to be the time since 1970-01-01.
|
|
||||||
/// </summary>
|
|
||||||
public class DateTimeConverter: JsonConverter
|
|
||||||
{
|
|
||||||
private static readonly DateTime _epoch = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
|
||||||
private const long _ticksPerSecond = TimeSpan.TicksPerMillisecond * 1000;
|
|
||||||
private const decimal _ticksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000;
|
|
||||||
private const decimal _ticksPerNanosecond = TimeSpan.TicksPerMillisecond / 1000m / 1000;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override bool CanConvert(Type objectType)
|
|
||||||
{
|
|
||||||
return objectType == typeof(DateTime) || objectType == typeof(DateTime?);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
|
||||||
{
|
|
||||||
if (reader.Value == null)
|
|
||||||
{
|
|
||||||
if (objectType == typeof(DateTime))
|
|
||||||
return default(DateTime);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(reader.TokenType is JsonToken.Integer)
|
|
||||||
{
|
|
||||||
var longValue = (long)reader.Value;
|
|
||||||
if (longValue == 0 || longValue == -1)
|
|
||||||
return objectType == typeof(DateTime) ? default(DateTime): null;
|
|
||||||
|
|
||||||
return ParseFromLong(longValue);
|
|
||||||
}
|
|
||||||
else if (reader.TokenType is JsonToken.Float)
|
|
||||||
{
|
|
||||||
var doubleValue = (double)reader.Value;
|
|
||||||
if (doubleValue == 0 || doubleValue == -1)
|
|
||||||
return objectType == typeof(DateTime) ? default(DateTime) : null;
|
|
||||||
|
|
||||||
if (doubleValue < 19999999999)
|
|
||||||
return ConvertFromSeconds(doubleValue);
|
|
||||||
|
|
||||||
return ConvertFromMilliseconds(doubleValue);
|
|
||||||
}
|
|
||||||
else if(reader.TokenType is JsonToken.String)
|
|
||||||
{
|
|
||||||
var stringValue = (string)reader.Value;
|
|
||||||
if (string.IsNullOrWhiteSpace(stringValue)
|
|
||||||
|| stringValue == "-1"
|
|
||||||
|| (double.TryParse(stringValue, out var doubleVal) && doubleVal == 0))
|
|
||||||
{
|
|
||||||
return objectType == typeof(DateTime) ? default(DateTime) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return ParseFromString(stringValue);
|
|
||||||
}
|
|
||||||
else if(reader.TokenType == JsonToken.Date)
|
|
||||||
{
|
|
||||||
return (DateTime)reader.Value;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Trace.WriteLine("{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + reader.Value);
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Parse a long value to datetime
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="longValue"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static DateTime ParseFromLong(long 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 string value to datetime
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="stringValue"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static DateTime ParseFromString(string stringValue)
|
|
||||||
{
|
|
||||||
if (stringValue.Length == 12 && stringValue.StartsWith("202"))
|
|
||||||
{
|
|
||||||
// Parse 202303261200 format
|
|
||||||
if (!int.TryParse(stringValue.Substring(0, 4), out var year)
|
|
||||||
|| !int.TryParse(stringValue.Substring(4, 2), out var month)
|
|
||||||
|| !int.TryParse(stringValue.Substring(6, 2), out var day)
|
|
||||||
|| !int.TryParse(stringValue.Substring(8, 2), out var hour)
|
|
||||||
|| !int.TryParse(stringValue.Substring(10, 2), out var minute))
|
|
||||||
{
|
|
||||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
return new DateTime(year, month, day, hour, minute, 0, DateTimeKind.Utc);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stringValue.Length == 8)
|
|
||||||
{
|
|
||||||
// Parse 20211103 format
|
|
||||||
if (!int.TryParse(stringValue.Substring(0, 4), out var year)
|
|
||||||
|| !int.TryParse(stringValue.Substring(4, 2), out var month)
|
|
||||||
|| !int.TryParse(stringValue.Substring(6, 2), out var day))
|
|
||||||
{
|
|
||||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stringValue.Length == 6)
|
|
||||||
{
|
|
||||||
// Parse 211103 format
|
|
||||||
if (!int.TryParse(stringValue.Substring(0, 2), out var year)
|
|
||||||
|| !int.TryParse(stringValue.Substring(2, 2), out var month)
|
|
||||||
|| !int.TryParse(stringValue.Substring(4, 2), out var day))
|
|
||||||
{
|
|
||||||
Trace.WriteLine("{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
return new DateTime(year + 2000, month, day, 0, 0, 0, DateTimeKind.Utc);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (double.TryParse(stringValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var doubleValue))
|
|
||||||
{
|
|
||||||
// Parse 1637745563.000 format
|
|
||||||
if (doubleValue < 19999999999)
|
|
||||||
return ConvertFromSeconds(doubleValue);
|
|
||||||
if (doubleValue < 19999999999999)
|
|
||||||
return ConvertFromMilliseconds((long)doubleValue);
|
|
||||||
if (doubleValue < 19999999999999999)
|
|
||||||
return ConvertFromMicroseconds((long)doubleValue);
|
|
||||||
|
|
||||||
return ConvertFromNanoseconds((long)doubleValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stringValue.Length == 10)
|
|
||||||
{
|
|
||||||
// Parse 2021-11-03 format
|
|
||||||
var values = stringValue.Split('-');
|
|
||||||
if (!int.TryParse(values[0], out var year)
|
|
||||||
|| !int.TryParse(values[1], out var month)
|
|
||||||
|| !int.TryParse(values[2], out var day))
|
|
||||||
{
|
|
||||||
Trace.WriteLine("{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc);
|
|
||||||
}
|
|
||||||
|
|
||||||
return DateTime.Parse(stringValue, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Convert a seconds since epoch (01-01-1970) value to DateTime
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="seconds"></param>
|
|
||||||
/// <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(long microseconds) => _epoch.AddTicks((long)Math.Round(microseconds * _ticksPerMicrosecond));
|
|
||||||
/// <summary>
|
|
||||||
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="nanoseconds"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static DateTime ConvertFromNanoseconds(long nanoseconds) => _epoch.AddTicks((long)Math.Round(nanoseconds * _ticksPerNanosecond));
|
|
||||||
/// <summary>
|
|
||||||
/// Convert a DateTime value to seconds since epoch (01-01-1970) value
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="time"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[return: NotNullIfNotNull("time")]
|
|
||||||
public static long? ConvertToSeconds(DateTime? time) => time == null ? null: (long)Math.Round((time.Value - _epoch).TotalSeconds);
|
|
||||||
/// <summary>
|
|
||||||
/// Convert a DateTime value to milliseconds since epoch (01-01-1970) value
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="time"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[return: NotNullIfNotNull("time")]
|
|
||||||
public static long? ConvertToMilliseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).TotalMilliseconds);
|
|
||||||
/// <summary>
|
|
||||||
/// Convert a DateTime value to microseconds since epoch (01-01-1970) value
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="time"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[return: NotNullIfNotNull("time")]
|
|
||||||
public static long? ConvertToMicroseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / _ticksPerMicrosecond);
|
|
||||||
/// <summary>
|
|
||||||
/// Convert a DateTime value to nanoseconds since epoch (01-01-1970) value
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="time"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[return: NotNullIfNotNull("time")]
|
|
||||||
public static long? ConvertToNanoseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / _ticksPerNanosecond);
|
|
||||||
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
|
||||||
{
|
|
||||||
var datetimeValue = (DateTime?)value;
|
|
||||||
if (datetimeValue == null)
|
|
||||||
writer.WriteValue((DateTime?)null);
|
|
||||||
if(datetimeValue == default(DateTime))
|
|
||||||
writer.WriteValue((DateTime?)null);
|
|
||||||
else
|
|
||||||
writer.WriteValue((long)Math.Round(((DateTime)value! - new DateTime(1970, 1, 1)).TotalMilliseconds));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
using Newtonsoft.Json;
|
|
||||||
using System;
|
|
||||||
using System.Globalization;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.JsonNet
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Converter for serializing decimal values as string
|
|
||||||
/// </summary>
|
|
||||||
public class DecimalStringWriterConverter : JsonConverter
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override bool CanRead => false;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override bool CanConvert(Type objectType) => objectType == typeof(decimal) || objectType == typeof(decimal?);
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) => writer.WriteValue(((decimal?)value)?.ToString(CultureInfo.InvariantCulture) ?? null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,176 +0,0 @@
|
|||||||
using CryptoExchange.Net.Attributes;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.JsonNet
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Converter for enum values. Enums entries should be noted with a MapAttribute to map the enum value to a string value
|
|
||||||
/// </summary>
|
|
||||||
public class EnumConverter : JsonConverter
|
|
||||||
{
|
|
||||||
private bool _warnOnMissingEntry = true;
|
|
||||||
private bool _writeAsInt;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// </summary>
|
|
||||||
public EnumConverter() { }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="writeAsInt"></param>
|
|
||||||
/// <param name="warnOnMissingEntry"></param>
|
|
||||||
public EnumConverter(bool writeAsInt, bool warnOnMissingEntry)
|
|
||||||
{
|
|
||||||
_writeAsInt = writeAsInt;
|
|
||||||
_warnOnMissingEntry = warnOnMissingEntry;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static readonly ConcurrentDictionary<Type, List<KeyValuePair<object, string>>> _mapping = new();
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override bool CanConvert(Type objectType)
|
|
||||||
{
|
|
||||||
return objectType.IsEnum || Nullable.GetUnderlyingType(objectType)?.IsEnum == true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
|
||||||
{
|
|
||||||
var enumType = Nullable.GetUnderlyingType(objectType) ?? objectType;
|
|
||||||
if (!_mapping.TryGetValue(enumType, out var mapping))
|
|
||||||
mapping = AddMapping(enumType);
|
|
||||||
|
|
||||||
var stringValue = reader.Value?.ToString();
|
|
||||||
if (stringValue == null || stringValue == "")
|
|
||||||
{
|
|
||||||
// Received null value
|
|
||||||
var emptyResult = GetDefaultValue(objectType, enumType);
|
|
||||||
if(emptyResult != null)
|
|
||||||
// If the property we're parsing to isn't nullable there isn't a correct way to return this as null will either throw an exception (.net framework) or the default enum value (dotnet core).
|
|
||||||
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: {enumType.Name}. If you think {enumType.Name} should be nullable please open an issue on the Github repo");
|
|
||||||
|
|
||||||
return emptyResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!GetValue(enumType, mapping, stringValue!, out var result))
|
|
||||||
{
|
|
||||||
var defaultValue = GetDefaultValue(objectType, enumType);
|
|
||||||
if (string.IsNullOrWhiteSpace(stringValue))
|
|
||||||
{
|
|
||||||
if (defaultValue != null)
|
|
||||||
// We received an empty string and have no mapping for it, and the property isn't nullable
|
|
||||||
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: {enumType.Name}. If you think {enumType.Name} should be nullable please open an issue on the Github repo");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// We received an enum value but weren't able to parse it.
|
|
||||||
if (_warnOnMissingEntry)
|
|
||||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Cannot map enum value. EnumType: {enumType.Name}, Value: {reader.Value}, Known values: {string.Join(", ", mapping.Select(m => m.Value))}. If you think {reader.Value} should added please open an issue on the Github repo");
|
|
||||||
}
|
|
||||||
|
|
||||||
return defaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static object? GetDefaultValue(Type objectType, Type enumType)
|
|
||||||
{
|
|
||||||
if (Nullable.GetUnderlyingType(objectType) != null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return Activator.CreateInstance(enumType); // return default value
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<KeyValuePair<object, string>> AddMapping(Type objectType)
|
|
||||||
{
|
|
||||||
var mapping = new List<KeyValuePair<object, string>>();
|
|
||||||
var enumMembers = objectType.GetMembers();
|
|
||||||
foreach (var member in enumMembers)
|
|
||||||
{
|
|
||||||
var maps = member.GetCustomAttributes(typeof(MapAttribute), false);
|
|
||||||
foreach (MapAttribute attribute in maps)
|
|
||||||
{
|
|
||||||
foreach (var value in attribute.Values)
|
|
||||||
mapping.Add(new KeyValuePair<object, string>(Enum.Parse(objectType, member.Name), value));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_mapping.TryAdd(objectType, mapping);
|
|
||||||
return mapping;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool GetValue(Type objectType, List<KeyValuePair<object, string>> enumMapping, string value, out object? result)
|
|
||||||
{
|
|
||||||
// Check for exact match first, then if not found fallback to a case insensitive match
|
|
||||||
var mapping = enumMapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCulture));
|
|
||||||
if (mapping.Equals(default(KeyValuePair<object, string>)))
|
|
||||||
mapping = enumMapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase));
|
|
||||||
|
|
||||||
if (!mapping.Equals(default(KeyValuePair<object, string>)))
|
|
||||||
{
|
|
||||||
result = mapping.Key;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// If no explicit mapping is found try to parse string
|
|
||||||
result = Enum.Parse(objectType, value, true);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
result = default;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get the string value for an enum value using the MapAttribute mapping. When multiple values are mapped for a enum entry the first value will be returned
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T"></typeparam>
|
|
||||||
/// <param name="enumValue"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[return: NotNullIfNotNull("enumValue")]
|
|
||||||
public static string? GetString<T>(T enumValue) => GetString(typeof(T), enumValue);
|
|
||||||
|
|
||||||
|
|
||||||
[return: NotNullIfNotNull("enumValue")]
|
|
||||||
private static string? GetString(Type objectType, object? enumValue)
|
|
||||||
{
|
|
||||||
objectType = Nullable.GetUnderlyingType(objectType) ?? objectType;
|
|
||||||
|
|
||||||
if (!_mapping.TryGetValue(objectType, out var mapping))
|
|
||||||
mapping = AddMapping(objectType);
|
|
||||||
|
|
||||||
return enumValue == null ? null : (mapping.FirstOrDefault(v => v.Key.Equals(enumValue)).Value ?? enumValue.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
|
||||||
{
|
|
||||||
if (value == null)
|
|
||||||
{
|
|
||||||
writer.WriteNull();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (!_writeAsInt)
|
|
||||||
{
|
|
||||||
var stringValue = GetString(value.GetType(), value);
|
|
||||||
writer.WriteValue(stringValue);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
writer.WriteValue((int)value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,338 +0,0 @@
|
|||||||
using CryptoExchange.Net.Converters.MessageParsing;
|
|
||||||
using CryptoExchange.Net.Interfaces;
|
|
||||||
using CryptoExchange.Net.Objects;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.JsonNet
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Json.Net message accessor
|
|
||||||
/// </summary>
|
|
||||||
public abstract class JsonNetMessageAccessor : IMessageAccessor
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The json token loaded
|
|
||||||
/// </summary>
|
|
||||||
protected JToken? _token;
|
|
||||||
private static readonly JsonSerializer _serializer = JsonSerializer.Create(SerializerOptions.WithConverters);
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public bool IsJson { get; protected set; }
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public abstract bool OriginalDataAvailable { get; }
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public object? Underlying => _token;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public CallResult<object> Deserialize(Type type, MessagePath? path = null)
|
|
||||||
{
|
|
||||||
if (!IsJson)
|
|
||||||
return new CallResult<object>(GetOriginalString());
|
|
||||||
|
|
||||||
var source = _token;
|
|
||||||
if (path != null)
|
|
||||||
source = GetPathNode(path.Value);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var result = source!.ToObject(type, _serializer)!;
|
|
||||||
return new CallResult<object>(result);
|
|
||||||
}
|
|
||||||
catch (JsonReaderException jre)
|
|
||||||
{
|
|
||||||
var info = $"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}";
|
|
||||||
return new CallResult<object>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
|
|
||||||
}
|
|
||||||
catch (JsonSerializationException jse)
|
|
||||||
{
|
|
||||||
var info = $"Deserialize JsonSerializationException: {jse.Message}";
|
|
||||||
return new CallResult<object>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
var exceptionInfo = ex.ToLogString();
|
|
||||||
var info = $"Deserialize Unknown Exception: {exceptionInfo}";
|
|
||||||
return new CallResult<object>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public CallResult<T> Deserialize<T>(MessagePath? path = null)
|
|
||||||
{
|
|
||||||
var source = _token;
|
|
||||||
if (path != null)
|
|
||||||
source = GetPathNode(path.Value);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var result = source!.ToObject<T>(_serializer)!;
|
|
||||||
return new CallResult<T>(result);
|
|
||||||
}
|
|
||||||
catch (JsonReaderException jre)
|
|
||||||
{
|
|
||||||
var info = $"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}";
|
|
||||||
return new CallResult<T>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
|
|
||||||
}
|
|
||||||
catch (JsonSerializationException jse)
|
|
||||||
{
|
|
||||||
var info = $"Deserialize JsonSerializationException: {jse.Message}";
|
|
||||||
return new CallResult<T>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
var exceptionInfo = ex.ToLogString();
|
|
||||||
var info = $"Deserialize Unknown Exception: {exceptionInfo}";
|
|
||||||
return new CallResult<T>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public NodeType? GetNodeType()
|
|
||||||
{
|
|
||||||
if (!IsJson)
|
|
||||||
throw new InvalidOperationException("Can't access json data on non-json message");
|
|
||||||
|
|
||||||
if (_token == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
if (_token.Type == JTokenType.Object)
|
|
||||||
return NodeType.Object;
|
|
||||||
|
|
||||||
if (_token.Type == JTokenType.Array)
|
|
||||||
return NodeType.Array;
|
|
||||||
|
|
||||||
return NodeType.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public NodeType? GetNodeType(MessagePath path)
|
|
||||||
{
|
|
||||||
if (!IsJson)
|
|
||||||
throw new InvalidOperationException("Can't access json data on non-json message");
|
|
||||||
|
|
||||||
var node = GetPathNode(path);
|
|
||||||
if (node == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
if (node.Type == JTokenType.Object)
|
|
||||||
return NodeType.Object;
|
|
||||||
|
|
||||||
if (node.Type == JTokenType.Array)
|
|
||||||
return NodeType.Array;
|
|
||||||
|
|
||||||
return NodeType.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public T? GetValue<T>(MessagePath path)
|
|
||||||
{
|
|
||||||
if (!IsJson)
|
|
||||||
throw new InvalidOperationException("Can't access json data on non-json message");
|
|
||||||
|
|
||||||
var value = GetPathNode(path);
|
|
||||||
if (value == null)
|
|
||||||
return default;
|
|
||||||
|
|
||||||
if (value.Type == JTokenType.Object || value.Type == JTokenType.Array)
|
|
||||||
return default;
|
|
||||||
|
|
||||||
return value!.Value<T>();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public List<T?>? GetValues<T>(MessagePath path)
|
|
||||||
{
|
|
||||||
if (!IsJson)
|
|
||||||
throw new InvalidOperationException("Can't access json data on non-json message");
|
|
||||||
|
|
||||||
var value = GetPathNode(path);
|
|
||||||
if (value == null)
|
|
||||||
return default;
|
|
||||||
|
|
||||||
if (value.Type == JTokenType.Object)
|
|
||||||
return default;
|
|
||||||
|
|
||||||
return value!.Values<T>().ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
private JToken? GetPathNode(MessagePath path)
|
|
||||||
{
|
|
||||||
if (!IsJson)
|
|
||||||
throw new InvalidOperationException("Can't access json data on non-json message");
|
|
||||||
|
|
||||||
var currentToken = _token;
|
|
||||||
foreach (var node in path)
|
|
||||||
{
|
|
||||||
if (node.Type == 0)
|
|
||||||
{
|
|
||||||
// Int value
|
|
||||||
var val = node.Index!.Value;
|
|
||||||
if (currentToken!.Type != JTokenType.Array || ((JArray)currentToken).Count <= val)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
currentToken = currentToken[val];
|
|
||||||
}
|
|
||||||
else if (node.Type == 1)
|
|
||||||
{
|
|
||||||
// String value
|
|
||||||
if (currentToken!.Type != JTokenType.Object)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
currentToken = currentToken[node.Property!];
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Property name
|
|
||||||
if (currentToken!.Type != JTokenType.Object)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
currentToken = (currentToken.First as JProperty)?.Name;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentToken == null)
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return currentToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public abstract string GetOriginalString();
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public abstract void Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Json.Net stream message accessor
|
|
||||||
/// </summary>
|
|
||||||
public class JsonNetStreamMessageAccessor : JsonNetMessageAccessor, IStreamMessageAccessor
|
|
||||||
{
|
|
||||||
private Stream? _stream;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override bool OriginalDataAvailable => _stream?.CanSeek == true;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public async 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
|
|
||||||
}
|
|
||||||
|
|
||||||
var readStream = _stream ?? stream;
|
|
||||||
var length = readStream.CanSeek ? readStream.Length : 4096;
|
|
||||||
using var reader = new StreamReader(readStream, Encoding.UTF8, false, (int)Math.Max(2, length), true);
|
|
||||||
using var jsonTextReader = new JsonTextReader(reader);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_token = await JToken.LoadAsync(jsonTextReader).ConfigureAwait(false);
|
|
||||||
IsJson = true;
|
|
||||||
return new CallResult(null);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
// Not a json message
|
|
||||||
IsJson = false;
|
|
||||||
return new CallResult(new ServerError("JsonError: " + ex.Message));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
/// <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;
|
|
||||||
_token = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Json.Net byte message accessor
|
|
||||||
/// </summary>
|
|
||||||
public class JsonNetByteMessageAccessor : JsonNetMessageAccessor, IByteMessageAccessor
|
|
||||||
{
|
|
||||||
private ReadOnlyMemory<byte> _bytes;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public CallResult Read(ReadOnlyMemory<byte> data)
|
|
||||||
{
|
|
||||||
_bytes = data;
|
|
||||||
|
|
||||||
// Try getting the underlying byte[] instead of the ToArray to prevent creating a copy
|
|
||||||
using var stream = MemoryMarshal.TryGetArray(data, out var arraySegment)
|
|
||||||
? new MemoryStream(arraySegment.Array!, arraySegment.Offset, arraySegment.Count)
|
|
||||||
: new MemoryStream(data.ToArray());
|
|
||||||
using var reader = new StreamReader(stream, Encoding.UTF8, false, Math.Max(2, data.Length), true);
|
|
||||||
using var jsonTextReader = new JsonTextReader(reader);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_token = JToken.Load(jsonTextReader);
|
|
||||||
IsJson = true;
|
|
||||||
return new CallResult(null);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
// Not a json message
|
|
||||||
IsJson = false;
|
|
||||||
return new CallResult(new ServerError("JsonError: " + ex.Message));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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;
|
|
||||||
_token = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
using CryptoExchange.Net.Interfaces;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.JsonNet
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public class JsonNetMessageSerializer : IMessageSerializer
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public string Serialize(object message) => JsonConvert.SerializeObject(message, Formatting.None);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
using Newtonsoft.Json;
|
|
||||||
using System.Globalization;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.JsonNet
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Serializer options
|
|
||||||
/// </summary>
|
|
||||||
public static class SerializerOptions
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Json serializer settings which includes the EnumConverter, DateTimeConverter and BoolConverter
|
|
||||||
/// </summary>
|
|
||||||
public static JsonSerializerSettings WithConverters => new JsonSerializerSettings
|
|
||||||
{
|
|
||||||
DateTimeZoneHandling = DateTimeZoneHandling.Utc,
|
|
||||||
Culture = CultureInfo.InvariantCulture,
|
|
||||||
Converters =
|
|
||||||
{
|
|
||||||
new EnumConverter(),
|
|
||||||
new DateTimeConverter(),
|
|
||||||
new BoolConverter()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Default json serializer settings
|
|
||||||
/// </summary>
|
|
||||||
public static JsonSerializerSettings Default => new JsonSerializerSettings
|
|
||||||
{
|
|
||||||
DateTimeZoneHandling = DateTimeZoneHandling.Utc,
|
|
||||||
Culture = CultureInfo.InvariantCulture
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,9 @@ using System.Text.Json.Serialization;
|
|||||||
using System.Text.Json;
|
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.Threading;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||||
{
|
{
|
||||||
@@ -14,215 +17,218 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
/// Converter for arrays to objects. Can deserialize data like [0.1, 0.2, "test"] to an object. Mapping is done by marking the class with [JsonConverter(typeof(ArrayConverter))] and the properties
|
/// Converter for arrays to objects. Can deserialize data like [0.1, 0.2, "test"] to an object. Mapping is done by marking the class with [JsonConverter(typeof(ArrayConverter))] and the properties
|
||||||
/// 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>
|
||||||
public class ArrayConverter : JsonConverterFactory
|
#if NET5_0_OR_GREATER
|
||||||
|
public class ArrayConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T> : JsonConverter<T> where T : new()
|
||||||
|
#else
|
||||||
|
public class ArrayConverter<T> : JsonConverter<T> where T : new()
|
||||||
|
#endif
|
||||||
{
|
{
|
||||||
|
private static readonly Lazy<List<ArrayPropertyInfo>> _typePropertyInfo = new Lazy<List<ArrayPropertyInfo>>(CacheTypeAttributes, LazyThreadSafetyMode.PublicationOnly);
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override bool CanConvert(Type typeToConvert) => true;
|
#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")]
|
||||||
|
#endif
|
||||||
|
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
if (value == null)
|
||||||
|
{
|
||||||
|
writer.WriteNullValue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.WriteStartArray();
|
||||||
|
|
||||||
|
var ordered = _typePropertyInfo.Value.Where(x => x.ArrayProperty != null).OrderBy(p => p.ArrayProperty.Index);
|
||||||
|
var last = -1;
|
||||||
|
foreach (var prop in ordered)
|
||||||
|
{
|
||||||
|
if (prop.ArrayProperty.Index == last)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
while (prop.ArrayProperty.Index != last + 1)
|
||||||
|
{
|
||||||
|
writer.WriteNullValue();
|
||||||
|
last += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
last = prop.ArrayProperty.Index;
|
||||||
|
|
||||||
|
var objValue = prop.PropertyInfo.GetValue(value);
|
||||||
|
if (objValue == null)
|
||||||
|
{
|
||||||
|
writer.WriteNullValue();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonSerializerOptions? typeOptions = null;
|
||||||
|
if (prop.JsonConverter != null)
|
||||||
|
{
|
||||||
|
typeOptions = new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals,
|
||||||
|
PropertyNameCaseInsensitive = false,
|
||||||
|
TypeInfoResolver = options.TypeInfoResolver,
|
||||||
|
};
|
||||||
|
typeOptions.Converters.Add(prop.JsonConverter);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prop.JsonConverter == null && IsSimple(prop.PropertyInfo.PropertyType))
|
||||||
|
{
|
||||||
|
if (prop.TargetType == typeof(string))
|
||||||
|
writer.WriteStringValue(Convert.ToString(objValue, CultureInfo.InvariantCulture));
|
||||||
|
else if (prop.TargetType == typeof(bool))
|
||||||
|
writer.WriteBooleanValue((bool)objValue);
|
||||||
|
else
|
||||||
|
writer.WriteRawValue(Convert.ToString(objValue, CultureInfo.InvariantCulture)!);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
JsonSerializer.Serialize(writer, objValue, typeOptions ?? options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.WriteEndArray();
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
|
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
{
|
{
|
||||||
Type converterType = typeof(ArrayConverterInner<>).MakeGenericType(typeToConvert);
|
if (reader.TokenType == JsonTokenType.Null)
|
||||||
return (JsonConverter)Activator.CreateInstance(converterType)!;
|
return default;
|
||||||
|
|
||||||
|
var result = new T();
|
||||||
|
return ParseObject(ref reader, result, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#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 T ParseObject(ref Utf8JsonReader reader, T result, JsonSerializerOptions options)
|
||||||
|
#else
|
||||||
|
private static T ParseObject(ref Utf8JsonReader reader, T result, JsonSerializerOptions options)
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
if (reader.TokenType != JsonTokenType.StartArray)
|
||||||
|
throw new Exception("Not an array");
|
||||||
|
|
||||||
|
int index = 0;
|
||||||
|
while (reader.Read())
|
||||||
|
{
|
||||||
|
if (reader.TokenType == JsonTokenType.EndArray)
|
||||||
|
break;
|
||||||
|
|
||||||
|
var indexAttributes = _typePropertyInfo.Value.Where(a => a.ArrayProperty.Index == index);
|
||||||
|
if (!indexAttributes.Any())
|
||||||
|
{
|
||||||
|
index++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var attribute in indexAttributes)
|
||||||
|
{
|
||||||
|
var targetType = attribute.TargetType;
|
||||||
|
object? value = null;
|
||||||
|
if (attribute.JsonConverter != null)
|
||||||
|
{
|
||||||
|
if (attribute.JsonSerializerOptions == null)
|
||||||
|
{
|
||||||
|
attribute.JsonSerializerOptions = new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals,
|
||||||
|
PropertyNameCaseInsensitive = false,
|
||||||
|
Converters = { attribute.JsonConverter },
|
||||||
|
TypeInfoResolver = options.TypeInfoResolver,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var doc = JsonDocument.ParseValue(ref reader);
|
||||||
|
value = doc.Deserialize(attribute.PropertyInfo.PropertyType, attribute.JsonSerializerOptions);
|
||||||
|
}
|
||||||
|
else if (attribute.DefaultDeserialization)
|
||||||
|
{
|
||||||
|
value = JsonDocument.ParseValue(ref reader).Deserialize(options.GetTypeInfo(attribute.PropertyInfo.PropertyType));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
value = reader.TokenType switch
|
||||||
|
{
|
||||||
|
JsonTokenType.Null => null,
|
||||||
|
JsonTokenType.False => false,
|
||||||
|
JsonTokenType.True => true,
|
||||||
|
JsonTokenType.String => reader.GetString(),
|
||||||
|
JsonTokenType.Number => reader.GetDecimal(),
|
||||||
|
JsonTokenType.StartObject => JsonSerializer.Deserialize(ref reader, attribute.TargetType, options),
|
||||||
|
_ => throw new NotImplementedException($"Array deserialization of type {reader.TokenType} not supported"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetType.IsAssignableFrom(value?.GetType()))
|
||||||
|
attribute.PropertyInfo.SetValue(result, value);
|
||||||
|
else
|
||||||
|
attribute.PropertyInfo.SetValue(result, value == null ? null : Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture));
|
||||||
|
}
|
||||||
|
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
|
||||||
|
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!;
|
||||||
public ArrayPropertyAttribute ArrayProperty { get; set; } = null!;
|
public ArrayPropertyAttribute ArrayProperty { get; set; } = null!;
|
||||||
public Type? JsonConverterType { 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;
|
||||||
|
|
||||||
private class ArrayConverterInner<T> : JsonConverter<T>
|
|
||||||
{
|
|
||||||
private static readonly ConcurrentDictionary<Type, List<ArrayPropertyInfo>> _typeAttributesCache = new ConcurrentDictionary<Type, List<ArrayPropertyInfo>>();
|
|
||||||
private static readonly ConcurrentDictionary<Type, JsonSerializerOptions> _converterOptionsCache = new ConcurrentDictionary<Type, JsonSerializerOptions>();
|
|
||||||
|
|
||||||
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
if (value == null)
|
|
||||||
{
|
|
||||||
writer.WriteNullValue();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
writer.WriteStartArray();
|
|
||||||
|
|
||||||
var valueType = value.GetType();
|
|
||||||
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;
|
|
||||||
foreach (var prop in ordered)
|
|
||||||
{
|
|
||||||
if (prop.ArrayProperty.Index == last)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
while (prop.ArrayProperty.Index != last + 1)
|
|
||||||
{
|
|
||||||
writer.WriteNullValue();
|
|
||||||
last += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
last = prop.ArrayProperty.Index;
|
|
||||||
|
|
||||||
var objValue = prop.PropertyInfo.GetValue(value);
|
|
||||||
if (objValue == null)
|
|
||||||
{
|
|
||||||
writer.WriteNullValue();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
JsonSerializerOptions? typeOptions = null;
|
|
||||||
if (prop.JsonConverterType != null)
|
|
||||||
{
|
|
||||||
var converter = (JsonConverter)Activator.CreateInstance(prop.JsonConverterType)!;
|
|
||||||
typeOptions = new JsonSerializerOptions();
|
|
||||||
typeOptions.Converters.Clear();
|
|
||||||
typeOptions.Converters.Add(converter);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prop.JsonConverterType == null && IsSimple(prop.PropertyInfo.PropertyType))
|
|
||||||
{
|
|
||||||
if (prop.TargetType == typeof(string))
|
|
||||||
writer.WriteStringValue(Convert.ToString(objValue, CultureInfo.InvariantCulture));
|
|
||||||
else if(prop.TargetType.IsEnum)
|
|
||||||
writer.WriteStringValue(EnumConverter.GetString(objValue));
|
|
||||||
else if (prop.TargetType == typeof(bool))
|
|
||||||
writer.WriteBooleanValue((bool)objValue);
|
|
||||||
else
|
|
||||||
writer.WriteRawValue(Convert.ToString(objValue, CultureInfo.InvariantCulture)!);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
JsonSerializer.Serialize(writer, objValue, typeOptions ?? options);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
writer.WriteEndArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
if (reader.TokenType == JsonTokenType.Null)
|
|
||||||
return default;
|
|
||||||
|
|
||||||
var result = Activator.CreateInstance(typeToConvert)!;
|
|
||||||
return (T)ParseObject(ref reader, result, typeToConvert, 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<ArrayPropertyInfo> CacheTypeAttributes(Type type)
|
|
||||||
{
|
|
||||||
var attributes = new List<ArrayPropertyInfo>();
|
|
||||||
var properties = type.GetProperties();
|
|
||||||
foreach (var property in properties)
|
|
||||||
{
|
|
||||||
var att = property.GetCustomAttribute<ArrayPropertyAttribute>();
|
|
||||||
if (att == null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
attributes.Add(new ArrayPropertyInfo
|
|
||||||
{
|
|
||||||
ArrayProperty = att,
|
|
||||||
PropertyInfo = property,
|
|
||||||
DefaultDeserialization = property.GetCustomAttribute<JsonConversionAttribute>() != null,
|
|
||||||
JsonConverterType = property.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType ?? property.PropertyType.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType,
|
|
||||||
TargetType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_typeAttributesCache.TryAdd(type, attributes);
|
|
||||||
return attributes;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static object ParseObject(ref Utf8JsonReader reader, object result, Type objectType, JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
if (reader.TokenType != JsonTokenType.StartArray)
|
|
||||||
throw new Exception("Not an array");
|
|
||||||
|
|
||||||
if (!_typeAttributesCache.TryGetValue(objectType, out var attributes))
|
|
||||||
attributes = CacheTypeAttributes(objectType);
|
|
||||||
|
|
||||||
int index = 0;
|
|
||||||
while (reader.Read())
|
|
||||||
{
|
|
||||||
if (reader.TokenType == JsonTokenType.EndArray)
|
|
||||||
break;
|
|
||||||
|
|
||||||
var indexAttributes = attributes.Where(a => a.ArrayProperty.Index == index);
|
|
||||||
if (!indexAttributes.Any())
|
|
||||||
{
|
|
||||||
index++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var attribute in indexAttributes)
|
|
||||||
{
|
|
||||||
var targetType = attribute.TargetType;
|
|
||||||
object? value = null;
|
|
||||||
if (attribute.JsonConverterType != null)
|
|
||||||
{
|
|
||||||
if (!_converterOptionsCache.TryGetValue(attribute.JsonConverterType, out var newOptions))
|
|
||||||
{
|
|
||||||
var converter = (JsonConverter)Activator.CreateInstance(attribute.JsonConverterType)!;
|
|
||||||
newOptions = new JsonSerializerOptions
|
|
||||||
{
|
|
||||||
NumberHandling = SerializerOptions.WithConverters.NumberHandling,
|
|
||||||
PropertyNameCaseInsensitive = SerializerOptions.WithConverters.PropertyNameCaseInsensitive,
|
|
||||||
Converters = { converter },
|
|
||||||
};
|
|
||||||
_converterOptionsCache.TryAdd(attribute.JsonConverterType, newOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
value = JsonDocument.ParseValue(ref reader).Deserialize(attribute.PropertyInfo.PropertyType, newOptions);
|
|
||||||
}
|
|
||||||
else if (attribute.DefaultDeserialization)
|
|
||||||
{
|
|
||||||
// Use default deserialization
|
|
||||||
value = JsonDocument.ParseValue(ref reader).Deserialize(attribute.PropertyInfo.PropertyType, SerializerOptions.WithConverters);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
value = reader.TokenType switch
|
|
||||||
{
|
|
||||||
JsonTokenType.Null => null,
|
|
||||||
JsonTokenType.False => false,
|
|
||||||
JsonTokenType.True => true,
|
|
||||||
JsonTokenType.String => reader.GetString(),
|
|
||||||
JsonTokenType.Number => reader.GetDecimal(),
|
|
||||||
JsonTokenType.StartObject => JsonSerializer.Deserialize(ref reader, attribute.TargetType, options),
|
|
||||||
_ => throw new NotImplementedException($"Array deserialization of type {reader.TokenType} not supported"),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targetType.IsAssignableFrom(value?.GetType()))
|
|
||||||
attribute.PropertyInfo.SetValue(result, value);
|
|
||||||
else
|
|
||||||
attribute.PropertyInfo.SetValue(result, value == null ? null : Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture));
|
|
||||||
}
|
|
||||||
|
|
||||||
index++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,8 +20,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
|
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
|
||||||
{
|
{
|
||||||
Type converterType = typeof(BoolConverterInner<>).MakeGenericType(typeToConvert);
|
return typeToConvert == typeof(bool) ? new BoolConverterInner<bool>() : new BoolConverterInner<bool?>();
|
||||||
return (JsonConverter)Activator.CreateInstance(converterType)!;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private class BoolConverterInner<T> : JsonConverter<T>
|
private class BoolConverterInner<T> : JsonConverter<T>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
@@ -8,18 +9,27 @@ using System.Text.Json.Serialization;
|
|||||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Converter for comma seperated enum values
|
/// Converter for comma separated enum values
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class CommaSplitEnumConverter<T> : JsonConverter<IEnumerable<T>> where T : Enum
|
#if NET5_0_OR_GREATER
|
||||||
|
public class CommaSplitEnumConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T> : JsonConverter<T[]> where T : struct, Enum
|
||||||
|
#else
|
||||||
|
public class CommaSplitEnumConverter<T> : JsonConverter<T[]> where T : struct, Enum
|
||||||
|
#endif
|
||||||
|
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override IEnumerable<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 => EnumConverter.ParseString<T>(x)).ToArray() ?? new T[0])!;
|
var str = reader.GetString();
|
||||||
|
if (string.IsNullOrEmpty(str))
|
||||||
|
return [];
|
||||||
|
|
||||||
|
return str!.Split(',').Select(x => (T)EnumConverter.ParseString<T>(x)!).ToArray() ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override void Write(Utf8JsonWriter writer, IEnumerable<T> value, JsonSerializerOptions options)
|
public override void Write(Utf8JsonWriter writer, T[] value, JsonSerializerOptions options)
|
||||||
{
|
{
|
||||||
writer.WriteStringValue(string.Join(",", value.Select(x => EnumConverter.GetString(x))));
|
writer.WriteStringValue(string.Join(",", value.Select(x => EnumConverter.GetString(x))));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,8 +26,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
|
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
|
||||||
{
|
{
|
||||||
Type converterType = typeof(DateTimeConverterInner<>).MakeGenericType(typeToConvert);
|
return typeToConvert == typeof(DateTime) ? new DateTimeConverterInner<DateTime>() : new DateTimeConverterInner<DateTime?>();
|
||||||
return (JsonConverter)Activator.CreateInstance(converterType)!;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private class DateTimeConverterInner<T> : JsonConverter<T>
|
private class DateTimeConverterInner<T> : JsonConverter<T>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Converter mapping to an object but also handles when an empty array is send
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T"></typeparam>
|
|
||||||
public class EmptyArrayObjectConverter<T> : JsonConverter<T>
|
|
||||||
{
|
|
||||||
private static JsonSerializerOptions _defaultConverter = SerializerOptions.WithConverters;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override T? Read(
|
|
||||||
ref Utf8JsonReader reader,
|
|
||||||
Type typeToConvert,
|
|
||||||
JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
switch (reader.TokenType)
|
|
||||||
{
|
|
||||||
case JsonTokenType.StartArray:
|
|
||||||
_ = JsonSerializer.Deserialize<object[]>(ref reader, options);
|
|
||||||
return default;
|
|
||||||
case JsonTokenType.StartObject:
|
|
||||||
return JsonSerializer.Deserialize<T>(ref reader, _defaultConverter);
|
|
||||||
};
|
|
||||||
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
|
|
||||||
=> JsonSerializer.Serialize(writer, (object?)value, options);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -11,127 +11,78 @@ using System.Text.Json.Serialization;
|
|||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Static EnumConverter methods
|
||||||
|
/// </summary>
|
||||||
|
public static class EnumConverter
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Get the enum value from a string
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">String value</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
#if NET5_0_OR_GREATER
|
||||||
|
public static T? ParseString<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string value) where T : struct, Enum
|
||||||
|
#else
|
||||||
|
public static T? ParseString<T>(string value) where T : struct, Enum
|
||||||
|
#endif
|
||||||
|
=> EnumConverter<T>.ParseString(value);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the string value for an enum value using the MapAttribute mapping. When multiple values are mapped for a enum entry the first value will be returned
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="enumValue"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
#if NET5_0_OR_GREATER
|
||||||
|
public static string GetString<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(T enumValue) where T : struct, Enum
|
||||||
|
#else
|
||||||
|
public static string GetString<T>(T enumValue) where T : struct, Enum
|
||||||
|
#endif
|
||||||
|
=> EnumConverter<T>.GetString(enumValue);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the string value for an enum value using the MapAttribute mapping. When multiple values are mapped for a enum entry the first value will be returned
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="enumValue"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[return: NotNullIfNotNull("enumValue")]
|
||||||
|
#if NET5_0_OR_GREATER
|
||||||
|
public static string? GetString<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(T? enumValue) where T : struct, Enum
|
||||||
|
#else
|
||||||
|
public static string? GetString<T>(T? enumValue) where T : struct, Enum
|
||||||
|
#endif
|
||||||
|
=> EnumConverter<T>.GetString(enumValue);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Converter for enum values. Enums entries should be noted with a MapAttribute to map the enum value to a string value
|
/// Converter for enum values. Enums entries should be noted with a MapAttribute to map the enum value to a string value
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class EnumConverter : JsonConverterFactory
|
#if NET5_0_OR_GREATER
|
||||||
|
public class EnumConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>
|
||||||
|
#else
|
||||||
|
public class EnumConverter<T>
|
||||||
|
#endif
|
||||||
|
: JsonConverter<T>, INullableConverterFactory where T : struct, Enum
|
||||||
{
|
{
|
||||||
private bool _warnOnMissingEntry = true;
|
private static List<KeyValuePair<T, string>>? _mapping = null;
|
||||||
private bool _writeAsInt;
|
private NullableEnumConverter? _nullableEnumConverter = null;
|
||||||
private static readonly ConcurrentDictionary<Type, List<KeyValuePair<object, string>>> _mapping = new();
|
|
||||||
|
|
||||||
/// <summary>
|
private static ConcurrentBag<string> _unknownValuesWarned = new ConcurrentBag<string>();
|
||||||
/// </summary>
|
|
||||||
public EnumConverter() { }
|
|
||||||
|
|
||||||
/// <summary>
|
internal class NullableEnumConverter : JsonConverter<T?>
|
||||||
/// </summary>
|
|
||||||
/// <param name="writeAsInt"></param>
|
|
||||||
/// <param name="warnOnMissingEntry"></param>
|
|
||||||
public EnumConverter(bool writeAsInt, bool warnOnMissingEntry)
|
|
||||||
{
|
{
|
||||||
_writeAsInt = writeAsInt;
|
private readonly EnumConverter<T> _enumConverter;
|
||||||
_warnOnMissingEntry = warnOnMissingEntry;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
public NullableEnumConverter(EnumConverter<T> enumConverter)
|
||||||
public override bool CanConvert(Type typeToConvert)
|
|
||||||
{
|
|
||||||
return typeToConvert.IsEnum || Nullable.GetUnderlyingType(typeToConvert)?.IsEnum == true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
JsonConverter converter = (JsonConverter)Activator.CreateInstance(
|
|
||||||
typeof(EnumConverterInner<>).MakeGenericType(
|
|
||||||
new Type[] { typeToConvert }),
|
|
||||||
BindingFlags.Instance | BindingFlags.Public,
|
|
||||||
binder: null,
|
|
||||||
args: new object[] { _writeAsInt, _warnOnMissingEntry },
|
|
||||||
culture: null)!;
|
|
||||||
|
|
||||||
return converter;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<KeyValuePair<object, string>> AddMapping(Type objectType)
|
|
||||||
{
|
|
||||||
var mapping = new List<KeyValuePair<object, string>>();
|
|
||||||
var enumMembers = objectType.GetMembers();
|
|
||||||
foreach (var member in enumMembers)
|
|
||||||
{
|
{
|
||||||
var maps = member.GetCustomAttributes(typeof(MapAttribute), false);
|
_enumConverter = enumConverter;
|
||||||
foreach (MapAttribute attribute in maps)
|
|
||||||
{
|
|
||||||
foreach (var value in attribute.Values)
|
|
||||||
mapping.Add(new KeyValuePair<object, string>(Enum.Parse(objectType, member.Name), value));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
_mapping.TryAdd(objectType, mapping);
|
|
||||||
return mapping;
|
|
||||||
}
|
|
||||||
|
|
||||||
private class EnumConverterInner<T> : JsonConverter<T>
|
|
||||||
{
|
|
||||||
private bool _warnOnMissingEntry = true;
|
|
||||||
private bool _writeAsInt;
|
|
||||||
|
|
||||||
public EnumConverterInner(bool writeAsInt, bool warnOnMissingEntry)
|
|
||||||
{
|
|
||||||
_warnOnMissingEntry = warnOnMissingEntry;
|
|
||||||
_writeAsInt = writeAsInt;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
{
|
{
|
||||||
var enumType = Nullable.GetUnderlyingType(typeToConvert) ?? typeToConvert;
|
return _enumConverter.ReadNullable(ref reader, typeToConvert, options, out var isEmptyString, out var warn);
|
||||||
if (!_mapping.TryGetValue(enumType, out var mapping))
|
|
||||||
mapping = AddMapping(enumType);
|
|
||||||
|
|
||||||
var stringValue = reader.TokenType switch
|
|
||||||
{
|
|
||||||
JsonTokenType.String => reader.GetString(),
|
|
||||||
JsonTokenType.Number => reader.GetInt16().ToString(),
|
|
||||||
JsonTokenType.True => reader.GetBoolean().ToString(),
|
|
||||||
JsonTokenType.False => reader.GetBoolean().ToString(),
|
|
||||||
JsonTokenType.Null => null,
|
|
||||||
_ => throw new Exception("Invalid token type for enum deserialization: " + reader.TokenType)
|
|
||||||
};
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(stringValue))
|
|
||||||
{
|
|
||||||
// Received null value
|
|
||||||
var emptyResult = GetDefaultValue(typeToConvert, enumType);
|
|
||||||
if (emptyResult != null)
|
|
||||||
// If the property we're parsing to isn't nullable there isn't a correct way to return this as null will either throw an exception (.net framework) or the default enum value (dotnet core).
|
|
||||||
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: {enumType.Name}. If you think {enumType.Name} should be nullable please open an issue on the Github repo");
|
|
||||||
|
|
||||||
return (T?)emptyResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!GetValue(enumType, mapping, stringValue!, out var result))
|
|
||||||
{
|
|
||||||
var defaultValue = GetDefaultValue(typeToConvert, enumType);
|
|
||||||
if (string.IsNullOrWhiteSpace(stringValue))
|
|
||||||
{
|
|
||||||
if (defaultValue != null)
|
|
||||||
// We received an empty string and have no mapping for it, and the property isn't nullable
|
|
||||||
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: {enumType.Name}. If you think {enumType.Name} should be nullable please open an issue on the Github repo");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// We received an enum value but weren't able to parse it.
|
|
||||||
if (_warnOnMissingEntry)
|
|
||||||
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 (T?)defaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (T?)result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
|
public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options)
|
||||||
{
|
{
|
||||||
if (value == null)
|
if (value == null)
|
||||||
{
|
{
|
||||||
@@ -139,106 +90,183 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (!_writeAsInt)
|
_enumConverter.Write(writer, value.Value, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
var t = ReadNullable(ref reader, typeToConvert, options, out var isEmptyString, out var warn);
|
||||||
|
if (t == null)
|
||||||
|
{
|
||||||
|
if (warn)
|
||||||
|
{
|
||||||
|
if (isEmptyString)
|
||||||
{
|
{
|
||||||
var stringValue = GetString(value.GetType(), value);
|
// We received an empty string and have no mapping for it, and the property isn't nullable
|
||||||
writer.WriteStringValue(stringValue);
|
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");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
writer.WriteNumberValue((int)Convert.ChangeType(value, typeof(int)));
|
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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private static object? GetDefaultValue(Type objectType, Type enumType)
|
return new T(); // return default value
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
if (Nullable.GetUnderlyingType(objectType) != null)
|
return t.Value;
|
||||||
return null;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return Activator.CreateInstance(enumType); // return default value
|
private T? ReadNullable(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, out bool isEmptyString, out bool warn)
|
||||||
|
{
|
||||||
|
isEmptyString = false;
|
||||||
|
warn = false;
|
||||||
|
var enumType = typeof(T);
|
||||||
|
if (_mapping == null)
|
||||||
|
_mapping = AddMapping();
|
||||||
|
|
||||||
|
var stringValue = reader.TokenType switch
|
||||||
|
{
|
||||||
|
JsonTokenType.String => reader.GetString(),
|
||||||
|
JsonTokenType.Number => reader.GetInt32().ToString(),
|
||||||
|
JsonTokenType.True => reader.GetBoolean().ToString(),
|
||||||
|
JsonTokenType.False => reader.GetBoolean().ToString(),
|
||||||
|
JsonTokenType.Null => null,
|
||||||
|
_ => throw new Exception("Invalid token type for enum deserialization: " + reader.TokenType)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(stringValue))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (!GetValue(enumType, stringValue!, out var result))
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(stringValue))
|
||||||
|
{
|
||||||
|
isEmptyString = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// We received an enum value but weren't able to parse it.
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool GetValue(Type objectType, List<KeyValuePair<object, string>> enumMapping, string value, out object? result)
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
var stringValue = GetString(value);
|
||||||
|
writer.WriteStringValue(stringValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool GetValue(Type objectType, string value, out T? result)
|
||||||
|
{
|
||||||
|
if (_mapping != null)
|
||||||
{
|
{
|
||||||
// Check for exact match first, then if not found fallback to a case insensitive match
|
// Check for exact match first, then if not found fallback to a case insensitive match
|
||||||
var mapping = enumMapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCulture));
|
var mapping = _mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCulture));
|
||||||
if (mapping.Equals(default(KeyValuePair<object, string>)))
|
if (mapping.Equals(default(KeyValuePair<T, string>)))
|
||||||
mapping = enumMapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase));
|
mapping = _mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase));
|
||||||
|
|
||||||
if (!mapping.Equals(default(KeyValuePair<object, string>)))
|
if (!mapping.Equals(default(KeyValuePair<T, string>)))
|
||||||
{
|
{
|
||||||
result = mapping.Key;
|
result = mapping.Key;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (objectType.IsDefined(typeof(FlagsAttribute)))
|
if (objectType.IsDefined(typeof(FlagsAttribute)))
|
||||||
{
|
{
|
||||||
var intValue = int.Parse(value);
|
var intValue = int.Parse(value);
|
||||||
result = Enum.ToObject(objectType, intValue);
|
result = (T)Enum.ToObject(objectType, intValue);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// If no explicit mapping is found try to parse string
|
||||||
|
result = (T)Enum.Parse(objectType, value, true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
result = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<KeyValuePair<T, string>> AddMapping()
|
||||||
|
{
|
||||||
|
var mapping = new List<KeyValuePair<T, string>>();
|
||||||
|
var enumType = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
|
||||||
|
var enumMembers = enumType.GetFields();
|
||||||
|
foreach (var member in enumMembers)
|
||||||
|
{
|
||||||
|
var maps = member.GetCustomAttributes(typeof(MapAttribute), false);
|
||||||
|
foreach (MapAttribute attribute in maps)
|
||||||
{
|
{
|
||||||
// If no explicit mapping is found try to parse string
|
foreach (var value in attribute.Values)
|
||||||
result = Enum.Parse(objectType, value, true);
|
mapping.Add(new KeyValuePair<T, string>((T)Enum.Parse(enumType, member.Name), value));
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
result = default;
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_mapping = mapping;
|
||||||
|
return mapping;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get the string value for an enum value using the MapAttribute mapping. When multiple values are mapped for a enum entry the first value will be returned
|
/// Get the string value for an enum value using the MapAttribute mapping. When multiple values are mapped for a enum entry the first value will be returned
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="T"></typeparam>
|
|
||||||
/// <param name="enumValue"></param>
|
/// <param name="enumValue"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[return: NotNullIfNotNull("enumValue")]
|
[return: NotNullIfNotNull("enumValue")]
|
||||||
public static string? GetString<T>(T enumValue) => GetString(typeof(T), enumValue);
|
public static string? GetString(T? enumValue)
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get the string value for an enum value using the MapAttribute mapping. When multiple values are mapped for a enum entry the first value will be returned
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="objectType"></param>
|
|
||||||
/// <param name="enumValue"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[return: NotNullIfNotNull("enumValue")]
|
|
||||||
public static string? GetString(Type objectType, object? enumValue)
|
|
||||||
{
|
{
|
||||||
objectType = Nullable.GetUnderlyingType(objectType) ?? objectType;
|
if (_mapping == null)
|
||||||
|
_mapping = AddMapping();
|
||||||
|
|
||||||
if (!_mapping.TryGetValue(objectType, out var mapping))
|
return enumValue == null ? null : (_mapping.FirstOrDefault(v => v.Key.Equals(enumValue)).Value ?? enumValue.ToString());
|
||||||
mapping = AddMapping(objectType);
|
|
||||||
|
|
||||||
return enumValue == null ? null : (mapping.FirstOrDefault(v => v.Key.Equals(enumValue)).Value ?? enumValue.ToString());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get the enum value from a string
|
/// Get the enum value from a string
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="T">Enum type</typeparam>
|
|
||||||
/// <param name="value">String value</param>
|
/// <param name="value">String value</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static T? ParseString<T>(string value) where T : Enum
|
public static T? ParseString(string value)
|
||||||
{
|
{
|
||||||
var type = typeof(T);
|
var type = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
|
||||||
if (!_mapping.TryGetValue(type, out var enumMapping))
|
if (_mapping == null)
|
||||||
enumMapping = AddMapping(type);
|
_mapping = AddMapping();
|
||||||
|
|
||||||
var mapping = enumMapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCulture));
|
var mapping = _mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCulture));
|
||||||
if (mapping.Equals(default(KeyValuePair<object, string>)))
|
if (mapping.Equals(default(KeyValuePair<T, string>)))
|
||||||
mapping = enumMapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase));
|
mapping = _mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase));
|
||||||
|
|
||||||
if (!mapping.Equals(default(KeyValuePair<object, string>)))
|
if (!mapping.Equals(default(KeyValuePair<T, string>)))
|
||||||
{
|
return mapping.Key;
|
||||||
return (T)mapping.Key;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -250,5 +278,12 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
return default;
|
return default;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public JsonConverter CreateNullableConverter()
|
||||||
|
{
|
||||||
|
_nullableEnumConverter ??= new NullableEnumConverter(this);
|
||||||
|
return _nullableEnumConverter;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Converter for serializing enum values as int
|
||||||
|
/// </summary>
|
||||||
|
public class EnumIntWriterConverter<T> : JsonConverter<T> where T: struct, Enum
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
|
||||||
|
=> writer.WriteNumberValue((int)(object)value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||||
|
{
|
||||||
|
internal interface INullableConverterFactory
|
||||||
|
{
|
||||||
|
JsonConverter CreateNullableConverter();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Attribute for allowing specifying a JsonConverter with constructor parameters
|
|
||||||
/// </summary>
|
|
||||||
[AttributeUsage(AttributeTargets.Property)]
|
|
||||||
public class JsonConverterCtorAttribute : JsonConverterAttribute
|
|
||||||
{
|
|
||||||
private readonly object[] _parameters;
|
|
||||||
private readonly Type _type;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
public JsonConverterCtorAttribute(Type type, params object[] parameters)
|
|
||||||
{
|
|
||||||
_type = type;
|
|
||||||
_parameters = parameters;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override JsonConverter CreateConverter(Type typeToConvert)
|
|
||||||
{
|
|
||||||
return (JsonConverter)Activator.CreateInstance(_type, _parameters)!;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json.Serialization.Metadata;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||||
|
{
|
||||||
|
internal class NullableEnumConverterFactory : JsonConverterFactory
|
||||||
|
{
|
||||||
|
private readonly IJsonTypeInfoResolver _jsonTypeInfoResolver;
|
||||||
|
private static readonly JsonSerializerOptions _options = new JsonSerializerOptions();
|
||||||
|
|
||||||
|
public NullableEnumConverterFactory(IJsonTypeInfoResolver jsonTypeInfoResolver)
|
||||||
|
{
|
||||||
|
_jsonTypeInfoResolver = jsonTypeInfoResolver;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool CanConvert(Type typeToConvert)
|
||||||
|
{
|
||||||
|
var b = Nullable.GetUnderlyingType(typeToConvert);
|
||||||
|
if (b == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var typeInfo = _jsonTypeInfoResolver.GetTypeInfo(b, _options);
|
||||||
|
if (typeInfo == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return typeInfo.Converter is INullableConverterFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
var b = Nullable.GetUnderlyingType(typeToConvert) ?? throw new ArgumentNullException($"Not nullable {typeToConvert.Name}");
|
||||||
|
var typeInfo = _jsonTypeInfoResolver.GetTypeInfo(b, _options) ?? throw new ArgumentNullException($"Can find type {typeToConvert.Name}");
|
||||||
|
if (typeInfo.Converter is not INullableConverterFactory nullConverterFactory)
|
||||||
|
throw new ArgumentNullException($"Can find type converter for {typeToConvert.Name}");
|
||||||
|
|
||||||
|
return nullConverterFactory.CreateNullableConverter();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +1,20 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
/// Converter for values which contain a nested json value
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="T"></typeparam>
|
|
||||||
public class ObjectStringConverter<T> : JsonConverter<T>
|
public class ObjectStringConverter<T> : JsonConverter<T>
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
#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")]
|
||||||
|
#endif
|
||||||
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
{
|
{
|
||||||
if (reader.TokenType == JsonTokenType.Null)
|
if (reader.TokenType == JsonTokenType.Null)
|
||||||
@@ -20,10 +24,14 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
if (string.IsNullOrEmpty(value))
|
if (string.IsNullOrEmpty(value))
|
||||||
return default;
|
return default;
|
||||||
|
|
||||||
return (T?)JsonDocument.Parse(value!).Deserialize(typeof(T));
|
return (T?)JsonDocument.Parse(value!).Deserialize(typeof(T), options);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
#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")]
|
||||||
|
#endif
|
||||||
public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options)
|
public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options)
|
||||||
{
|
{
|
||||||
if (value is null)
|
if (value is null)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Replace a value on a string property
|
/// Replace a value on a string property
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ReplaceConverter : JsonConverter<string>
|
public abstract class ReplaceConverter : JsonConverter<string>
|
||||||
{
|
{
|
||||||
private readonly (string ValueToReplace, string ValueToReplaceWith)[] _replacementSets;
|
private readonly (string ValueToReplace, string ValueToReplaceWith)[] _replacementSets;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute to mark a model as json serializable. Used for AOT compilation.
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(System.AttributeTargets.Class | AttributeTargets.Enum | System.AttributeTargets.Interface)]
|
||||||
|
public class SerializationModelAttribute : Attribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
public SerializationModelAttribute() { }
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type"></param>
|
||||||
|
public SerializationModelAttribute(Type type) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Text.Json;
|
using System.Collections.Concurrent;
|
||||||
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||||
@@ -8,22 +9,39 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static class SerializerOptions
|
public static class SerializerOptions
|
||||||
{
|
{
|
||||||
|
private static readonly ConcurrentDictionary<JsonSerializerContext, JsonSerializerOptions> _cache = new ConcurrentDictionary<JsonSerializerContext, JsonSerializerOptions>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Json serializer settings which includes the EnumConverter, DateTimeConverter, BoolConverter and DecimalConverter
|
/// Get Json serializer settings which includes standard converters for DateTime, bool, enum and number types
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static JsonSerializerOptions WithConverters { get; } = new JsonSerializerOptions
|
public static JsonSerializerOptions WithConverters(JsonSerializerContext typeResolver, params JsonConverter[] additionalConverters)
|
||||||
{
|
{
|
||||||
NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals,
|
if (!_cache.TryGetValue(typeResolver, out var options))
|
||||||
PropertyNameCaseInsensitive = false,
|
{
|
||||||
Converters =
|
options = new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals,
|
||||||
|
PropertyNameCaseInsensitive = false,
|
||||||
|
Converters =
|
||||||
{
|
{
|
||||||
new DateTimeConverter(),
|
new DateTimeConverter(),
|
||||||
new EnumConverter(),
|
|
||||||
new BoolConverter(),
|
new BoolConverter(),
|
||||||
new DecimalConverter(),
|
new DecimalConverter(),
|
||||||
new IntConverter(),
|
new IntConverter(),
|
||||||
new LongConverter()
|
new LongConverter(),
|
||||||
}
|
new NullableEnumConverterFactory(typeResolver)
|
||||||
};
|
},
|
||||||
|
TypeInfoResolver = typeResolver,
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var converter in additionalConverters)
|
||||||
|
options.Converters.Add(converter);
|
||||||
|
|
||||||
|
options.TypeInfoResolver = typeResolver;
|
||||||
|
_cache.TryAdd(typeResolver, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ using CryptoExchange.Net.Interfaces;
|
|||||||
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.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
@@ -20,11 +21,10 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
protected JsonDocument? _document;
|
protected JsonDocument? _document;
|
||||||
|
|
||||||
private static readonly JsonSerializerOptions _serializerOptions = SerializerOptions.WithConverters;
|
|
||||||
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; }
|
||||||
@@ -32,13 +32,6 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public object? Underlying => throw new NotImplementedException();
|
public object? Underlying => throw new NotImplementedException();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
public SystemTextJsonMessageAccessor()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -48,9 +41,13 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
#if NET5_0_OR_GREATER
|
||||||
|
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||||
|
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||||
|
#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)
|
||||||
@@ -58,22 +55,25 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var result = _document.Deserialize(type, _customSerializerOptions ?? _serializerOptions);
|
var result = _document.Deserialize(type, _customSerializerOptions);
|
||||||
return new CallResult<object>(result!);
|
return new CallResult<object>(result!);
|
||||||
}
|
}
|
||||||
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]"));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
#if NET5_0_OR_GREATER
|
||||||
|
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||||
|
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||||
|
#endif
|
||||||
public CallResult<T> Deserialize<T>(MessagePath? path = null)
|
public CallResult<T> Deserialize<T>(MessagePath? path = null)
|
||||||
{
|
{
|
||||||
if (_document == null)
|
if (_document == null)
|
||||||
@@ -81,25 +81,24 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var result = _document.Deserialize<T>(_customSerializerOptions ?? _serializerOptions);
|
var result = _document.Deserialize<T>(_customSerializerOptions);
|
||||||
return new CallResult<T>(result!);
|
return new CallResult<T>(result!);
|
||||||
}
|
}
|
||||||
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)
|
||||||
@@ -116,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);
|
||||||
@@ -132,9 +131,13 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
#if NET5_0_OR_GREATER
|
||||||
|
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||||
|
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||||
|
#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);
|
||||||
@@ -145,7 +148,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return value.Value.Deserialize<T>(_customSerializerOptions ?? _serializerOptions);
|
return value.Value.Deserialize<T>(_customSerializerOptions);
|
||||||
}
|
}
|
||||||
catch { }
|
catch { }
|
||||||
|
|
||||||
@@ -158,13 +161,17 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
return (T)(object)value.Value.GetInt64().ToString();
|
return (T)(object)value.Value.GetInt64().ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
return value.Value.Deserialize<T>();
|
return value.Value.Deserialize<T>(_customSerializerOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public List<T?>? GetValues<T>(MessagePath path)
|
#if NET5_0_OR_GREATER
|
||||||
|
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||||
|
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||||
|
#endif
|
||||||
|
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);
|
||||||
@@ -174,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>>()!;
|
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)
|
||||||
@@ -240,13 +247,6 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override bool OriginalDataAvailable => _stream?.CanSeek == true;
|
public override bool OriginalDataAvailable => _stream?.CanSeek == true;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
public SystemTextJsonStreamMessageAccessor(): base()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -277,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 new CallResult(null);
|
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -317,13 +317,6 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
{
|
{
|
||||||
private ReadOnlyMemory<byte> _bytes;
|
private ReadOnlyMemory<byte> _bytes;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
public SystemTextJsonByteMessageAccessor() : base()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -342,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 new CallResult(null);
|
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,29 @@
|
|||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
public SystemTextJsonMessageSerializer(JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
_options = options;
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string Serialize(object message) => JsonSerializer.Serialize(message, SerializerOptions.WithConverters);
|
#if NET5_0_OR_GREATER
|
||||||
|
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "Everything referenced in the loaded assembly is manually preserved, so it's safe")]
|
||||||
|
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "Everything referenced in the loaded assembly is manually preserved, so it's safe")]
|
||||||
|
#endif
|
||||||
|
public string Serialize<T>(T message) => JsonSerializer.Serialize(message, _options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFrameworks>netstandard2.0;netstandard2.1;net9.0</TargetFrameworks>
|
<TargetFrameworks>netstandard2.0;netstandard2.1;net8.0;net9.0</TargetFrameworks>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<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.6.0</PackageVersion>
|
||||||
<AssemblyVersion>8.8.0</AssemblyVersion>
|
<AssemblyVersion>9.6.0</AssemblyVersion>
|
||||||
<FileVersion>8.8.0</FileVersion>
|
<FileVersion>9.6.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,6 +27,9 @@
|
|||||||
<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="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net7.0'))">
|
||||||
|
<IsAotCompatible>true</IsAotCompatible>
|
||||||
|
</PropertyGroup>
|
||||||
<PropertyGroup Label="Deterministic Build" Condition="'$(Configuration)' == 'Release'">
|
<PropertyGroup Label="Deterministic Build" Condition="'$(Configuration)' == 'Release'">
|
||||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||||
<IncludeSymbols>true</IncludeSymbols>
|
<IncludeSymbols>true</IncludeSymbols>
|
||||||
@@ -34,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>
|
||||||
@@ -52,13 +49,13 @@
|
|||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" />
|
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.6" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
|
<PackageReference Include="System.Text.Json" Version="9.0.6" />
|
||||||
<PackageReference Include="System.Text.Json" Version="9.0.0" />
|
</ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.0" />
|
<ItemGroup Label="Transitive Client Packages">
|
||||||
|
<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;
|
||||||
@@ -15,6 +16,7 @@ namespace CryptoExchange.Net
|
|||||||
public static class ExchangeHelpers
|
public static class ExchangeHelpers
|
||||||
{
|
{
|
||||||
private const string _allowedRandomChars = "ABCDEFGHIJKLMONOPQRSTUVWXYZabcdefghijklmonopqrstuvwxyz0123456789";
|
private const string _allowedRandomChars = "ABCDEFGHIJKLMONOPQRSTUVWXYZabcdefghijklmonopqrstuvwxyz0123456789";
|
||||||
|
private const string _allowedRandomHexChars = "0123456789ABCDEF";
|
||||||
|
|
||||||
private static readonly Dictionary<int, string> _monthSymbols = new Dictionary<int, string>()
|
private static readonly Dictionary<int, string> _monthSymbols = new Dictionary<int, string>()
|
||||||
{
|
{
|
||||||
@@ -111,6 +113,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>
|
||||||
@@ -192,6 +222,44 @@ namespace CryptoExchange.Net
|
|||||||
return new string(randomChars);
|
return new string(randomChars);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generate a random string of specified length
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="length">Length of the random string</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string RandomHexString(int length)
|
||||||
|
{
|
||||||
|
#if NET9_0_OR_GREATER
|
||||||
|
return "0x" + RandomNumberGenerator.GetHexString(length * 2);
|
||||||
|
#else
|
||||||
|
var randomChars = new char[length * 2];
|
||||||
|
var random = new Random();
|
||||||
|
for (int i = 0; i < length * 2; i++)
|
||||||
|
randomChars[i] = _allowedRandomHexChars[random.Next(0, _allowedRandomHexChars.Length)];
|
||||||
|
return "0x" + new string(randomChars);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generate a long value
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="maxLength">Max character length</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static long RandomLong(int maxLength)
|
||||||
|
{
|
||||||
|
#if NETSTANDARD2_1_OR_GREATER || NET9_0_OR_GREATER
|
||||||
|
var value = RandomNumberGenerator.GetInt32(0, int.MaxValue);
|
||||||
|
#else
|
||||||
|
var random = new Random();
|
||||||
|
var value = random.Next(0, int.MaxValue);
|
||||||
|
#endif
|
||||||
|
var val = value.ToString();
|
||||||
|
if (val.Length > maxLength)
|
||||||
|
return int.Parse(val.Substring(0, maxLength));
|
||||||
|
else
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Generate a random string of specified length
|
/// Generate a random string of specified length
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -225,10 +293,10 @@ namespace CryptoExchange.Net
|
|||||||
/// <param name="request">The request parameters</param>
|
/// <param name="request">The request parameters</param>
|
||||||
/// <param name="ct">Cancellation token</param>
|
/// <param name="ct">Cancellation token</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static async IAsyncEnumerable<ExchangeWebResult<IEnumerable<T>>> ExecutePages<T, U>(Func<U, INextPageToken?, CancellationToken, Task<ExchangeWebResult<IEnumerable<T>>>> paginatedFunc, U request, [EnumeratorCancellation]CancellationToken ct = default)
|
public static async IAsyncEnumerable<ExchangeWebResult<T[]>> ExecutePages<T, U>(Func<U, INextPageToken?, CancellationToken, Task<ExchangeWebResult<T[]>>> paginatedFunc, U request, [EnumeratorCancellation]CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
var result = new List<T>();
|
var result = new List<T>();
|
||||||
ExchangeWebResult<IEnumerable<T>> batch;
|
ExchangeWebResult<T[]> batch;
|
||||||
INextPageToken? nextPageToken = null;
|
INextPageToken? nextPageToken = null;
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
@@ -274,5 +342,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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
using CryptoExchange.Net.SharedApis;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Cache for symbol parsing
|
||||||
|
/// </summary>
|
||||||
|
public static class ExchangeSymbolCache
|
||||||
|
{
|
||||||
|
private static ConcurrentDictionary<string, ExchangeInfo> _symbolInfos = new ConcurrentDictionary<string, ExchangeInfo>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Update the cached symbol data for an exchange
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="topicId">Id for the provided data</param>
|
||||||
|
/// <param name="updateData">Symbol data</param>
|
||||||
|
public static void UpdateSymbolInfo(string topicId, SharedSpotSymbol[] updateData)
|
||||||
|
{
|
||||||
|
if(!_symbolInfos.TryGetValue(topicId, out var exchangeInfo))
|
||||||
|
{
|
||||||
|
exchangeInfo = new ExchangeInfo(DateTime.UtcNow, updateData.ToDictionary(x => x.Name, x => x.SharedSymbol));
|
||||||
|
_symbolInfos.TryAdd(topicId, exchangeInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DateTime.UtcNow - exchangeInfo.UpdateTime < TimeSpan.FromMinutes(60))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_symbolInfos[topicId] = new ExchangeInfo(DateTime.UtcNow, updateData.ToDictionary(x => x.Name, x => x.SharedSymbol));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parse a symbol name to a SharedSymbol
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="topicId">Id for the provided data</param>
|
||||||
|
/// <param name="symbolName">Symbol name</param>
|
||||||
|
public static SharedSymbol? ParseSymbol(string topicId, string? symbolName)
|
||||||
|
{
|
||||||
|
if (symbolName == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (!_symbolInfos.TryGetValue(topicId, out var exchangeInfo))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (!exchangeInfo.Symbols.TryGetValue(symbolName, out var symbolInfo))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return new SharedSymbol(symbolInfo.TradingMode, symbolInfo.BaseAsset, symbolInfo.QuoteAsset, symbolName)
|
||||||
|
{
|
||||||
|
DeliverTime = symbolInfo.DeliverTime
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class ExchangeInfo
|
||||||
|
{
|
||||||
|
public DateTime UpdateTime { get; set; }
|
||||||
|
public Dictionary<string, SharedSymbol> Symbols { get; set; }
|
||||||
|
|
||||||
|
public ExchangeInfo(DateTime updateTime, Dictionary<string, SharedSymbol> symbols)
|
||||||
|
{
|
||||||
|
UpdateTime = updateTime;
|
||||||
|
Symbols = symbols;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,9 @@ using CryptoExchange.Net.Objects;
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using CryptoExchange.Net.SharedApis;
|
using CryptoExchange.Net.SharedApis;
|
||||||
|
using System.Text.Json.Serialization.Metadata;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace CryptoExchange.Net
|
namespace CryptoExchange.Net
|
||||||
{
|
{
|
||||||
@@ -440,6 +443,8 @@ 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(ISpotOrderRestClient).IsAssignableFrom(typeof(T)))
|
if (typeof(ISpotOrderRestClient).IsAssignableFrom(typeof(T)))
|
||||||
services.AddTransient(x => (ISpotOrderRestClient)client(x)!);
|
services.AddTransient(x => (ISpotOrderRestClient)client(x)!);
|
||||||
@@ -447,6 +452,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)!);
|
||||||
@@ -468,6 +477,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;
|
||||||
}
|
}
|
||||||
@@ -483,8 +498,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)))
|
||||||
|
|||||||
@@ -1,94 +0,0 @@
|
|||||||
using CryptoExchange.Net.CommonObjects;
|
|
||||||
using CryptoExchange.Net.Objects;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Interfaces.CommonClients
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
|
|
||||||
/// </summary>
|
|
||||||
public interface IBaseRestClient
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
|
|
||||||
/// </summary>
|
|
||||||
string ExchangeName { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
|
|
||||||
/// </summary>
|
|
||||||
event Action<OrderId> OnOrderPlaced;
|
|
||||||
/// <summary>
|
|
||||||
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
|
|
||||||
/// </summary>
|
|
||||||
event Action<OrderId> OnOrderCanceled;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
|
|
||||||
/// </summary>
|
|
||||||
string GetSymbolName(string baseAsset, string quoteAsset);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
|
|
||||||
/// </summary>
|
|
||||||
Task<WebCallResult<IEnumerable<Symbol>>> GetSymbolsAsync(CancellationToken ct = default);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
|
|
||||||
/// </summary>
|
|
||||||
Task<WebCallResult<Ticker>> GetTickerAsync(string symbol, CancellationToken ct = default);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
|
|
||||||
/// </summary>
|
|
||||||
Task<WebCallResult<IEnumerable<Ticker>>> GetTickersAsync(CancellationToken ct = default);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
|
|
||||||
/// </summary>
|
|
||||||
Task<WebCallResult<IEnumerable<Kline>>> GetKlinesAsync(string symbol, TimeSpan timespan, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, CancellationToken ct = default);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
|
|
||||||
/// </summary>
|
|
||||||
Task<WebCallResult<CommonObjects.OrderBook>> GetOrderBookAsync(string symbol, CancellationToken ct = default);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
|
|
||||||
/// </summary>
|
|
||||||
Task<WebCallResult<IEnumerable<Trade>>> GetRecentTradesAsync(string symbol, CancellationToken ct = default);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
|
|
||||||
/// </summary>
|
|
||||||
Task<WebCallResult<IEnumerable<Balance>>> GetBalancesAsync(string? accountId = null, CancellationToken ct = default);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
|
|
||||||
/// </summary>
|
|
||||||
Task<WebCallResult<Order>> GetOrderAsync(string orderId, string? symbol = null, CancellationToken ct = default);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
|
|
||||||
/// </summary>
|
|
||||||
Task<WebCallResult<IEnumerable<UserTrade>>> GetOrderTradesAsync(string orderId, string? symbol = null, CancellationToken ct = default);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
|
|
||||||
/// </summary>
|
|
||||||
Task<WebCallResult<IEnumerable<Order>>> GetOpenOrdersAsync(string? symbol = null, CancellationToken ct = default);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
|
|
||||||
/// </summary>
|
|
||||||
Task<WebCallResult<IEnumerable<Order>>> GetClosedOrdersAsync(string? symbol = null, CancellationToken ct = default);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
|
|
||||||
/// </summary>
|
|
||||||
Task<WebCallResult<OrderId>> CancelOrderAsync(string orderId, string? symbol = null, CancellationToken ct = default);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using CryptoExchange.Net.CommonObjects;
|
|
||||||
using CryptoExchange.Net.Objects;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Interfaces.CommonClients
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
|
|
||||||
/// </summary>
|
|
||||||
public interface IFuturesClient : IBaseRestClient
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
|
|
||||||
/// </summary>
|
|
||||||
Task<WebCallResult<OrderId>> PlaceOrderAsync(string symbol, CommonOrderSide side, CommonOrderType type, decimal quantity, decimal? price = null, int? leverage = null, string? accountId = null, string? clientOrderId = null, CancellationToken ct = default);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
|
|
||||||
/// </summary>
|
|
||||||
Task<WebCallResult<IEnumerable<Position>>> GetPositionsAsync(CancellationToken ct = default);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
using CryptoExchange.Net.CommonObjects;
|
|
||||||
using CryptoExchange.Net.Objects;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Interfaces.CommonClients
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="" /> for more info.
|
|
||||||
/// </summary>
|
|
||||||
public interface ISpotClient: IBaseRestClient
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
|
|
||||||
/// </summary>
|
|
||||||
Task<WebCallResult<OrderId>> PlaceOrderAsync(string symbol, CommonOrderSide side, CommonOrderType type, decimal quantity, decimal? price = null, string? accountId = null, string? clientOrderId = null, CancellationToken ct = default);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
using CryptoExchange.Net.Interfaces.CommonClients;
|
using System;
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Interfaces
|
namespace CryptoExchange.Net.Interfaces
|
||||||
{
|
{
|
||||||
@@ -9,19 +7,6 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public interface ICryptoRestClient
|
public interface ICryptoRestClient
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Get a list of all registered common ISpotClient types
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
IEnumerable<ISpotClient> GetSpotClients();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get an ISpotClient implementation by exchange name
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="exchangeName"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
ISpotClient? SpotClient(string exchangeName);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Try get
|
/// Try get
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -29,4 +14,4 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
T TryGet<T>(Func<T> createFunc);
|
T TryGet<T>(Func<T> createFunc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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,15 +1,37 @@
|
|||||||
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>
|
/// <summary>
|
||||||
/// Serialize an object to a string
|
/// Serialize an object to a string
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="message"></param>
|
/// <param name="message"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
string Serialize(object message);
|
byte[] Serialize<T>(T message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Serialize to string
|
||||||
|
/// </summary>
|
||||||
|
public interface IStringMessageSerializer: IMessageSerializer
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Serialize an object to a string
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
string Serialize<T>(T message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// Get all headers
|
/// Get all headers
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Dictionary<string, IEnumerable<string>> GetHeaders();
|
KeyValuePair<string, string[]>[] GetHeaders();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get the response
|
/// Get the response
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The response headers
|
/// The response headers
|
||||||
/// </summary>
|
/// </summary>
|
||||||
IEnumerable<KeyValuePair<string, IEnumerable<string>>> ResponseHeaders { get; }
|
KeyValuePair<string, string[]>[] ResponseHeaders { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get the response stream
|
/// Get the response stream
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Event when order book was updated. Be careful! It can generate a lot of events at high-liquidity markets
|
/// Event when order book was updated. Be careful! It can generate a lot of events at high-liquidity markets
|
||||||
/// </summary>
|
/// </summary>
|
||||||
event Action<(IEnumerable<ISymbolOrderBookEntry> Bids, IEnumerable<ISymbolOrderBookEntry> Asks)> OnOrderBookUpdate;
|
event Action<(ISymbolOrderBookEntry[] Bids, ISymbolOrderBookEntry[] Asks)> OnOrderBookUpdate;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Event when the BestBid or BestAsk changes ie a Pricing Tick
|
/// Event when the BestBid or BestAsk changes ie a Pricing Tick
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -64,17 +64,17 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get a snapshot of the book at this moment
|
/// Get a snapshot of the book at this moment
|
||||||
/// </summary>
|
/// </summary>
|
||||||
(IEnumerable<ISymbolOrderBookEntry> bids, IEnumerable<ISymbolOrderBookEntry> asks) Book { get; }
|
(ISymbolOrderBookEntry[] bids, ISymbolOrderBookEntry[] asks) Book { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The list of asks
|
/// The list of asks
|
||||||
/// </summary>
|
/// </summary>
|
||||||
IEnumerable<ISymbolOrderBookEntry> Asks { get; }
|
ISymbolOrderBookEntry[] Asks { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The list of bids
|
/// The list of bids
|
||||||
/// </summary>
|
/// </summary>
|
||||||
IEnumerable<ISymbolOrderBookEntry> Bids { get; }
|
ISymbolOrderBookEntry[] Bids { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The best bid currently in the order book
|
/// The best bid currently in the order book
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
+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)
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,11 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class CallResult
|
public class CallResult
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Static success result
|
||||||
|
/// </summary>
|
||||||
|
public static CallResult SuccessResult { get; } = new CallResult(null);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// An error if the call didn't succeed, will always be filled if Success = false
|
/// An error if the call didn't succeed, will always be filled if Success = false
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -149,7 +154,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public CallResult AsDataless()
|
public CallResult AsDataless()
|
||||||
{
|
{
|
||||||
return new CallResult(null);
|
return SuccessResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -161,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>
|
||||||
@@ -192,7 +209,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The headers sent with the request
|
/// The headers sent with the request
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IEnumerable<KeyValuePair<string, IEnumerable<string>>>? RequestHeaders { get; set; }
|
public KeyValuePair<string, string[]>[]? RequestHeaders { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The request id
|
/// The request id
|
||||||
@@ -209,6 +226,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>
|
||||||
@@ -217,7 +239,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The response headers
|
/// The response headers
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IEnumerable<KeyValuePair<string, IEnumerable<string>>>? ResponseHeaders { get; set; }
|
public KeyValuePair<string, string[]>[]? ResponseHeaders { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The time between sending the request and receiving the response
|
/// The time between sending the request and receiving the response
|
||||||
@@ -227,30 +249,23 @@ 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,
|
||||||
IEnumerable<KeyValuePair<string, IEnumerable<string>>>? responseHeaders,
|
KeyValuePair<string, string[]>[]? responseHeaders,
|
||||||
TimeSpan? responseTime,
|
TimeSpan? responseTime,
|
||||||
|
string? originalData,
|
||||||
int? requestId,
|
int? requestId,
|
||||||
string? requestUrl,
|
string? requestUrl,
|
||||||
string? requestBody,
|
string? requestBody,
|
||||||
HttpMethod? requestMethod,
|
HttpMethod? requestMethod,
|
||||||
IEnumerable<KeyValuePair<string, IEnumerable<string>>>? requestHeaders,
|
KeyValuePair<string, string[]>[]? requestHeaders,
|
||||||
Error? error) : base(error)
|
Error? error) : base(error)
|
||||||
{
|
{
|
||||||
ResponseStatusCode = code;
|
ResponseStatusCode = code;
|
||||||
ResponseHeaders = responseHeaders;
|
ResponseHeaders = responseHeaders;
|
||||||
ResponseTime = responseTime;
|
ResponseTime = responseTime;
|
||||||
RequestId = requestId;
|
RequestId = requestId;
|
||||||
|
OriginalData = originalData;
|
||||||
|
|
||||||
RequestUrl = requestUrl;
|
RequestUrl = requestUrl;
|
||||||
RequestBody = requestBody;
|
RequestBody = requestBody;
|
||||||
@@ -271,7 +286,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, ResponseHeaders, ResponseTime, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -343,7 +358,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The headers sent with the request
|
/// The headers sent with the request
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IEnumerable<KeyValuePair<string, IEnumerable<string>>>? RequestHeaders { get; set; }
|
public KeyValuePair<string, string[]>[]? RequestHeaders { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The request id
|
/// The request id
|
||||||
@@ -373,7 +388,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The response headers
|
/// The response headers
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IEnumerable<KeyValuePair<string, IEnumerable<string>>>? ResponseHeaders { get; set; }
|
public KeyValuePair<string, string[]>[]? ResponseHeaders { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The time between sending the request and receiving the response
|
/// The time between sending the request and receiving the response
|
||||||
@@ -403,7 +418,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <param name="error"></param>
|
/// <param name="error"></param>
|
||||||
public WebCallResult(
|
public WebCallResult(
|
||||||
HttpStatusCode? code,
|
HttpStatusCode? code,
|
||||||
IEnumerable<KeyValuePair<string, IEnumerable<string>>>? responseHeaders,
|
KeyValuePair<string, string[]>[]? responseHeaders,
|
||||||
TimeSpan? responseTime,
|
TimeSpan? responseTime,
|
||||||
long? responseLength,
|
long? responseLength,
|
||||||
string? originalData,
|
string? originalData,
|
||||||
@@ -411,7 +426,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
string? requestUrl,
|
string? requestUrl,
|
||||||
string? requestBody,
|
string? requestBody,
|
||||||
HttpMethod? requestMethod,
|
HttpMethod? requestMethod,
|
||||||
IEnumerable<KeyValuePair<string, IEnumerable<string>>>? requestHeaders,
|
KeyValuePair<string, string[]>[]? requestHeaders,
|
||||||
ResultDataSource dataSource,
|
ResultDataSource dataSource,
|
||||||
[AllowNull] T data,
|
[AllowNull] T data,
|
||||||
Error? error) : base(data, originalData, error)
|
Error? error) : base(data, originalData, error)
|
||||||
@@ -435,7 +450,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, ResponseHeaders, ResponseTime, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, Error);
|
||||||
}
|
}
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Copy as a dataless result
|
/// Copy as a dataless result
|
||||||
@@ -443,7 +458,7 @@ 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, ResponseHeaders, ResponseTime, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -474,6 +489,18 @@ namespace CryptoExchange.Net.Objects
|
|||||||
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, default, error);
|
return new WebCallResult<K>(ResponseStatusCode, 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, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, data, error);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Copy the WebCallResult to an ExchangeWebResult of a new data type
|
/// Copy the WebCallResult to an ExchangeWebResult of a new data type
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -553,7 +580,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,20 @@ 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
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user