mirror of
https://github.com/JKorf/CryptoExchange.Net.git
synced 2026-08-12 17:03:10 +00:00
Compare commits
99 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 73764970b0 | |||
| 9ba29035b2 | |||
| b215cccda4 | |||
| 3eda488361 | |||
| 993a44de35 | |||
| 99465f99a1 | |||
| d42de1fe90 | |||
| 4c953e2c87 | |||
| 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 | |||
| 3d6267da93 | |||
| 8def7f32af | |||
| ac295de9f6 | |||
| d412e0895e | |||
| 1f9e2b4fcb | |||
| b13cff5a95 | |||
| 4c050744ad | |||
| 3b15c35a02 | |||
| cd78dbf575 | |||
| a258532d6a | |||
| d2a87a1069 | |||
| e07f24ea0a | |||
| 024e8dcfe2 | |||
| 4bb5aae40a | |||
| dec94678ec | |||
| 1a49fc8251 | |||
| 29b0875960 | |||
| 976ccab1da | |||
| 02bbd37bb6 | |||
| 1bbbec7f2b | |||
| 0262f04913 | |||
| fd1ec17d72 | |||
| 4bdad7fe0c | |||
| 74f73dc790 | |||
| 0527a8a76e | |||
| c693eb8c02 | |||
| 3eb28c7fed | |||
| 618c4922b9 | |||
| c81b15861d | |||
| 4a5832cccd | |||
| 4e47c4cbdf | |||
| 2af1520ecc | |||
| cf397af3ab | |||
| a1479705e2 | |||
| 175e23f110 | |||
| 9b7019ded2 | |||
| 7904aa9ba7 |
@@ -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
|
||||||
@@ -106,6 +106,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
for(var i = 1; i <= 10; i++)
|
for(var i = 1; i <= 10; i++)
|
||||||
{
|
{
|
||||||
evnt.Set();
|
evnt.Set();
|
||||||
|
await Task.Delay(1); // Wait for the continuation.
|
||||||
Assert.That(10 - i == waiters.Count(w => w.Status != TaskStatus.RanToCompletion));
|
Assert.That(10 - i == waiters.Count(w => w.Status != TaskStatus.RanToCompletion));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,14 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0"></PackageReference>
|
<None Include="..\CryptoExchange.Net\.editorconfig" Link=".editorconfig" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<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 },
|
||||||
@@ -176,19 +170,19 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
|
|
||||||
for (var i = 0; i < requests + 1; i++)
|
for (var i = 0; i < requests + 1; i++)
|
||||||
{
|
{
|
||||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, default);
|
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||||
Assert.That(i == requests? triggered : !triggered);
|
Assert.That(i == requests? triggered : !triggered);
|
||||||
}
|
}
|
||||||
triggered = false;
|
triggered = false;
|
||||||
await Task.Delay((int)Math.Round(perSeconds * 1000) + 10);
|
await Task.Delay((int)Math.Round(perSeconds * 1000) + 10);
|
||||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, default);
|
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||||
Assert.That(!triggered);
|
Assert.That(!triggered);
|
||||||
}
|
}
|
||||||
|
|
||||||
[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)
|
||||||
{
|
{
|
||||||
@@ -201,7 +195,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||||
for (var i = 0; i < 2; i++)
|
for (var i = 0; i < 2; i++)
|
||||||
{
|
{
|
||||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, default);
|
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||||
bool expected = i == 1 ? (expectLimiting ? evnt.DelayTime > TimeSpan.Zero : evnt == null) : evnt == null;
|
bool expected = i == 1 ? (expectLimiting ? evnt.DelayTime > TimeSpan.Zero : evnt == null) : evnt == null;
|
||||||
Assert.That(expected);
|
Assert.That(expected);
|
||||||
}
|
}
|
||||||
@@ -222,9 +216,9 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
RateLimitEvent evnt = null;
|
RateLimitEvent evnt = null;
|
||||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||||
|
|
||||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, default);
|
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||||
Assert.That(evnt == null);
|
Assert.That(evnt == null);
|
||||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, default);
|
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||||
Assert.That(expectLimiting ? evnt != null : evnt == null);
|
Assert.That(expectLimiting ? evnt != null : evnt == null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,12 +237,12 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
|
|
||||||
for (var i = 0; i < requests + 1; i++)
|
for (var i = 0; i < requests + 1; i++)
|
||||||
{
|
{
|
||||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, default);
|
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||||
Assert.That(i == requests ? triggered : !triggered);
|
Assert.That(i == requests ? triggered : !triggered);
|
||||||
}
|
}
|
||||||
triggered = false;
|
triggered = false;
|
||||||
await Task.Delay((int)Math.Round(perSeconds * 1000) + 10);
|
await Task.Delay((int)Math.Round(perSeconds * 1000) + 10);
|
||||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, default);
|
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||||
Assert.That(!triggered);
|
Assert.That(!triggered);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,7 +260,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||||
for (var i = 0; i < 2; i++)
|
for (var i = 0; i < 2; i++)
|
||||||
{
|
{
|
||||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, default);
|
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||||
bool expected = i == 1 ? (expectLimited ? evnt.DelayTime > TimeSpan.Zero : evnt == null) : evnt == null;
|
bool expected = i == 1 ? (expectLimited ? evnt.DelayTime > TimeSpan.Zero : evnt == null) : evnt == null;
|
||||||
Assert.That(expected);
|
Assert.That(expected);
|
||||||
}
|
}
|
||||||
@@ -286,7 +280,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||||
for (var i = 0; i < 2; i++)
|
for (var i = 0; i < 2; i++)
|
||||||
{
|
{
|
||||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, default);
|
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||||
bool expected = i == 1 ? (expectLimited ? evnt.DelayTime > TimeSpan.Zero : evnt == null) : evnt == null;
|
bool expected = i == 1 ? (expectLimited ? evnt.DelayTime > TimeSpan.Zero : evnt == null) : evnt == null;
|
||||||
Assert.That(expected);
|
Assert.That(expected);
|
||||||
}
|
}
|
||||||
@@ -309,9 +303,9 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
RateLimitEvent evnt = null;
|
RateLimitEvent evnt = null;
|
||||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||||
|
|
||||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", key1, 1, RateLimitingBehaviour.Wait, default);
|
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", key1, 1, RateLimitingBehaviour.Wait, null, default);
|
||||||
Assert.That(evnt == null);
|
Assert.That(evnt == null);
|
||||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", key2, 1, RateLimitingBehaviour.Wait, default);
|
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", key2, 1, RateLimitingBehaviour.Wait, null, default);
|
||||||
Assert.That(expectLimited ? evnt != null : evnt == null);
|
Assert.That(expectLimited ? evnt != null : evnt == null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -328,9 +322,9 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
RateLimitEvent evnt = null;
|
RateLimitEvent evnt = null;
|
||||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||||
|
|
||||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, default);
|
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||||
Assert.That(evnt == null);
|
Assert.That(evnt == null);
|
||||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", null, 1, RateLimitingBehaviour.Wait, default);
|
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", null, 1, RateLimitingBehaviour.Wait, null, default);
|
||||||
Assert.That(expectLimited ? evnt != null : evnt == null);
|
Assert.That(expectLimited ? evnt != null : evnt == null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -348,9 +342,9 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
RateLimitEvent evnt = null;
|
RateLimitEvent evnt = null;
|
||||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||||
|
|
||||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, host1, "123", 1, RateLimitingBehaviour.Wait, default);
|
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, host1, "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||||
Assert.That(evnt == null);
|
Assert.That(evnt == null);
|
||||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, host2, "123", 1, RateLimitingBehaviour.Wait, default);
|
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, host2, "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||||
Assert.That(expectLimited ? evnt != null : evnt == null);
|
Assert.That(expectLimited ? evnt != null : evnt == null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -365,9 +359,9 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
RateLimitEvent evnt = null;
|
RateLimitEvent evnt = null;
|
||||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||||
|
|
||||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), host1, "123", 1, RateLimitingBehaviour.Wait, default);
|
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), host1, "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||||
Assert.That(evnt == null);
|
Assert.That(evnt == null);
|
||||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), host2, "123", 1, RateLimitingBehaviour.Wait, default);
|
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), host2, "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||||
Assert.That(expectLimited ? evnt != null : evnt == null);
|
Assert.That(expectLimited ? evnt != null : evnt == null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -381,8 +375,8 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||||
var ct = new CancellationTokenSource(TimeSpan.FromSeconds(0.2));
|
var ct = new CancellationTokenSource(TimeSpan.FromSeconds(0.2));
|
||||||
|
|
||||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), "https://test.com", "123", 1, RateLimitingBehaviour.Wait, ct.Token);
|
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, ct.Token);
|
||||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), "https://test.com", "123", 1, RateLimitingBehaviour.Wait, ct.Token);
|
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, ct.Token);
|
||||||
Assert.That(result2.Error, Is.TypeOf<CancellationRequestedError>());
|
Assert.That(result2.Error, Is.TypeOf<CancellationRequestedError>());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,132 +0,0 @@
|
|||||||
//using System;
|
|
||||||
//using System.IO;
|
|
||||||
//using System.Net.WebSockets;
|
|
||||||
//using System.Security.Authentication;
|
|
||||||
//using System.Text;
|
|
||||||
//using System.Threading.Tasks;
|
|
||||||
//using CryptoExchange.Net.Interfaces;
|
|
||||||
//using CryptoExchange.Net.Objects;
|
|
||||||
|
|
||||||
//namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|
||||||
//{
|
|
||||||
// public class TestSocket: IWebsocket
|
|
||||||
// {
|
|
||||||
// public bool CanConnect { get; set; }
|
|
||||||
// public bool Connected { get; set; }
|
|
||||||
|
|
||||||
// public event Func<Task> OnClose;
|
|
||||||
//#pragma warning disable 0067
|
|
||||||
// public event Func<Task> OnReconnected;
|
|
||||||
// public event Func<Task> OnReconnecting;
|
|
||||||
// public event Func<int, Task> OnRequestRateLimited;
|
|
||||||
//#pragma warning restore 0067
|
|
||||||
// public event Func<int, Task> OnRequestSent;
|
|
||||||
// public event Func<WebSocketMessageType, ReadOnlyMemory<byte>, Task> OnStreamMessage;
|
|
||||||
// public event Func<Exception, Task> OnError;
|
|
||||||
// public event Func<Task> OnOpen;
|
|
||||||
// public Func<Task<Uri>> GetReconnectionUrl { get; set; }
|
|
||||||
|
|
||||||
// public int Id { get; }
|
|
||||||
// public bool ShouldReconnect { get; set; }
|
|
||||||
// public TimeSpan Timeout { get; set; }
|
|
||||||
// public Func<string, string> DataInterpreterString { get; set; }
|
|
||||||
// public Func<byte[], string> DataInterpreterBytes { get; set; }
|
|
||||||
// public DateTime? DisconnectTime { get; set; }
|
|
||||||
// public string Url { get; }
|
|
||||||
// public bool IsClosed => !Connected;
|
|
||||||
// public bool IsOpen => Connected;
|
|
||||||
// public bool PingConnection { get; set; }
|
|
||||||
// public TimeSpan PingInterval { get; set; }
|
|
||||||
// public SslProtocols SSLProtocols { get; set; }
|
|
||||||
// public Encoding Encoding { get; set; }
|
|
||||||
|
|
||||||
// public int ConnectCalls { get; private set; }
|
|
||||||
// public bool Reconnecting { get; set; }
|
|
||||||
// public string Origin { get; set; }
|
|
||||||
// public int? RatelimitPerSecond { get; set; }
|
|
||||||
|
|
||||||
// public double IncomingKbps => throw new NotImplementedException();
|
|
||||||
|
|
||||||
// public Uri Uri => new Uri("");
|
|
||||||
|
|
||||||
// public TimeSpan KeepAliveInterval { get; set; }
|
|
||||||
|
|
||||||
// public static int lastId = 0;
|
|
||||||
// public static object lastIdLock = new object();
|
|
||||||
|
|
||||||
// public TestSocket()
|
|
||||||
// {
|
|
||||||
// lock (lastIdLock)
|
|
||||||
// {
|
|
||||||
// Id = lastId + 1;
|
|
||||||
// lastId++;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// public Task<CallResult> ConnectAsync()
|
|
||||||
// {
|
|
||||||
// Connected = CanConnect;
|
|
||||||
// ConnectCalls++;
|
|
||||||
// if (CanConnect)
|
|
||||||
// InvokeOpen();
|
|
||||||
// return Task.FromResult(CanConnect ? new CallResult(null) : new CallResult(new CantConnectError()));
|
|
||||||
// }
|
|
||||||
|
|
||||||
// public bool Send(int requestId, string data, int weight)
|
|
||||||
// {
|
|
||||||
// if(!Connected)
|
|
||||||
// throw new Exception("Socket not connected");
|
|
||||||
// OnRequestSent?.Invoke(requestId);
|
|
||||||
// return true;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// public void Reset()
|
|
||||||
// {
|
|
||||||
// }
|
|
||||||
|
|
||||||
// public Task CloseAsync()
|
|
||||||
// {
|
|
||||||
// Connected = false;
|
|
||||||
// DisconnectTime = DateTime.UtcNow;
|
|
||||||
// OnClose?.Invoke();
|
|
||||||
// return Task.FromResult(0);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// public void SetProxy(string host, int port)
|
|
||||||
// {
|
|
||||||
// throw new NotImplementedException();
|
|
||||||
// }
|
|
||||||
// public void Dispose()
|
|
||||||
// {
|
|
||||||
// }
|
|
||||||
|
|
||||||
// public void InvokeClose()
|
|
||||||
// {
|
|
||||||
// Connected = false;
|
|
||||||
// DisconnectTime = DateTime.UtcNow;
|
|
||||||
// Reconnecting = true;
|
|
||||||
// OnClose?.Invoke();
|
|
||||||
// }
|
|
||||||
|
|
||||||
// public void InvokeOpen()
|
|
||||||
// {
|
|
||||||
// OnOpen?.Invoke();
|
|
||||||
// }
|
|
||||||
|
|
||||||
// public void InvokeMessage(string data)
|
|
||||||
// {
|
|
||||||
// OnStreamMessage?.Invoke(WebSocketMessageType.Text, new ReadOnlyMemory<byte>(Encoding.UTF8.GetBytes(data))).Wait();
|
|
||||||
// }
|
|
||||||
|
|
||||||
// public void SetProxy(ApiProxy proxy)
|
|
||||||
// {
|
|
||||||
// throw new NotImplementedException();
|
|
||||||
// }
|
|
||||||
|
|
||||||
// public void InvokeError(Exception error)
|
|
||||||
// {
|
|
||||||
// OnError?.Invoke(error);
|
|
||||||
// }
|
|
||||||
// public Task ReconnectAsync() => Task.CompletedTask;
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -0,0 +1,183 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
|
||||||
|
# Indentation and spacing
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
charset = utf-8
|
||||||
|
max_line_length = 140
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
# ReSharper code style properties
|
||||||
|
resharper_csharp_keep_existing_embedded_arrangement = false
|
||||||
|
resharper_csharp_place_accessorholder_attribute_on_same_line = false
|
||||||
|
resharper_csharp_wrap_after_declaration_lpar = true
|
||||||
|
resharper_csharp_wrap_parameters_style = chop_if_long
|
||||||
|
resharper_csharp_blank_lines_around_single_line_auto_property = 1
|
||||||
|
resharper_csharp_keep_blank_lines_in_declarations = 1
|
||||||
|
resharper_trailing_comma_in_multiline_lists = true
|
||||||
|
|
||||||
|
[*.cs]
|
||||||
|
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
# Code style conventions
|
||||||
|
dotnet_style_predefined_type_for_member_access = true:suggestion
|
||||||
|
dotnet_style_collection_initializer = true:suggestion
|
||||||
|
dotnet_style_object_initializer = true:suggestion
|
||||||
|
csharp_style_var_when_type_is_apparent = true:suggestion
|
||||||
|
csharp_style_expression_bodied_methods = true:suggestion
|
||||||
|
csharp_style_namespace_declarations = file_scoped:warning
|
||||||
|
dotnet_style_coalesce_expression = true:suggestion
|
||||||
|
dotnet_style_null_propagation = true:suggestion
|
||||||
|
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
|
||||||
|
dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
|
||||||
|
csharp_prefer_braces = when_multiline:warning
|
||||||
|
|
||||||
|
# Analyzer preferences
|
||||||
|
dotnet_diagnostic.CA2007.severity = warning # Call ConfigureAwait on the awaited Task.
|
||||||
|
dotnet_code_quality.CA2007.exclude_async_void_methods = true
|
||||||
|
dotnet_code_quality.CA2007.output_kind = DynamicallyLinkedLibrary
|
||||||
|
|
||||||
|
dotnet_diagnostic.CA1000.severity = none # Do not declare static members on generic types
|
||||||
|
dotnet_diagnostic.CA1051.severity = none # Do not declare visible instance fields
|
||||||
|
dotnet_diagnostic.CA1510.severity = none # Use ArgumentNullException throw helper
|
||||||
|
dotnet_diagnostic.CA1720.severity = none # Identifiers should not contain type names
|
||||||
|
dotnet_diagnostic.CA1716.severity = none # Identifiers should not match keywords
|
||||||
|
dotnet_diagnostic.CA1835.severity = none # Use ArgumentNullException throw helper
|
||||||
|
dotnet_diagnostic.CA1846.severity = none # Prefer AsSpan over Substring
|
||||||
|
dotnet_diagnostic.CA1848.severity = none # Use the LoggerMessage delegates
|
||||||
|
dotnet_diagnostic.CA1850.severity = none # Prefer static HashData method over ComputeHash
|
||||||
|
dotnet_diagnostic.CA1866.severity = none # Use 'string.Method(char)' instead of 'string.Method(string)' for string with single char
|
||||||
|
dotnet_diagnostic.CA2201.severity = none # Do not raise reserved exception types
|
||||||
|
dotnet_diagnostic.CA2208.severity = none # Do not raise reserved exception types
|
||||||
|
dotnet_diagnostic.IDE0005.severity = warning # Using directive is unnecessary
|
||||||
|
|
||||||
|
[*.xml]
|
||||||
|
ij_xml_space_inside_empty_tag = true
|
||||||
|
[*.cs]
|
||||||
|
#### Naming styles ####
|
||||||
|
|
||||||
|
# Naming rules
|
||||||
|
|
||||||
|
dotnet_naming_rule.interface_should_be_begins_with_i.severity = warning
|
||||||
|
dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
|
||||||
|
dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
|
||||||
|
|
||||||
|
dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = warning
|
||||||
|
dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
|
||||||
|
dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
|
||||||
|
|
||||||
|
dotnet_naming_rule.private_or_internal_field_should_be_fields_start_with__.severity = warning
|
||||||
|
dotnet_naming_rule.private_or_internal_field_should_be_fields_start_with__.symbols = private_or_internal_field
|
||||||
|
dotnet_naming_rule.private_or_internal_field_should_be_fields_start_with__.style = fields_start_with__
|
||||||
|
|
||||||
|
# Symbol specifications
|
||||||
|
|
||||||
|
dotnet_naming_symbols.interface.applicable_kinds = interface
|
||||||
|
dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||||
|
dotnet_naming_symbols.interface.required_modifiers =
|
||||||
|
|
||||||
|
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
|
||||||
|
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||||
|
dotnet_naming_symbols.non_field_members.required_modifiers =
|
||||||
|
|
||||||
|
dotnet_naming_symbols.private_or_internal_field.applicable_kinds = field
|
||||||
|
dotnet_naming_symbols.private_or_internal_field.applicable_accessibilities = internal, private, private_protected
|
||||||
|
dotnet_naming_symbols.private_or_internal_field.required_modifiers =
|
||||||
|
|
||||||
|
# Naming styles
|
||||||
|
|
||||||
|
dotnet_naming_style.begins_with_i.required_prefix = I
|
||||||
|
dotnet_naming_style.begins_with_i.required_suffix =
|
||||||
|
dotnet_naming_style.begins_with_i.word_separator =
|
||||||
|
dotnet_naming_style.begins_with_i.capitalization = pascal_case
|
||||||
|
|
||||||
|
dotnet_naming_style.pascal_case.required_prefix =
|
||||||
|
dotnet_naming_style.pascal_case.required_suffix =
|
||||||
|
dotnet_naming_style.pascal_case.word_separator =
|
||||||
|
dotnet_naming_style.pascal_case.capitalization = pascal_case
|
||||||
|
|
||||||
|
dotnet_naming_style.fields_start_with__.required_prefix = _
|
||||||
|
dotnet_naming_style.fields_start_with__.required_suffix =
|
||||||
|
dotnet_naming_style.fields_start_with__.word_separator =
|
||||||
|
dotnet_naming_style.fields_start_with__.capitalization = camel_case
|
||||||
|
csharp_indent_labels = one_less_than_current
|
||||||
|
csharp_using_directive_placement = outside_namespace:suggestion
|
||||||
|
csharp_prefer_simple_using_statement = true:suggestion
|
||||||
|
csharp_style_prefer_method_group_conversion = true:silent
|
||||||
|
csharp_style_prefer_top_level_statements = true:silent
|
||||||
|
csharp_style_prefer_primary_constructors = true:suggestion
|
||||||
|
csharp_prefer_system_threading_lock = true:suggestion
|
||||||
|
csharp_style_expression_bodied_constructors = false:silent
|
||||||
|
csharp_style_expression_bodied_operators = false:silent
|
||||||
|
csharp_style_expression_bodied_properties = true:suggestion
|
||||||
|
csharp_style_expression_bodied_indexers = true:suggestion
|
||||||
|
csharp_style_expression_bodied_accessors = true:suggestion
|
||||||
|
csharp_style_expression_bodied_lambdas = true:silent
|
||||||
|
csharp_style_expression_bodied_local_functions = true:silent
|
||||||
|
|
||||||
|
[*.vb]
|
||||||
|
#### Naming styles ####
|
||||||
|
|
||||||
|
# Naming rules
|
||||||
|
|
||||||
|
dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
|
||||||
|
dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
|
||||||
|
dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
|
||||||
|
|
||||||
|
dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
|
||||||
|
dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
|
||||||
|
dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
|
||||||
|
|
||||||
|
# Symbol specifications
|
||||||
|
|
||||||
|
dotnet_naming_symbols.interface.applicable_kinds = interface
|
||||||
|
dotnet_naming_symbols.interface.applicable_accessibilities = public, friend, private, protected, protected_friend, private_protected
|
||||||
|
dotnet_naming_symbols.interface.required_modifiers =
|
||||||
|
|
||||||
|
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
|
||||||
|
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, friend, private, protected, protected_friend, private_protected
|
||||||
|
dotnet_naming_symbols.non_field_members.required_modifiers =
|
||||||
|
|
||||||
|
# Naming styles
|
||||||
|
|
||||||
|
dotnet_naming_style.begins_with_i.required_prefix = I
|
||||||
|
dotnet_naming_style.begins_with_i.required_suffix =
|
||||||
|
dotnet_naming_style.begins_with_i.word_separator =
|
||||||
|
dotnet_naming_style.begins_with_i.capitalization = pascal_case
|
||||||
|
|
||||||
|
dotnet_naming_style.pascal_case.required_prefix =
|
||||||
|
dotnet_naming_style.pascal_case.required_suffix =
|
||||||
|
dotnet_naming_style.pascal_case.word_separator =
|
||||||
|
dotnet_naming_style.pascal_case.capitalization = pascal_case
|
||||||
|
|
||||||
|
[*.{cs,vb}]
|
||||||
|
#### Naming styles ####
|
||||||
|
|
||||||
|
# Naming rules
|
||||||
|
|
||||||
|
dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
|
||||||
|
dotnet_naming_rule.types_should_be_pascal_case.symbols = types
|
||||||
|
dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
|
||||||
|
|
||||||
|
# Symbol specifications
|
||||||
|
|
||||||
|
dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
|
||||||
|
dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||||
|
dotnet_naming_symbols.types.required_modifiers =
|
||||||
|
|
||||||
|
# Naming styles
|
||||||
|
|
||||||
|
dotnet_naming_style.pascal_case.required_prefix =
|
||||||
|
dotnet_naming_style.pascal_case.required_suffix =
|
||||||
|
dotnet_naming_style.pascal_case.word_separator =
|
||||||
|
dotnet_naming_style.pascal_case.capitalization = pascal_case
|
||||||
|
dotnet_style_operator_placement_when_wrapping = beginning_of_line
|
||||||
|
tab_width = 4
|
||||||
|
end_of_line = crlf
|
||||||
|
dotnet_style_coalesce_expression = true:suggestion
|
||||||
|
dotnet_style_null_propagation = true:suggestion
|
||||||
|
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("CryptoExchange.Net.UnitTests")]
|
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("CryptoExchange.Net.UnitTests")]
|
||||||
|
|
||||||
namespace System.Runtime.CompilerServices
|
namespace System.Runtime.CompilerServices;
|
||||||
{
|
|
||||||
internal static class IsExternalInit { }
|
internal static class IsExternalInit { }
|
||||||
}
|
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Attributes
|
namespace CryptoExchange.Net.Attributes;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Used for conversion in ArrayConverter
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Property)]
|
||||||
|
public class JsonConversionAttribute: Attribute
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Used for conversion in ArrayConverter
|
|
||||||
/// </summary>
|
|
||||||
[AttributeUsage(AttributeTargets.Property)]
|
|
||||||
public class JsonConversionAttribute: Attribute
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,24 @@
|
|||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Attributes
|
namespace CryptoExchange.Net.Attributes;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Map a enum entry to string values
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Field)]
|
||||||
|
public class MapAttribute : Attribute
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Map a enum entry to string values
|
/// Values mapping to the enum entry
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class MapAttribute : Attribute
|
public string[] Values { get; set; }
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Values mapping to the enum entry
|
|
||||||
/// </summary>
|
|
||||||
public string[] Values { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="maps"></param>
|
/// <param name="maps"></param>
|
||||||
public MapAttribute(params string[] maps)
|
public MapAttribute(params string[] maps)
|
||||||
{
|
{
|
||||||
Values = maps;
|
Values = maps;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,74 +1,56 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
|
||||||
using CryptoExchange.Net.Converters.SystemTextJson;
|
|
||||||
using CryptoExchange.Net.Converters.MessageParsing;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Authentication
|
namespace CryptoExchange.Net.Authentication;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Api credentials, used to sign requests accessing private endpoints
|
||||||
|
/// </summary>
|
||||||
|
public class ApiCredentials
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Api credentials, used to sign requests accessing private endpoints
|
/// The api key / label to authenticate requests
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ApiCredentials
|
public string Key { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The api secret or private key to authenticate requests
|
||||||
|
/// </summary>
|
||||||
|
public string Secret { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The api passphrase. Not needed on all exchanges
|
||||||
|
/// </summary>
|
||||||
|
public string? Pass { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Type of the credentials
|
||||||
|
/// </summary>
|
||||||
|
public ApiCredentialsType CredentialType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create Api credentials providing an api key and secret for authentication
|
||||||
|
/// </summary>
|
||||||
|
/// <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="pass">The api pass for the key. Not always needed</param>
|
||||||
|
/// <param name="credentialType">The type of credentials</param>
|
||||||
|
public ApiCredentials(string key, string secret, string? pass = null, ApiCredentialsType credentialType = ApiCredentialsType.Hmac)
|
||||||
{
|
{
|
||||||
/// <summary>
|
if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(secret))
|
||||||
/// The api key / label to authenticate requests
|
throw new ArgumentException("Key and secret can't be null/empty");
|
||||||
/// </summary>
|
|
||||||
public string Key { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
CredentialType = credentialType;
|
||||||
/// The api secret or private key to authenticate requests
|
Key = key;
|
||||||
/// </summary>
|
Secret = secret;
|
||||||
public string Secret { get; set; }
|
Pass = pass;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Type of the credentials
|
/// Copy the credentials
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ApiCredentialsType CredentialType { get; set; }
|
/// <returns></returns>
|
||||||
|
public virtual ApiCredentials Copy()
|
||||||
/// <summary>
|
{
|
||||||
/// Create Api credentials providing an api key and secret for authentication
|
return new ApiCredentials(Key, Secret, Pass, CredentialType);
|
||||||
/// </summary>
|
|
||||||
/// <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="credentialType">The type of credentials</param>
|
|
||||||
public ApiCredentials(string key, string secret, ApiCredentialsType credentialType = ApiCredentialsType.Hmac)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(secret))
|
|
||||||
throw new ArgumentException("Key and secret can't be null/empty");
|
|
||||||
|
|
||||||
CredentialType = credentialType;
|
|
||||||
Key = key;
|
|
||||||
Secret = secret;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Copy the credentials
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
public virtual ApiCredentials Copy()
|
|
||||||
{
|
|
||||||
return new ApiCredentials(Key, Secret, 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,20 @@
|
|||||||
namespace CryptoExchange.Net.Authentication
|
namespace CryptoExchange.Net.Authentication;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Credentials type
|
||||||
|
/// </summary>
|
||||||
|
public enum ApiCredentialsType
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Credentials type
|
/// Hmac keys credentials
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public enum ApiCredentialsType
|
Hmac,
|
||||||
{
|
/// <summary>
|
||||||
/// <summary>
|
/// Rsa keys credentials in xml format
|
||||||
/// Hmac keys credentials
|
/// </summary>
|
||||||
/// </summary>
|
RsaXml,
|
||||||
Hmac,
|
/// <summary>
|
||||||
/// <summary>
|
/// Rsa keys credentials in pem/base64 format. Only available for .NetStandard 2.1 and up, use xml format for lower.
|
||||||
/// Rsa keys credentials in xml format
|
/// </summary>
|
||||||
/// </summary>
|
RsaPem
|
||||||
RsaXml,
|
|
||||||
/// <summary>
|
|
||||||
/// Rsa keys credentials in pem/base64 format. Only available for .NetStandard 2.1 and up, use xml format for lower.
|
|
||||||
/// </summary>
|
|
||||||
RsaPem
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,475 +1,481 @@
|
|||||||
using CryptoExchange.Net.Clients;
|
using CryptoExchange.Net.Clients;
|
||||||
using CryptoExchange.Net.Converters.SystemTextJson;
|
using CryptoExchange.Net.Converters.SystemTextJson;
|
||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces;
|
||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Net.Http;
|
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Authentication
|
namespace CryptoExchange.Net.Authentication;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Base class for authentication providers
|
||||||
|
/// </summary>
|
||||||
|
public abstract class AuthenticationProvider
|
||||||
{
|
{
|
||||||
|
internal IAuthTimeProvider TimeProvider { get; set; } = new AuthTimeProvider();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Base class for authentication providers
|
/// Provided credentials
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class AuthenticationProvider
|
protected internal readonly ApiCredentials _credentials;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Byte representation of the secret
|
||||||
|
/// </summary>
|
||||||
|
protected byte[] _sBytes;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the API key of the current credentials
|
||||||
|
/// </summary>
|
||||||
|
public string ApiKey => _credentials.Key!;
|
||||||
|
/// <summary>
|
||||||
|
/// Get the Passphrase of the current credentials
|
||||||
|
/// </summary>
|
||||||
|
public string? Pass => _credentials.Pass;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="credentials"></param>
|
||||||
|
protected AuthenticationProvider(ApiCredentials credentials)
|
||||||
{
|
{
|
||||||
internal IAuthTimeProvider TimeProvider { get; set; } = new AuthTimeProvider();
|
if (credentials.Key == null || credentials.Secret == null)
|
||||||
|
throw new ArgumentException("ApiKey/Secret needed");
|
||||||
|
|
||||||
/// <summary>
|
_credentials = credentials;
|
||||||
/// Provided credentials
|
_sBytes = Encoding.UTF8.GetBytes(credentials.Secret);
|
||||||
/// </summary>
|
|
||||||
protected internal readonly ApiCredentials _credentials;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Byte representation of the secret
|
|
||||||
/// </summary>
|
|
||||||
protected byte[] _sBytes;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get the API key of the current credentials
|
|
||||||
/// </summary>
|
|
||||||
public string ApiKey => _credentials.Key!;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="credentials"></param>
|
|
||||||
protected AuthenticationProvider(ApiCredentials credentials)
|
|
||||||
{
|
|
||||||
if (credentials.Key == null || credentials.Secret == null)
|
|
||||||
throw new ArgumentException("ApiKey/Secret needed");
|
|
||||||
|
|
||||||
_credentials = credentials;
|
|
||||||
_sBytes = Encoding.UTF8.GetBytes(credentials.Secret);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Authenticate a request. Output parameters should include the providedParameters input
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="apiClient">The Api client sending the request</param>
|
|
||||||
/// <param name="uri">The uri for the request</param>
|
|
||||||
/// <param name="method">The method of the request</param>
|
|
||||||
/// <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>
|
|
||||||
/// SHA256 sign the data and return the bytes
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="data"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected static byte[] SignSHA256Bytes(string data)
|
|
||||||
{
|
|
||||||
using var encryptor = SHA256.Create();
|
|
||||||
return encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// SHA256 sign the data and return the bytes
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="data"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected static byte[] SignSHA256Bytes(byte[] data)
|
|
||||||
{
|
|
||||||
using var encryptor = SHA256.Create();
|
|
||||||
return encryptor.ComputeHash(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// SHA256 sign the data and return the hash
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="data">Data to sign</param>
|
|
||||||
/// <param name="outputType">String type</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected static string SignSHA256(string data, SignOutputType? outputType = null)
|
|
||||||
{
|
|
||||||
using var encryptor = SHA256.Create();
|
|
||||||
var resultBytes = encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
|
|
||||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// SHA256 sign the data and return the hash
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="data">Data to sign</param>
|
|
||||||
/// <param name="outputType">String type</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected static string SignSHA256(byte[] data, SignOutputType? outputType = null)
|
|
||||||
{
|
|
||||||
using var encryptor = SHA256.Create();
|
|
||||||
var resultBytes = encryptor.ComputeHash(data);
|
|
||||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// SHA384 sign the data and return the hash
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="data">Data to sign</param>
|
|
||||||
/// <param name="outputType">String type</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected static string SignSHA384(string data, SignOutputType? outputType = null)
|
|
||||||
{
|
|
||||||
using var encryptor = SHA384.Create();
|
|
||||||
var resultBytes = encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
|
|
||||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// SHA384 sign the data and return the hash
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="data">Data to sign</param>
|
|
||||||
/// <param name="outputType">String type</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected static string SignSHA384(byte[] data, SignOutputType? outputType = null)
|
|
||||||
{
|
|
||||||
using var encryptor = SHA384.Create();
|
|
||||||
var resultBytes = encryptor.ComputeHash(data);
|
|
||||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// SHA384 sign the data and return the hash
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="data">Data to sign</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected static byte[] SignSHA384Bytes(string data)
|
|
||||||
{
|
|
||||||
using var encryptor = SHA384.Create();
|
|
||||||
return encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// SHA384 sign the data and return the hash
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="data">Data to sign</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected static byte[] SignSHA384Bytes(byte[] data)
|
|
||||||
{
|
|
||||||
using var encryptor = SHA384.Create();
|
|
||||||
return encryptor.ComputeHash(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// SHA512 sign the data and return the hash
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="data">Data to sign</param>
|
|
||||||
/// <param name="outputType">String type</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected static string SignSHA512(string data, SignOutputType? outputType = null)
|
|
||||||
{
|
|
||||||
using var encryptor = SHA512.Create();
|
|
||||||
var resultBytes = encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
|
|
||||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// SHA512 sign the data and return the hash
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="data">Data to sign</param>
|
|
||||||
/// <param name="outputType">String type</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected static string SignSHA512(byte[] data, SignOutputType? outputType = null)
|
|
||||||
{
|
|
||||||
using var encryptor = SHA512.Create();
|
|
||||||
var resultBytes = encryptor.ComputeHash(data);
|
|
||||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// SHA512 sign the data and return the hash
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="data">Data to sign</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected static byte[] SignSHA512Bytes(string data)
|
|
||||||
{
|
|
||||||
using var encryptor = SHA512.Create();
|
|
||||||
return encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// SHA512 sign the data and return the hash
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="data">Data to sign</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected static byte[] SignSHA512Bytes(byte[] data)
|
|
||||||
{
|
|
||||||
using var encryptor = SHA512.Create();
|
|
||||||
return encryptor.ComputeHash(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// MD5 sign the data and return the hash
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="data">Data to sign</param>
|
|
||||||
/// <param name="outputType">String type</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected static string SignMD5(string data, SignOutputType? outputType = null)
|
|
||||||
{
|
|
||||||
using var encryptor = MD5.Create();
|
|
||||||
var resultBytes = encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
|
|
||||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// MD5 sign the data and return the hash
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="data">Data to sign</param>
|
|
||||||
/// <param name="outputType">String type</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected static string SignMD5(byte[] data, SignOutputType? outputType = null)
|
|
||||||
{
|
|
||||||
using var encryptor = MD5.Create();
|
|
||||||
var resultBytes = encryptor.ComputeHash(data);
|
|
||||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// MD5 sign the data and return the hash
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="data">Data to sign</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected static byte[] SignMD5Bytes(string data)
|
|
||||||
{
|
|
||||||
using var encryptor = MD5.Create();
|
|
||||||
return encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// HMACSHA256 sign the data and return the hash
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="data">Data to sign</param>
|
|
||||||
/// <param name="outputType">String type</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected string SignHMACSHA256(string data, SignOutputType? outputType = null)
|
|
||||||
=> SignHMACSHA256(Encoding.UTF8.GetBytes(data), outputType);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// HMACSHA256 sign the data and return the hash
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="data">Data to sign</param>
|
|
||||||
/// <param name="outputType">String type</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected string SignHMACSHA256(byte[] data, SignOutputType? outputType = null)
|
|
||||||
{
|
|
||||||
using var encryptor = new HMACSHA256(_sBytes);
|
|
||||||
var resultBytes = encryptor.ComputeHash(data);
|
|
||||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// HMACSHA384 sign the data and return the hash
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="data">Data to sign</param>
|
|
||||||
/// <param name="outputType">String type</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected string SignHMACSHA384(string data, SignOutputType? outputType = null)
|
|
||||||
=> SignHMACSHA384(Encoding.UTF8.GetBytes(data), outputType);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// HMACSHA384 sign the data and return the hash
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="data">Data to sign</param>
|
|
||||||
/// <param name="outputType">String type</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected string SignHMACSHA384(byte[] data, SignOutputType? outputType = null)
|
|
||||||
{
|
|
||||||
using var encryptor = new HMACSHA384(_sBytes);
|
|
||||||
var resultBytes = encryptor.ComputeHash(data);
|
|
||||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// HMACSHA512 sign the data and return the hash
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="data">Data to sign</param>
|
|
||||||
/// <param name="outputType">String type</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected string SignHMACSHA512(string data, SignOutputType? outputType = null)
|
|
||||||
=> SignHMACSHA512(Encoding.UTF8.GetBytes(data), outputType);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// HMACSHA512 sign the data and return the hash
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="data">Data to sign</param>
|
|
||||||
/// <param name="outputType">String type</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected string SignHMACSHA512(byte[] data, SignOutputType? outputType = null)
|
|
||||||
{
|
|
||||||
using var encryptor = new HMACSHA512(_sBytes);
|
|
||||||
var resultBytes = encryptor.ComputeHash(data);
|
|
||||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// SHA256 sign the data
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="data"></param>
|
|
||||||
/// <param name="outputType"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected string SignRSASHA256(byte[] data, SignOutputType? outputType = null)
|
|
||||||
{
|
|
||||||
using var rsa = CreateRSA();
|
|
||||||
using var sha256 = SHA256.Create();
|
|
||||||
var hash = sha256.ComputeHash(data);
|
|
||||||
var resultBytes = rsa.SignHash(hash, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
|
||||||
return outputType == SignOutputType.Base64? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// SHA384 sign the data
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="data"></param>
|
|
||||||
/// <param name="outputType"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected string SignRSASHA384(byte[] data, SignOutputType? outputType = null)
|
|
||||||
{
|
|
||||||
using var rsa = CreateRSA();
|
|
||||||
using var sha384 = SHA384.Create();
|
|
||||||
var hash = sha384.ComputeHash(data);
|
|
||||||
var resultBytes = rsa.SignHash(hash, HashAlgorithmName.SHA384, RSASignaturePadding.Pkcs1);
|
|
||||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// SHA512 sign the data
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="data"></param>
|
|
||||||
/// <param name="outputType"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected string SignRSASHA512(byte[] data, SignOutputType? outputType = null)
|
|
||||||
{
|
|
||||||
using var rsa = CreateRSA();
|
|
||||||
using var sha512 = SHA512.Create();
|
|
||||||
var hash = sha512.ComputeHash(data);
|
|
||||||
var resultBytes = rsa.SignHash(hash, HashAlgorithmName.SHA512, RSASignaturePadding.Pkcs1);
|
|
||||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
private RSA CreateRSA()
|
|
||||||
{
|
|
||||||
var rsa = RSA.Create();
|
|
||||||
if (_credentials.CredentialType == ApiCredentialsType.RsaPem)
|
|
||||||
{
|
|
||||||
#if NETSTANDARD2_1_OR_GREATER || NET9_0_OR_GREATER
|
|
||||||
// Read from pem private key
|
|
||||||
var key = _credentials.Secret!
|
|
||||||
.Replace("\n", "")
|
|
||||||
.Replace("-----BEGIN PRIVATE KEY-----", "")
|
|
||||||
.Replace("-----END PRIVATE KEY-----", "")
|
|
||||||
.Trim();
|
|
||||||
rsa.ImportPkcs8PrivateKey(Convert.FromBase64String(
|
|
||||||
key)
|
|
||||||
, out _);
|
|
||||||
#else
|
|
||||||
throw new Exception("Pem format not supported when running from .NetStandard2.0. Convert the private key to xml format.");
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
else if (_credentials.CredentialType == ApiCredentialsType.RsaXml)
|
|
||||||
{
|
|
||||||
// Read from xml private key format
|
|
||||||
rsa.FromXmlString(_credentials.Secret!);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new Exception("Invalid credentials type");
|
|
||||||
}
|
|
||||||
|
|
||||||
return rsa;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Convert byte array to hex string
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="buff"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected static string BytesToHexString(byte[] buff)
|
|
||||||
{
|
|
||||||
#if NET9_0_OR_GREATER
|
|
||||||
return Convert.ToHexString(buff);
|
|
||||||
#else
|
|
||||||
var result = string.Empty;
|
|
||||||
foreach (var t in buff)
|
|
||||||
result += t.ToString("X2");
|
|
||||||
return result;
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Convert byte array to base64 string
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="buff"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected static string BytesToBase64String(byte[] buff)
|
|
||||||
{
|
|
||||||
return Convert.ToBase64String(buff);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get current timestamp including the time sync offset from the api client
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="apiClient"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected DateTime GetTimestamp(RestApiClient apiClient)
|
|
||||||
{
|
|
||||||
return TimeProvider.GetTime().Add(apiClient.GetTimeOffset() ?? TimeSpan.Zero)!;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get millisecond timestamp as a string including the time sync offset from the api client
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="apiClient"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected string GetMillisecondTimestamp(RestApiClient apiClient)
|
|
||||||
{
|
|
||||||
return DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient)).Value.ToString(CultureInfo.InvariantCulture);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Return the serialized request body
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="serializer"></param>
|
|
||||||
/// <param name="parameters"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected string GetSerializedBody(IMessageSerializer serializer, IDictionary<string, object> parameters)
|
|
||||||
{
|
|
||||||
if (parameters.Count == 1 && parameters.ContainsKey(Constants.BodyPlaceHolderKey))
|
|
||||||
return serializer.Serialize(parameters[Constants.BodyPlaceHolderKey]);
|
|
||||||
else
|
|
||||||
return serializer.Serialize(parameters);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <summary>
|
||||||
public abstract class AuthenticationProvider<TApiCredentials> : AuthenticationProvider where TApiCredentials : ApiCredentials
|
/// Authenticate a request
|
||||||
{
|
/// </summary>
|
||||||
/// <inheritdoc />
|
/// <param name="apiClient">The Api client sending the request</param>
|
||||||
protected new TApiCredentials _credentials => (TApiCredentials)base._credentials;
|
/// <param name="requestConfig">The request configuration</param>
|
||||||
|
public abstract void ProcessRequest(RestApiClient apiClient, RestRequestConfiguration requestConfig);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// SHA256 sign the data and return the bytes
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="credentials"></param>
|
/// <param name="data"></param>
|
||||||
protected AuthenticationProvider(TApiCredentials credentials) : base(credentials)
|
/// <returns></returns>
|
||||||
|
protected static byte[] SignSHA256Bytes(string data)
|
||||||
|
{
|
||||||
|
using var encryptor = SHA256.Create();
|
||||||
|
return encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// SHA256 sign the data and return the bytes
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected static byte[] SignSHA256Bytes(byte[] data)
|
||||||
|
{
|
||||||
|
using var encryptor = SHA256.Create();
|
||||||
|
return encryptor.ComputeHash(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// SHA256 sign the data and return the hash
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data">Data to sign</param>
|
||||||
|
/// <param name="outputType">String type</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected static string SignSHA256(string data, SignOutputType? outputType = null)
|
||||||
|
{
|
||||||
|
using var encryptor = SHA256.Create();
|
||||||
|
var resultBytes = encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
|
||||||
|
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// SHA256 sign the data and return the hash
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data">Data to sign</param>
|
||||||
|
/// <param name="outputType">String type</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected static string SignSHA256(byte[] data, SignOutputType? outputType = null)
|
||||||
|
{
|
||||||
|
using var encryptor = SHA256.Create();
|
||||||
|
var resultBytes = encryptor.ComputeHash(data);
|
||||||
|
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// SHA384 sign the data and return the hash
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data">Data to sign</param>
|
||||||
|
/// <param name="outputType">String type</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected static string SignSHA384(string data, SignOutputType? outputType = null)
|
||||||
|
{
|
||||||
|
using var encryptor = SHA384.Create();
|
||||||
|
var resultBytes = encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
|
||||||
|
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// SHA384 sign the data and return the hash
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data">Data to sign</param>
|
||||||
|
/// <param name="outputType">String type</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected static string SignSHA384(byte[] data, SignOutputType? outputType = null)
|
||||||
|
{
|
||||||
|
using var encryptor = SHA384.Create();
|
||||||
|
var resultBytes = encryptor.ComputeHash(data);
|
||||||
|
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// SHA384 sign the data and return the hash
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data">Data to sign</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected static byte[] SignSHA384Bytes(string data)
|
||||||
|
{
|
||||||
|
using var encryptor = SHA384.Create();
|
||||||
|
return encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// SHA384 sign the data and return the hash
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data">Data to sign</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected static byte[] SignSHA384Bytes(byte[] data)
|
||||||
|
{
|
||||||
|
using var encryptor = SHA384.Create();
|
||||||
|
return encryptor.ComputeHash(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// SHA512 sign the data and return the hash
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data">Data to sign</param>
|
||||||
|
/// <param name="outputType">String type</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected static string SignSHA512(string data, SignOutputType? outputType = null)
|
||||||
|
{
|
||||||
|
using var encryptor = SHA512.Create();
|
||||||
|
var resultBytes = encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
|
||||||
|
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// SHA512 sign the data and return the hash
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data">Data to sign</param>
|
||||||
|
/// <param name="outputType">String type</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected static string SignSHA512(byte[] data, SignOutputType? outputType = null)
|
||||||
|
{
|
||||||
|
using var encryptor = SHA512.Create();
|
||||||
|
var resultBytes = encryptor.ComputeHash(data);
|
||||||
|
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// SHA512 sign the data and return the hash
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data">Data to sign</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected static byte[] SignSHA512Bytes(string data)
|
||||||
|
{
|
||||||
|
using var encryptor = SHA512.Create();
|
||||||
|
return encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// SHA512 sign the data and return the hash
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data">Data to sign</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected static byte[] SignSHA512Bytes(byte[] data)
|
||||||
|
{
|
||||||
|
using var encryptor = SHA512.Create();
|
||||||
|
return encryptor.ComputeHash(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MD5 sign the data and return the hash
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data">Data to sign</param>
|
||||||
|
/// <param name="outputType">String type</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected static string SignMD5(string data, SignOutputType? outputType = null)
|
||||||
|
{
|
||||||
|
#pragma warning disable CA5351
|
||||||
|
using var encryptor = MD5.Create();
|
||||||
|
#pragma warning restore CA5351
|
||||||
|
var resultBytes = encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
|
||||||
|
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MD5 sign the data and return the hash
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data">Data to sign</param>
|
||||||
|
/// <param name="outputType">String type</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected static string SignMD5(byte[] data, SignOutputType? outputType = null)
|
||||||
|
{
|
||||||
|
#pragma warning disable CA5351
|
||||||
|
using var encryptor = MD5.Create();
|
||||||
|
#pragma warning restore CA5351
|
||||||
|
var resultBytes = encryptor.ComputeHash(data);
|
||||||
|
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MD5 sign the data and return the hash
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data">Data to sign</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected static byte[] SignMD5Bytes(string data)
|
||||||
|
{
|
||||||
|
#pragma warning disable CA5351
|
||||||
|
using var encryptor = MD5.Create();
|
||||||
|
#pragma warning restore CA5351
|
||||||
|
return encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// HMACSHA256 sign the data and return the hash
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data">Data to sign</param>
|
||||||
|
/// <param name="outputType">String type</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected string SignHMACSHA256(string data, SignOutputType? outputType = null)
|
||||||
|
=> SignHMACSHA256(Encoding.UTF8.GetBytes(data), outputType);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// HMACSHA256 sign the data and return the hash
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data">Data to sign</param>
|
||||||
|
/// <param name="outputType">String type</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected string SignHMACSHA256(byte[] data, SignOutputType? outputType = null)
|
||||||
|
{
|
||||||
|
using var encryptor = new HMACSHA256(_sBytes);
|
||||||
|
var resultBytes = encryptor.ComputeHash(data);
|
||||||
|
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// HMACSHA384 sign the data and return the hash
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data">Data to sign</param>
|
||||||
|
/// <param name="outputType">String type</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected string SignHMACSHA384(string data, SignOutputType? outputType = null)
|
||||||
|
=> SignHMACSHA384(Encoding.UTF8.GetBytes(data), outputType);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// HMACSHA384 sign the data and return the hash
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data">Data to sign</param>
|
||||||
|
/// <param name="outputType">String type</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected string SignHMACSHA384(byte[] data, SignOutputType? outputType = null)
|
||||||
|
{
|
||||||
|
using var encryptor = new HMACSHA384(_sBytes);
|
||||||
|
var resultBytes = encryptor.ComputeHash(data);
|
||||||
|
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// HMACSHA512 sign the data and return the hash
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data">Data to sign</param>
|
||||||
|
/// <param name="outputType">String type</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected string SignHMACSHA512(string data, SignOutputType? outputType = null)
|
||||||
|
=> SignHMACSHA512(Encoding.UTF8.GetBytes(data), outputType);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// HMACSHA512 sign the data and return the hash
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data">Data to sign</param>
|
||||||
|
/// <param name="outputType">String type</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected string SignHMACSHA512(byte[] data, SignOutputType? outputType = null)
|
||||||
|
{
|
||||||
|
using var encryptor = new HMACSHA512(_sBytes);
|
||||||
|
var resultBytes = encryptor.ComputeHash(data);
|
||||||
|
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// SHA256 sign the data
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data"></param>
|
||||||
|
/// <param name="outputType"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected string SignRSASHA256(byte[] data, SignOutputType? outputType = null)
|
||||||
|
{
|
||||||
|
using var rsa = CreateRSA();
|
||||||
|
using var sha256 = SHA256.Create();
|
||||||
|
var hash = sha256.ComputeHash(data);
|
||||||
|
var resultBytes = rsa.SignHash(hash, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
||||||
|
return outputType == SignOutputType.Base64? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// SHA384 sign the data
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data"></param>
|
||||||
|
/// <param name="outputType"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected string SignRSASHA384(byte[] data, SignOutputType? outputType = null)
|
||||||
|
{
|
||||||
|
using var rsa = CreateRSA();
|
||||||
|
using var sha384 = SHA384.Create();
|
||||||
|
var hash = sha384.ComputeHash(data);
|
||||||
|
var resultBytes = rsa.SignHash(hash, HashAlgorithmName.SHA384, RSASignaturePadding.Pkcs1);
|
||||||
|
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// SHA512 sign the data
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data"></param>
|
||||||
|
/// <param name="outputType"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected string SignRSASHA512(byte[] data, SignOutputType? outputType = null)
|
||||||
|
{
|
||||||
|
using var rsa = CreateRSA();
|
||||||
|
using var sha512 = SHA512.Create();
|
||||||
|
var hash = sha512.ComputeHash(data);
|
||||||
|
var resultBytes = rsa.SignHash(hash, HashAlgorithmName.SHA512, RSASignaturePadding.Pkcs1);
|
||||||
|
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private RSA CreateRSA()
|
||||||
|
{
|
||||||
|
var rsa = RSA.Create();
|
||||||
|
if (_credentials.CredentialType == ApiCredentialsType.RsaPem)
|
||||||
{
|
{
|
||||||
|
#if NETSTANDARD2_1_OR_GREATER || NET9_0_OR_GREATER
|
||||||
|
// Read from pem private key
|
||||||
|
var key = _credentials.Secret!
|
||||||
|
.Replace("\n", "")
|
||||||
|
.Replace("-----BEGIN PRIVATE KEY-----", "")
|
||||||
|
.Replace("-----END PRIVATE KEY-----", "")
|
||||||
|
.Trim();
|
||||||
|
rsa.ImportPkcs8PrivateKey(Convert.FromBase64String(
|
||||||
|
key)
|
||||||
|
, out _);
|
||||||
|
#else
|
||||||
|
throw new Exception("Pem format not supported when running from .NetStandard2.0. Convert the private key to xml format.");
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
else if (_credentials.CredentialType == ApiCredentialsType.RsaXml)
|
||||||
|
{
|
||||||
|
// Read from xml private key format
|
||||||
|
rsa.FromXmlString(_credentials.Secret!);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new Exception("Invalid credentials type");
|
||||||
|
}
|
||||||
|
|
||||||
|
return rsa;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Convert byte array to hex string
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="buff"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected static string BytesToHexString(byte[] buff)
|
||||||
|
{
|
||||||
|
#if NET9_0_OR_GREATER
|
||||||
|
return Convert.ToHexString(buff);
|
||||||
|
#else
|
||||||
|
var result = string.Empty;
|
||||||
|
foreach (var t in buff)
|
||||||
|
result += t.ToString("X2");
|
||||||
|
return result;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Convert byte array to base64 string
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="buff"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected static string BytesToBase64String(byte[] buff)
|
||||||
|
{
|
||||||
|
return Convert.ToBase64String(buff);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get current timestamp including the time sync offset from the api client
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="apiClient"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected DateTime GetTimestamp(RestApiClient apiClient)
|
||||||
|
{
|
||||||
|
return TimeProvider.GetTime().Add(apiClient.GetTimeOffset() ?? TimeSpan.Zero)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get millisecond timestamp as a string including the time sync offset from the api client
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="apiClient"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected string GetMillisecondTimestamp(RestApiClient apiClient)
|
||||||
|
{
|
||||||
|
return DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient)).Value.ToString(CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get millisecond timestamp as a long including the time sync offset from the api client
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="apiClient"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected long GetMillisecondTimestampLong(RestApiClient apiClient)
|
||||||
|
{
|
||||||
|
return DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient)).Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Return the serialized request body
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serializer"></param>
|
||||||
|
/// <param name="parameters"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
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))
|
||||||
|
return stringSerializer.Serialize(value);
|
||||||
|
else
|
||||||
|
return stringSerializer.Serialize(parameters);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public abstract class AuthenticationProvider<TApiCredentials> : AuthenticationProvider where TApiCredentials : ApiCredentials
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
#pragma warning disable IDE1006 // Naming Styles
|
||||||
|
#pragma warning disable CA1707 // Naming Styles
|
||||||
|
protected new TApiCredentials _credentials => (TApiCredentials)base._credentials;
|
||||||
|
#pragma warning restore IDE1006 // Naming Styles
|
||||||
|
#pragma warning restore CA1707 // Naming Styles
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="credentials"></param>
|
||||||
|
protected AuthenticationProvider(TApiCredentials credentials) : base(credentials)
|
||||||
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
namespace CryptoExchange.Net.Authentication
|
namespace CryptoExchange.Net.Authentication;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Output string type
|
||||||
|
/// </summary>
|
||||||
|
public enum SignOutputType
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Output string type
|
/// Hex string
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public enum SignOutputType
|
Hex,
|
||||||
{
|
/// <summary>
|
||||||
/// <summary>
|
/// Base64 string
|
||||||
/// Hex string
|
/// </summary>
|
||||||
/// </summary>
|
Base64
|
||||||
Hex,
|
|
||||||
/// <summary>
|
|
||||||
/// Base64 string
|
|
||||||
/// </summary>
|
|
||||||
Base64
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,54 +1,52 @@
|
|||||||
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 object _lock = new object();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add a new cache entry. Will override an existing entry if it already exists
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">The key identifier</param>
|
||||||
|
/// <param name="value">Cache value</param>
|
||||||
|
public void Add(string key, object value)
|
||||||
{
|
{
|
||||||
private readonly ConcurrentDictionary<string, CacheItem> _cache = new ConcurrentDictionary<string, CacheItem>();
|
var cacheItem = new CacheItem(DateTime.UtcNow, value);
|
||||||
|
_cache.AddOrUpdate(key, cacheItem, (key, val1) => cacheItem);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Add a new cache entry. Will override an existing entry if it already exists
|
/// Get a cached value
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="key">The key identifier</param>
|
/// <param name="key">The key identifier</param>
|
||||||
/// <param name="value">Cache value</param>
|
/// <param name="maxAge">The max age of the cached entry</param>
|
||||||
public void Add(string key, object value)
|
/// <returns>Cached value if it was in cache</returns>
|
||||||
|
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);
|
||||||
|
if (value == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return value.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class CacheItem
|
||||||
|
{
|
||||||
|
public DateTime CacheTime { get; }
|
||||||
|
public object Value { get; }
|
||||||
|
|
||||||
|
public CacheItem(DateTime cacheTime, object value)
|
||||||
{
|
{
|
||||||
var cacheItem = new CacheItem(DateTime.UtcNow, value);
|
CacheTime = cacheTime;
|
||||||
_cache.AddOrUpdate(key, cacheItem, (key, val1) => cacheItem);
|
Value = value;
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get a cached value
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="key">The key identifier</param>
|
|
||||||
/// <param name="maxAge">The max age of the cached entry</param>
|
|
||||||
/// <returns>Cached value if it was in cache</returns>
|
|
||||||
public object? Get(string key, TimeSpan maxAge)
|
|
||||||
{
|
|
||||||
_cache.TryGetValue(key, out CacheItem? value);
|
|
||||||
if (value == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
if (DateTime.UtcNow - value.CacheTime > maxAge)
|
|
||||||
{
|
|
||||||
_cache.TryRemove(key, out _);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return value.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
private class CacheItem
|
|
||||||
{
|
|
||||||
public DateTime CacheTime { get; }
|
|
||||||
public object Value { get; }
|
|
||||||
|
|
||||||
public CacheItem(DateTime cacheTime, object value)
|
|
||||||
{
|
|
||||||
CacheTime = cacheTime;
|
|
||||||
Value = value;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,115 +1,140 @@
|
|||||||
using System;
|
using System;
|
||||||
using CryptoExchange.Net.Authentication;
|
using CryptoExchange.Net.Authentication;
|
||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces;
|
||||||
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;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Clients
|
namespace CryptoExchange.Net.Clients;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Base API for all API clients
|
||||||
|
/// </summary>
|
||||||
|
public abstract class BaseApiClient : IDisposable, IBaseApiClient
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Base API for all API clients
|
/// Logger
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class BaseApiClient : IDisposable, IBaseApiClient
|
protected ILogger _logger;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// If we are disposing
|
||||||
|
/// </summary>
|
||||||
|
protected bool _disposing;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The authentication provider for this API client. (null if no credentials are set)
|
||||||
|
/// </summary>
|
||||||
|
public AuthenticationProvider? AuthenticationProvider { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The environment this client communicates to
|
||||||
|
/// </summary>
|
||||||
|
public string BaseAddress { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Output the original string data along with the deserialized object
|
||||||
|
/// </summary>
|
||||||
|
public bool OutputOriginalData { get; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool Authenticated => ApiCredentials != null;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public ApiCredentials? ApiCredentials { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Api options
|
||||||
|
/// </summary>
|
||||||
|
public ApiOptions ApiOptions { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Client Options
|
||||||
|
/// </summary>
|
||||||
|
public ExchangeOptions ClientOptions { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Mapping of a response code to known error types
|
||||||
|
/// </summary>
|
||||||
|
protected internal virtual ErrorMapping ErrorMapping { get; } = new ErrorMapping([]);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="logger">Logger</param>
|
||||||
|
/// <param name="outputOriginalData">Should data from this client include the original data in the call result</param>
|
||||||
|
/// <param name="baseAddress">Base address for this API client</param>
|
||||||
|
/// <param name="apiCredentials">Api credentials</param>
|
||||||
|
/// <param name="clientOptions">Client options</param>
|
||||||
|
/// <param name="apiOptions">Api options</param>
|
||||||
|
protected BaseApiClient(ILogger logger, bool outputOriginalData, ApiCredentials? apiCredentials, string baseAddress, ExchangeOptions clientOptions, ApiOptions apiOptions)
|
||||||
{
|
{
|
||||||
/// <summary>
|
_logger = logger;
|
||||||
/// Logger
|
|
||||||
/// </summary>
|
|
||||||
protected ILogger _logger;
|
|
||||||
|
|
||||||
/// <summary>
|
ClientOptions = clientOptions;
|
||||||
/// If we are disposing
|
ApiOptions = apiOptions;
|
||||||
/// </summary>
|
OutputOriginalData = outputOriginalData;
|
||||||
protected bool _disposing;
|
BaseAddress = baseAddress;
|
||||||
|
ApiCredentials = apiCredentials?.Copy();
|
||||||
|
|
||||||
/// <summary>
|
if (ApiCredentials != null)
|
||||||
/// The authentication provider for this API client. (null if no credentials are set)
|
AuthenticationProvider = CreateAuthenticationProvider(ApiCredentials);
|
||||||
/// </summary>
|
}
|
||||||
public AuthenticationProvider? AuthenticationProvider { get; private set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The environment this client communicates to
|
/// Create an AuthenticationProvider implementation instance based on the provided credentials
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string BaseAddress { get; }
|
/// <param name="credentials"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected abstract AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials);
|
||||||
|
|
||||||
/// <summary>
|
/// <inheritdoc />
|
||||||
/// Output the original string data along with the deserialized object
|
public abstract string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverDate = null);
|
||||||
/// </summary>
|
|
||||||
public bool OutputOriginalData { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether or not API credentials have been configured for this client. Does not check the credentials are actually valid.
|
/// Get error info for a response code
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool Authenticated => ApiOptions.ApiCredentials != null || ClientOptions.ApiCredentials != null;
|
public ErrorInfo GetErrorInfo(int code, string? message = null) => GetErrorInfo(code.ToString(), message);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Api options
|
/// Get error info for a response code
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ApiOptions ApiOptions { get; }
|
public ErrorInfo GetErrorInfo(string code, string? message = null) => ErrorMapping.GetErrorInfo(code.ToString(), message);
|
||||||
|
|
||||||
/// <summary>
|
/// <inheritdoc />
|
||||||
/// Client Options
|
public void SetApiCredentials<T>(T credentials) where T : ApiCredentials
|
||||||
/// </summary>
|
{
|
||||||
public ExchangeOptions ClientOptions { get; }
|
ApiCredentials = credentials?.Copy();
|
||||||
|
if (ApiCredentials != null)
|
||||||
|
AuthenticationProvider = CreateAuthenticationProvider(ApiCredentials);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <inheritdoc />
|
||||||
/// ctor
|
public virtual void SetOptions<T>(UpdateOptions<T> options) where T : ApiCredentials
|
||||||
/// </summary>
|
{
|
||||||
/// <param name="logger">Logger</param>
|
ClientOptions.Proxy = options.Proxy;
|
||||||
/// <param name="outputOriginalData">Should data from this client include the orginal data in the call result</param>
|
ClientOptions.RequestTimeout = options.RequestTimeout ?? ClientOptions.RequestTimeout;
|
||||||
/// <param name="baseAddress">Base address for this API client</param>
|
|
||||||
/// <param name="apiCredentials">Api credentials</param>
|
|
||||||
/// <param name="clientOptions">Client options</param>
|
|
||||||
/// <param name="apiOptions">Api options</param>
|
|
||||||
protected BaseApiClient(ILogger logger, bool outputOriginalData, ApiCredentials? apiCredentials, string baseAddress, ExchangeOptions clientOptions, ApiOptions apiOptions)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
|
|
||||||
ClientOptions = clientOptions;
|
ApiCredentials = options.ApiCredentials?.Copy() ?? ApiCredentials;
|
||||||
ApiOptions = apiOptions;
|
if (ApiCredentials != null)
|
||||||
OutputOriginalData = outputOriginalData;
|
AuthenticationProvider = CreateAuthenticationProvider(ApiCredentials);
|
||||||
BaseAddress = baseAddress;
|
}
|
||||||
|
|
||||||
if (apiCredentials != null)
|
/// <summary>
|
||||||
AuthenticationProvider = CreateAuthenticationProvider(apiCredentials.Copy());
|
/// Dispose
|
||||||
}
|
/// </summary>
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Dispose(true);
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create an AuthenticationProvider implementation instance based on the provided credentials
|
/// Dispose
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="credentials"></param>
|
public virtual void Dispose(bool disposing)
|
||||||
/// <returns></returns>
|
{
|
||||||
protected abstract AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials);
|
_disposing = true;
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public abstract string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverDate = null);
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public void SetApiCredentials<T>(T credentials) where T : ApiCredentials
|
|
||||||
{
|
|
||||||
ApiOptions.ApiCredentials = credentials;
|
|
||||||
if (credentials != null)
|
|
||||||
AuthenticationProvider = CreateAuthenticationProvider(credentials.Copy());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public virtual void SetOptions<T>(UpdateOptions<T> options) where T : ApiCredentials
|
|
||||||
{
|
|
||||||
ClientOptions.Proxy = options.Proxy;
|
|
||||||
ClientOptions.RequestTimeout = options.RequestTimeout ?? ClientOptions.RequestTimeout;
|
|
||||||
|
|
||||||
ApiOptions.ApiCredentials = options.ApiCredentials ?? ClientOptions.ApiCredentials;
|
|
||||||
if (options.ApiCredentials != null)
|
|
||||||
AuthenticationProvider = CreateAuthenticationProvider(options.ApiCredentials.Copy());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Dispose
|
|
||||||
/// </summary>
|
|
||||||
public virtual void Dispose()
|
|
||||||
{
|
|
||||||
_disposing = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,132 +1,142 @@
|
|||||||
using CryptoExchange.Net.Authentication;
|
using CryptoExchange.Net.Authentication;
|
||||||
using CryptoExchange.Net.Objects.Options;
|
using CryptoExchange.Net.Objects.Options;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Clients
|
namespace CryptoExchange.Net.Clients;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The base for all clients, websocket client and rest client
|
||||||
|
/// </summary>
|
||||||
|
public abstract class BaseClient : IDisposable
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The base for all clients, websocket client and rest client
|
/// Version of the CryptoExchange.Net base library
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class BaseClient : IDisposable
|
public Version CryptoExchangeLibVersion { get; } = typeof(BaseClient).Assembly.GetName().Version!;
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Version of the CryptoExchange.Net base library
|
|
||||||
/// </summary>
|
|
||||||
public Version CryptoExchangeLibVersion { get; } = typeof(BaseClient).Assembly.GetName().Version!;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Version of the client implementation
|
/// Version of the client implementation
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Version ExchangeLibVersion
|
public Version ExchangeLibVersion
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
|
{
|
||||||
|
lock(_versionLock)
|
||||||
{
|
{
|
||||||
lock(_versionLock)
|
if (_exchangeVersion == null)
|
||||||
{
|
_exchangeVersion = GetType().Assembly.GetName().Version!;
|
||||||
if (_exchangeVersion == null)
|
|
||||||
_exchangeVersion = GetType().Assembly.GetName().Version!;
|
|
||||||
|
|
||||||
return _exchangeVersion;
|
return _exchangeVersion;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The name of the API the client is for
|
/// The name of the API the client is for
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Exchange { get; }
|
public string Exchange { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Api clients in this client
|
/// Api clients in this client
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal List<BaseApiClient> ApiClients { get; } = new List<BaseApiClient>();
|
internal List<BaseApiClient> ApiClients { get; } = new List<BaseApiClient>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The log object
|
/// The log object
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected internal ILogger _logger;
|
protected internal ILogger _logger;
|
||||||
|
|
||||||
private object _versionLock = new object();
|
private readonly object _versionLock = new object();
|
||||||
private Version _exchangeVersion;
|
private Version _exchangeVersion;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Provided client options
|
/// Provided client options
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ExchangeOptions ClientOptions { get; private set; }
|
public ExchangeOptions ClientOptions { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="logger">Logger</param>
|
/// <param name="logger">Logger</param>
|
||||||
/// <param name="exchange">The name of the exchange this client is for</param>
|
/// <param name="exchange">The name of the exchange this client is for</param>
|
||||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||||
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;
|
/// <summary>
|
||||||
}
|
/// Initialize the client with the specified options
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="options"></param>
|
||||||
|
/// <exception cref="ArgumentNullException"></exception>
|
||||||
|
protected virtual void Initialize(ExchangeOptions options)
|
||||||
|
{
|
||||||
|
if (options == null)
|
||||||
|
throw new ArgumentNullException(nameof(options));
|
||||||
|
|
||||||
/// <summary>
|
ClientOptions = options;
|
||||||
/// Initialize the client with the specified options
|
_logger.Log(LogLevel.Trace, "Client configuration: {Options}, CryptoExchange.Net: v{CryptoExchangeVersion}, {Exchange}.Net: v{ExchangeVersion}", options, CryptoExchangeLibVersion, Exchange, ExchangeLibVersion);
|
||||||
/// </summary>
|
}
|
||||||
/// <param name="options"></param>
|
|
||||||
/// <exception cref="ArgumentNullException"></exception>
|
|
||||||
protected virtual void Initialize(ExchangeOptions options)
|
|
||||||
{
|
|
||||||
if (options == null)
|
|
||||||
throw new ArgumentNullException(nameof(options));
|
|
||||||
|
|
||||||
ClientOptions = options;
|
/// <summary>
|
||||||
_logger.Log(LogLevel.Trace, $"Client configuration: {options}, CryptoExchange.Net: v{CryptoExchangeLibVersion}, {Exchange}.Net: v{ExchangeLibVersion}");
|
/// Set the API credentials for this client. All Api clients in this client will use the new credentials, regardless of earlier set options.
|
||||||
}
|
/// </summary>
|
||||||
|
/// <param name="credentials">The credentials to set</param>
|
||||||
|
protected virtual void SetApiCredentials<T>(T credentials) where T : ApiCredentials
|
||||||
|
{
|
||||||
|
foreach (var apiClient in ApiClients)
|
||||||
|
apiClient.SetApiCredentials(credentials);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Set the API credentials for this client. All Api clients in this client will use the new credentials, regardless of earlier set options.
|
/// Register an API client
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="credentials">The credentials to set</param>
|
/// <param name="apiClient">The client</param>
|
||||||
protected virtual void SetApiCredentials<T>(T credentials) where T : ApiCredentials
|
protected T AddApiClient<T>(T apiClient) where T : BaseApiClient
|
||||||
{
|
{
|
||||||
foreach (var apiClient in ApiClients)
|
if (ClientOptions == null)
|
||||||
apiClient.SetApiCredentials(credentials);
|
throw new InvalidOperationException("Client should have called Initialize before adding API clients");
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
_logger.Log(LogLevel.Trace, " {ApiClient}, base address: {BaseAddress}", apiClient.GetType().Name, apiClient.BaseAddress);
|
||||||
/// Register an API client
|
ApiClients.Add(apiClient);
|
||||||
/// </summary>
|
return apiClient;
|
||||||
/// <param name="apiClient">The client</param>
|
}
|
||||||
protected T AddApiClient<T>(T apiClient) where T : BaseApiClient
|
|
||||||
{
|
|
||||||
if (ClientOptions == null)
|
|
||||||
throw new InvalidOperationException("Client should have called Initialize before adding API clients");
|
|
||||||
|
|
||||||
_logger.Log(LogLevel.Trace, $" {apiClient.GetType().Name}, base address: {apiClient.BaseAddress}");
|
/// <summary>
|
||||||
ApiClients.Add(apiClient);
|
/// Apply the options delegate to a new options instance
|
||||||
return apiClient;
|
/// </summary>
|
||||||
}
|
protected static T ApplyOptionsDelegate<T>(Action<T>? del) where T: new()
|
||||||
|
{
|
||||||
|
var opts = new T();
|
||||||
|
del?.Invoke(opts);
|
||||||
|
return opts;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Apply the options delegate to a new options instance
|
/// Dispose
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected static T ApplyOptionsDelegate<T>(Action<T>? del) where T: new()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
var opts = new T();
|
Dispose(true);
|
||||||
del?.Invoke(opts);
|
GC.SuppressFinalize(this);
|
||||||
return opts;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Dispose
|
/// Dispose
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual void Dispose()
|
public virtual void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (disposing)
|
||||||
{
|
{
|
||||||
_logger.Log(LogLevel.Debug, "Disposing client");
|
_logger.Log(LogLevel.Debug, "Disposing client");
|
||||||
foreach (var client in ApiClients)
|
foreach (var client in ApiClients)
|
||||||
client.Dispose();
|
client.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,25 @@
|
|||||||
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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Base rest client
|
||||||
|
/// </summary>
|
||||||
|
public abstract class BaseRestClient : BaseClient, IRestClient
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <inheritdoc />
|
||||||
/// Base rest client
|
public int TotalRequestsMade => ApiClients.OfType<RestApiClient>().Sum(s => s.TotalRequestsMade);
|
||||||
/// </summary>
|
|
||||||
public abstract class BaseRestClient : BaseClient, IRestClient
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public int TotalRequestsMade => ApiClients.OfType<RestApiClient>().Sum(s => s.TotalRequestsMade);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="loggerFactory">Logger factory</param>
|
/// <param name="loggerFactory">Logger factory</param>
|
||||||
/// <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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
@@ -7,120 +7,124 @@ 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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Base for socket client implementations
|
||||||
|
/// </summary>
|
||||||
|
public abstract class BaseSocketClient : BaseClient, ISocketClient
|
||||||
{
|
{
|
||||||
|
#region fields
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Base for socket client implementations
|
/// If client is disposing
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class BaseSocketClient : BaseClient, ISocketClient
|
protected bool _disposing;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public int CurrentConnections => ApiClients.OfType<SocketApiClient>().Sum(c => c.CurrentConnections);
|
||||||
|
/// <inheritdoc />
|
||||||
|
public int CurrentSubscriptions => ApiClients.OfType<SocketApiClient>().Sum(s => s.CurrentSubscriptions);
|
||||||
|
/// <inheritdoc />
|
||||||
|
public double IncomingKbps => ApiClients.OfType<SocketApiClient>().Sum(s => s.IncomingKbps);
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="loggerFactory">Logger factory</param>
|
||||||
|
/// <param name="name">The name of the exchange this client is for</param>
|
||||||
|
protected BaseSocketClient(ILoggerFactory? loggerFactory, string name) : base(loggerFactory, name)
|
||||||
{
|
{
|
||||||
#region fields
|
_logger = loggerFactory?.CreateLogger(name + ".SocketClient") ?? NullLoggerFactory.Instance.CreateLogger(name);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// If client is disposing
|
/// Unsubscribe an update subscription
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected bool _disposing;
|
/// <param name="subscriptionId">The id of the subscription to unsubscribe</param>
|
||||||
|
/// <returns></returns>
|
||||||
/// <inheritdoc />
|
public virtual async Task UnsubscribeAsync(int subscriptionId)
|
||||||
public int CurrentConnections => ApiClients.OfType<SocketApiClient>().Sum(c => c.CurrentConnections);
|
{
|
||||||
/// <inheritdoc />
|
foreach (var socket in ApiClients.OfType<SocketApiClient>())
|
||||||
public int CurrentSubscriptions => ApiClients.OfType<SocketApiClient>().Sum(s => s.CurrentSubscriptions);
|
|
||||||
/// <inheritdoc />
|
|
||||||
public double IncomingKbps => ApiClients.OfType<SocketApiClient>().Sum(s => s.IncomingKbps);
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="logger">Logger</param>
|
|
||||||
/// <param name="exchange">The name of the exchange this client is for</param>
|
|
||||||
protected BaseSocketClient(ILoggerFactory? logger, string exchange) : base(logger, exchange)
|
|
||||||
{
|
{
|
||||||
}
|
var result = await socket.UnsubscribeAsync(subscriptionId).ConfigureAwait(false);
|
||||||
|
if (result)
|
||||||
/// <summary>
|
break;
|
||||||
/// Unsubscribe an update subscription
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="subscriptionId">The id of the subscription to unsubscribe</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public virtual async Task UnsubscribeAsync(int subscriptionId)
|
|
||||||
{
|
|
||||||
foreach (var socket in ApiClients.OfType<SocketApiClient>())
|
|
||||||
{
|
|
||||||
var result = await socket.UnsubscribeAsync(subscriptionId).ConfigureAwait(false);
|
|
||||||
if (result)
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Unsubscribe an update subscription
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="subscription">The subscription to unsubscribe</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public virtual async Task UnsubscribeAsync(UpdateSubscription subscription)
|
|
||||||
{
|
|
||||||
if (subscription == null)
|
|
||||||
throw new ArgumentNullException(nameof(subscription));
|
|
||||||
|
|
||||||
_logger.UnsubscribingSubscription(subscription.SocketId, subscription.Id);
|
|
||||||
await subscription.CloseAsync().ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Unsubscribe all subscriptions
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
public virtual async Task UnsubscribeAllAsync()
|
|
||||||
{
|
|
||||||
var tasks = new List<Task>();
|
|
||||||
foreach (var client in ApiClients.OfType<SocketApiClient>())
|
|
||||||
tasks.Add(client.UnsubscribeAllAsync());
|
|
||||||
|
|
||||||
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Reconnect all connections
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
public virtual async Task ReconnectAsync()
|
|
||||||
{
|
|
||||||
_logger.ReconnectingAllConnections(CurrentConnections);
|
|
||||||
var tasks = new List<Task>();
|
|
||||||
foreach (var client in ApiClients.OfType<SocketApiClient>())
|
|
||||||
{
|
|
||||||
tasks.Add(client.ReconnectAsync());
|
|
||||||
}
|
|
||||||
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Log the current state of connections and subscriptions
|
|
||||||
/// </summary>
|
|
||||||
public string GetSubscriptionsState()
|
|
||||||
{
|
|
||||||
var result = new StringBuilder();
|
|
||||||
foreach (var client in ApiClients.OfType<SocketApiClient>().Where(c => c.CurrentSubscriptions > 0))
|
|
||||||
{
|
|
||||||
result.AppendLine(client.GetSubscriptionsState());
|
|
||||||
}
|
|
||||||
return result.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the state of all socket api clients
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
public List<SocketApiClient.SocketApiClientState> GetSocketApiClientStates()
|
|
||||||
{
|
|
||||||
var result = new List<SocketApiClient.SocketApiClientState>();
|
|
||||||
foreach (var client in ApiClients.OfType<SocketApiClient>())
|
|
||||||
{
|
|
||||||
result.Add(client.GetState());
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unsubscribe an update subscription
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="subscription">The subscription to unsubscribe</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public virtual async Task UnsubscribeAsync(UpdateSubscription subscription)
|
||||||
|
{
|
||||||
|
if (subscription == null)
|
||||||
|
throw new ArgumentNullException(nameof(subscription));
|
||||||
|
|
||||||
|
_logger.UnsubscribingSubscription(subscription.SocketId, subscription.Id);
|
||||||
|
await subscription.CloseAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unsubscribe all subscriptions
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public virtual async Task UnsubscribeAllAsync()
|
||||||
|
{
|
||||||
|
var tasks = new List<Task>();
|
||||||
|
foreach (var client in ApiClients.OfType<SocketApiClient>())
|
||||||
|
tasks.Add(client.UnsubscribeAllAsync());
|
||||||
|
|
||||||
|
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reconnect all connections
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public virtual async Task ReconnectAsync()
|
||||||
|
{
|
||||||
|
_logger.ReconnectingAllConnections(CurrentConnections);
|
||||||
|
var tasks = new List<Task>();
|
||||||
|
foreach (var client in ApiClients.OfType<SocketApiClient>())
|
||||||
|
{
|
||||||
|
tasks.Add(client.ReconnectAsync());
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Log the current state of connections and subscriptions
|
||||||
|
/// </summary>
|
||||||
|
public string GetSubscriptionsState()
|
||||||
|
{
|
||||||
|
var result = new StringBuilder();
|
||||||
|
foreach (var client in ApiClients.OfType<SocketApiClient>().Where(c => c.CurrentSubscriptions > 0))
|
||||||
|
{
|
||||||
|
result.AppendLine(client.GetSubscriptionsState());
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the state of all socket api clients
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public List<SocketApiClient.SocketApiClientState> GetSocketApiClientStates()
|
||||||
|
{
|
||||||
|
var result = new List<SocketApiClient.SocketApiClientState>();
|
||||||
|
foreach (var client in ApiClients.OfType<SocketApiClient>())
|
||||||
|
{
|
||||||
|
result.Add(client.GetState());
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,67 +1,78 @@
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Clients
|
namespace CryptoExchange.Net.Clients;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Base crypto client
|
||||||
|
/// </summary>
|
||||||
|
public class CryptoBaseClient : IDisposable
|
||||||
{
|
{
|
||||||
|
private readonly Dictionary<Type, object> _serviceCache = new Dictionary<Type, object>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Base crypto client
|
/// Service provider
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class CryptoBaseClient : IDisposable
|
protected readonly IServiceProvider? _serviceProvider;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
public CryptoBaseClient() { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serviceProvider"></param>
|
||||||
|
public CryptoBaseClient(IServiceProvider serviceProvider)
|
||||||
{
|
{
|
||||||
private Dictionary<Type, object> _serviceCache = new Dictionary<Type, object>();
|
_serviceProvider = serviceProvider;
|
||||||
|
_serviceCache = new Dictionary<Type, object>();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Service provider
|
/// Try get a client by type for the service collection
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected readonly IServiceProvider? _serviceProvider;
|
/// <typeparam name="T"></typeparam>
|
||||||
|
/// <returns></returns>
|
||||||
|
public T TryGet<T>(Func<T> createFunc)
|
||||||
|
{
|
||||||
|
var type = typeof(T);
|
||||||
|
if (_serviceCache.TryGetValue(type, out var value))
|
||||||
|
return (T)value;
|
||||||
|
|
||||||
/// <summary>
|
if (_serviceProvider == null)
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
public CryptoBaseClient() { }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="serviceProvider"></param>
|
|
||||||
public CryptoBaseClient(IServiceProvider serviceProvider)
|
|
||||||
{
|
{
|
||||||
_serviceProvider = serviceProvider;
|
// Create with default options
|
||||||
_serviceCache = new Dictionary<Type, object>();
|
var createResult = createFunc();
|
||||||
|
_serviceCache.Add(typeof(T), createResult!);
|
||||||
|
return createResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
var result = _serviceProvider.GetService<T>()
|
||||||
/// Try get a client by type for the service collection
|
?? throw new InvalidOperationException($"No service was found for {typeof(T).Name}, make sure the exchange is registered in dependency injection with the `services.Add[Exchange]()` method");
|
||||||
/// </summary>
|
_serviceCache.Add(type, result!);
|
||||||
/// <typeparam name="T"></typeparam>
|
return result;
|
||||||
/// <returns></returns>
|
}
|
||||||
public T TryGet<T>(Func<T> createFunc)
|
|
||||||
{
|
|
||||||
var type = typeof(T);
|
|
||||||
if (_serviceCache.TryGetValue(type, out var value))
|
|
||||||
return (T)value;
|
|
||||||
|
|
||||||
if (_serviceProvider == null)
|
/// <summary>
|
||||||
{
|
/// Dispose
|
||||||
// Create with default options
|
/// </summary>
|
||||||
var createResult = createFunc();
|
public void Dispose(bool disposing)
|
||||||
_serviceCache.Add(typeof(T), createResult!);
|
{
|
||||||
return createResult;
|
if (disposing)
|
||||||
}
|
|
||||||
|
|
||||||
var result = _serviceProvider.GetService<T>()
|
|
||||||
?? throw new InvalidOperationException($"No service was found for {typeof(T).Name}, make sure the exchange is registered in dependency injection with the `services.Add[Exchange]()` method");
|
|
||||||
_serviceCache.Add(type, result!);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Dispose
|
|
||||||
/// </summary>
|
|
||||||
public void Dispose()
|
|
||||||
{
|
{
|
||||||
_serviceCache.Clear();
|
_serviceCache.Clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Dispose
|
||||||
|
/// </summary>
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Dispose(true);
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,47 +1,23 @@
|
|||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces;
|
||||||
using CryptoExchange.Net.Interfaces.CommonClients;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Clients
|
namespace CryptoExchange.Net.Clients;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public class CryptoRestClient : CryptoBaseClient, ICryptoRestClient
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <summary>
|
||||||
public class CryptoRestClient : CryptoBaseClient, ICryptoRestClient
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
public CryptoRestClient()
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
public CryptoRestClient()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="serviceProvider"></param>
|
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serviceProvider"></param>
|
||||||
|
public CryptoRestClient(IServiceProvider serviceProvider) : base(serviceProvider)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,24 +1,23 @@
|
|||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces;
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Clients
|
namespace CryptoExchange.Net.Clients;
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public class CryptoSocketClient : CryptoBaseClient, ICryptoSocketClient
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
public CryptoSocketClient()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <inheritdoc />
|
||||||
/// ctor
|
public class CryptoSocketClient : CryptoBaseClient, ICryptoSocketClient
|
||||||
/// </summary>
|
{
|
||||||
/// <param name="serviceProvider"></param>
|
/// <summary>
|
||||||
public CryptoSocketClient(IServiceProvider serviceProvider) : base(serviceProvider)
|
/// ctor
|
||||||
{
|
/// </summary>
|
||||||
}
|
public CryptoSocketClient()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serviceProvider"></param>
|
||||||
|
public CryptoSocketClient(IServiceProvider serviceProvider) : base(serviceProvider)
|
||||||
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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,25 +1,24 @@
|
|||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters
|
namespace CryptoExchange.Net.Converters;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Mark property as an index in the array
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Property)]
|
||||||
|
public class ArrayPropertyAttribute : Attribute
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Mark property as an index in the array
|
/// The index in the array
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[AttributeUsage(AttributeTargets.Property)]
|
public int Index { get; }
|
||||||
public class ArrayPropertyAttribute : Attribute
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The index in the array
|
|
||||||
/// </summary>
|
|
||||||
public int Index { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="index"></param>
|
/// <param name="index"></param>
|
||||||
public ArrayPropertyAttribute(int index)
|
public ArrayPropertyAttribute(int index)
|
||||||
{
|
{
|
||||||
Index = index;
|
Index = index;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,28 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,49 +1,48 @@
|
|||||||
namespace CryptoExchange.Net.Converters.MessageParsing
|
namespace CryptoExchange.Net.Converters.MessageParsing;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Node accessor
|
||||||
|
/// </summary>
|
||||||
|
public readonly struct NodeAccessor
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Node accessor
|
/// Index
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public struct NodeAccessor
|
public int? Index { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Property name
|
||||||
|
/// </summary>
|
||||||
|
public string? Property { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Type (0 = int, 1 = string, 2 = prop name)
|
||||||
|
/// </summary>
|
||||||
|
public int Type { get; }
|
||||||
|
|
||||||
|
private NodeAccessor(int? index, string? property, int type)
|
||||||
{
|
{
|
||||||
/// <summary>
|
Index = index;
|
||||||
/// Index
|
Property = property;
|
||||||
/// </summary>
|
Type = type;
|
||||||
public int? Index { get; }
|
|
||||||
/// <summary>
|
|
||||||
/// Property name
|
|
||||||
/// </summary>
|
|
||||||
public string? Property { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Type (0 = int, 1 = string, 2 = prop name)
|
|
||||||
/// </summary>
|
|
||||||
public int Type { get; }
|
|
||||||
|
|
||||||
private NodeAccessor(int? index, string? property, int type)
|
|
||||||
{
|
|
||||||
Index = index;
|
|
||||||
Property = property;
|
|
||||||
Type = type;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create an int node accessor
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="value"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static NodeAccessor Int(int value) { return new NodeAccessor(value, null, 0); }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create a string node accessor
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="value"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static NodeAccessor String(string value) { return new NodeAccessor(null, value, 1); }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create a property name node accessor
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static NodeAccessor PropertyName() { return new NodeAccessor(null, null, 2); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create an int node accessor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static NodeAccessor Int(int value) { return new NodeAccessor(value, null, 0); }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a string node accessor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static NodeAccessor String(string value) { return new NodeAccessor(null, value, 1); }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a property name node accessor
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static NodeAccessor PropertyName() { return new NodeAccessor(null, null, 2); }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,50 +1,49 @@
|
|||||||
using System.Collections;
|
using System.Collections;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.MessageParsing
|
namespace CryptoExchange.Net.Converters.MessageParsing;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Message access definition
|
||||||
|
/// </summary>
|
||||||
|
public readonly struct MessagePath : IEnumerable<NodeAccessor>
|
||||||
{
|
{
|
||||||
/// <summary>
|
private readonly List<NodeAccessor> _path;
|
||||||
/// Message access definition
|
|
||||||
/// </summary>
|
internal void Add(NodeAccessor node)
|
||||||
public struct MessagePath : IEnumerable<NodeAccessor>
|
|
||||||
{
|
{
|
||||||
private List<NodeAccessor> _path;
|
_path.Add(node);
|
||||||
|
}
|
||||||
|
|
||||||
internal void Add(NodeAccessor node)
|
/// <summary>
|
||||||
{
|
/// ctor
|
||||||
_path.Add(node);
|
/// </summary>
|
||||||
}
|
public MessagePath()
|
||||||
|
{
|
||||||
|
_path = new List<NodeAccessor>();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// Create a new message path
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public MessagePath()
|
/// <returns></returns>
|
||||||
{
|
public static MessagePath Get()
|
||||||
_path = new List<NodeAccessor>();
|
{
|
||||||
}
|
return new MessagePath();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a new message path
|
/// IEnumerable implementation
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static MessagePath Get()
|
public IEnumerator<NodeAccessor> GetEnumerator()
|
||||||
{
|
{
|
||||||
return new MessagePath();
|
for (var i = 0; i < _path.Count; i++)
|
||||||
}
|
yield return _path[i];
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
IEnumerator IEnumerable.GetEnumerator()
|
||||||
/// IEnumerable implementation
|
{
|
||||||
/// </summary>
|
return GetEnumerator();
|
||||||
/// <returns></returns>
|
|
||||||
public IEnumerator<NodeAccessor> GetEnumerator()
|
|
||||||
{
|
|
||||||
for (var i = 0; i < _path.Count; i++)
|
|
||||||
yield return _path[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
IEnumerator IEnumerable.GetEnumerator()
|
|
||||||
{
|
|
||||||
return GetEnumerator();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,43 +1,42 @@
|
|||||||
namespace CryptoExchange.Net.Converters.MessageParsing
|
namespace CryptoExchange.Net.Converters.MessageParsing;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Message path extension methods
|
||||||
|
/// </summary>
|
||||||
|
public static class MessagePathExtension
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Message path extension methods
|
/// Add a string node accessor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class MessagePathExtension
|
/// <param name="path"></param>
|
||||||
|
/// <param name="propName"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static MessagePath Property(this MessagePath path, string propName)
|
||||||
{
|
{
|
||||||
/// <summary>
|
path.Add(NodeAccessor.String(propName));
|
||||||
/// Add a string node accessor
|
return path;
|
||||||
/// </summary>
|
}
|
||||||
/// <param name="path"></param>
|
|
||||||
/// <param name="propName"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static MessagePath Property(this MessagePath path, string propName)
|
|
||||||
{
|
|
||||||
path.Add(NodeAccessor.String(propName));
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Add a property name node accessor
|
/// Add a property name node accessor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="path"></param>
|
/// <param name="path"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static MessagePath PropertyName(this MessagePath path)
|
public static MessagePath PropertyName(this MessagePath path)
|
||||||
{
|
{
|
||||||
path.Add(NodeAccessor.PropertyName());
|
path.Add(NodeAccessor.PropertyName());
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Add a int node accessor
|
/// Add a int node accessor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="path"></param>
|
/// <param name="path"></param>
|
||||||
/// <param name="index"></param>
|
/// <param name="index"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static MessagePath Index(this MessagePath path, int index)
|
public static MessagePath Index(this MessagePath path, int index)
|
||||||
{
|
{
|
||||||
path.Add(NodeAccessor.Int(index));
|
path.Add(NodeAccessor.Int(index));
|
||||||
return path;
|
return path;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,20 @@
|
|||||||
namespace CryptoExchange.Net.Converters.MessageParsing
|
namespace CryptoExchange.Net.Converters.MessageParsing;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Message node type
|
||||||
|
/// </summary>
|
||||||
|
public enum NodeType
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Message node type
|
/// Array node
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public enum NodeType
|
Array,
|
||||||
{
|
/// <summary>
|
||||||
/// <summary>
|
/// Object node
|
||||||
/// Array node
|
/// </summary>
|
||||||
/// </summary>
|
Object,
|
||||||
Array,
|
/// <summary>
|
||||||
/// <summary>
|
/// Value node
|
||||||
/// Object node
|
/// </summary>
|
||||||
/// </summary>
|
Value
|
||||||
Object,
|
|
||||||
/// <summary>
|
|
||||||
/// Value node
|
|
||||||
/// </summary>
|
|
||||||
Value
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,224 +1,232 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using CryptoExchange.Net.Attributes;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
#if NET5_0_OR_GREATER
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
#endif
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
namespace CryptoExchange.Net.Converters.SystemTextJson;
|
||||||
|
|
||||||
|
/// <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>
|
||||||
|
#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
|
||||||
{
|
{
|
||||||
/// <summary>
|
private static readonly Lazy<List<ArrayPropertyInfo>> _typePropertyInfo = new Lazy<List<ArrayPropertyInfo>>(CacheTypeAttributes, LazyThreadSafetyMode.PublicationOnly);
|
||||||
/// 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
|
/// <inheritdoc />
|
||||||
/// </summary>
|
#if NET5_0_OR_GREATER
|
||||||
public class ArrayConverter : JsonConverterFactory
|
[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)
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
if (value == null)
|
||||||
public override bool CanConvert(Type typeToConvert) => true;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
|
|
||||||
{
|
{
|
||||||
Type converterType = typeof(ArrayConverterInner<>).MakeGenericType(typeToConvert);
|
writer.WriteNullValue();
|
||||||
return (JsonConverter)Activator.CreateInstance(converterType)!;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ArrayPropertyInfo
|
writer.WriteStartArray();
|
||||||
|
|
||||||
|
var ordered = _typePropertyInfo.Value.Where(x => x.ArrayProperty != null).OrderBy(p => p.ArrayProperty.Index);
|
||||||
|
var last = -1;
|
||||||
|
foreach (var prop in ordered)
|
||||||
{
|
{
|
||||||
public PropertyInfo PropertyInfo { get; set; } = null!;
|
if (prop.ArrayProperty.Index == last)
|
||||||
public ArrayPropertyAttribute ArrayProperty { get; set; } = null!;
|
continue;
|
||||||
public Type? JsonConverterType { get; set; }
|
|
||||||
public bool DefaultDeserialization { get; set; }
|
while (prop.ArrayProperty.Index != last + 1)
|
||||||
public Type TargetType { get; set; } = null!;
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ArrayConverterInner<T> : JsonConverter<T>
|
writer.WriteEndArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
if (reader.TokenType == JsonTokenType.Null)
|
||||||
|
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())
|
||||||
{
|
{
|
||||||
private static readonly ConcurrentDictionary<Type, List<ArrayPropertyInfo>> _typeAttributesCache = new ConcurrentDictionary<Type, List<ArrayPropertyInfo>>();
|
if (reader.TokenType == JsonTokenType.EndArray)
|
||||||
private static readonly ConcurrentDictionary<Type, JsonSerializerOptions> _converterOptionsCache = new ConcurrentDictionary<Type, JsonSerializerOptions>();
|
break;
|
||||||
|
|
||||||
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
|
var indexAttributes = _typePropertyInfo.Value.Where(a => a.ArrayProperty.Index == index);
|
||||||
|
if (!indexAttributes.Any())
|
||||||
{
|
{
|
||||||
if (value == null)
|
index++;
|
||||||
{
|
continue;
|
||||||
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.PropertyInfo.PropertyType == typeof(string))
|
|
||||||
writer.WriteStringValue(Convert.ToString(objValue, CultureInfo.InvariantCulture));
|
|
||||||
else
|
|
||||||
writer.WriteRawValue(Convert.ToString(objValue, CultureInfo.InvariantCulture)!);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
JsonSerializer.Serialize(writer, objValue, typeOptions ?? options);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
writer.WriteEndArray();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
foreach (var attribute in indexAttributes)
|
||||||
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
|
||||||
{
|
{
|
||||||
if (reader.TokenType == JsonTokenType.Null)
|
var targetType = attribute.TargetType;
|
||||||
return default;
|
object? value = null;
|
||||||
|
if (attribute.JsonConverter != null)
|
||||||
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.
|
if (attribute.JsonSerializerOptions == null)
|
||||||
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,
|
attribute.JsonSerializerOptions = new JsonSerializerOptions
|
||||||
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))
|
NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals,
|
||||||
{
|
PropertyNameCaseInsensitive = false,
|
||||||
var converter = (JsonConverter)Activator.CreateInstance(attribute.JsonConverterType)!;
|
Converters = { attribute.JsonConverter },
|
||||||
newOptions = new JsonSerializerOptions
|
TypeInfoResolver = options.TypeInfoResolver,
|
||||||
{
|
};
|
||||||
NumberHandling = SerializerOptions.WithConverters.NumberHandling,
|
|
||||||
PropertyNameCaseInsensitive = SerializerOptions.WithConverters.PropertyNameCaseInsensitive,
|
|
||||||
Converters = { converter },
|
|
||||||
};
|
|
||||||
_converterOptionsCache.TryAdd(attribute.JsonConverterType, newOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
value = JsonDocument.ParseValue(ref reader).Deserialize(targetType, newOptions);
|
|
||||||
}
|
|
||||||
else if (attribute.DefaultDeserialization)
|
|
||||||
{
|
|
||||||
// Use default deserialization
|
|
||||||
value = JsonDocument.ParseValue(ref reader).Deserialize(targetType, options);
|
|
||||||
}
|
|
||||||
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++;
|
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"),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
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
|
||||||
|
{
|
||||||
|
public PropertyInfo PropertyInfo { get; set; } = null!;
|
||||||
|
public ArrayPropertyAttribute ArrayProperty { get; set; } = null!;
|
||||||
|
public JsonConverter? JsonConverter { get; set; }
|
||||||
|
public bool DefaultDeserialization { get; set; }
|
||||||
|
public Type TargetType { get; set; } = null!;
|
||||||
|
public JsonSerializerOptions? JsonSerializerOptions { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,46 +1,45 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
namespace CryptoExchange.Net.Converters.SystemTextJson;
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Decimal converter that handles overflowing decimal values (by setting it to decimal.MaxValue)
|
|
||||||
/// </summary>
|
|
||||||
public class BigDecimalConverter : JsonConverter<decimal>
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override decimal Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
if (reader.TokenType == JsonTokenType.String)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return decimal.Parse(reader.GetString()!, NumberStyles.Float, CultureInfo.InvariantCulture);
|
|
||||||
}
|
|
||||||
catch(OverflowException)
|
|
||||||
{
|
|
||||||
// Value doesn't fit decimal, default to max value
|
|
||||||
return decimal.MaxValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Decimal converter that handles overflowing decimal values (by setting it to decimal.MaxValue)
|
||||||
|
/// </summary>
|
||||||
|
public class BigDecimalConverter : JsonConverter<decimal>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override decimal Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
if (reader.TokenType == JsonTokenType.String)
|
||||||
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return reader.GetDecimal();
|
return decimal.Parse(reader.GetString()!, NumberStyles.Float, CultureInfo.InvariantCulture);
|
||||||
}
|
}
|
||||||
catch(FormatException)
|
catch(OverflowException)
|
||||||
{
|
{
|
||||||
// Format issue, assume value is too large
|
// Value doesn't fit decimal, default to max value
|
||||||
return decimal.MaxValue;
|
return decimal.MaxValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
try
|
||||||
public override void Write(Utf8JsonWriter writer, decimal value, JsonSerializerOptions options)
|
|
||||||
{
|
{
|
||||||
writer.WriteNumberValue(value);
|
return reader.GetDecimal();
|
||||||
|
}
|
||||||
|
catch(FormatException)
|
||||||
|
{
|
||||||
|
// Format issue, assume value is too large
|
||||||
|
return decimal.MaxValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override void Write(Utf8JsonWriter writer, decimal value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
writer.WriteNumberValue(value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,85 +1,83 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Runtime.Serialization;
|
using System.Runtime.Serialization;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
namespace CryptoExchange.Net.Converters.SystemTextJson;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bool converter
|
||||||
|
/// </summary>
|
||||||
|
public class BoolConverter : JsonConverterFactory
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <inheritdoc />
|
||||||
/// Bool converter
|
public override bool CanConvert(Type typeToConvert)
|
||||||
/// </summary>
|
|
||||||
public class BoolConverter : JsonConverterFactory
|
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
return typeToConvert == typeof(bool) || typeToConvert == typeof(bool?);
|
||||||
public override bool CanConvert(Type typeToConvert)
|
|
||||||
{
|
|
||||||
return typeToConvert == typeof(bool) || typeToConvert == typeof(bool?);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
Type converterType = typeof(BoolConverterInner<>).MakeGenericType(typeToConvert);
|
|
||||||
return (JsonConverter)Activator.CreateInstance(converterType)!;
|
|
||||||
}
|
|
||||||
|
|
||||||
private class BoolConverterInner<T> : JsonConverter<T>
|
|
||||||
{
|
|
||||||
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
|
||||||
=> (T)((object?)ReadBool(ref reader, typeToConvert, options) ?? default(T))!;
|
|
||||||
|
|
||||||
public bool? ReadBool(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
if (reader.TokenType == JsonTokenType.True)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
if (reader.TokenType == JsonTokenType.False)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
var value = reader.TokenType switch
|
|
||||||
{
|
|
||||||
JsonTokenType.String => reader.GetString(),
|
|
||||||
JsonTokenType.Number => reader.GetInt16().ToString(),
|
|
||||||
_ => null
|
|
||||||
};
|
|
||||||
|
|
||||||
value = value?.ToLowerInvariant().Trim();
|
|
||||||
if (string.IsNullOrEmpty(value))
|
|
||||||
{
|
|
||||||
if (typeToConvert == typeof(bool))
|
|
||||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Received null bool value, but property type is not a nullable bool");
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new SerializationException($"Can't convert bool value {value}");
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
if (value is bool boolVal)
|
|
||||||
writer.WriteBooleanValue(boolVal);
|
|
||||||
else
|
|
||||||
writer.WriteNullValue();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
return typeToConvert == typeof(bool) ? new BoolConverterInner<bool>() : new BoolConverterInner<bool?>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private class BoolConverterInner<T> : JsonConverter<T>
|
||||||
|
{
|
||||||
|
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
=> (T)((object?)ReadBool(ref reader, typeToConvert, options) ?? default(T))!;
|
||||||
|
|
||||||
|
public static bool? ReadBool(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
if (reader.TokenType == JsonTokenType.True)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (reader.TokenType == JsonTokenType.False)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var value = reader.TokenType switch
|
||||||
|
{
|
||||||
|
JsonTokenType.String => reader.GetString(),
|
||||||
|
JsonTokenType.Number => reader.GetInt16().ToString(),
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
|
||||||
|
value = value?.ToLowerInvariant().Trim();
|
||||||
|
if (string.IsNullOrEmpty(value))
|
||||||
|
{
|
||||||
|
if (typeToConvert == typeof(bool))
|
||||||
|
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Received null bool value, but property type is not a nullable bool");
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new SerializationException($"Can't convert bool value {value}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
if (value is bool boolVal)
|
||||||
|
writer.WriteBooleanValue(boolVal);
|
||||||
|
else
|
||||||
|
writer.WriteNullValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,36 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
#if NET5_0_OR_GREATER
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
#endif
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
namespace CryptoExchange.Net.Converters.SystemTextJson;
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Converter for comma seperated enum values
|
|
||||||
/// </summary>
|
|
||||||
public class CommaSplitEnumConverter<T> : JsonConverter<IEnumerable<T>> where T : Enum
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override IEnumerable<T>? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
return (reader.GetString()?.Split(',').Select(x => EnumConverter.ParseString<T>(x)).ToArray() ?? new T[0])!;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <summary>
|
||||||
public override void Write(Utf8JsonWriter writer, IEnumerable<T> value, JsonSerializerOptions options)
|
/// Converter for comma separated enum values
|
||||||
{
|
/// </summary>
|
||||||
writer.WriteStringValue(string.Join(",", value.Select(x => EnumConverter.GetString(x))));
|
#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 />
|
||||||
|
public override T[]? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
var str = reader.GetString();
|
||||||
|
if (string.IsNullOrEmpty(str))
|
||||||
|
return [];
|
||||||
|
|
||||||
|
return str!.Split(',').Select(x => (T)EnumConverter.ParseString<T>(x)!).ToArray() ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override void Write(Utf8JsonWriter writer, T[] value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
writer.WriteStringValue(string.Join(",", value.Select(x => EnumConverter.GetString(x))));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,243 +1,241 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
namespace CryptoExchange.Net.Converters.SystemTextJson;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Date time converter
|
||||||
|
/// </summary>
|
||||||
|
public class DateTimeConverter : JsonConverterFactory
|
||||||
{
|
{
|
||||||
/// <summary>
|
private static readonly DateTime _epoch = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||||
/// Date time converter
|
private const long _ticksPerSecond = TimeSpan.TicksPerMillisecond * 1000;
|
||||||
/// </summary>
|
private const double _ticksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000d;
|
||||||
public class DateTimeConverter : JsonConverterFactory
|
private const double _ticksPerNanosecond = TimeSpan.TicksPerMillisecond / 1000d / 1000;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override bool CanConvert(Type typeToConvert)
|
||||||
{
|
{
|
||||||
private static readonly DateTime _epoch = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
return typeToConvert == typeof(DateTime) || typeToConvert == typeof(DateTime?);
|
||||||
private const long _ticksPerSecond = TimeSpan.TicksPerMillisecond * 1000;
|
|
||||||
private const double _ticksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000d;
|
|
||||||
private const double _ticksPerNanosecond = TimeSpan.TicksPerMillisecond / 1000d / 1000;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override bool CanConvert(Type typeToConvert)
|
|
||||||
{
|
|
||||||
return typeToConvert == typeof(DateTime) || typeToConvert == typeof(DateTime?);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
Type converterType = typeof(DateTimeConverterInner<>).MakeGenericType(typeToConvert);
|
|
||||||
return (JsonConverter)Activator.CreateInstance(converterType)!;
|
|
||||||
}
|
|
||||||
|
|
||||||
private class DateTimeConverterInner<T> : JsonConverter<T>
|
|
||||||
{
|
|
||||||
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
|
||||||
=> (T)((object?)ReadDateTime(ref reader, typeToConvert, options) ?? default(T))!;
|
|
||||||
|
|
||||||
private DateTime? ReadDateTime(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
if (reader.TokenType == JsonTokenType.Null)
|
|
||||||
{
|
|
||||||
if (typeToConvert == typeof(DateTime))
|
|
||||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | DateTime value of null, but property is not nullable");
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reader.TokenType is JsonTokenType.Number)
|
|
||||||
{
|
|
||||||
var longValue = reader.GetDouble();
|
|
||||||
if (longValue == 0 || longValue == -1)
|
|
||||||
return default;
|
|
||||||
|
|
||||||
return ParseFromDouble(longValue);
|
|
||||||
}
|
|
||||||
else if (reader.TokenType is JsonTokenType.String)
|
|
||||||
{
|
|
||||||
var stringValue = reader.GetString();
|
|
||||||
if (string.IsNullOrWhiteSpace(stringValue)
|
|
||||||
|| stringValue == "-1"
|
|
||||||
|| stringValue == "0001-01-01T00:00:00Z"
|
|
||||||
|| double.TryParse(stringValue, out var doubleVal) && doubleVal == 0)
|
|
||||||
{
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
|
|
||||||
return ParseFromString(stringValue!);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return reader.GetDateTime();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
if (value == null)
|
|
||||||
{
|
|
||||||
writer.WriteNullValue();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var dtValue = (DateTime)(object)value;
|
|
||||||
if (dtValue == default)
|
|
||||||
writer.WriteStringValue(default(DateTime));
|
|
||||||
else
|
|
||||||
writer.WriteNumberValue((long)Math.Round((dtValue - new DateTime(1970, 1, 1)).TotalMilliseconds));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Parse a long value to datetime
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="longValue"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static DateTime ParseFromDouble(double longValue)
|
|
||||||
{
|
|
||||||
if (longValue < 19999999999)
|
|
||||||
return ConvertFromSeconds(longValue);
|
|
||||||
if (longValue < 19999999999999)
|
|
||||||
return ConvertFromMilliseconds(longValue);
|
|
||||||
if (longValue < 19999999999999999)
|
|
||||||
return ConvertFromMicroseconds(longValue);
|
|
||||||
|
|
||||||
return ConvertFromNanoseconds(longValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Parse a 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 <= 0)
|
|
||||||
return default;
|
|
||||||
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(double 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(double 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 JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
return typeToConvert == typeof(DateTime) ? new DateTimeConverterInner<DateTime>() : new DateTimeConverterInner<DateTime?>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private class DateTimeConverterInner<T> : JsonConverter<T>
|
||||||
|
{
|
||||||
|
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
=> (T)((object?)ReadDateTime(ref reader, typeToConvert, options) ?? default(T))!;
|
||||||
|
|
||||||
|
private static DateTime? ReadDateTime(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
if (reader.TokenType == JsonTokenType.Null)
|
||||||
|
{
|
||||||
|
if (typeToConvert == typeof(DateTime))
|
||||||
|
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | DateTime value of null, but property is not nullable");
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reader.TokenType is JsonTokenType.Number)
|
||||||
|
{
|
||||||
|
var longValue = reader.GetDouble();
|
||||||
|
if (longValue == 0 || longValue < 0)
|
||||||
|
return default;
|
||||||
|
|
||||||
|
return ParseFromDouble(longValue);
|
||||||
|
}
|
||||||
|
else if (reader.TokenType is JsonTokenType.String)
|
||||||
|
{
|
||||||
|
var stringValue = reader.GetString();
|
||||||
|
if (string.IsNullOrWhiteSpace(stringValue)
|
||||||
|
|| stringValue == "-1"
|
||||||
|
|| stringValue == "0001-01-01T00:00:00Z"
|
||||||
|
|| double.TryParse(stringValue, out var doubleVal) && doubleVal == 0)
|
||||||
|
{
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ParseFromString(stringValue!);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return reader.GetDateTime();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
if (value == null)
|
||||||
|
{
|
||||||
|
writer.WriteNullValue();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var dtValue = (DateTime)(object)value;
|
||||||
|
if (dtValue == default)
|
||||||
|
writer.WriteStringValue(default(DateTime));
|
||||||
|
else
|
||||||
|
writer.WriteNumberValue((long)Math.Round((dtValue - new DateTime(1970, 1, 1)).TotalMilliseconds));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parse a long value to datetime
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="longValue"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static DateTime ParseFromDouble(double longValue)
|
||||||
|
{
|
||||||
|
if (longValue < 19999999999)
|
||||||
|
return ConvertFromSeconds(longValue);
|
||||||
|
if (longValue < 19999999999999)
|
||||||
|
return ConvertFromMilliseconds(longValue);
|
||||||
|
if (longValue < 19999999999999999)
|
||||||
|
return ConvertFromMicroseconds(longValue);
|
||||||
|
|
||||||
|
return ConvertFromNanoseconds(longValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parse a 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 <= 0)
|
||||||
|
return default;
|
||||||
|
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(double 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(double 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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,60 +1,43 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
namespace CryptoExchange.Net.Converters.SystemTextJson;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Decimal converter
|
||||||
|
/// </summary>
|
||||||
|
public class DecimalConverter : JsonConverter<decimal?>
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <inheritdoc />
|
||||||
/// Decimal converter
|
public override decimal? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
/// </summary>
|
|
||||||
public class DecimalConverter : JsonConverter<decimal?>
|
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
if (reader.TokenType == JsonTokenType.Null)
|
||||||
public override decimal? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
return null;
|
||||||
|
|
||||||
|
if (reader.TokenType == JsonTokenType.String)
|
||||||
{
|
{
|
||||||
if (reader.TokenType == JsonTokenType.Null)
|
var value = reader.GetString();
|
||||||
return null;
|
return ExchangeHelpers.ParseDecimal(value);
|
||||||
|
|
||||||
if (reader.TokenType == JsonTokenType.String)
|
|
||||||
{
|
|
||||||
var value = reader.GetString();
|
|
||||||
if (string.IsNullOrEmpty(value) || string.Equals("null", value, StringComparison.OrdinalIgnoreCase))
|
|
||||||
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
|
|
||||||
{
|
|
||||||
return reader.GetDecimal();
|
|
||||||
}
|
|
||||||
catch(FormatException)
|
|
||||||
{
|
|
||||||
// Format issue, assume value is too large
|
|
||||||
return decimal.MaxValue;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
try
|
||||||
public override void Write(Utf8JsonWriter writer, decimal? value, JsonSerializerOptions options)
|
|
||||||
{
|
{
|
||||||
if (value == null)
|
return reader.GetDecimal();
|
||||||
writer.WriteNullValue();
|
}
|
||||||
else
|
catch(FormatException)
|
||||||
writer.WriteNumberValue(value.Value);
|
{
|
||||||
|
// Format issue, assume value is too large
|
||||||
|
return decimal.MaxValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override void Write(Utf8JsonWriter writer, decimal? value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
if (value == null)
|
||||||
|
writer.WriteNullValue();
|
||||||
|
else
|
||||||
|
writer.WriteNumberValue(value.Value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,22 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
namespace CryptoExchange.Net.Converters.SystemTextJson;
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Converter for serializing decimal values as string
|
|
||||||
/// </summary>
|
|
||||||
public class DecimalStringWriterConverter : JsonConverter<decimal>
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override decimal Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <summary>
|
||||||
public override void Write(Utf8JsonWriter writer, decimal value, JsonSerializerOptions options)
|
/// Converter for serializing decimal values as string
|
||||||
=> writer.WriteStringValue(value.ToString(CultureInfo.InvariantCulture) ?? null);
|
/// </summary>
|
||||||
|
public class DecimalStringWriterConverter : JsonConverter<decimal>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override decimal Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override void Write(Utf8JsonWriter writer, decimal value, JsonSerializerOptions options)
|
||||||
|
=> writer.WriteStringValue(value.ToString(CultureInfo.InvariantCulture) ?? null);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using CryptoExchange.Net.Attributes;
|
using CryptoExchange.Net.Attributes;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@@ -9,239 +9,280 @@ using System.Reflection;
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
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>
|
/// <summary>
|
||||||
/// Converter for enum values. Enums entries should be noted with a MapAttribute to map the enum value to a string value
|
/// Get the enum value from a string
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class EnumConverter : JsonConverterFactory
|
/// <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>
|
||||||
|
/// Converter for enum values. Enums entries should be noted with a MapAttribute to map the enum value to a string value
|
||||||
|
/// </summary>
|
||||||
|
#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 static List<KeyValuePair<T, string>>? _mapping;
|
||||||
|
private NullableEnumConverter? _nullableEnumConverter;
|
||||||
|
|
||||||
|
private static ConcurrentBag<string> _unknownValuesWarned = new ConcurrentBag<string>();
|
||||||
|
|
||||||
|
internal class NullableEnumConverter : JsonConverter<T?>
|
||||||
{
|
{
|
||||||
private bool _warnOnMissingEntry = true;
|
private readonly EnumConverter<T> _enumConverter;
|
||||||
private bool _writeAsInt;
|
|
||||||
private static readonly ConcurrentDictionary<Type, List<KeyValuePair<object, string>>> _mapping = new();
|
|
||||||
|
|
||||||
/// <summary>
|
public NullableEnumConverter(EnumConverter<T> enumConverter)
|
||||||
/// </summary>
|
|
||||||
public EnumConverter() { }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="writeAsInt"></param>
|
|
||||||
/// <param name="warnOnMissingEntry"></param>
|
|
||||||
public EnumConverter(bool writeAsInt, bool warnOnMissingEntry)
|
|
||||||
{
|
{
|
||||||
_writeAsInt = writeAsInt;
|
_enumConverter = enumConverter;
|
||||||
_warnOnMissingEntry = warnOnMissingEntry;
|
}
|
||||||
|
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
return EnumConverter<T>.ReadNullable(ref reader, typeToConvert, options, out var isEmptyString, out var warn);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options)
|
||||||
public override bool CanConvert(Type typeToConvert)
|
|
||||||
{
|
{
|
||||||
return typeToConvert.IsEnum || Nullable.GetUnderlyingType(typeToConvert)?.IsEnum == true;
|
if (value == null)
|
||||||
}
|
|
||||||
|
|
||||||
/// <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);
|
writer.WriteNullValue();
|
||||||
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);
|
else
|
||||||
return mapping;
|
|
||||||
}
|
|
||||||
|
|
||||||
private class EnumConverterInner<T> : JsonConverter<T>
|
|
||||||
{
|
|
||||||
private bool _warnOnMissingEntry = true;
|
|
||||||
private bool _writeAsInt;
|
|
||||||
|
|
||||||
public EnumConverterInner(bool writeAsInt, bool warnOnMissingEntry)
|
|
||||||
{
|
{
|
||||||
_warnOnMissingEntry = warnOnMissingEntry;
|
_enumConverter.Write(writer, value.Value, options);
|
||||||
_writeAsInt = writeAsInt;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
var enumType = Nullable.GetUnderlyingType(typeToConvert) ?? typeToConvert;
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
if (value == null)
|
|
||||||
{
|
|
||||||
writer.WriteNullValue();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (!_writeAsInt)
|
|
||||||
{
|
|
||||||
var stringValue = GetString(value.GetType(), value);
|
|
||||||
writer.WriteStringValue(stringValue);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
writer.WriteNumberValue((int)Convert.ChangeType(value, typeof(int)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static object? GetDefaultValue(Type objectType, Type enumType)
|
|
||||||
{
|
|
||||||
if (Nullable.GetUnderlyingType(objectType) != null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return Activator.CreateInstance(enumType); // return default value
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
/// <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.TryGetValue(objectType, out var mapping))
|
|
||||||
mapping = AddMapping(objectType);
|
|
||||||
|
|
||||||
return enumValue == null ? null : (mapping.FirstOrDefault(v => v.Key.Equals(enumValue)).Value ?? enumValue.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get the enum value from a string
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T">Enum type</typeparam>
|
|
||||||
/// <param name="value">String value</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static T? ParseString<T>(string value) where T : Enum
|
|
||||||
{
|
|
||||||
var type = typeof(T);
|
|
||||||
if (!_mapping.TryGetValue(type, out var enumMapping))
|
|
||||||
enumMapping = AddMapping(type);
|
|
||||||
|
|
||||||
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>)))
|
|
||||||
{
|
|
||||||
return (T)mapping.Key;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// If no explicit mapping is found try to parse string
|
|
||||||
return (T)Enum.Parse(type, value, true);
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
return default;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <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)
|
||||||
|
{
|
||||||
|
// 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: {typeof(T).Name}. If you think {typeof(T).Name} should be nullable please open an issue on the Github repo");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Received null enum value, but property type is not a nullable enum. EnumType: {typeof(T).Name}. If you think {typeof(T).Name} should be nullable please open an issue on the Github repo");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new T(); // return default value
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return t.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (objectType.IsDefined(typeof(FlagsAttribute)))
|
||||||
|
{
|
||||||
|
var intValue = int.Parse(value);
|
||||||
|
result = (T)Enum.ToObject(objectType, intValue);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_unknownValuesWarned.Contains(value))
|
||||||
|
{
|
||||||
|
// Check if it is an known unknown value
|
||||||
|
// Done here to prevent lookup overhead for normal conversions, but prevent expensive exception throwing
|
||||||
|
result = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
foreach (var value in attribute.Values)
|
||||||
|
mapping.Add(new KeyValuePair<T, string>((T)Enum.Parse(enumType, member.Name), value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_mapping = mapping;
|
||||||
|
return mapping;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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")]
|
||||||
|
public static string? GetString(T? enumValue)
|
||||||
|
{
|
||||||
|
if (_mapping == null)
|
||||||
|
_mapping = AddMapping();
|
||||||
|
|
||||||
|
return enumValue == null ? null : (_mapping.FirstOrDefault(v => v.Key.Equals(enumValue)).Value ?? enumValue.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the enum value from a string
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">String value</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static T? ParseString(string value)
|
||||||
|
{
|
||||||
|
var type = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
|
||||||
|
if (_mapping == null)
|
||||||
|
_mapping = AddMapping();
|
||||||
|
|
||||||
|
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>)))
|
||||||
|
return mapping.Key;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// If no explicit mapping is found try to parse string
|
||||||
|
return (T)Enum.Parse(type, value, true);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public JsonConverter CreateNullableConverter()
|
||||||
|
{
|
||||||
|
_nullableEnumConverter ??= new NullableEnumConverter(this);
|
||||||
|
return _nullableEnumConverter;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using System;
|
||||||
|
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,8 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Converters.SystemTextJson;
|
||||||
|
|
||||||
|
internal interface INullableConverterFactory
|
||||||
|
{
|
||||||
|
JsonConverter CreateNullableConverter();
|
||||||
|
}
|
||||||
@@ -1,40 +1,39 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
namespace CryptoExchange.Net.Converters.SystemTextJson;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Int converter
|
||||||
|
/// </summary>
|
||||||
|
public class IntConverter : JsonConverter<int?>
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <inheritdoc />
|
||||||
/// Int converter
|
public override int? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
/// </summary>
|
|
||||||
public class IntConverter : JsonConverter<int?>
|
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
if (reader.TokenType == JsonTokenType.Null)
|
||||||
public override int? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
return null;
|
||||||
|
|
||||||
|
if (reader.TokenType == JsonTokenType.String)
|
||||||
{
|
{
|
||||||
if (reader.TokenType == JsonTokenType.Null)
|
var value = reader.GetString();
|
||||||
|
if (string.IsNullOrEmpty(value))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
if (reader.TokenType == JsonTokenType.String)
|
return int.Parse(value, NumberStyles.Integer, CultureInfo.InvariantCulture);
|
||||||
{
|
|
||||||
var value = reader.GetString();
|
|
||||||
if (string.IsNullOrEmpty(value))
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return int.Parse(value, NumberStyles.Integer, CultureInfo.InvariantCulture);
|
|
||||||
}
|
|
||||||
|
|
||||||
return reader.GetInt32();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
return reader.GetInt32();
|
||||||
public override void Write(Utf8JsonWriter writer, int? value, JsonSerializerOptions options)
|
}
|
||||||
{
|
|
||||||
if (value == null)
|
/// <inheritdoc />
|
||||||
writer.WriteNullValue();
|
public override void Write(Utf8JsonWriter writer, int? value, JsonSerializerOptions options)
|
||||||
else
|
{
|
||||||
writer.WriteNumberValue(value.Value);
|
if (value == null)
|
||||||
}
|
writer.WriteNullValue();
|
||||||
|
else
|
||||||
|
writer.WriteNumberValue(value.Value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)!;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,40 +1,39 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
namespace CryptoExchange.Net.Converters.SystemTextJson;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Int converter
|
||||||
|
/// </summary>
|
||||||
|
public class LongConverter : JsonConverter<long?>
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <inheritdoc />
|
||||||
/// Int converter
|
public override long? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
/// </summary>
|
|
||||||
public class LongConverter : JsonConverter<long?>
|
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
if (reader.TokenType == JsonTokenType.Null)
|
||||||
public override long? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
return null;
|
||||||
|
|
||||||
|
if (reader.TokenType == JsonTokenType.String)
|
||||||
{
|
{
|
||||||
if (reader.TokenType == JsonTokenType.Null)
|
var value = reader.GetString();
|
||||||
|
if (string.IsNullOrEmpty(value))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
if (reader.TokenType == JsonTokenType.String)
|
return long.Parse(value, NumberStyles.Integer, CultureInfo.InvariantCulture);
|
||||||
{
|
|
||||||
var value = reader.GetString();
|
|
||||||
if (string.IsNullOrEmpty(value))
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return long.Parse(value, NumberStyles.Integer, CultureInfo.InvariantCulture);
|
|
||||||
}
|
|
||||||
|
|
||||||
return reader.GetInt64();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
return reader.GetInt64();
|
||||||
public override void Write(Utf8JsonWriter writer, long? value, JsonSerializerOptions options)
|
}
|
||||||
{
|
|
||||||
if (value == null)
|
/// <inheritdoc />
|
||||||
writer.WriteNullValue();
|
public override void Write(Utf8JsonWriter writer, long? value, JsonSerializerOptions options)
|
||||||
else
|
{
|
||||||
writer.WriteNumberValue(value.Value);
|
if (value == null)
|
||||||
}
|
writer.WriteNullValue();
|
||||||
|
else
|
||||||
|
writer.WriteNumberValue(value.Value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
using System;
|
||||||
|
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,42 +1,41 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
namespace CryptoExchange.Net.Converters.SystemTextJson;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Read string or number as string
|
||||||
|
/// </summary>
|
||||||
|
public class NumberStringConverter : JsonConverter<string?>
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <inheritdoc />
|
||||||
/// Read string or number as string
|
public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
/// </summary>
|
|
||||||
public class NumberStringConverter : JsonConverter<string?>
|
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
if (reader.TokenType == JsonTokenType.Null)
|
||||||
public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
return null;
|
||||||
|
|
||||||
|
if (reader.TokenType == JsonTokenType.Number)
|
||||||
{
|
{
|
||||||
if (reader.TokenType == JsonTokenType.Null)
|
if (reader.TryGetInt64(out var value))
|
||||||
return null;
|
return value.ToString();
|
||||||
|
|
||||||
if (reader.TokenType == JsonTokenType.Number)
|
return reader.GetDecimal().ToString();
|
||||||
{
|
|
||||||
if (reader.TryGetInt64(out var value))
|
|
||||||
return value.ToString();
|
|
||||||
|
|
||||||
return reader.GetDecimal().ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return reader.GetString();
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
try
|
||||||
public override void Write(Utf8JsonWriter writer, string? value, JsonSerializerOptions options)
|
|
||||||
{
|
{
|
||||||
writer.WriteStringValue(value);
|
return reader.GetString();
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override void Write(Utf8JsonWriter writer, string? value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
writer.WriteStringValue(value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,35 +1,44 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
#if NET5_0_OR_GREATER
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
namespace CryptoExchange.Net.Converters.SystemTextJson;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converter for values which contain a nested json value
|
||||||
|
/// </summary>
|
||||||
|
public class ObjectStringConverter<T> : JsonConverter<T>
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <inheritdoc />
|
||||||
///
|
#if NET5_0_OR_GREATER
|
||||||
/// </summary>
|
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||||
/// <typeparam name="T"></typeparam>
|
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||||
public class ObjectStringConverter<T> : JsonConverter<T>
|
#endif
|
||||||
|
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
if (reader.TokenType == JsonTokenType.Null)
|
||||||
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
return default;
|
||||||
{
|
|
||||||
if (reader.TokenType == JsonTokenType.Null)
|
|
||||||
return default;
|
|
||||||
|
|
||||||
var value = reader.GetString();
|
var value = reader.GetString();
|
||||||
if (string.IsNullOrEmpty(value))
|
if (string.IsNullOrEmpty(value))
|
||||||
return default;
|
return default;
|
||||||
|
|
||||||
return (T?)JsonDocument.Parse(value!).Deserialize(typeof(T));
|
return JsonDocument.Parse(value!).Deserialize<T>(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options)
|
#if NET5_0_OR_GREATER
|
||||||
{
|
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||||
if (value is null)
|
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||||
writer.WriteStringValue("");
|
#endif
|
||||||
|
public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
if (value is null)
|
||||||
|
writer.WriteStringValue("");
|
||||||
|
|
||||||
writer.WriteStringValue(JsonSerializer.Serialize(value, options));
|
writer.WriteStringValue(JsonSerializer.Serialize(value, options));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,41 +1,40 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
namespace CryptoExchange.Net.Converters.SystemTextJson;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Replace a value on a string property
|
||||||
|
/// </summary>
|
||||||
|
public abstract class ReplaceConverter : JsonConverter<string>
|
||||||
{
|
{
|
||||||
|
private readonly (string ValueToReplace, string ValueToReplaceWith)[] _replacementSets;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Replace a value on a string property
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ReplaceConverter : JsonConverter<string>
|
public ReplaceConverter(params string[] replaceSets)
|
||||||
{
|
{
|
||||||
private readonly (string ValueToReplace, string ValueToReplaceWith)[] _replacementSets;
|
_replacementSets = replaceSets.Select(x =>
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
public ReplaceConverter(params string[] replaceSets)
|
|
||||||
{
|
{
|
||||||
_replacementSets = replaceSets.Select(x =>
|
var split = x.Split(["->"], StringSplitOptions.None);
|
||||||
{
|
if (split.Length != 2)
|
||||||
var split = x.Split(new string[] { "->" }, StringSplitOptions.None);
|
throw new ArgumentException("Invalid replacement config");
|
||||||
if (split.Length != 2)
|
return (split[0], split[1]);
|
||||||
throw new ArgumentException("Invalid replacement config");
|
}).ToArray();
|
||||||
return (split[0], split[1]);
|
|
||||||
}).ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
var value = reader.GetString();
|
|
||||||
foreach (var set in _replacementSets)
|
|
||||||
value = value?.Replace(set.ValueToReplace, set.ValueToReplaceWith);
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) => writer.WriteStringValue(value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
var value = reader.GetString();
|
||||||
|
foreach (var set in _replacementSets)
|
||||||
|
value = value?.Replace(set.ValueToReplace, set.ValueToReplaceWith);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) => writer.WriteStringValue(value);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
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,29 +1,46 @@
|
|||||||
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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Serializer options
|
||||||
|
/// </summary>
|
||||||
|
public static class SerializerOptions
|
||||||
{
|
{
|
||||||
|
private static readonly ConcurrentDictionary<JsonSerializerContext, JsonSerializerOptions> _cache = new ConcurrentDictionary<JsonSerializerContext, JsonSerializerOptions>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Serializer options
|
/// Get Json serializer settings which includes standard converters for DateTime, bool, enum and number types
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class SerializerOptions
|
public static JsonSerializerOptions WithConverters(JsonSerializerContext typeResolver, params JsonConverter[] additionalConverters)
|
||||||
{
|
{
|
||||||
/// <summary>
|
if (!_cache.TryGetValue(typeResolver, out var options))
|
||||||
/// Json serializer settings which includes the EnumConverter, DateTimeConverter, BoolConverter and DecimalConverter
|
|
||||||
/// </summary>
|
|
||||||
public static JsonSerializerOptions WithConverters { get; } = new JsonSerializerOptions
|
|
||||||
{
|
{
|
||||||
NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals,
|
options = new JsonSerializerOptions
|
||||||
PropertyNameCaseInsensitive = false,
|
{
|
||||||
Converters =
|
NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals,
|
||||||
{
|
PropertyNameCaseInsensitive = false,
|
||||||
new DateTimeConverter(),
|
Converters =
|
||||||
new EnumConverter(),
|
{
|
||||||
new BoolConverter(),
|
new DateTimeConverter(),
|
||||||
new DecimalConverter(),
|
new BoolConverter(),
|
||||||
new IntConverter(),
|
new DecimalConverter(),
|
||||||
new LongConverter()
|
new IntConverter(),
|
||||||
}
|
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,57 @@
|
|||||||
|
using CryptoExchange.Net.SharedApis;
|
||||||
|
using System;
|
||||||
|
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,43 @@
|
|||||||
|
using CryptoExchange.Net.SharedApis;
|
||||||
|
using System;
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,380 +1,376 @@
|
|||||||
using CryptoExchange.Net.Converters.MessageParsing;
|
using CryptoExchange.Net.Converters.MessageParsing;
|
||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces;
|
||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
#if NET5_0_OR_GREATER
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
#endif
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
namespace CryptoExchange.Net.Converters.SystemTextJson;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// System.Text.Json message accessor
|
||||||
|
/// </summary>
|
||||||
|
public abstract class SystemTextJsonMessageAccessor : IMessageAccessor
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// System.Text.Json message accessor
|
/// The JsonDocument loaded
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class SystemTextJsonMessageAccessor : IMessageAccessor
|
protected JsonDocument? _document;
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The JsonDocument loaded
|
|
||||||
/// </summary>
|
|
||||||
protected JsonDocument? _document;
|
|
||||||
|
|
||||||
private static JsonSerializerOptions _serializerOptions = SerializerOptions.WithConverters;
|
private readonly JsonSerializerOptions? _customSerializerOptions;
|
||||||
private 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; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public object? Underlying => throw new NotImplementedException();
|
public object? Underlying => throw new NotImplementedException();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
public SystemTextJsonMessageAccessor()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
public SystemTextJsonMessageAccessor(JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
_customSerializerOptions = options;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public CallResult<object> Deserialize(Type type, MessagePath? path = null)
|
|
||||||
{
|
|
||||||
if (!IsJson)
|
|
||||||
return new CallResult<object>(GetOriginalString());
|
|
||||||
|
|
||||||
if (_document == null)
|
|
||||||
throw new InvalidOperationException("No json document loaded");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var result = _document.Deserialize(type, _customSerializerOptions ?? _serializerOptions);
|
|
||||||
return new CallResult<object>(result!);
|
|
||||||
}
|
|
||||||
catch (JsonException ex)
|
|
||||||
{
|
|
||||||
var info = $"Deserialize JsonException: {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]"));
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
var info = $"Deserialize unknown Exception: {ex.Message}";
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
if (_document == null)
|
|
||||||
throw new InvalidOperationException("No json document loaded");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var result = _document.Deserialize<T>(_customSerializerOptions ?? _serializerOptions);
|
|
||||||
return new CallResult<T>(result!);
|
|
||||||
}
|
|
||||||
catch (JsonException ex)
|
|
||||||
{
|
|
||||||
var info = $"Deserialize JsonException: {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]"));
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
var info = $"Unknown exception: {ex.Message}";
|
|
||||||
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 (_document == null)
|
|
||||||
throw new InvalidOperationException("No json document loaded");
|
|
||||||
|
|
||||||
return _document.RootElement.ValueKind switch
|
|
||||||
{
|
|
||||||
JsonValueKind.Object => NodeType.Object,
|
|
||||||
JsonValueKind.Array => NodeType.Array,
|
|
||||||
_ => 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.HasValue)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return node.Value.ValueKind switch
|
|
||||||
{
|
|
||||||
JsonValueKind.Object => NodeType.Object,
|
|
||||||
JsonValueKind.Array => NodeType.Array,
|
|
||||||
_ => 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.Value.ValueKind == JsonValueKind.Object || value.Value.ValueKind == JsonValueKind.Array)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return value.Value.Deserialize<T>(_customSerializerOptions ?? _serializerOptions);
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof(T) == typeof(string))
|
|
||||||
{
|
|
||||||
if (value.Value.ValueKind == JsonValueKind.Number)
|
|
||||||
return (T)(object)value.Value.GetInt64().ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
return value.Value.Deserialize<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.Value.ValueKind != JsonValueKind.Array)
|
|
||||||
return default;
|
|
||||||
|
|
||||||
return value.Value.Deserialize<List<T>>()!;
|
|
||||||
}
|
|
||||||
|
|
||||||
private JsonElement? GetPathNode(MessagePath path)
|
|
||||||
{
|
|
||||||
if (!IsJson)
|
|
||||||
throw new InvalidOperationException("Can't access json data on non-json message");
|
|
||||||
|
|
||||||
if (_document == null)
|
|
||||||
throw new InvalidOperationException("No json document loaded");
|
|
||||||
|
|
||||||
JsonElement? currentToken = _document.RootElement;
|
|
||||||
foreach (var node in path)
|
|
||||||
{
|
|
||||||
if (node.Type == 0)
|
|
||||||
{
|
|
||||||
// Int value
|
|
||||||
var val = node.Index!.Value;
|
|
||||||
if (currentToken!.Value.ValueKind != JsonValueKind.Array || currentToken.Value.GetArrayLength() <= val)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
currentToken = currentToken.Value[val];
|
|
||||||
}
|
|
||||||
else if (node.Type == 1)
|
|
||||||
{
|
|
||||||
// String value
|
|
||||||
if (currentToken!.Value.ValueKind != JsonValueKind.Object)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
if (!currentToken.Value.TryGetProperty(node.Property!, out var token))
|
|
||||||
return null;
|
|
||||||
currentToken = token;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Property name
|
|
||||||
if (currentToken!.Value.ValueKind != JsonValueKind.Object)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentToken == null)
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return currentToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public abstract string GetOriginalString();
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public abstract void Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// System.Text.Json stream message accessor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class SystemTextJsonStreamMessageAccessor : SystemTextJsonMessageAccessor, IStreamMessageAccessor
|
public SystemTextJsonMessageAccessor(JsonSerializerOptions options)
|
||||||
{
|
{
|
||||||
private Stream? _stream;
|
_customSerializerOptions = options;
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override bool OriginalDataAvailable => _stream?.CanSeek == true;
|
#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)
|
||||||
|
{
|
||||||
|
if (!IsValid)
|
||||||
|
return new CallResult<object>(GetOriginalString());
|
||||||
|
|
||||||
/// <summary>
|
if (_document == null)
|
||||||
/// ctor
|
throw new InvalidOperationException("No json document loaded");
|
||||||
/// </summary>
|
|
||||||
public SystemTextJsonStreamMessageAccessor(): base()
|
try
|
||||||
{
|
{
|
||||||
|
var result = _document.Deserialize(type, _customSerializerOptions);
|
||||||
|
return new CallResult<object>(result!);
|
||||||
}
|
}
|
||||||
|
catch (JsonException ex)
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
public SystemTextJsonStreamMessageAccessor(JsonSerializerOptions options): base(options)
|
|
||||||
{
|
{
|
||||||
|
var info = $"Json deserialization failed: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}";
|
||||||
|
return new CallResult<object>(new DeserializeError(info, ex));
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
/// <inheritdoc />
|
|
||||||
public async Task<CallResult> Read(Stream stream, bool bufferStream)
|
|
||||||
{
|
{
|
||||||
if (bufferStream && stream is not MemoryStream)
|
return new CallResult<object>(new DeserializeError($"Json deserialization failed: {ex.Message}", ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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)
|
||||||
|
{
|
||||||
|
if (_document == null)
|
||||||
|
throw new InvalidOperationException("No json document loaded");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = _document.Deserialize<T>(_customSerializerOptions);
|
||||||
|
return new CallResult<T>(result!);
|
||||||
|
}
|
||||||
|
catch (JsonException ex)
|
||||||
|
{
|
||||||
|
var info = $"Json deserialization failed: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}";
|
||||||
|
return new CallResult<T>(new DeserializeError(info, ex));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return new CallResult<T>(new DeserializeError($"Json deserialization failed: {ex.Message}", ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public NodeType? GetNodeType()
|
||||||
|
{
|
||||||
|
if (!IsValid)
|
||||||
|
throw new InvalidOperationException("Can't access json data on non-json message");
|
||||||
|
|
||||||
|
if (_document == null)
|
||||||
|
throw new InvalidOperationException("No json document loaded");
|
||||||
|
|
||||||
|
return _document.RootElement.ValueKind switch
|
||||||
|
{
|
||||||
|
JsonValueKind.Object => NodeType.Object,
|
||||||
|
JsonValueKind.Array => NodeType.Array,
|
||||||
|
_ => NodeType.Value
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public NodeType? GetNodeType(MessagePath path)
|
||||||
|
{
|
||||||
|
if (!IsValid)
|
||||||
|
throw new InvalidOperationException("Can't access json data on non-json message");
|
||||||
|
|
||||||
|
var node = GetPathNode(path);
|
||||||
|
if (!node.HasValue)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return node.Value.ValueKind switch
|
||||||
|
{
|
||||||
|
JsonValueKind.Object => NodeType.Object,
|
||||||
|
JsonValueKind.Array => NodeType.Array,
|
||||||
|
_ => NodeType.Value
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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)
|
||||||
|
{
|
||||||
|
if (!IsValid)
|
||||||
|
throw new InvalidOperationException("Can't access json data on non-json message");
|
||||||
|
|
||||||
|
var value = GetPathNode(path);
|
||||||
|
if (value == null)
|
||||||
|
return default;
|
||||||
|
|
||||||
|
if (value.Value.ValueKind == JsonValueKind.Object || value.Value.ValueKind == JsonValueKind.Array)
|
||||||
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
// We need to be buffer the stream, and it's not currently a seekable stream, so copy it to a new memory stream
|
return value.Value.Deserialize<T>(_customSerializerOptions);
|
||||||
_stream = new MemoryStream();
|
|
||||||
stream.CopyTo(_stream);
|
|
||||||
_stream.Position = 0;
|
|
||||||
}
|
}
|
||||||
else if (bufferStream)
|
catch { }
|
||||||
|
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof(T) == typeof(string))
|
||||||
|
{
|
||||||
|
if (value.Value.ValueKind == JsonValueKind.Number)
|
||||||
|
return (T)(object)value.Value.GetInt64().ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.Value.Deserialize<T>(_customSerializerOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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?[]? GetValues<T>(MessagePath path)
|
||||||
|
{
|
||||||
|
if (!IsValid)
|
||||||
|
throw new InvalidOperationException("Can't access json data on non-json message");
|
||||||
|
|
||||||
|
var value = GetPathNode(path);
|
||||||
|
if (value == null)
|
||||||
|
return default;
|
||||||
|
|
||||||
|
if (value.Value.ValueKind != JsonValueKind.Array)
|
||||||
|
return default;
|
||||||
|
|
||||||
|
return value.Value.Deserialize<T[]>(_customSerializerOptions)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JsonElement? GetPathNode(MessagePath path)
|
||||||
|
{
|
||||||
|
if (!IsValid)
|
||||||
|
throw new InvalidOperationException("Can't access json data on non-json message");
|
||||||
|
|
||||||
|
if (_document == null)
|
||||||
|
throw new InvalidOperationException("No json document loaded");
|
||||||
|
|
||||||
|
JsonElement? currentToken = _document.RootElement;
|
||||||
|
foreach (var node in path)
|
||||||
|
{
|
||||||
|
if (node.Type == 0)
|
||||||
{
|
{
|
||||||
// We need to buffer the stream, and the current stream is seekable, store as is
|
// Int value
|
||||||
_stream = stream;
|
var val = node.Index!.Value;
|
||||||
|
if (currentToken!.Value.ValueKind != JsonValueKind.Array || currentToken.Value.GetArrayLength() <= val)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
currentToken = currentToken.Value[val];
|
||||||
|
}
|
||||||
|
else if (node.Type == 1)
|
||||||
|
{
|
||||||
|
// String value
|
||||||
|
if (currentToken!.Value.ValueKind != JsonValueKind.Object)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (!currentToken.Value.TryGetProperty(node.Property!, out var token))
|
||||||
|
return null;
|
||||||
|
currentToken = token;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// We don't need to buffer the stream, so don't bother keeping the reference
|
// Property name
|
||||||
|
if (currentToken!.Value.ValueKind != JsonValueKind.Object)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
if (currentToken == null)
|
||||||
{
|
return null;
|
||||||
_document = await JsonDocument.ParseAsync(_stream ?? stream).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;
|
|
||||||
_document?.Dispose();
|
|
||||||
_document = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return currentToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public abstract string GetOriginalString();
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public abstract void Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// System.Text.Json stream message accessor
|
||||||
|
/// </summary>
|
||||||
|
#pragma warning disable CA1001 // Types that own disposable fields should be disposable
|
||||||
|
public class SystemTextJsonStreamMessageAccessor : SystemTextJsonMessageAccessor, IStreamMessageAccessor
|
||||||
|
#pragma warning restore CA1001 // Types that own disposable fields should be disposable
|
||||||
|
{
|
||||||
|
private Stream? _stream;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override bool OriginalDataAvailable => _stream?.CanSeek == true;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// System.Text.Json byte message accessor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class SystemTextJsonByteMessageAccessor : SystemTextJsonMessageAccessor, IByteMessageAccessor
|
public SystemTextJsonStreamMessageAccessor(JsonSerializerOptions options): base(options)
|
||||||
{
|
{
|
||||||
private ReadOnlyMemory<byte> _bytes;
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <inheritdoc />
|
||||||
/// ctor
|
public async Task<CallResult> Read(Stream stream, bool bufferStream)
|
||||||
/// </summary>
|
{
|
||||||
public SystemTextJsonByteMessageAccessor() : base()
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
try
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
public SystemTextJsonByteMessageAccessor(JsonSerializerOptions options) : base(options)
|
|
||||||
{
|
{
|
||||||
|
_document = await JsonDocument.ParseAsync(_stream ?? stream).ConfigureAwait(false);
|
||||||
|
IsValid = true;
|
||||||
|
return CallResult.SuccessResult;
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
/// <inheritdoc />
|
|
||||||
public CallResult Read(ReadOnlyMemory<byte> data)
|
|
||||||
{
|
{
|
||||||
_bytes = data;
|
// Not a json message
|
||||||
|
IsValid = false;
|
||||||
try
|
return new CallResult(new DeserializeError($"Json deserialization failed: {ex.Message}", ex));
|
||||||
{
|
|
||||||
var firstByte = data.Span[0];
|
|
||||||
if (firstByte != 0x7b && firstByte != 0x5b)
|
|
||||||
{
|
|
||||||
// Value doesn't start with `{` or `[`, prevent deserialization attempt as it's slow
|
|
||||||
IsJson = false;
|
|
||||||
return new CallResult(new ServerError("Not a json value"));
|
|
||||||
}
|
|
||||||
|
|
||||||
_document = JsonDocument.Parse(data);
|
|
||||||
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;
|
|
||||||
_document?.Dispose();
|
|
||||||
_document = null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
/// <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;
|
||||||
|
_document?.Dispose();
|
||||||
|
_document = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// System.Text.Json byte message accessor
|
||||||
|
/// </summary>
|
||||||
|
public class SystemTextJsonByteMessageAccessor : SystemTextJsonMessageAccessor, IByteMessageAccessor
|
||||||
|
{
|
||||||
|
private ReadOnlyMemory<byte> _bytes;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
public SystemTextJsonByteMessageAccessor(JsonSerializerOptions options) : base(options)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public CallResult Read(ReadOnlyMemory<byte> data)
|
||||||
|
{
|
||||||
|
_bytes = data;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var firstByte = data.Span[0];
|
||||||
|
if (firstByte != 0x7b && firstByte != 0x5b)
|
||||||
|
{
|
||||||
|
// Value doesn't start with `{` or `[`, prevent deserialization attempt as it's slow
|
||||||
|
IsValid = false;
|
||||||
|
return new CallResult(new DeserializeError("Not a json value"));
|
||||||
|
}
|
||||||
|
|
||||||
|
_document = JsonDocument.Parse(data);
|
||||||
|
IsValid = true;
|
||||||
|
return CallResult.SuccessResult;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// Not a json message
|
||||||
|
IsValid = false;
|
||||||
|
return new CallResult(new DeserializeError($"Json 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;
|
||||||
|
_document?.Dispose();
|
||||||
|
_document = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,12 +1,28 @@
|
|||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces;
|
||||||
|
#if NET5_0_OR_GREATER
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
#endif
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
namespace CryptoExchange.Net.Converters.SystemTextJson;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public class SystemTextJsonMessageSerializer : IStringMessageSerializer
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
private readonly JsonSerializerOptions _options;
|
||||||
public class SystemTextJsonMessageSerializer : IMessageSerializer
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
public SystemTextJsonMessageSerializer(JsonSerializerOptions options)
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
_options = options;
|
||||||
public string Serialize(object message) => JsonSerializer.Serialize(message, SerializerOptions.WithConverters);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
#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.6.0</PackageVersion>
|
<PackageVersion>9.6.0</PackageVersion>
|
||||||
<AssemblyVersion>8.6.0</AssemblyVersion>
|
<AssemblyVersion>9.6.0</AssemblyVersion>
|
||||||
<FileVersion>8.6.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>
|
||||||
@@ -24,9 +24,13 @@
|
|||||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<None Include="C:\Projects\CryptoExchange.Net\CryptoExchange.Net\.editorconfig" />
|
||||||
<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,15 +38,15 @@
|
|||||||
<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>
|
||||||
|
<PropertyGroup>
|
||||||
|
<EnableNETAnalyzers>true</EnableNETAnalyzers>
|
||||||
|
<AnalysisMode>Recommended</AnalysisMode>
|
||||||
|
<AnalysisModeGlobalization>None</AnalysisModeGlobalization>
|
||||||
|
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
|
||||||
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ConfigureAwaitChecker.Analyzer" Version="5.0.0.1">
|
<PackageReference Include="ConfigureAwaitChecker.Analyzer" Version="5.0.0.1">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
@@ -52,13 +56,16 @@
|
|||||||
<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>
|
||||||
|
<EditorConfigFiles Remove="C:\Projects\CryptoExchange.Net\CryptoExchange.Net\.editorconfig" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@@ -1,277 +1,390 @@
|
|||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
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;
|
||||||
|
#if NETSTANDARD2_1_OR_GREATER || NET9_0_OR_GREATER
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
|
#endif
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace CryptoExchange.Net
|
namespace CryptoExchange.Net;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// General helpers functions
|
||||||
|
/// </summary>
|
||||||
|
public static class ExchangeHelpers
|
||||||
{
|
{
|
||||||
/// <summary>
|
private const string _allowedRandomChars = "ABCDEFGHIJKLMONOPQRSTUVWXYZabcdefghijklmonopqrstuvwxyz0123456789";
|
||||||
/// General helpers functions
|
private const string _allowedRandomHexChars = "0123456789ABCDEF";
|
||||||
/// </summary>
|
|
||||||
public static class ExchangeHelpers
|
private static readonly Dictionary<int, string> _monthSymbols = new Dictionary<int, string>()
|
||||||
{
|
{
|
||||||
private const string _allowedRandomChars = "ABCDEFGHIJKLMONOPQRSTUVWXYZabcdefghijklmonopqrstuvwxyz0123456789";
|
{ 1, "F" },
|
||||||
|
{ 2, "G" },
|
||||||
|
{ 3, "H" },
|
||||||
|
{ 4, "J" },
|
||||||
|
{ 5, "K" },
|
||||||
|
{ 6, "M" },
|
||||||
|
{ 7, "N" },
|
||||||
|
{ 8, "Q" },
|
||||||
|
{ 9, "U" },
|
||||||
|
{ 10, "V" },
|
||||||
|
{ 11, "X" },
|
||||||
|
{ 12, "Z" },
|
||||||
|
};
|
||||||
|
|
||||||
private static readonly Dictionary<int, string> _monthSymbols = new Dictionary<int, string>()
|
/// <summary>
|
||||||
{
|
/// The last used id, use NextId() to get the next id and up this
|
||||||
{ 1, "F" },
|
/// </summary>
|
||||||
{ 2, "G" },
|
private static int _lastId;
|
||||||
{ 3, "H" },
|
|
||||||
{ 4, "J" },
|
|
||||||
{ 5, "K" },
|
|
||||||
{ 6, "M" },
|
|
||||||
{ 7, "N" },
|
|
||||||
{ 8, "Q" },
|
|
||||||
{ 9, "U" },
|
|
||||||
{ 10, "V" },
|
|
||||||
{ 11, "X" },
|
|
||||||
{ 12, "Z" },
|
|
||||||
};
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The last used id, use NextId() to get the next id and up this
|
/// Clamp a value between a min and max
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static int _lastId;
|
/// <param name="min"></param>
|
||||||
|
/// <param name="max"></param>
|
||||||
|
/// <param name="value"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static decimal ClampValue(decimal min, decimal max, decimal value)
|
||||||
|
{
|
||||||
|
value = Math.Min(max, value);
|
||||||
|
value = Math.Max(min, value);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Clamp a value between a min and max
|
/// Adjust a value to be between the min and max parameters and rounded to the closest step.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="min"></param>
|
/// <param name="min">The min value</param>
|
||||||
/// <param name="max"></param>
|
/// <param name="max">The max value</param>
|
||||||
/// <param name="value"></param>
|
/// <param name="step">The step size the value should be floored to. For example, value 2.548 with a step size of 0.01 will output 2.54</param>
|
||||||
/// <returns></returns>
|
/// <param name="roundingType">How to round</param>
|
||||||
public static decimal ClampValue(decimal min, decimal max, decimal value)
|
/// <param name="value">The input value</param>
|
||||||
{
|
/// <returns></returns>
|
||||||
value = Math.Min(max, value);
|
public static decimal AdjustValueStep(decimal min, decimal max, decimal? step, RoundingType roundingType, decimal value)
|
||||||
value = Math.Max(min, value);
|
{
|
||||||
|
if(step == 0)
|
||||||
|
throw new ArgumentException($"0 not allowed for parameter {nameof(step)}, pass in null to ignore the step size", nameof(step));
|
||||||
|
|
||||||
|
value = Math.Min(max, value);
|
||||||
|
value = Math.Max(min, value);
|
||||||
|
if (step == null)
|
||||||
return value;
|
return value;
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
var offset = value % step.Value;
|
||||||
/// Adjust a value to be between the min and max parameters and rounded to the closest step.
|
if(roundingType == RoundingType.Down)
|
||||||
/// </summary>
|
|
||||||
/// <param name="min">The min value</param>
|
|
||||||
/// <param name="max">The max value</param>
|
|
||||||
/// <param name="step">The step size the value should be floored to. For example, value 2.548 with a step size of 0.01 will output 2.54</param>
|
|
||||||
/// <param name="roundingType">How to round</param>
|
|
||||||
/// <param name="value">The input value</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static decimal AdjustValueStep(decimal min, decimal max, decimal? step, RoundingType roundingType, decimal value)
|
|
||||||
{
|
{
|
||||||
if(step == 0)
|
value -= offset;
|
||||||
throw new ArgumentException($"0 not allowed for parameter {nameof(step)}, pass in null to ignore the step size", nameof(step));
|
}
|
||||||
|
else if(roundingType == RoundingType.Up)
|
||||||
value = Math.Min(max, value);
|
{
|
||||||
value = Math.Max(min, value);
|
if (offset != 0)
|
||||||
if (step == null)
|
value += (step.Value - offset);
|
||||||
return value;
|
}
|
||||||
|
else
|
||||||
var offset = value % step.Value;
|
{
|
||||||
if(roundingType == RoundingType.Down)
|
if (offset < step / 2)
|
||||||
{
|
|
||||||
value -= offset;
|
value -= offset;
|
||||||
}
|
else value += (step.Value - offset);
|
||||||
else if(roundingType == RoundingType.Up)
|
}
|
||||||
|
|
||||||
|
value = RoundDown(value, 8);
|
||||||
|
|
||||||
|
return value.Normalize();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adjust a value to be between the min and max parameters and rounded to the closest precision.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="min">The min value</param>
|
||||||
|
/// <param name="max">The max value</param>
|
||||||
|
/// <param name="precision">The precision the value should be rounded to. For example, value 2.554215 with a precision of 5 will output 2.5542</param>
|
||||||
|
/// <param name="roundingType">How to round</param>
|
||||||
|
/// <param name="value">The input value</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static decimal AdjustValuePrecision(decimal min, decimal max, int? precision, RoundingType roundingType, decimal value)
|
||||||
|
{
|
||||||
|
value = Math.Min(max, value);
|
||||||
|
value = Math.Max(min, value);
|
||||||
|
if (precision == null)
|
||||||
|
return value;
|
||||||
|
|
||||||
|
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 != 0)
|
if (offset < valueStep.Value / 2)
|
||||||
value += (step.Value - offset);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (offset < step / 2)
|
|
||||||
value -= offset;
|
value -= offset;
|
||||||
else value += (step.Value - offset);
|
else value += (valueStep.Value - offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
value = RoundDown(value, 8);
|
|
||||||
|
|
||||||
return value.Normalize();
|
|
||||||
}
|
}
|
||||||
|
if (decimals.HasValue)
|
||||||
|
value = Math.Round(value, decimals.Value);
|
||||||
|
|
||||||
/// <summary>
|
return value;
|
||||||
/// Adjust a value to be between the min and max parameters and rounded to the closest precision.
|
}
|
||||||
/// </summary>
|
|
||||||
/// <param name="min">The min value</param>
|
|
||||||
/// <param name="max">The max value</param>
|
|
||||||
/// <param name="precision">The precision the value should be rounded to. For example, value 2.554215 with a precision of 5 will output 2.5542</param>
|
|
||||||
/// <param name="roundingType">How to round</param>
|
|
||||||
/// <param name="value">The input value</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static decimal AdjustValuePrecision(decimal min, decimal max, int? precision, RoundingType roundingType, decimal value)
|
|
||||||
{
|
|
||||||
value = Math.Min(max, value);
|
|
||||||
value = Math.Max(min, value);
|
|
||||||
if (precision == null)
|
|
||||||
return value;
|
|
||||||
|
|
||||||
return RoundToSignificantDigits(value, precision.Value, roundingType);
|
/// <summary>
|
||||||
}
|
/// Round a value to have the provided total number of digits. For example, value 253.12332 with 5 digits would be 253.12
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">The value to round</param>
|
||||||
|
/// <param name="digits">The total amount of digits (NOT decimal places) to round to</param>
|
||||||
|
/// <param name="roundingType">How to round</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static decimal RoundToSignificantDigits(decimal value, int digits, RoundingType roundingType)
|
||||||
|
{
|
||||||
|
var val = (double)value;
|
||||||
|
if (value == 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
/// <summary>
|
double scale = Math.Pow(10, Math.Floor(Math.Log10(Math.Abs(val))) + 1);
|
||||||
/// Round a value to have the provided total number of digits. For example, value 253.12332 with 5 digits would be 253.12
|
if(roundingType == RoundingType.Closest)
|
||||||
/// </summary>
|
return (decimal)(scale * Math.Round(val / scale, digits));
|
||||||
/// <param name="value">The value to round</param>
|
else
|
||||||
/// <param name="digits">The total amount of digits (NOT decimal places) to round to</param>
|
return (decimal)(scale * (double)RoundDown((decimal)(val / scale), digits));
|
||||||
/// <param name="roundingType">How to round</param>
|
}
|
||||||
/// <returns></returns>
|
|
||||||
public static decimal RoundToSignificantDigits(decimal value, int digits, RoundingType roundingType)
|
|
||||||
{
|
|
||||||
var val = (double)value;
|
|
||||||
if (value == 0)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
double scale = Math.Pow(10, Math.Floor(Math.Log10(Math.Abs(val))) + 1);
|
/// <summary>
|
||||||
if(roundingType == RoundingType.Closest)
|
/// Rounds a value down
|
||||||
return (decimal)(scale * Math.Round(val / scale, digits));
|
/// </summary>
|
||||||
else
|
public static decimal RoundDown(decimal i, double decimalPlaces)
|
||||||
return (decimal)(scale * (double)RoundDown((decimal)(val / scale), digits));
|
{
|
||||||
}
|
var power = Convert.ToDecimal(Math.Pow(10, decimalPlaces));
|
||||||
|
return Math.Floor(i * power) / power;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Rounds a value down
|
/// Rounds a value up
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static decimal RoundDown(decimal i, double decimalPlaces)
|
public static decimal RoundUp(decimal i, double decimalPlaces)
|
||||||
{
|
{
|
||||||
var power = Convert.ToDecimal(Math.Pow(10, decimalPlaces));
|
var power = Convert.ToDecimal(Math.Pow(10, decimalPlaces));
|
||||||
return Math.Floor(i * power) / power;
|
return Math.Ceiling(i * power) / power;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Rounds a value up
|
/// Strips any trailing zero's of a decimal value, useful when converting the value to string.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static decimal RoundUp(decimal i, double decimalPlaces)
|
/// <param name="value"></param>
|
||||||
{
|
/// <returns></returns>
|
||||||
var power = Convert.ToDecimal(Math.Pow(10, decimalPlaces));
|
public static decimal Normalize(this decimal value)
|
||||||
return Math.Ceiling(i * power) / power;
|
{
|
||||||
}
|
return value / 1.000000000000000000000000000000000m;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Strips any trailing zero's of a decimal value, useful when converting the value to string.
|
/// Generate a new unique id. The id is statically stored so it is guaranteed to be unique
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="value"></param>
|
/// <returns></returns>
|
||||||
/// <returns></returns>
|
public static int NextId() => Interlocked.Increment(ref _lastId);
|
||||||
public static decimal Normalize(this decimal value)
|
|
||||||
{
|
|
||||||
return value / 1.000000000000000000000000000000000m;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Generate a new unique id. The id is staticly stored so it is guarenteed to be unique
|
/// Return the last unique id that was generated
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static int NextId() => Interlocked.Increment(ref _lastId);
|
public static int LastId() => _lastId;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Return the last unique id that was generated
|
/// Generate a random string of specified length
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <param name="length">Length of the random string</param>
|
||||||
public static int LastId() => _lastId;
|
/// <returns></returns>
|
||||||
|
public static string RandomString(int length)
|
||||||
/// <summary>
|
{
|
||||||
/// Generate a random string of specified length
|
var randomChars = new char[length];
|
||||||
/// </summary>
|
|
||||||
/// <param name="length">Length of the random string</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string RandomString(int length)
|
|
||||||
{
|
|
||||||
var randomChars = new char[length];
|
|
||||||
|
|
||||||
#if NETSTANDARD2_1_OR_GREATER || NET9_0_OR_GREATER
|
#if NETSTANDARD2_1_OR_GREATER || NET9_0_OR_GREATER
|
||||||
for (int i = 0; i < length; i++)
|
for (int i = 0; i < length; i++)
|
||||||
randomChars[i] = _allowedRandomChars[RandomNumberGenerator.GetInt32(0, _allowedRandomChars.Length)];
|
randomChars[i] = _allowedRandomChars[RandomNumberGenerator.GetInt32(0, _allowedRandomChars.Length)];
|
||||||
#else
|
#else
|
||||||
var random = new Random();
|
var random = new Random();
|
||||||
for (int i = 0; i < length; i++)
|
for (int i = 0; i < length; i++)
|
||||||
randomChars[i] = _allowedRandomChars[random.Next(0, _allowedRandomChars.Length)];
|
randomChars[i] = _allowedRandomChars[random.Next(0, _allowedRandomChars.Length)];
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
return new string(randomChars);
|
return new string(randomChars);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Generate a random string of specified length
|
/// Generate a random string of specified length
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="source">The initial string</param>
|
/// <param name="length">Length of the random string</param>
|
||||||
/// <param name="totalLength">Total length of the resulting string</param>
|
/// <returns></returns>
|
||||||
/// <returns></returns>
|
public static string RandomHexString(int length)
|
||||||
public static string AppendRandomString(string source, int totalLength)
|
{
|
||||||
|
#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>
|
||||||
|
/// Generate a random string of specified length
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="source">The initial string</param>
|
||||||
|
/// <param name="totalLength">Total length of the resulting string</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string AppendRandomString(string source, int totalLength)
|
||||||
|
{
|
||||||
|
if (totalLength < source.Length)
|
||||||
|
throw new ArgumentException("Total length smaller than source string length", nameof(totalLength));
|
||||||
|
|
||||||
|
if (totalLength == source.Length)
|
||||||
|
return source;
|
||||||
|
|
||||||
|
return source + RandomString(totalLength - source.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the month representation for futures symbol based on the delivery month
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="time">Delivery time</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string GetDeliveryMonthSymbol(DateTime time) => _monthSymbols[time.Month];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Execute multiple requests to retrieve multiple pages of the result set
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TResult">Type of the client</typeparam>
|
||||||
|
/// <typeparam name="TRequest">Type of the request</typeparam>
|
||||||
|
/// <param name="paginatedFunc">The func to execute with each request</param>
|
||||||
|
/// <param name="request">The request parameters</param>
|
||||||
|
/// <param name="ct">Cancellation token</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static async IAsyncEnumerable<ExchangeWebResult<TResult[]>> ExecutePages<TResult, TRequest>(Func<TRequest, INextPageToken?, CancellationToken, Task<ExchangeWebResult<TResult[]>>> paginatedFunc, TRequest request, [EnumeratorCancellation]CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var result = new List<TResult>();
|
||||||
|
ExchangeWebResult<TResult[]> batch;
|
||||||
|
INextPageToken? nextPageToken = null;
|
||||||
|
while (true)
|
||||||
{
|
{
|
||||||
if (totalLength < source.Length)
|
batch = await paginatedFunc(request, nextPageToken, ct).ConfigureAwait(false);
|
||||||
throw new ArgumentException("Total length smaller than source string length", nameof(totalLength));
|
yield return batch;
|
||||||
|
if (!batch || ct.IsCancellationRequested)
|
||||||
if (totalLength == source.Length)
|
break;
|
||||||
return source;
|
|
||||||
|
|
||||||
return source + RandomString(totalLength - source.Length);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get the month representation for futures symbol based on the delivery month
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="time">Delivery time</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string GetDeliveryMonthSymbol(DateTime time) => _monthSymbols[time.Month];
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Execute multiple requests to retrieve multiple pages of the result set
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T">Type of the client</typeparam>
|
|
||||||
/// <typeparam name="U">Type of the request</typeparam>
|
|
||||||
/// <param name="paginatedFunc">The func to execute with each request</param>
|
|
||||||
/// <param name="request">The request parameters</param>
|
|
||||||
/// <param name="ct">Cancellation token</param>
|
|
||||||
/// <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)
|
|
||||||
{
|
|
||||||
var result = new List<T>();
|
|
||||||
ExchangeWebResult<IEnumerable<T>> batch;
|
|
||||||
INextPageToken? nextPageToken = null;
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
batch = await paginatedFunc(request, nextPageToken, ct).ConfigureAwait(false);
|
|
||||||
yield return batch;
|
|
||||||
if (!batch || ct.IsCancellationRequested)
|
|
||||||
break;
|
|
||||||
|
|
||||||
result.AddRange(batch.Data);
|
|
||||||
nextPageToken = batch.NextPageToken;
|
|
||||||
if (nextPageToken == null)
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Apply the rules (price and quantity step size and decimals precision, min/max quantity) from the symbol to the quantity and price
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="symbol">The symbol as retrieved from the exchange</param>
|
|
||||||
/// <param name="quantity">Quantity to trade</param>
|
|
||||||
/// <param name="price">Price to trade at</param>
|
|
||||||
/// <param name="adjustedQuantity">Quantity adjusted to match all trading rules</param>
|
|
||||||
/// <param name="adjustedPrice">Price adjusted to match all trading rules</param>
|
|
||||||
public static void ApplySymbolRules(SharedSpotSymbol symbol, decimal quantity, decimal? price, out decimal adjustedQuantity, out decimal? adjustedPrice)
|
|
||||||
{
|
|
||||||
adjustedPrice = price;
|
|
||||||
adjustedQuantity = quantity;
|
|
||||||
var minNotionalAdjust = false;
|
|
||||||
|
|
||||||
if (price != null)
|
|
||||||
{
|
|
||||||
adjustedPrice = AdjustValueStep(0, decimal.MaxValue, symbol.PriceStep, RoundingType.Down, price.Value);
|
|
||||||
adjustedPrice = symbol.PriceDecimals.HasValue ? RoundDown(price.Value, symbol.PriceDecimals.Value) : adjustedPrice;
|
|
||||||
if (adjustedPrice != 0 && adjustedPrice * quantity < symbol.MinNotionalValue)
|
|
||||||
{
|
|
||||||
adjustedQuantity = symbol.MinNotionalValue.Value / adjustedPrice.Value;
|
|
||||||
minNotionalAdjust = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
adjustedQuantity = AdjustValueStep(symbol.MinTradeQuantity ?? 0, symbol.MaxTradeQuantity ?? decimal.MaxValue, symbol.QuantityStep, minNotionalAdjust ? RoundingType.Up : RoundingType.Down, adjustedQuantity);
|
|
||||||
adjustedQuantity = symbol.QuantityDecimals.HasValue ? (minNotionalAdjust ? RoundUp(adjustedQuantity, symbol.QuantityDecimals.Value) : RoundDown(adjustedQuantity, symbol.QuantityDecimals.Value)) : adjustedQuantity;
|
|
||||||
|
|
||||||
|
result.AddRange(batch.Data);
|
||||||
|
nextPageToken = batch.NextPageToken;
|
||||||
|
if (nextPageToken == null)
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Apply the rules (price and quantity step size and decimals precision, min/max quantity) from the symbol to the quantity and price
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="symbol">The symbol as retrieved from the exchange</param>
|
||||||
|
/// <param name="quantity">Quantity to trade</param>
|
||||||
|
/// <param name="price">Price to trade at</param>
|
||||||
|
/// <param name="adjustedQuantity">Quantity adjusted to match all trading rules</param>
|
||||||
|
/// <param name="adjustedPrice">Price adjusted to match all trading rules</param>
|
||||||
|
public static void ApplySymbolRules(SharedSpotSymbol symbol, decimal quantity, decimal? price, out decimal adjustedQuantity, out decimal? adjustedPrice)
|
||||||
|
{
|
||||||
|
adjustedPrice = price;
|
||||||
|
adjustedQuantity = quantity;
|
||||||
|
var minNotionalAdjust = false;
|
||||||
|
|
||||||
|
if (price != null)
|
||||||
|
{
|
||||||
|
adjustedPrice = AdjustValueStep(0, decimal.MaxValue, symbol.PriceStep, RoundingType.Down, price.Value);
|
||||||
|
adjustedPrice = symbol.PriceSignificantFigures.HasValue ? RoundToSignificantDigits(adjustedPrice.Value, symbol.PriceSignificantFigures.Value, RoundingType.Closest) : adjustedPrice;
|
||||||
|
adjustedPrice = symbol.PriceDecimals.HasValue ? RoundDown(price.Value, symbol.PriceDecimals.Value) : adjustedPrice;
|
||||||
|
if (adjustedPrice != 0 && adjustedPrice * quantity < symbol.MinNotionalValue)
|
||||||
|
{
|
||||||
|
adjustedQuantity = symbol.MinNotionalValue.Value / adjustedPrice.Value;
|
||||||
|
minNotionalAdjust = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
adjustedQuantity = AdjustValueStep(symbol.MinTradeQuantity ?? 0, symbol.MaxTradeQuantity ?? decimal.MaxValue, symbol.QuantityStep, minNotionalAdjust ? RoundingType.Up : RoundingType.Down, 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,68 @@
|
|||||||
|
using CryptoExchange.Net.SharedApis;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO.Compression;
|
using System.IO.Compression;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
@@ -11,496 +11,510 @@ using System.Globalization;
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using CryptoExchange.Net.SharedApis;
|
using CryptoExchange.Net.SharedApis;
|
||||||
|
|
||||||
namespace CryptoExchange.Net
|
namespace CryptoExchange.Net;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Helper methods
|
||||||
|
/// </summary>
|
||||||
|
public static class ExtensionMethods
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Helper methods
|
/// Add a parameter
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class ExtensionMethods
|
/// <param name="parameters"></param>
|
||||||
|
/// <param name="key"></param>
|
||||||
|
/// <param name="value"></param>
|
||||||
|
public static void AddParameter(this Dictionary<string, object> parameters, string key, string value)
|
||||||
{
|
{
|
||||||
/// <summary>
|
parameters.Add(key, value);
|
||||||
/// Add a parameter
|
}
|
||||||
/// </summary>
|
|
||||||
/// <param name="parameters"></param>
|
/// <summary>
|
||||||
/// <param name="key"></param>
|
/// Add a parameter
|
||||||
/// <param name="value"></param>
|
/// </summary>
|
||||||
public static void AddParameter(this Dictionary<string, object> parameters, string key, string value)
|
/// <param name="parameters"></param>
|
||||||
{
|
/// <param name="key"></param>
|
||||||
|
/// <param name="value"></param>
|
||||||
|
public static void AddParameter(this Dictionary<string, object> parameters, string key, object value)
|
||||||
|
{
|
||||||
|
parameters.Add(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add an optional parameter. Not added if value is null
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="parameters"></param>
|
||||||
|
/// <param name="key"></param>
|
||||||
|
/// <param name="value"></param>
|
||||||
|
public static void AddOptionalParameter(this Dictionary<string, object> parameters, string key, object? value)
|
||||||
|
{
|
||||||
|
if (value != null)
|
||||||
parameters.Add(key, value);
|
parameters.Add(key, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Add a parameter
|
/// Create a query string of the specified parameters
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="parameters"></param>
|
/// <param name="parameters">The parameters to use</param>
|
||||||
/// <param name="key"></param>
|
/// <param name="urlEncodeValues">Whether or not the values should be url encoded</param>
|
||||||
/// <param name="value"></param>
|
/// <param name="serializationType">How to serialize array parameters</param>
|
||||||
public static void AddParameter(this Dictionary<string, object> parameters, string key, object value)
|
/// <returns></returns>
|
||||||
|
public static string CreateParamString(this IDictionary<string, object> parameters, bool urlEncodeValues, ArrayParametersSerialization serializationType)
|
||||||
|
{
|
||||||
|
var uriString = string.Empty;
|
||||||
|
var arraysParameters = parameters.Where(p => p.Value.GetType().IsArray).ToList();
|
||||||
|
foreach (var arrayEntry in arraysParameters)
|
||||||
{
|
{
|
||||||
parameters.Add(key, value);
|
if (serializationType == ArrayParametersSerialization.Array)
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Add an optional parameter. Not added if value is null
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="parameters"></param>
|
|
||||||
/// <param name="key"></param>
|
|
||||||
/// <param name="value"></param>
|
|
||||||
public static void AddOptionalParameter(this Dictionary<string, object> parameters, string key, object? value)
|
|
||||||
{
|
|
||||||
if (value != null)
|
|
||||||
parameters.Add(key, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create a query string of the specified parameters
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="parameters">The parameters to use</param>
|
|
||||||
/// <param name="urlEncodeValues">Whether or not the values should be url encoded</param>
|
|
||||||
/// <param name="serializationType">How to serialize array parameters</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string CreateParamString(this IDictionary<string, object> parameters, bool urlEncodeValues, ArrayParametersSerialization serializationType)
|
|
||||||
{
|
|
||||||
var uriString = string.Empty;
|
|
||||||
var arraysParameters = parameters.Where(p => p.Value.GetType().IsArray).ToList();
|
|
||||||
foreach (var arrayEntry in arraysParameters)
|
|
||||||
{
|
{
|
||||||
if (serializationType == ArrayParametersSerialization.Array)
|
uriString += $"{string.Join("&", ((object[])(urlEncodeValues ? Uri.EscapeDataString(arrayEntry.Value.ToString()!) : arrayEntry.Value)).Select(v => $"{arrayEntry.Key}[]={string.Format(CultureInfo.InvariantCulture, "{0}", v)}"))}&";
|
||||||
|
}
|
||||||
|
else if (serializationType == ArrayParametersSerialization.MultipleValues)
|
||||||
|
{
|
||||||
|
var array = (Array)arrayEntry.Value;
|
||||||
|
uriString += string.Join("&", array.OfType<object>().Select(a => $"{arrayEntry.Key}={Uri.EscapeDataString(string.Format(CultureInfo.InvariantCulture, "{0}", a))}"));
|
||||||
|
uriString += "&";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var array = (Array)arrayEntry.Value;
|
||||||
|
uriString += $"{arrayEntry.Key}=[{string.Join(",", array.OfType<object>().Select(a => string.Format(CultureInfo.InvariantCulture, "{0}", a)))}]&";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uriString += $"{string.Join("&", parameters.Where(p => !p.Value.GetType().IsArray).Select(s => $"{s.Key}={(urlEncodeValues ? Uri.EscapeDataString(string.Format(CultureInfo.InvariantCulture, "{0}", s.Value)) : string.Format(CultureInfo.InvariantCulture, "{0}", s.Value))}"))}";
|
||||||
|
uriString = uriString.TrimEnd('&');
|
||||||
|
return uriString;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Convert a dictionary to formdata string
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="parameters"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string ToFormData(this IDictionary<string, object> parameters)
|
||||||
|
{
|
||||||
|
var formData = HttpUtility.ParseQueryString(string.Empty);
|
||||||
|
foreach (var kvp in parameters)
|
||||||
|
{
|
||||||
|
if (kvp.Value is null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (kvp.Value.GetType().IsArray)
|
||||||
|
{
|
||||||
|
var array = (Array)kvp.Value;
|
||||||
|
foreach (var value in array)
|
||||||
|
formData.Add(kvp.Key, string.Format(CultureInfo.InvariantCulture, "{0}", value));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
formData.Add(kvp.Key, string.Format(CultureInfo.InvariantCulture, "{0}", kvp.Value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return formData.ToString()!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validates an int is one of the allowed values
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">Value of the int</param>
|
||||||
|
/// <param name="argumentName">Name of the parameter</param>
|
||||||
|
/// <param name="allowedValues">Allowed values</param>
|
||||||
|
public static void ValidateIntValues(this int value, string argumentName, params int[] allowedValues)
|
||||||
|
{
|
||||||
|
if (!allowedValues.Contains(value))
|
||||||
|
{
|
||||||
|
throw new ArgumentException(
|
||||||
|
$"{value} not allowed for parameter {argumentName}, allowed values: {string.Join(", ", allowedValues)}", argumentName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validates an int is between two values
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">The value of the int</param>
|
||||||
|
/// <param name="argumentName">Name of the parameter</param>
|
||||||
|
/// <param name="minValue">Min value</param>
|
||||||
|
/// <param name="maxValue">Max value</param>
|
||||||
|
public static void ValidateIntBetween(this int value, string argumentName, int minValue, int maxValue)
|
||||||
|
{
|
||||||
|
if (value < minValue || value > maxValue)
|
||||||
|
{
|
||||||
|
throw new ArgumentException(
|
||||||
|
$"{value} not allowed for parameter {argumentName}, min: {minValue}, max: {maxValue}", argumentName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validates a string is not null or empty
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">The value of the string</param>
|
||||||
|
/// <param name="argumentName">Name of the parameter</param>
|
||||||
|
public static void ValidateNotNull(this string value, string argumentName)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(value))
|
||||||
|
throw new ArgumentException($"No value provided for parameter {argumentName}", argumentName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validates a string is null or not empty
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value"></param>
|
||||||
|
/// <param name="argumentName"></param>
|
||||||
|
public static void ValidateNullOrNotEmpty(this string value, string argumentName)
|
||||||
|
{
|
||||||
|
if (value != null && string.IsNullOrEmpty(value))
|
||||||
|
throw new ArgumentException($"No value provided for parameter {argumentName}", argumentName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validates an object is not null
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">The value of the object</param>
|
||||||
|
/// <param name="argumentName">Name of the parameter</param>
|
||||||
|
public static void ValidateNotNull(this object value, string argumentName)
|
||||||
|
{
|
||||||
|
if (value == null)
|
||||||
|
throw new ArgumentException($"No value provided for parameter {argumentName}", argumentName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validates a list is not null or empty
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">The value of the object</param>
|
||||||
|
/// <param name="argumentName">Name of the parameter</param>
|
||||||
|
public static void ValidateNotNull<T>(this IEnumerable<T> value, string argumentName)
|
||||||
|
{
|
||||||
|
if (value == null || !value.Any())
|
||||||
|
throw new ArgumentException($"No values provided for parameter {argumentName}", argumentName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Format a string to RFC3339/ISO8601 string
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dateTime"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string ToRfc3339String(this DateTime dateTime)
|
||||||
|
{
|
||||||
|
return dateTime.ToString("yyyy-MM-dd'T'HH:mm:ss.fffzzz", DateTimeFormatInfo.InvariantInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Format an exception and inner exception to a readable string
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="exception"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string ToLogString(this Exception? exception)
|
||||||
|
{
|
||||||
|
var message = new StringBuilder();
|
||||||
|
var indent = 0;
|
||||||
|
while (exception != null)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < indent; i++)
|
||||||
|
message.Append(' ');
|
||||||
|
message.Append(exception.GetType().Name);
|
||||||
|
message.Append(" - ");
|
||||||
|
message.AppendLine(exception.Message);
|
||||||
|
for (var i = 0; i < indent; i++)
|
||||||
|
message.Append(' ');
|
||||||
|
message.AppendLine(exception.StackTrace);
|
||||||
|
|
||||||
|
indent += 2;
|
||||||
|
exception = exception.InnerException;
|
||||||
|
}
|
||||||
|
|
||||||
|
return message.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Append a base url with provided path
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="url"></param>
|
||||||
|
/// <param name="path"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string AppendPath(this string url, params string[] path)
|
||||||
|
{
|
||||||
|
if (!url.EndsWith("/"))
|
||||||
|
url += "/";
|
||||||
|
|
||||||
|
foreach (var item in path)
|
||||||
|
url += item.Trim('/') + "/";
|
||||||
|
|
||||||
|
return url.TrimEnd('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new uri with the provided parameters as query
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="parameters"></param>
|
||||||
|
/// <param name="baseUri"></param>
|
||||||
|
/// <param name="arraySerialization"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static Uri SetParameters(this Uri baseUri, IDictionary<string, object> parameters, ArrayParametersSerialization arraySerialization)
|
||||||
|
{
|
||||||
|
var uriBuilder = new UriBuilder();
|
||||||
|
uriBuilder.Scheme = baseUri.Scheme;
|
||||||
|
uriBuilder.Host = baseUri.Host;
|
||||||
|
uriBuilder.Port = baseUri.Port;
|
||||||
|
uriBuilder.Path = baseUri.AbsolutePath;
|
||||||
|
var httpValueCollection = HttpUtility.ParseQueryString(string.Empty);
|
||||||
|
foreach (var parameter in parameters)
|
||||||
|
{
|
||||||
|
if (parameter.Value.GetType().IsArray)
|
||||||
|
{
|
||||||
|
if (arraySerialization == ArrayParametersSerialization.JsonArray)
|
||||||
{
|
{
|
||||||
uriString += $"{string.Join("&", ((object[])(urlEncodeValues ? Uri.EscapeDataString(arrayEntry.Value.ToString()!) : arrayEntry.Value)).Select(v => $"{arrayEntry.Key}[]={string.Format(CultureInfo.InvariantCulture, "{0}", v)}"))}&";
|
httpValueCollection.Add(parameter.Key, $"[{string.Join(",", (object[])parameter.Value)}]");
|
||||||
}
|
|
||||||
else if (serializationType == ArrayParametersSerialization.MultipleValues)
|
|
||||||
{
|
|
||||||
var array = (Array)arrayEntry.Value;
|
|
||||||
uriString += string.Join("&", array.OfType<object>().Select(a => $"{arrayEntry.Key}={Uri.EscapeDataString(string.Format(CultureInfo.InvariantCulture, "{0}", a))}"));
|
|
||||||
uriString += "&";
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var array = (Array)arrayEntry.Value;
|
foreach (var item in (object[])parameter.Value)
|
||||||
uriString += $"{arrayEntry.Key}=[{string.Join(",", array.OfType<object>().Select(a => string.Format(CultureInfo.InvariantCulture, "{0}", a)))}]&";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
uriString += $"{string.Join("&", parameters.Where(p => !p.Value.GetType().IsArray).Select(s => $"{s.Key}={(urlEncodeValues ? Uri.EscapeDataString(string.Format(CultureInfo.InvariantCulture, "{0}", s.Value)) : string.Format(CultureInfo.InvariantCulture, "{0}", s.Value))}"))}";
|
|
||||||
uriString = uriString.TrimEnd('&');
|
|
||||||
return uriString;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Convert a dictionary to formdata string
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="parameters"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string ToFormData(this IDictionary<string, object> parameters)
|
|
||||||
{
|
|
||||||
var formData = HttpUtility.ParseQueryString(string.Empty);
|
|
||||||
foreach (var kvp in parameters)
|
|
||||||
{
|
|
||||||
if (kvp.Value is null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (kvp.Value.GetType().IsArray)
|
|
||||||
{
|
|
||||||
var array = (Array)kvp.Value;
|
|
||||||
foreach (var value in array)
|
|
||||||
formData.Add(kvp.Key, string.Format(CultureInfo.InvariantCulture, "{0}", value));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
formData.Add(kvp.Key, string.Format(CultureInfo.InvariantCulture, "{0}", kvp.Value));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return formData.ToString()!;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Validates an int is one of the allowed values
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="value">Value of the int</param>
|
|
||||||
/// <param name="argumentName">Name of the parameter</param>
|
|
||||||
/// <param name="allowedValues">Allowed values</param>
|
|
||||||
public static void ValidateIntValues(this int value, string argumentName, params int[] allowedValues)
|
|
||||||
{
|
|
||||||
if (!allowedValues.Contains(value))
|
|
||||||
{
|
|
||||||
throw new ArgumentException(
|
|
||||||
$"{value} not allowed for parameter {argumentName}, allowed values: {string.Join(", ", allowedValues)}", argumentName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Validates an int is between two values
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="value">The value of the int</param>
|
|
||||||
/// <param name="argumentName">Name of the parameter</param>
|
|
||||||
/// <param name="minValue">Min value</param>
|
|
||||||
/// <param name="maxValue">Max value</param>
|
|
||||||
public static void ValidateIntBetween(this int value, string argumentName, int minValue, int maxValue)
|
|
||||||
{
|
|
||||||
if (value < minValue || value > maxValue)
|
|
||||||
{
|
|
||||||
throw new ArgumentException(
|
|
||||||
$"{value} not allowed for parameter {argumentName}, min: {minValue}, max: {maxValue}", argumentName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Validates a string is not null or empty
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="value">The value of the string</param>
|
|
||||||
/// <param name="argumentName">Name of the parameter</param>
|
|
||||||
public static void ValidateNotNull(this string value, string argumentName)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(value))
|
|
||||||
throw new ArgumentException($"No value provided for parameter {argumentName}", argumentName);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Validates a string is null or not empty
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="value"></param>
|
|
||||||
/// <param name="argumentName"></param>
|
|
||||||
public static void ValidateNullOrNotEmpty(this string value, string argumentName)
|
|
||||||
{
|
|
||||||
if (value != null && string.IsNullOrEmpty(value))
|
|
||||||
throw new ArgumentException($"No value provided for parameter {argumentName}", argumentName);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Validates an object is not null
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="value">The value of the object</param>
|
|
||||||
/// <param name="argumentName">Name of the parameter</param>
|
|
||||||
public static void ValidateNotNull(this object value, string argumentName)
|
|
||||||
{
|
|
||||||
if (value == null)
|
|
||||||
throw new ArgumentException($"No value provided for parameter {argumentName}", argumentName);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Validates a list is not null or empty
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="value">The value of the object</param>
|
|
||||||
/// <param name="argumentName">Name of the parameter</param>
|
|
||||||
public static void ValidateNotNull<T>(this IEnumerable<T> value, string argumentName)
|
|
||||||
{
|
|
||||||
if (value == null || !value.Any())
|
|
||||||
throw new ArgumentException($"No values provided for parameter {argumentName}", argumentName);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Format a string to RFC3339/ISO8601 string
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="dateTime"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string ToRfc3339String(this DateTime dateTime)
|
|
||||||
{
|
|
||||||
return dateTime.ToString("yyyy-MM-dd'T'HH:mm:ss.fffzzz", DateTimeFormatInfo.InvariantInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Format an exception and inner exception to a readable string
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="exception"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string ToLogString(this Exception? exception)
|
|
||||||
{
|
|
||||||
var message = new StringBuilder();
|
|
||||||
var indent = 0;
|
|
||||||
while (exception != null)
|
|
||||||
{
|
|
||||||
for (var i = 0; i < indent; i++)
|
|
||||||
message.Append(' ');
|
|
||||||
message.Append(exception.GetType().Name);
|
|
||||||
message.Append(" - ");
|
|
||||||
message.AppendLine(exception.Message);
|
|
||||||
for (var i = 0; i < indent; i++)
|
|
||||||
message.Append(' ');
|
|
||||||
message.AppendLine(exception.StackTrace);
|
|
||||||
|
|
||||||
indent += 2;
|
|
||||||
exception = exception.InnerException;
|
|
||||||
}
|
|
||||||
|
|
||||||
return message.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Append a base url with provided path
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="url"></param>
|
|
||||||
/// <param name="path"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string AppendPath(this string url, params string[] path)
|
|
||||||
{
|
|
||||||
if (!url.EndsWith("/"))
|
|
||||||
url += "/";
|
|
||||||
|
|
||||||
foreach (var item in path)
|
|
||||||
url += item.Trim('/') + "/";
|
|
||||||
|
|
||||||
return url.TrimEnd('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create a new uri with the provided parameters as query
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="parameters"></param>
|
|
||||||
/// <param name="baseUri"></param>
|
|
||||||
/// <param name="arraySerialization"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static Uri SetParameters(this Uri baseUri, IDictionary<string, object> parameters, ArrayParametersSerialization arraySerialization)
|
|
||||||
{
|
|
||||||
var uriBuilder = new UriBuilder();
|
|
||||||
uriBuilder.Scheme = baseUri.Scheme;
|
|
||||||
uriBuilder.Host = baseUri.Host;
|
|
||||||
uriBuilder.Port = baseUri.Port;
|
|
||||||
uriBuilder.Path = baseUri.AbsolutePath;
|
|
||||||
var httpValueCollection = HttpUtility.ParseQueryString(string.Empty);
|
|
||||||
foreach (var parameter in parameters)
|
|
||||||
{
|
|
||||||
if (parameter.Value.GetType().IsArray)
|
|
||||||
{
|
|
||||||
if (arraySerialization == ArrayParametersSerialization.JsonArray)
|
|
||||||
{
|
{
|
||||||
httpValueCollection.Add(parameter.Key, $"[{string.Join(",", (object[])parameter.Value)}]");
|
if (arraySerialization == ArrayParametersSerialization.Array)
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
foreach (var item in (object[])parameter.Value)
|
|
||||||
{
|
{
|
||||||
if (arraySerialization == ArrayParametersSerialization.Array)
|
httpValueCollection.Add(parameter.Key + "[]", item.ToString());
|
||||||
{
|
}
|
||||||
httpValueCollection.Add(parameter.Key + "[]", item.ToString());
|
else
|
||||||
}
|
{
|
||||||
else
|
httpValueCollection.Add(parameter.Key, item.ToString());
|
||||||
{
|
|
||||||
httpValueCollection.Add(parameter.Key, item.ToString());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
httpValueCollection.Add(parameter.Key, parameter.Value.ToString());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
uriBuilder.Query = httpValueCollection.ToString();
|
else
|
||||||
return uriBuilder.Uri;
|
{
|
||||||
|
httpValueCollection.Add(parameter.Key, parameter.Value.ToString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
uriBuilder.Query = httpValueCollection.ToString();
|
||||||
/// Create a new uri with the provided parameters as query
|
return uriBuilder.Uri;
|
||||||
/// </summary>
|
}
|
||||||
/// <param name="parameters"></param>
|
|
||||||
/// <param name="baseUri"></param>
|
/// <summary>
|
||||||
/// <param name="arraySerialization"></param>
|
/// Create a new uri with the provided parameters as query
|
||||||
/// <returns></returns>
|
/// </summary>
|
||||||
public static Uri SetParameters(this Uri baseUri, IOrderedEnumerable<KeyValuePair<string, object>> parameters, ArrayParametersSerialization arraySerialization)
|
/// <param name="parameters"></param>
|
||||||
|
/// <param name="baseUri"></param>
|
||||||
|
/// <param name="arraySerialization"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static Uri SetParameters(this Uri baseUri, IOrderedEnumerable<KeyValuePair<string, object>> parameters, ArrayParametersSerialization arraySerialization)
|
||||||
|
{
|
||||||
|
var uriBuilder = new UriBuilder();
|
||||||
|
uriBuilder.Scheme = baseUri.Scheme;
|
||||||
|
uriBuilder.Host = baseUri.Host;
|
||||||
|
uriBuilder.Port = baseUri.Port;
|
||||||
|
uriBuilder.Path = baseUri.AbsolutePath;
|
||||||
|
var httpValueCollection = HttpUtility.ParseQueryString(string.Empty);
|
||||||
|
foreach (var parameter in parameters)
|
||||||
{
|
{
|
||||||
var uriBuilder = new UriBuilder();
|
if (parameter.Value.GetType().IsArray)
|
||||||
uriBuilder.Scheme = baseUri.Scheme;
|
|
||||||
uriBuilder.Host = baseUri.Host;
|
|
||||||
uriBuilder.Port = baseUri.Port;
|
|
||||||
uriBuilder.Path = baseUri.AbsolutePath;
|
|
||||||
var httpValueCollection = HttpUtility.ParseQueryString(string.Empty);
|
|
||||||
foreach (var parameter in parameters)
|
|
||||||
{
|
{
|
||||||
if (parameter.Value.GetType().IsArray)
|
if (arraySerialization == ArrayParametersSerialization.JsonArray)
|
||||||
{
|
{
|
||||||
if (arraySerialization == ArrayParametersSerialization.JsonArray)
|
httpValueCollection.Add(parameter.Key, $"[{string.Join(",", (object[])parameter.Value)}]");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach (var item in (object[])parameter.Value)
|
||||||
{
|
{
|
||||||
httpValueCollection.Add(parameter.Key, $"[{string.Join(",", (object[])parameter.Value)}]");
|
if (arraySerialization == ArrayParametersSerialization.Array)
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
foreach (var item in (object[])parameter.Value)
|
|
||||||
{
|
{
|
||||||
if (arraySerialization == ArrayParametersSerialization.Array)
|
httpValueCollection.Add(parameter.Key + "[]", item.ToString());
|
||||||
{
|
}
|
||||||
httpValueCollection.Add(parameter.Key + "[]", item.ToString());
|
else
|
||||||
}
|
{
|
||||||
else
|
httpValueCollection.Add(parameter.Key, item.ToString());
|
||||||
{
|
|
||||||
httpValueCollection.Add(parameter.Key, item.ToString());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
httpValueCollection.Add(parameter.Key, parameter.Value.ToString());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
uriBuilder.Query = httpValueCollection.ToString();
|
else
|
||||||
return uriBuilder.Uri;
|
{
|
||||||
|
httpValueCollection.Add(parameter.Key, parameter.Value.ToString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
uriBuilder.Query = httpValueCollection.ToString();
|
||||||
/// Add parameter to URI
|
return uriBuilder.Uri;
|
||||||
/// </summary>
|
}
|
||||||
/// <param name="uri"></param>
|
|
||||||
/// <param name="name"></param>
|
|
||||||
/// <param name="value"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static Uri AddQueryParmeter(this Uri uri, string name, string value)
|
|
||||||
{
|
|
||||||
var httpValueCollection = HttpUtility.ParseQueryString(uri.Query);
|
|
||||||
|
|
||||||
httpValueCollection.Remove(name);
|
/// <summary>
|
||||||
httpValueCollection.Add(name, value);
|
/// Add parameter to URI
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="uri"></param>
|
||||||
|
/// <param name="name"></param>
|
||||||
|
/// <param name="value"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static Uri AddQueryParameter(this Uri uri, string name, string value)
|
||||||
|
{
|
||||||
|
var httpValueCollection = HttpUtility.ParseQueryString(uri.Query);
|
||||||
|
|
||||||
var ub = new UriBuilder(uri);
|
httpValueCollection.Remove(name);
|
||||||
ub.Query = httpValueCollection.ToString();
|
httpValueCollection.Add(name, value);
|
||||||
|
|
||||||
return ub.Uri;
|
var ub = new UriBuilder(uri);
|
||||||
}
|
ub.Query = httpValueCollection.ToString();
|
||||||
|
|
||||||
/// <summary>
|
return ub.Uri;
|
||||||
/// Decompress using GzipStream
|
}
|
||||||
/// </summary>
|
|
||||||
/// <param name="data"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static ReadOnlyMemory<byte> DecompressGzip(this ReadOnlyMemory<byte> data)
|
|
||||||
{
|
|
||||||
using var decompressedStream = new MemoryStream();
|
|
||||||
using var dataStream = MemoryMarshal.TryGetArray(data, out var arraySegment)
|
|
||||||
? new MemoryStream(arraySegment.Array!, arraySegment.Offset, arraySegment.Count)
|
|
||||||
: new MemoryStream(data.ToArray());
|
|
||||||
using var deflateStream = new GZipStream(new MemoryStream(data.ToArray()), CompressionMode.Decompress);
|
|
||||||
deflateStream.CopyTo(decompressedStream);
|
|
||||||
return new ReadOnlyMemory<byte>(decompressedStream.GetBuffer(), 0, (int)decompressedStream.Length);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Decompress using DeflateStream
|
/// Decompress using GzipStream
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="input"></param>
|
/// <param name="data"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static ReadOnlyMemory<byte> Decompress(this ReadOnlyMemory<byte> input)
|
public static ReadOnlyMemory<byte> DecompressGzip(this ReadOnlyMemory<byte> data)
|
||||||
{
|
{
|
||||||
var output = new MemoryStream();
|
using var decompressedStream = new MemoryStream();
|
||||||
|
using var dataStream = MemoryMarshal.TryGetArray(data, out var arraySegment)
|
||||||
|
? new MemoryStream(arraySegment.Array!, arraySegment.Offset, arraySegment.Count)
|
||||||
|
: new MemoryStream(data.ToArray());
|
||||||
|
using var deflateStream = new GZipStream(new MemoryStream(data.ToArray()), CompressionMode.Decompress);
|
||||||
|
deflateStream.CopyTo(decompressedStream);
|
||||||
|
return new ReadOnlyMemory<byte>(decompressedStream.GetBuffer(), 0, (int)decompressedStream.Length);
|
||||||
|
}
|
||||||
|
|
||||||
using (var compressStream = new MemoryStream(input.ToArray()))
|
/// <summary>
|
||||||
using (var decompressor = new DeflateStream(compressStream, CompressionMode.Decompress))
|
/// Decompress using DeflateStream
|
||||||
decompressor.CopyTo(output);
|
/// </summary>
|
||||||
|
/// <param name="input"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static ReadOnlyMemory<byte> Decompress(this ReadOnlyMemory<byte> input)
|
||||||
|
{
|
||||||
|
var output = new MemoryStream();
|
||||||
|
|
||||||
output.Position = 0;
|
using (var compressStream = new MemoryStream(input.ToArray()))
|
||||||
return new ReadOnlyMemory<byte>(output.GetBuffer(), 0, (int)output.Length);
|
using (var decompressor = new DeflateStream(compressStream, CompressionMode.Decompress))
|
||||||
}
|
decompressor.CopyTo(output);
|
||||||
|
|
||||||
/// <summary>
|
output.Position = 0;
|
||||||
/// Whether the trading mode is linear
|
return new ReadOnlyMemory<byte>(output.GetBuffer(), 0, (int)output.Length);
|
||||||
/// </summary>
|
}
|
||||||
public static bool IsLinear(this TradingMode type) => type == TradingMode.PerpetualLinear || type == TradingMode.DeliveryLinear;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether the trading mode is inverse
|
/// Whether the trading mode is linear
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static bool IsInverse(this TradingMode type) => type == TradingMode.PerpetualInverse || type == TradingMode.DeliveryInverse;
|
public static bool IsLinear(this TradingMode type) => type == TradingMode.PerpetualLinear || type == TradingMode.DeliveryLinear;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether the trading mode is perpetual
|
|
||||||
/// </summary>
|
|
||||||
public static bool IsPerpetual(this TradingMode type) => type == TradingMode.PerpetualInverse || type == TradingMode.PerpetualLinear;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether the trading mode is delivery
|
/// Whether the trading mode is inverse
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static bool IsDelivery(this TradingMode type) => type == TradingMode.DeliveryInverse || type == TradingMode.DeliveryLinear;
|
public static bool IsInverse(this TradingMode type) => type == TradingMode.PerpetualInverse || type == TradingMode.DeliveryInverse;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the trading mode is perpetual
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsPerpetual(this TradingMode type) => type == TradingMode.PerpetualInverse || type == TradingMode.PerpetualLinear;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Register rest client interfaces
|
/// Whether the trading mode is delivery
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static IServiceCollection RegisterSharedRestInterfaces<T>(this IServiceCollection services, Func<IServiceProvider, T> client)
|
public static bool IsDelivery(this TradingMode type) => type == TradingMode.DeliveryInverse || type == TradingMode.DeliveryLinear;
|
||||||
{
|
|
||||||
if (typeof(IAssetsRestClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (IAssetsRestClient)client(x)!);
|
|
||||||
if (typeof(IBalanceRestClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (IBalanceRestClient)client(x)!);
|
|
||||||
if (typeof(IDepositRestClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (IDepositRestClient)client(x)!);
|
|
||||||
if (typeof(IKlineRestClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (IKlineRestClient)client(x)!);
|
|
||||||
if (typeof(IListenKeyRestClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (IListenKeyRestClient)client(x)!);
|
|
||||||
if (typeof(IOrderBookRestClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (IOrderBookRestClient)client(x)!);
|
|
||||||
if (typeof(IRecentTradeRestClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (IRecentTradeRestClient)client(x)!);
|
|
||||||
if (typeof(ITradeHistoryRestClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (ITradeHistoryRestClient)client(x)!);
|
|
||||||
if (typeof(IWithdrawalRestClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (IWithdrawalRestClient)client(x)!);
|
|
||||||
if (typeof(IWithdrawRestClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (IWithdrawRestClient)client(x)!);
|
|
||||||
if (typeof(IFeeRestClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (IFeeRestClient)client(x)!);
|
|
||||||
|
|
||||||
if (typeof(ISpotOrderRestClient).IsAssignableFrom(typeof(T)))
|
/// <summary>
|
||||||
services.AddTransient(x => (ISpotOrderRestClient)client(x)!);
|
/// Register rest client interfaces
|
||||||
if (typeof(ISpotSymbolRestClient).IsAssignableFrom(typeof(T)))
|
/// </summary>
|
||||||
services.AddTransient(x => (ISpotSymbolRestClient)client(x)!);
|
public static IServiceCollection RegisterSharedRestInterfaces<T>(this IServiceCollection services, Func<IServiceProvider, T> client)
|
||||||
if (typeof(ISpotTickerRestClient).IsAssignableFrom(typeof(T)))
|
{
|
||||||
services.AddTransient(x => (ISpotTickerRestClient)client(x)!);
|
if (typeof(IAssetsRestClient).IsAssignableFrom(typeof(T)))
|
||||||
|
services.AddTransient(x => (IAssetsRestClient)client(x)!);
|
||||||
|
if (typeof(IBalanceRestClient).IsAssignableFrom(typeof(T)))
|
||||||
|
services.AddTransient(x => (IBalanceRestClient)client(x)!);
|
||||||
|
if (typeof(IDepositRestClient).IsAssignableFrom(typeof(T)))
|
||||||
|
services.AddTransient(x => (IDepositRestClient)client(x)!);
|
||||||
|
if (typeof(IKlineRestClient).IsAssignableFrom(typeof(T)))
|
||||||
|
services.AddTransient(x => (IKlineRestClient)client(x)!);
|
||||||
|
if (typeof(IListenKeyRestClient).IsAssignableFrom(typeof(T)))
|
||||||
|
services.AddTransient(x => (IListenKeyRestClient)client(x)!);
|
||||||
|
if (typeof(IOrderBookRestClient).IsAssignableFrom(typeof(T)))
|
||||||
|
services.AddTransient(x => (IOrderBookRestClient)client(x)!);
|
||||||
|
if (typeof(IRecentTradeRestClient).IsAssignableFrom(typeof(T)))
|
||||||
|
services.AddTransient(x => (IRecentTradeRestClient)client(x)!);
|
||||||
|
if (typeof(ITradeHistoryRestClient).IsAssignableFrom(typeof(T)))
|
||||||
|
services.AddTransient(x => (ITradeHistoryRestClient)client(x)!);
|
||||||
|
if (typeof(IWithdrawalRestClient).IsAssignableFrom(typeof(T)))
|
||||||
|
services.AddTransient(x => (IWithdrawalRestClient)client(x)!);
|
||||||
|
if (typeof(IWithdrawRestClient).IsAssignableFrom(typeof(T)))
|
||||||
|
services.AddTransient(x => (IWithdrawRestClient)client(x)!);
|
||||||
|
if (typeof(IFeeRestClient).IsAssignableFrom(typeof(T)))
|
||||||
|
services.AddTransient(x => (IFeeRestClient)client(x)!);
|
||||||
|
if (typeof(IBookTickerRestClient).IsAssignableFrom(typeof(T)))
|
||||||
|
services.AddTransient(x => (IBookTickerRestClient)client(x)!);
|
||||||
|
|
||||||
if (typeof(IFundingRateRestClient).IsAssignableFrom(typeof(T)))
|
if (typeof(ISpotOrderRestClient).IsAssignableFrom(typeof(T)))
|
||||||
services.AddTransient(x => (IFundingRateRestClient)client(x)!);
|
services.AddTransient(x => (ISpotOrderRestClient)client(x)!);
|
||||||
if (typeof(IFuturesOrderRestClient).IsAssignableFrom(typeof(T)))
|
if (typeof(ISpotSymbolRestClient).IsAssignableFrom(typeof(T)))
|
||||||
services.AddTransient(x => (IFuturesOrderRestClient)client(x)!);
|
services.AddTransient(x => (ISpotSymbolRestClient)client(x)!);
|
||||||
if (typeof(IFuturesSymbolRestClient).IsAssignableFrom(typeof(T)))
|
if (typeof(ISpotTickerRestClient).IsAssignableFrom(typeof(T)))
|
||||||
services.AddTransient(x => (IFuturesSymbolRestClient)client(x)!);
|
services.AddTransient(x => (ISpotTickerRestClient)client(x)!);
|
||||||
if (typeof(IFuturesTickerRestClient).IsAssignableFrom(typeof(T)))
|
if (typeof(ISpotTriggerOrderRestClient).IsAssignableFrom(typeof(T)))
|
||||||
services.AddTransient(x => (IFuturesTickerRestClient)client(x)!);
|
services.AddTransient(x => (ISpotTriggerOrderRestClient)client(x)!);
|
||||||
if (typeof(IIndexPriceKlineRestClient).IsAssignableFrom(typeof(T)))
|
if (typeof(ISpotOrderClientIdRestClient).IsAssignableFrom(typeof(T)))
|
||||||
services.AddTransient(x => (IIndexPriceKlineRestClient)client(x)!);
|
services.AddTransient(x => (ISpotOrderClientIdRestClient)client(x)!);
|
||||||
if (typeof(ILeverageRestClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (ILeverageRestClient)client(x)!);
|
|
||||||
if (typeof(IMarkPriceKlineRestClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (IMarkPriceKlineRestClient)client(x)!);
|
|
||||||
if (typeof(IOpenInterestRestClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (IOpenInterestRestClient)client(x)!);
|
|
||||||
if (typeof(IPositionHistoryRestClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (IPositionHistoryRestClient)client(x)!);
|
|
||||||
if (typeof(IPositionModeRestClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (IPositionModeRestClient)client(x)!);
|
|
||||||
|
|
||||||
return services;
|
if (typeof(IFundingRateRestClient).IsAssignableFrom(typeof(T)))
|
||||||
}
|
services.AddTransient(x => (IFundingRateRestClient)client(x)!);
|
||||||
|
if (typeof(IFuturesOrderRestClient).IsAssignableFrom(typeof(T)))
|
||||||
|
services.AddTransient(x => (IFuturesOrderRestClient)client(x)!);
|
||||||
|
if (typeof(IFuturesSymbolRestClient).IsAssignableFrom(typeof(T)))
|
||||||
|
services.AddTransient(x => (IFuturesSymbolRestClient)client(x)!);
|
||||||
|
if (typeof(IFuturesTickerRestClient).IsAssignableFrom(typeof(T)))
|
||||||
|
services.AddTransient(x => (IFuturesTickerRestClient)client(x)!);
|
||||||
|
if (typeof(IIndexPriceKlineRestClient).IsAssignableFrom(typeof(T)))
|
||||||
|
services.AddTransient(x => (IIndexPriceKlineRestClient)client(x)!);
|
||||||
|
if (typeof(ILeverageRestClient).IsAssignableFrom(typeof(T)))
|
||||||
|
services.AddTransient(x => (ILeverageRestClient)client(x)!);
|
||||||
|
if (typeof(IMarkPriceKlineRestClient).IsAssignableFrom(typeof(T)))
|
||||||
|
services.AddTransient(x => (IMarkPriceKlineRestClient)client(x)!);
|
||||||
|
if (typeof(IOpenInterestRestClient).IsAssignableFrom(typeof(T)))
|
||||||
|
services.AddTransient(x => (IOpenInterestRestClient)client(x)!);
|
||||||
|
if (typeof(IPositionHistoryRestClient).IsAssignableFrom(typeof(T)))
|
||||||
|
services.AddTransient(x => (IPositionHistoryRestClient)client(x)!);
|
||||||
|
if (typeof(IPositionModeRestClient).IsAssignableFrom(typeof(T)))
|
||||||
|
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)!);
|
||||||
|
|
||||||
/// <summary>
|
return services;
|
||||||
/// Register socket client interfaces
|
}
|
||||||
/// </summary>
|
|
||||||
public static IServiceCollection RegisterSharedSocketInterfaces<T>(this IServiceCollection services, Func<IServiceProvider, T> client)
|
|
||||||
{
|
|
||||||
if (typeof(IBalanceSocketClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (IBalanceSocketClient)client(x)!);
|
|
||||||
if (typeof(IBookTickerSocketClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (IBookTickerSocketClient)client(x)!);
|
|
||||||
if (typeof(IKlineSocketClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (IKlineSocketClient)client(x)!);
|
|
||||||
if (typeof(IOrderBookRestClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (IOrderBookRestClient)client(x)!);
|
|
||||||
if (typeof(ITickerSocketClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (ITickerSocketClient)client(x)!);
|
|
||||||
if (typeof(ITickersSocketClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (ITickersSocketClient)client(x)!);
|
|
||||||
if (typeof(ITradeSocketClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (ITradeSocketClient)client(x)!);
|
|
||||||
if (typeof(IUserTradeSocketClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (IUserTradeSocketClient)client(x)!);
|
|
||||||
|
|
||||||
if (typeof(ISpotOrderSocketClient).IsAssignableFrom(typeof(T)))
|
/// <summary>
|
||||||
services.AddTransient(x => (ISpotOrderSocketClient)client(x)!);
|
/// Register socket client interfaces
|
||||||
|
/// </summary>
|
||||||
|
public static IServiceCollection RegisterSharedSocketInterfaces<T>(this IServiceCollection services, Func<IServiceProvider, T> client)
|
||||||
|
{
|
||||||
|
if (typeof(IBalanceSocketClient).IsAssignableFrom(typeof(T)))
|
||||||
|
services.AddTransient(x => (IBalanceSocketClient)client(x)!);
|
||||||
|
if (typeof(IBookTickerSocketClient).IsAssignableFrom(typeof(T)))
|
||||||
|
services.AddTransient(x => (IBookTickerSocketClient)client(x)!);
|
||||||
|
if (typeof(IKlineSocketClient).IsAssignableFrom(typeof(T)))
|
||||||
|
services.AddTransient(x => (IKlineSocketClient)client(x)!);
|
||||||
|
if (typeof(IOrderBookSocketClient).IsAssignableFrom(typeof(T)))
|
||||||
|
services.AddTransient(x => (IOrderBookSocketClient)client(x)!);
|
||||||
|
if (typeof(ITickerSocketClient).IsAssignableFrom(typeof(T)))
|
||||||
|
services.AddTransient(x => (ITickerSocketClient)client(x)!);
|
||||||
|
if (typeof(ITickersSocketClient).IsAssignableFrom(typeof(T)))
|
||||||
|
services.AddTransient(x => (ITickersSocketClient)client(x)!);
|
||||||
|
if (typeof(ITradeSocketClient).IsAssignableFrom(typeof(T)))
|
||||||
|
services.AddTransient(x => (ITradeSocketClient)client(x)!);
|
||||||
|
if (typeof(IUserTradeSocketClient).IsAssignableFrom(typeof(T)))
|
||||||
|
services.AddTransient(x => (IUserTradeSocketClient)client(x)!);
|
||||||
|
|
||||||
if (typeof(IFuturesOrderSocketClient).IsAssignableFrom(typeof(T)))
|
if (typeof(ISpotOrderSocketClient).IsAssignableFrom(typeof(T)))
|
||||||
services.AddTransient(x => (IFuturesOrderSocketClient)client(x)!);
|
services.AddTransient(x => (ISpotOrderSocketClient)client(x)!);
|
||||||
if (typeof(IPositionSocketClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (IPositionSocketClient)client(x)!);
|
|
||||||
|
|
||||||
return services;
|
if (typeof(IFuturesOrderSocketClient).IsAssignableFrom(typeof(T)))
|
||||||
}
|
services.AddTransient(x => (IFuturesOrderSocketClient)client(x)!);
|
||||||
|
if (typeof(IPositionSocketClient).IsAssignableFrom(typeof(T)))
|
||||||
|
services.AddTransient(x => (IPositionSocketClient)client(x)!);
|
||||||
|
|
||||||
|
return services;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user